From ab6c8b096f12722f30c302977cf6d2d9c7c361ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 5 Jul 2024 13:03:21 +0100 Subject: [PATCH 1/7] fix(rust): add missing elements to the OpenAPI spec --- rust/Cargo.lock | 1 + rust/agama-lib/src/network/settings.rs | 6 +++--- rust/agama-lib/src/network/types.rs | 6 +++--- rust/agama-locale-data/Cargo.toml | 1 + rust/agama-locale-data/src/locale.rs | 6 ++++-- rust/agama-server/src/network/model.rs | 26 +++++++++++++------------- rust/agama-server/src/web/docs.rs | 19 +++++++++++++++++++ 7 files changed, 44 insertions(+), 21 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index b5be81ef8..e9b7f39ac 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -80,6 +80,7 @@ dependencies = [ "regex", "serde", "thiserror", + "utoipa", ] [[package]] diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index b70128cf3..9fc19b2ba 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -27,14 +27,14 @@ use std::default::Default; use std::net::IpAddr; /// Network settings for installation -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct NetworkSettings { /// Connections to use in the installation pub connections: Vec, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] pub struct MatchSettings { #[serde(skip_serializing_if = "Vec::is_empty", default)] pub driver: Vec, @@ -56,7 +56,7 @@ impl MatchSettings { } /// Wireless configuration -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct WirelessSettings { /// Password of the wireless network diff --git a/rust/agama-lib/src/network/types.rs b/rust/agama-lib/src/network/types.rs index 4cc796a68..509edb09d 100644 --- a/rust/agama-lib/src/network/types.rs +++ b/rust/agama-lib/src/network/types.rs @@ -36,7 +36,7 @@ pub struct Device { pub state: DeviceState, } -#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct SSID(pub Vec); impl SSID { @@ -80,7 +80,7 @@ pub enum DeviceType { // For now this mirrors NetworkManager, because it was less mental work than coming up with // what exactly Agama needs. Expected to be adapted. -#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub enum DeviceState { #[default] @@ -145,7 +145,7 @@ impl fmt::Display for DeviceState { } } -#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub enum Status { #[default] diff --git a/rust/agama-locale-data/Cargo.toml b/rust/agama-locale-data/Cargo.toml index a1b2274f8..c95c3214c 100644 --- a/rust/agama-locale-data/Cargo.toml +++ b/rust/agama-locale-data/Cargo.toml @@ -13,3 +13,4 @@ flate2 = "1.0.34" chrono-tz = "0.8.6" regex = "1" thiserror = "1.0.64" +utoipa = "4.2.3" diff --git a/rust/agama-locale-data/src/locale.rs b/rust/agama-locale-data/src/locale.rs index 7f83074b6..65bb72b91 100644 --- a/rust/agama-locale-data/src/locale.rs +++ b/rust/agama-locale-data/src/locale.rs @@ -26,7 +26,7 @@ use std::sync::OnceLock; use std::{fmt::Display, str::FromStr}; use thiserror::Error; -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize, utoipa::ToSchema)] pub struct LocaleId { // ISO-639 pub language: String, @@ -100,9 +100,11 @@ static KEYMAP_ID_REGEX: OnceLock = OnceLock::new(); /// let id_with_dashes: KeymapId = "es-ast".parse().unwrap(); /// assert_eq!(id, id_with_dashes); /// ``` -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize, utoipa::ToSchema)] pub struct KeymapId { + /// Keyboard layout (e.g., "es" in "es(ast)") pub layout: String, + /// Keyboard variante (e.g., "ast" in "es(ast)") pub variant: Option, } diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-server/src/network/model.rs index 14518cb45..12075e02a 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -693,7 +693,7 @@ impl TryFrom for NetworkConnection { } } -#[derive(Default, Debug, PartialEq, Clone, Serialize)] +#[derive(Default, Debug, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub enum ConnectionConfig { #[default] Ethernet, @@ -707,7 +707,7 @@ pub enum ConnectionConfig { Tun(TunConfig), } -#[derive(Default, Debug, PartialEq, Clone, Serialize)] +#[derive(Default, Debug, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub enum PortConfig { #[default] None, @@ -730,7 +730,7 @@ impl From for ConnectionConfig { #[error("Invalid MAC address: {0}")] pub struct InvalidMacAddress(String); -#[derive(Debug, Default, Clone, PartialEq, Serialize)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, utoipa::ToSchema)] pub enum MacAddress { MacAddress(macaddr::MacAddr6), Preserve, @@ -791,7 +791,7 @@ impl From for zbus::fdo::Error { } #[skip_serializing_none] -#[derive(Default, Debug, PartialEq, Clone, Serialize)] +#[derive(Default, Debug, PartialEq, Clone, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct IpConfig { pub method4: Ipv4Method, @@ -810,7 +810,7 @@ pub struct IpConfig { } #[skip_serializing_none] -#[derive(Debug, Default, PartialEq, Clone, Serialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub struct MatchConfig { #[serde(skip_serializing_if = "Vec::is_empty")] pub driver: Vec, @@ -939,7 +939,7 @@ impl From<&IpRoute> for HashMap<&str, Value<'_>> { } } -#[derive(Debug, Default, PartialEq, Clone, Serialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub enum VlanProtocol { #[default] IEEE802_1Q, @@ -972,7 +972,7 @@ impl fmt::Display for VlanProtocol { } } -#[derive(Debug, Default, PartialEq, Clone, Serialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub struct VlanConfig { pub parent: String, pub id: u32, @@ -980,7 +980,7 @@ pub struct VlanConfig { } #[serde_as] -#[derive(Debug, Default, PartialEq, Clone, Serialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct WirelessConfig { pub mode: WirelessMode, @@ -1160,7 +1160,7 @@ impl fmt::Display for WirelessMode { } } -#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, utoipa::ToSchema)] pub enum SecurityProtocol { #[default] WEP, // No encryption or WEP ("none") @@ -1434,7 +1434,7 @@ impl fmt::Display for BondOptions { } } -#[derive(Debug, Default, PartialEq, Clone, Serialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub struct BondConfig { pub mode: BondMode, pub options: BondOptions, @@ -1478,7 +1478,7 @@ impl TryFrom for BondSettings { } } -#[derive(Debug, Default, PartialEq, Clone, Serialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub struct BridgeConfig { pub stp: bool, #[serde(skip_serializing_if = "Option::is_none")] @@ -1501,7 +1501,7 @@ pub struct BridgePortConfig { pub path_cost: Option, } -#[derive(Default, Debug, PartialEq, Clone, Serialize)] +#[derive(Default, Debug, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub struct InfinibandConfig { pub p_key: Option, pub parent: Option, @@ -1548,7 +1548,7 @@ pub enum TunMode { Tap = 2, } -#[derive(Default, Debug, PartialEq, Clone, Serialize)] +#[derive(Default, Debug, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub struct TunConfig { pub mode: TunMode, pub group: Option, diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index fcafe61c0..7e4537ce2 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -92,9 +92,15 @@ use utoipa::OpenApi; super::http::ping ), components( + schemas(agama_locale_data::KeymapId), + schemas(agama_locale_data::LocaleId), schemas(agama_lib::manager::InstallationPhase), schemas(agama_lib::network::settings::NetworkConnection), + schemas(agama_lib::network::settings::NetworkSettings), + schemas(agama_lib::network::settings::WirelessSettings), + schemas(agama_lib::network::types::DeviceState), schemas(agama_lib::network::types::DeviceType), + schemas(agama_lib::network::types::Status), schemas(agama_lib::product::Product), schemas(agama_lib::software::Pattern), schemas(agama_lib::storage::model::Action), @@ -137,8 +143,21 @@ use utoipa::OpenApi; schemas(crate::l10n::TimezoneEntry), schemas(agama_lib::localization::model::LocaleConfig), schemas(crate::manager::web::InstallerStatus), + schemas(crate::network::model::BondConfig), + schemas(crate::network::model::BridgeConfig), schemas(crate::network::model::Connection), + schemas(crate::network::model::ConnectionConfig), schemas(crate::network::model::Device), + schemas(crate::network::model::InfinibandConfig), + schemas(crate::network::model::IpConfig), + schemas(crate::network::model::MacAddress), + schemas(crate::network::model::MatchConfig), + schemas(crate::network::model::PortConfig), + schemas(crate::network::model::SecurityProtocol), + schemas(crate::network::model::TunConfig), + schemas(crate::network::model::VlanConfig), + schemas(crate::network::model::VlanProtocol), + schemas(crate::network::model::WirelessConfig), schemas(agama_lib::questions::model::Answer), schemas(agama_lib::questions::model::GenericAnswer), schemas(agama_lib::questions::model::GenericQuestion), From d394b82321e121514f0e75d2a658817e0bacff79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 29 Aug 2024 22:34:51 +0100 Subject: [PATCH 2/7] feat(rust): revamp the generation of the OpenAPI spec --- rust/Cargo.lock | 1 + rust/agama-lib/src/network/settings.rs | 2 +- rust/agama-lib/src/network/types.rs | 2 +- rust/agama-lib/src/software/client.rs | 2 +- rust/agama-server/Cargo.toml | 2 +- rust/agama-server/src/agama-web-server.rs | 3 +- rust/agama-server/src/network/model.rs | 30 +-- rust/agama-server/src/software/web.rs | 3 +- rust/agama-server/src/storage/web.rs | 3 +- rust/agama-server/src/web/docs.rs | 215 ++++++-------------- rust/agama-server/src/web/docs/l10n.rs | 29 +++ rust/agama-server/src/web/docs/manager.rs | 24 +++ rust/agama-server/src/web/docs/network.rs | 91 +++++++++ rust/agama-server/src/web/docs/questions.rs | 29 +++ rust/agama-server/src/web/docs/software.rs | 32 +++ rust/agama-server/src/web/docs/storage.rs | 72 +++++++ rust/agama-server/src/web/docs/users.rs | 32 +++ 17 files changed, 391 insertions(+), 181 deletions(-) create mode 100644 rust/agama-server/src/web/docs/l10n.rs create mode 100644 rust/agama-server/src/web/docs/manager.rs create mode 100644 rust/agama-server/src/web/docs/network.rs create mode 100644 rust/agama-server/src/web/docs/questions.rs create mode 100644 rust/agama-server/src/web/docs/software.rs create mode 100644 rust/agama-server/src/web/docs/storage.rs create mode 100644 rust/agama-server/src/web/docs/users.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e9b7f39ac..08dc92750 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -4480,6 +4480,7 @@ dependencies = [ "quote", "regex", "syn 2.0.79", + "uuid", ] [[package]] diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index 9fc19b2ba..724b552f0 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -94,7 +94,7 @@ pub struct WirelessSettings { pub pmf: i32, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct BondSettings { pub mode: String, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/rust/agama-lib/src/network/types.rs b/rust/agama-lib/src/network/types.rs index 509edb09d..c2119b8f7 100644 --- a/rust/agama-lib/src/network/types.rs +++ b/rust/agama-lib/src/network/types.rs @@ -183,7 +183,7 @@ impl TryFrom<&str> for Status { } /// Bond mode -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, utoipa::ToSchema)] pub enum BondMode { #[serde(rename = "balance-rr")] RoundRobin = 0, diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs index 0bc383297..fde5d31c2 100644 --- a/rust/agama-lib/src/software/client.rs +++ b/rust/agama-lib/src/software/client.rs @@ -43,7 +43,7 @@ pub struct Pattern { } /// Represents the reason why a pattern is selected. -#[derive(Clone, Copy, Debug, PartialEq, Serialize_repr)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize_repr, utoipa::ToSchema)] #[repr(u8)] pub enum SelectedBy { /// The pattern was selected by the user. diff --git a/rust/agama-server/Cargo.toml b/rust/agama-server/Cargo.toml index 07202aa93..a1aaafa64 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -30,7 +30,7 @@ tracing-journald = "0.3.0" tracing = "0.1.40" clap = { version = "4.5.19", features = ["derive", "wrap_help"] } tower = { version = "0.4.13", features = ["util"] } -utoipa = { version = "4.2.3", features = ["axum_extras"] } +utoipa = { version = "4.2.0", features = ["axum_extras", "uuid"] } config = "0.14.0" rand = "0.8.5" axum-extra = { version = "0.9.4", features = ["cookie", "typed-header"] } diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index 94664fb86..2523063a0 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -46,7 +46,6 @@ use openssl::ssl::{Ssl, SslAcceptor, SslMethod}; use tokio::sync::broadcast::channel; use tokio_openssl::SslStream; use tower::Service; -use utoipa::OpenApi; const DEFAULT_WEB_UI_DIR: &str = "/usr/share/agama/web_ui"; const TOKEN_FILE: &str = "/run/agama/token"; @@ -381,7 +380,7 @@ async fn serve_command(args: ServeArgs) -> anyhow::Result<()> { /// Display the API documentation in OpenAPI format. fn openapi_command() -> anyhow::Result<()> { - println!("{}", web::ApiDoc::openapi().to_pretty_json().unwrap()); + println!("{}", web::ApiDoc::build().to_pretty_json().unwrap()); Ok(()) } diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-server/src/network/model.rs index 12075e02a..f2ec57c84 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -826,7 +826,7 @@ pub struct MatchConfig { #[error("Unknown IP configuration method name: {0}")] pub struct UnknownIpMethod(String); -#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub enum Ipv4Method { #[default] @@ -862,7 +862,7 @@ impl FromStr for Ipv4Method { } } -#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub enum Ipv6Method { #[default] @@ -910,7 +910,7 @@ impl From for zbus::fdo::Error { } } -#[derive(Debug, PartialEq, Clone, Serialize)] +#[derive(Debug, PartialEq, Clone, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct IpRoute { pub destination: IpInet, @@ -1122,7 +1122,7 @@ impl TryFrom for WirelessSettings { } } -#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, utoipa::ToSchema)] pub enum WirelessMode { Unknown = 0, AdHoc = 1, @@ -1206,7 +1206,7 @@ impl TryFrom<&str> for SecurityProtocol { } } -#[derive(Debug, Clone, Copy, PartialEq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, utoipa::ToSchema)] pub enum GroupAlgorithm { Wep40, Wep104, @@ -1244,7 +1244,7 @@ impl fmt::Display for GroupAlgorithm { } } -#[derive(Debug, Clone, Copy, PartialEq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, utoipa::ToSchema)] pub enum PairwiseAlgorithm { Tkip, Ccmp, @@ -1276,7 +1276,7 @@ impl fmt::Display for PairwiseAlgorithm { } } -#[derive(Debug, Clone, Copy, PartialEq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, utoipa::ToSchema)] pub enum WPAProtocolVersion { Wpa, Rsn, @@ -1308,7 +1308,7 @@ impl fmt::Display for WPAProtocolVersion { } } -#[derive(Debug, Default, PartialEq, Clone, Serialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub struct WEPSecurity { pub auth_alg: WEPAuthAlg, pub wep_key_type: WEPKeyType, @@ -1317,7 +1317,7 @@ pub struct WEPSecurity { pub wep_key_index: u32, } -#[derive(Debug, Default, PartialEq, Clone, Serialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub enum WEPKeyType { #[default] Unknown = 0, @@ -1338,7 +1338,7 @@ impl TryFrom for WEPKeyType { } } -#[derive(Debug, Default, PartialEq, Clone, Serialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub enum WEPAuthAlg { #[default] Unset, @@ -1373,7 +1373,7 @@ impl fmt::Display for WEPAuthAlg { } } -#[derive(Debug, Clone, Copy, PartialEq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, utoipa::ToSchema)] pub enum WirelessBand { A, // 5GHz BG, // 2.4GHz @@ -1401,7 +1401,7 @@ impl TryFrom<&str> for WirelessBand { } } -#[derive(Debug, Default, Clone, PartialEq, Serialize)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, utoipa::ToSchema)] pub struct BondOptions(pub HashMap); impl TryFrom<&str> for BondOptions { @@ -1493,7 +1493,7 @@ pub struct BridgeConfig { pub ageing_time: Option, } -#[derive(Debug, Default, PartialEq, Clone, Serialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub struct BridgePortConfig { #[serde(skip_serializing_if = "Option::is_none")] pub priority: Option, @@ -1508,7 +1508,7 @@ pub struct InfinibandConfig { pub transport_mode: InfinibandTransportMode, } -#[derive(Default, Debug, PartialEq, Clone, Serialize)] +#[derive(Default, Debug, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub enum InfinibandTransportMode { #[default] Datagram, @@ -1541,7 +1541,7 @@ impl fmt::Display for InfinibandTransportMode { } } -#[derive(Default, Debug, PartialEq, Clone, Serialize)] +#[derive(Default, Debug, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub enum TunMode { #[default] Tun = 1, diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 11fb6adff..a9d411c8a 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -456,7 +456,8 @@ async fn proposal(State(state): State>) -> Result>) -> Result, Error> { state.software.probe().await?; diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs index 5660d3939..4709da144 100644 --- a/rust/agama-server/src/storage/web.rs +++ b/rust/agama-server/src/storage/web.rs @@ -189,7 +189,8 @@ async fn set_config( responses( (status = 200, description = "Devices were probed and an initial proposal were performed"), (status = 400, description = "The D-Bus service could not perform the action") - ) + ), + operation_id = "storage_probe" )] async fn probe(State(state): State>) -> Result, Error> { Ok(Json(state.client.probe().await?)) diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index 7e4537ce2..b04f717da 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -18,162 +18,61 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use utoipa::OpenApi; -#[derive(OpenApi)] -#[openapi( - info(description = "Agama web API description"), - paths( - crate::l10n::web::get_config, - crate::l10n::web::keymaps, - crate::l10n::web::locales, - crate::l10n::web::set_config, - crate::l10n::web::timezones, - crate::manager::web::finish_action, - crate::manager::web::install_action, - crate::manager::web::installer_status, - crate::manager::web::probe_action, - crate::network::web::add_connection, - crate::network::web::apply, - crate::network::web::connect, - crate::network::web::connections, - crate::network::web::delete_connection, - crate::network::web::devices, - crate::network::web::disconnect, - crate::network::web::update_connection, - crate::questions::web::answer_question, - crate::questions::web::get_answer, - crate::questions::web::delete_question, - crate::questions::web::create_question, - crate::questions::web::list_questions, - crate::software::web::get_config, - crate::software::web::patterns, - crate::software::web::probe, - crate::software::web::products, - crate::software::web::proposal, - crate::software::web::set_config, - crate::storage::web::actions, - crate::storage::web::devices_dirty, - crate::storage::web::get_proposal_settings, - crate::storage::web::probe, - crate::storage::web::product_params, - crate::storage::web::set_proposal_settings, - crate::storage::web::staging_devices, - crate::storage::web::system_devices, - crate::storage::web::usable_devices, - crate::storage::web::volume_for, - crate::storage::web::iscsi::delete_node, - crate::storage::web::iscsi::discover, - crate::storage::web::iscsi::initiator, - crate::storage::web::iscsi::login_node, - crate::storage::web::iscsi::logout_node, - crate::storage::web::iscsi::nodes, - crate::storage::web::iscsi::update_initiator, - crate::storage::web::iscsi::update_node, - crate::storage::web::dasd::probe, - crate::storage::web::dasd::supported, - crate::storage::web::dasd::devices, - crate::storage::web::dasd::format, - crate::storage::web::dasd::enable, - crate::storage::web::dasd::disable, - crate::storage::web::dasd::set_diag, - crate::storage::web::zfcp::supported, - crate::storage::web::zfcp::controllers, - crate::storage::web::zfcp::activate_controller, - crate::storage::web::zfcp::activate_disk, - crate::storage::web::zfcp::deactivate_disk, - crate::storage::web::zfcp::get_disks, - crate::storage::web::zfcp::get_wwpns, - crate::storage::web::zfcp::get_luns, - crate::users::web::get_root_config, - crate::users::web::get_user_config, - crate::users::web::patch_root, - crate::users::web::remove_first_user, - crate::users::web::set_first_user, - super::http::ping - ), - components( - schemas(agama_locale_data::KeymapId), - schemas(agama_locale_data::LocaleId), - schemas(agama_lib::manager::InstallationPhase), - schemas(agama_lib::network::settings::NetworkConnection), - schemas(agama_lib::network::settings::NetworkSettings), - schemas(agama_lib::network::settings::WirelessSettings), - schemas(agama_lib::network::types::DeviceState), - schemas(agama_lib::network::types::DeviceType), - schemas(agama_lib::network::types::Status), - schemas(agama_lib::product::Product), - schemas(agama_lib::software::Pattern), - schemas(agama_lib::storage::model::Action), - schemas(agama_lib::storage::model::BlockDevice), - schemas(agama_lib::storage::model::Component), - schemas(agama_lib::storage::model::Device), - schemas(agama_lib::storage::model::DeviceInfo), - schemas(agama_lib::storage::model::DeviceSid), - schemas(agama_lib::storage::model::Drive), - schemas(agama_lib::storage::model::DriveInfo), - schemas(agama_lib::storage::model::DeviceSize), - schemas(agama_lib::storage::model::Filesystem), - schemas(agama_lib::storage::model::LvmLv), - schemas(agama_lib::storage::model::LvmVg), - schemas(agama_lib::storage::model::Md), - schemas(agama_lib::storage::model::Multipath), - schemas(agama_lib::storage::model::Partition), - schemas(agama_lib::storage::model::PartitionTable), - schemas(agama_lib::storage::model::ProposalSettings), - schemas(agama_lib::storage::model::ProposalSettingsPatch), - schemas(agama_lib::storage::model::ProposalTarget), - schemas(agama_lib::storage::model::Raid), - schemas(agama_lib::storage::model::SpaceAction), - schemas(agama_lib::storage::model::SpaceActionSettings), - schemas(agama_lib::storage::model::UnusedSlot), - schemas(agama_lib::storage::model::Volume), - schemas(agama_lib::storage::model::VolumeOutline), - schemas(agama_lib::storage::model::VolumeTarget), - schemas(agama_lib::storage::model::dasd::DASDDevice), - schemas(agama_lib::storage::model::dasd::DASDFormatSummary), - schemas(agama_lib::storage::model::zfcp::ZFCPDisk), - schemas(agama_lib::storage::model::zfcp::ZFCPController), - schemas(agama_lib::storage::client::iscsi::ISCSIAuth), - schemas(agama_lib::storage::client::iscsi::ISCSIInitiator), - schemas(agama_lib::storage::client::iscsi::ISCSINode), - schemas(agama_lib::storage::client::iscsi::LoginResult), - schemas(agama_lib::users::FirstUser), - schemas(crate::l10n::Keymap), - schemas(crate::l10n::LocaleEntry), - schemas(crate::l10n::TimezoneEntry), - schemas(agama_lib::localization::model::LocaleConfig), - schemas(crate::manager::web::InstallerStatus), - schemas(crate::network::model::BondConfig), - schemas(crate::network::model::BridgeConfig), - schemas(crate::network::model::Connection), - schemas(crate::network::model::ConnectionConfig), - schemas(crate::network::model::Device), - schemas(crate::network::model::InfinibandConfig), - schemas(crate::network::model::IpConfig), - schemas(crate::network::model::MacAddress), - schemas(crate::network::model::MatchConfig), - schemas(crate::network::model::PortConfig), - schemas(crate::network::model::SecurityProtocol), - schemas(crate::network::model::TunConfig), - schemas(crate::network::model::VlanConfig), - schemas(crate::network::model::VlanProtocol), - schemas(crate::network::model::WirelessConfig), - schemas(agama_lib::questions::model::Answer), - schemas(agama_lib::questions::model::GenericAnswer), - schemas(agama_lib::questions::model::GenericQuestion), - schemas(agama_lib::questions::model::PasswordAnswer), - schemas(agama_lib::questions::model::Question), - schemas(agama_lib::questions::model::QuestionWithPassword), - schemas(agama_lib::software::model::SoftwareConfig), - schemas(crate::software::web::SoftwareProposal), - schemas(crate::storage::web::ProductParams), - schemas(crate::storage::web::iscsi::DiscoverParams), - schemas(crate::storage::web::iscsi::InitiatorParams), - schemas(crate::storage::web::iscsi::LoginParams), - schemas(crate::storage::web::iscsi::NodeParams), - schemas(agama_lib::users::model::RootConfig), - schemas(agama_lib::users::model::RootPatchSettings), - schemas(super::http::PingResponse) - ) -)] +use utoipa::openapi::{ComponentsBuilder, InfoBuilder, PathsBuilder}; + +mod network; +pub use network::NetworkApiDocBuilder; +mod storage; +pub use storage::StorageApiDocBuilder; +mod software; +pub use software::SoftwareApiDocBuilder; +mod l10n; +pub use l10n::L10nApiDocBuilder; +mod questions; +pub use questions::QuestionsApiDocBuilder; +mod manager; +pub use manager::ManagerApiDocBuilder; +mod users; +pub use users::UsersApiDocBuilder; + pub struct ApiDoc; + +impl ApiDoc { + pub fn build() -> utoipa::openapi::OpenApi { + let info = InfoBuilder::new() + .title("Agama HTTP API") + .version("0.1.0") + .build(); + + let paths = PathsBuilder::new() + .path_from::() + .build(); + + let components = ComponentsBuilder::new() + .schema_from::() + .build(); + + let mut openapi = utoipa::openapi::OpenApiBuilder::new() + .info(info) + .paths(paths) + .components(Some(components)) + .build(); + + let l10n = L10nApiDocBuilder::build(); + let manager = ManagerApiDocBuilder::build(); + let network = NetworkApiDocBuilder::build(); + let questions = QuestionsApiDocBuilder::build(); + let software = SoftwareApiDocBuilder::build(); + let storage = StorageApiDocBuilder::build(); + let users = UsersApiDocBuilder::build(); + + openapi.merge(l10n); + openapi.merge(manager); + openapi.merge(network); + openapi.merge(questions); + openapi.merge(software); + openapi.merge(storage); + openapi.merge(users); + openapi + } +} diff --git a/rust/agama-server/src/web/docs/l10n.rs b/rust/agama-server/src/web/docs/l10n.rs new file mode 100644 index 000000000..99b58a445 --- /dev/null +++ b/rust/agama-server/src/web/docs/l10n.rs @@ -0,0 +1,29 @@ +use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; + +pub struct L10nApiDocBuilder; + +impl L10nApiDocBuilder { + pub fn build() -> OpenApi { + let paths = PathsBuilder::new() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .build(); + + let components = ComponentsBuilder::new() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .build(); + + OpenApiBuilder::new() + .paths(paths) + .components(Some(components)) + .build() + } +} diff --git a/rust/agama-server/src/web/docs/manager.rs b/rust/agama-server/src/web/docs/manager.rs new file mode 100644 index 000000000..2647dd8c7 --- /dev/null +++ b/rust/agama-server/src/web/docs/manager.rs @@ -0,0 +1,24 @@ +use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; + +pub struct ManagerApiDocBuilder; + +impl ManagerApiDocBuilder { + pub fn build() -> OpenApi { + let paths = PathsBuilder::new() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .build(); + + let components = ComponentsBuilder::new() + .schema_from::() + .schema_from::() + .build(); + + OpenApiBuilder::new() + .paths(paths) + .components(Some(components)) + .build() + } +} diff --git a/rust/agama-server/src/web/docs/network.rs b/rust/agama-server/src/web/docs/network.rs new file mode 100644 index 000000000..4195d00a5 --- /dev/null +++ b/rust/agama-server/src/web/docs/network.rs @@ -0,0 +1,91 @@ +use serde_json::json; +use utoipa::openapi::{ComponentsBuilder, ObjectBuilder, OpenApiBuilder, PathsBuilder}; + +pub struct NetworkApiDocBuilder; + +impl NetworkApiDocBuilder { + pub fn build() -> utoipa::openapi::OpenApi { + let paths = PathsBuilder::new() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .build(); + + let components = ComponentsBuilder::new() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema( + "IpAddr", + ObjectBuilder::new() + .schema_type(utoipa::openapi::SchemaType::String) + .description(Some("An IP address (IPv4 or IPv6)".to_string())) + .example(Some(json!("192.168.1.100"))) + .build(), + ) + .schema( + "IpInet", + ObjectBuilder::new() + .schema_type(utoipa::openapi::SchemaType::String) + .description(Some( + "An IP address (IPv4 or IPv6) including the prefix".to_string(), + )) + .example(Some(json!("192.168.1.254/24"))) + .build(), + ) + .schema( + "macaddr.MacAddr6", + ObjectBuilder::new() + .schema_type(utoipa::openapi::SchemaType::String) + .description(Some("MAC address in EUI-48 format".to_string())) + .build(), + ) + .build(); + + OpenApiBuilder::new() + .paths(paths) + .components(Some(components)) + .build() + } +} diff --git a/rust/agama-server/src/web/docs/questions.rs b/rust/agama-server/src/web/docs/questions.rs new file mode 100644 index 000000000..199e61155 --- /dev/null +++ b/rust/agama-server/src/web/docs/questions.rs @@ -0,0 +1,29 @@ +use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; + +pub struct QuestionsApiDocBuilder; + +impl QuestionsApiDocBuilder { + pub fn build() -> OpenApi { + let paths = PathsBuilder::new() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .build(); + + let components = ComponentsBuilder::new() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .build(); + + OpenApiBuilder::new() + .paths(paths) + .components(Some(components)) + .build() + } +} diff --git a/rust/agama-server/src/web/docs/software.rs b/rust/agama-server/src/web/docs/software.rs new file mode 100644 index 000000000..fe3c292d7 --- /dev/null +++ b/rust/agama-server/src/web/docs/software.rs @@ -0,0 +1,32 @@ +use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; + +pub struct SoftwareApiDocBuilder; + +impl SoftwareApiDocBuilder { + pub fn build() -> OpenApi { + let paths = PathsBuilder::new() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .build(); + + let components = ComponentsBuilder::new() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .build(); + + OpenApiBuilder::new() + .paths(paths) + .components(Some(components)) + .build() + } +} diff --git a/rust/agama-server/src/web/docs/storage.rs b/rust/agama-server/src/web/docs/storage.rs new file mode 100644 index 000000000..3d09a8b69 --- /dev/null +++ b/rust/agama-server/src/web/docs/storage.rs @@ -0,0 +1,72 @@ +use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; + +pub struct StorageApiDocBuilder; + +impl StorageApiDocBuilder { + pub fn build() -> OpenApi { + let paths = PathsBuilder::new() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .build(); + + let components = ComponentsBuilder::new() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .build(); + + OpenApiBuilder::new() + .paths(paths) + .components(Some(components)) + .build() + } +} diff --git a/rust/agama-server/src/web/docs/users.rs b/rust/agama-server/src/web/docs/users.rs new file mode 100644 index 000000000..da76dc3af --- /dev/null +++ b/rust/agama-server/src/web/docs/users.rs @@ -0,0 +1,32 @@ +use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; + +pub struct UsersApiDocBuilder; + +impl UsersApiDocBuilder { + pub fn build() -> OpenApi { + let paths = PathsBuilder::new() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .build(); + + let components = ComponentsBuilder::new() + .schema_from::() + .schema_from::() + .schema_from::() + .schema( + "zbus.zvariant.OwnedValue", + utoipa::openapi::ObjectBuilder::new() + .description(Some("Additional user information (unused)".to_string())) + .build(), + ) + .build(); + + OpenApiBuilder::new() + .paths(paths) + .components(Some(components)) + .build() + } +} From 9ec5db08fd5011e646e7d7f7baea3b8c1ce41058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 30 Aug 2024 12:42:00 +0100 Subject: [PATCH 3/7] fix(rust): do not crash when the "docs" command fails --- rust/agama-server/src/agama-web-server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index 2523063a0..341181e92 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -380,7 +380,7 @@ async fn serve_command(args: ServeArgs) -> anyhow::Result<()> { /// Display the API documentation in OpenAPI format. fn openapi_command() -> anyhow::Result<()> { - println!("{}", web::ApiDoc::build().to_pretty_json().unwrap()); + println!("{}", web::ApiDoc::build().to_pretty_json()?); Ok(()) } From 5f4a1602a615cff21ab3b0b30ebe5fd39c898685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 18 Oct 2024 13:51:02 +0100 Subject: [PATCH 4/7] feat(rust): add a xtask to generate the OpenAPI spec --- rust/Cargo.lock | 1 + rust/agama-lib/src/network/settings.rs | 2 +- rust/agama-server/src/agama-web-server.rs | 9 ---- rust/agama-server/src/network/model.rs | 6 +-- rust/agama-server/src/web.rs | 3 +- rust/agama-server/src/web/docs.rs | 51 +++++++-------------- rust/agama-server/src/web/docs/l10n.rs | 25 +++++----- rust/agama-server/src/web/docs/manager.rs | 25 +++++----- rust/agama-server/src/web/docs/misc.rs | 23 ++++++++++ rust/agama-server/src/web/docs/network.rs | 34 +++++++++----- rust/agama-server/src/web/docs/questions.rs | 24 +++++----- rust/agama-server/src/web/docs/software.rs | 25 +++++----- rust/agama-server/src/web/docs/storage.rs | 25 +++++----- rust/agama-server/src/web/docs/users.rs | 25 +++++----- rust/xtask/Cargo.toml | 1 + rust/xtask/src/main.rs | 43 +++++++++++++++-- 16 files changed, 191 insertions(+), 131 deletions(-) create mode 100644 rust/agama-server/src/web/docs/misc.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 08dc92750..c97431a08 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -4886,6 +4886,7 @@ name = "xtask" version = "0.1.0" dependencies = [ "agama-cli", + "agama-server", "clap", "clap-markdown", "clap_complete", diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index 724b552f0..dbce91e11 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -114,7 +114,7 @@ impl Default for BondSettings { } /// IEEE 802.1x (EAP) settings -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct IEEE8021XSettings { /// List of EAP methods used diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index 341181e92..b3ff78289 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -57,8 +57,6 @@ enum Commands { /// This command starts the server in the given ports. The secondary port, if enabled, uses SSL. /// If no certificate is specified, agama-web-server generates a self-signed one. Serve(ServeArgs), - /// Generates the API documentation in OpenAPI format. - Openapi, } /// Manage Agama's HTTP/JSON API. @@ -378,16 +376,9 @@ async fn serve_command(args: ServeArgs) -> anyhow::Result<()> { Ok(()) } -/// Display the API documentation in OpenAPI format. -fn openapi_command() -> anyhow::Result<()> { - println!("{}", web::ApiDoc::build().to_pretty_json()?); - Ok(()) -} - async fn run_command(cli: Cli) -> anyhow::Result<()> { match cli.command { Commands::Serve(options) => serve_command(options).await, - Commands::Openapi => openapi_command(), } } diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-server/src/network/model.rs index f2ec57c84..73f85dd63 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -1569,7 +1569,7 @@ pub enum NetworkChange { DeviceUpdated(String, Device), } -#[derive(Default, Debug, PartialEq, Clone, Serialize)] +#[derive(Default, Debug, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub struct IEEE8021XConfig { pub eap: Vec, pub phase2_auth: Option, @@ -1658,7 +1658,7 @@ impl TryFrom for IEEE8021XSettings { #[error("Invalid eap method: {0}")] pub struct InvalidEAPMethod(String); -#[derive(Debug, PartialEq, Clone, Serialize)] +#[derive(Debug, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub enum EAPMethod { LEAP, MD5, @@ -1705,7 +1705,7 @@ impl fmt::Display for EAPMethod { #[error("Invalid phase2-auth method: {0}")] pub struct InvalidPhase2AuthMethod(String); -#[derive(Debug, PartialEq, Clone, Serialize)] +#[derive(Debug, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub enum Phase2AuthMethod { PAP, CHAP, diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 7f77b8dfd..2a7ad06e7 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -41,7 +41,7 @@ use axum::Router; mod auth; pub mod common; mod config; -mod docs; +pub mod docs; mod event; mod http; mod service; @@ -50,7 +50,6 @@ mod ws; use agama_lib::{connection, error::ServiceError}; pub use config::ServiceConfig; -pub use docs::ApiDoc; pub use event::{Event, EventsReceiver, EventsSender}; pub use service::MainServiceBuilder; use std::path::Path; diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index b04f717da..ce9b8d240 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -18,7 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use utoipa::openapi::{ComponentsBuilder, InfoBuilder, PathsBuilder}; +use utoipa::openapi::{Components, InfoBuilder, OpenApiBuilder, Paths}; mod network; pub use network::NetworkApiDocBuilder; @@ -34,45 +34,28 @@ mod manager; pub use manager::ManagerApiDocBuilder; mod users; pub use users::UsersApiDocBuilder; +mod misc; +pub use misc::MiscApiDocBuilder; -pub struct ApiDoc; +pub trait ApiDocBuilder { + fn title(&self) -> String { + "Agama HTTP API".to_string() + } -impl ApiDoc { - pub fn build() -> utoipa::openapi::OpenApi { - let info = InfoBuilder::new() - .title("Agama HTTP API") - .version("0.1.0") - .build(); + fn paths(&self) -> Paths; - let paths = PathsBuilder::new() - .path_from::() - .build(); + fn components(&self) -> Components; - let components = ComponentsBuilder::new() - .schema_from::() + fn build(&self) -> utoipa::openapi::OpenApi { + let info = InfoBuilder::new() + .title(self.title()) + .version("0.1.0") .build(); - let mut openapi = utoipa::openapi::OpenApiBuilder::new() + OpenApiBuilder::new() .info(info) - .paths(paths) - .components(Some(components)) - .build(); - - let l10n = L10nApiDocBuilder::build(); - let manager = ManagerApiDocBuilder::build(); - let network = NetworkApiDocBuilder::build(); - let questions = QuestionsApiDocBuilder::build(); - let software = SoftwareApiDocBuilder::build(); - let storage = StorageApiDocBuilder::build(); - let users = UsersApiDocBuilder::build(); - - openapi.merge(l10n); - openapi.merge(manager); - openapi.merge(network); - openapi.merge(questions); - openapi.merge(software); - openapi.merge(storage); - openapi.merge(users); - openapi + .paths(self.paths()) + .components(Some(self.components())) + .build() } } diff --git a/rust/agama-server/src/web/docs/l10n.rs b/rust/agama-server/src/web/docs/l10n.rs index 99b58a445..a6a17ead4 100644 --- a/rust/agama-server/src/web/docs/l10n.rs +++ b/rust/agama-server/src/web/docs/l10n.rs @@ -1,29 +1,32 @@ -use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; +use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; + +use super::ApiDocBuilder; pub struct L10nApiDocBuilder; -impl L10nApiDocBuilder { - pub fn build() -> OpenApi { - let paths = PathsBuilder::new() +impl ApiDocBuilder for L10nApiDocBuilder { + fn title(&self) -> String { + "Localization HTTP API".to_string() + } + + fn paths(&self) -> Paths { + PathsBuilder::new() .path_from::() .path_from::() .path_from::() .path_from::() .path_from::() - .build(); + .build() + } - let components = ComponentsBuilder::new() + fn components(&self) -> Components { + ComponentsBuilder::new() .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() - .build(); - - OpenApiBuilder::new() - .paths(paths) - .components(Some(components)) .build() } } diff --git a/rust/agama-server/src/web/docs/manager.rs b/rust/agama-server/src/web/docs/manager.rs index 2647dd8c7..7df91f280 100644 --- a/rust/agama-server/src/web/docs/manager.rs +++ b/rust/agama-server/src/web/docs/manager.rs @@ -1,24 +1,27 @@ -use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; +use utoipa::openapi::{ComponentsBuilder, PathsBuilder}; + +use super::ApiDocBuilder; pub struct ManagerApiDocBuilder; -impl ManagerApiDocBuilder { - pub fn build() -> OpenApi { - let paths = PathsBuilder::new() +impl ApiDocBuilder for ManagerApiDocBuilder { + fn title(&self) -> String { + "Manager HTTP API".to_string() + } + + fn paths(&self) -> utoipa::openapi::Paths { + PathsBuilder::new() .path_from::() .path_from::() .path_from::() .path_from::() - .build(); + .build() + } - let components = ComponentsBuilder::new() + fn components(&self) -> utoipa::openapi::Components { + ComponentsBuilder::new() .schema_from::() .schema_from::() - .build(); - - OpenApiBuilder::new() - .paths(paths) - .components(Some(components)) .build() } } diff --git a/rust/agama-server/src/web/docs/misc.rs b/rust/agama-server/src/web/docs/misc.rs new file mode 100644 index 000000000..f788dccfe --- /dev/null +++ b/rust/agama-server/src/web/docs/misc.rs @@ -0,0 +1,23 @@ +use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; + +use super::ApiDocBuilder; + +pub struct MiscApiDocBuilder; + +impl ApiDocBuilder for MiscApiDocBuilder { + fn title(&self) -> String { + "Miscelaneous HTTP API".to_string() + } + + fn paths(&self) -> Paths { + PathsBuilder::new() + .path_from::() + .build() + } + + fn components(&self) -> Components { + ComponentsBuilder::new() + .schema_from::() + .build() + } +} diff --git a/rust/agama-server/src/web/docs/network.rs b/rust/agama-server/src/web/docs/network.rs index 4195d00a5..23ad3de3c 100644 --- a/rust/agama-server/src/web/docs/network.rs +++ b/rust/agama-server/src/web/docs/network.rs @@ -1,11 +1,17 @@ use serde_json::json; -use utoipa::openapi::{ComponentsBuilder, ObjectBuilder, OpenApiBuilder, PathsBuilder}; +use utoipa::openapi::{Components, ComponentsBuilder, ObjectBuilder, Paths, PathsBuilder}; + +use super::ApiDocBuilder; pub struct NetworkApiDocBuilder; -impl NetworkApiDocBuilder { - pub fn build() -> utoipa::openapi::OpenApi { - let paths = PathsBuilder::new() +impl ApiDocBuilder for NetworkApiDocBuilder { + fn title(&self) -> String { + "Network HTTP API".to_string() + } + + fn paths(&self) -> Paths { + PathsBuilder::new() .path_from::() .path_from::() .path_from::() @@ -15,10 +21,13 @@ impl NetworkApiDocBuilder { .path_from::() .path_from::() .path_from::() - .build(); + .build() + } - let components = ComponentsBuilder::new() + fn components(&self) -> Components { + ComponentsBuilder::new() .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() @@ -36,6 +45,9 @@ impl NetworkApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() @@ -44,15 +56,18 @@ impl NetworkApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() - .schema_from::() .schema_from::() .schema_from::() + .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() @@ -81,11 +96,6 @@ impl NetworkApiDocBuilder { .description(Some("MAC address in EUI-48 format".to_string())) .build(), ) - .build(); - - OpenApiBuilder::new() - .paths(paths) - .components(Some(components)) .build() } } diff --git a/rust/agama-server/src/web/docs/questions.rs b/rust/agama-server/src/web/docs/questions.rs index 199e61155..b69d30b58 100644 --- a/rust/agama-server/src/web/docs/questions.rs +++ b/rust/agama-server/src/web/docs/questions.rs @@ -1,29 +1,31 @@ -use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; +use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; + +use super::ApiDocBuilder; pub struct QuestionsApiDocBuilder; -impl QuestionsApiDocBuilder { - pub fn build() -> OpenApi { - let paths = PathsBuilder::new() +impl ApiDocBuilder for QuestionsApiDocBuilder { + fn title(&self) -> String { + "Questions HTTP API".to_string() + } + fn paths(&self) -> Paths { + PathsBuilder::new() .path_from::() .path_from::() .path_from::() .path_from::() .path_from::() - .build(); + .build() + } - let components = ComponentsBuilder::new() + fn components(&self) -> Components { + ComponentsBuilder::new() .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() - .build(); - - OpenApiBuilder::new() - .paths(paths) - .components(Some(components)) .build() } } diff --git a/rust/agama-server/src/web/docs/software.rs b/rust/agama-server/src/web/docs/software.rs index fe3c292d7..2fe9da279 100644 --- a/rust/agama-server/src/web/docs/software.rs +++ b/rust/agama-server/src/web/docs/software.rs @@ -1,19 +1,27 @@ -use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; +use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; + +use super::ApiDocBuilder; pub struct SoftwareApiDocBuilder; -impl SoftwareApiDocBuilder { - pub fn build() -> OpenApi { - let paths = PathsBuilder::new() +impl ApiDocBuilder for SoftwareApiDocBuilder { + fn title(&self) -> String { + "Software HTTP API".to_string() + } + + fn paths(&self) -> Paths { + PathsBuilder::new() .path_from::() .path_from::() .path_from::() .path_from::() .path_from::() .path_from::() - .build(); + .build() + } - let components = ComponentsBuilder::new() + fn components(&self) -> Components { + ComponentsBuilder::new() .schema_from::() .schema_from::() .schema_from::() @@ -22,11 +30,6 @@ impl SoftwareApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() - .build(); - - OpenApiBuilder::new() - .paths(paths) - .components(Some(components)) .build() } } diff --git a/rust/agama-server/src/web/docs/storage.rs b/rust/agama-server/src/web/docs/storage.rs index 3d09a8b69..0d492c405 100644 --- a/rust/agama-server/src/web/docs/storage.rs +++ b/rust/agama-server/src/web/docs/storage.rs @@ -1,10 +1,16 @@ -use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; +use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; + +use super::ApiDocBuilder; pub struct StorageApiDocBuilder; -impl StorageApiDocBuilder { - pub fn build() -> OpenApi { - let paths = PathsBuilder::new() +impl ApiDocBuilder for StorageApiDocBuilder { + fn title(&self) -> String { + "Storage HTTP API".to_string() + } + + fn paths(&self) -> Paths { + PathsBuilder::new() .path_from::() .path_from::() .path_from::() @@ -23,9 +29,11 @@ impl StorageApiDocBuilder { .path_from::() .path_from::() .path_from::() - .build(); + .build() + } - let components = ComponentsBuilder::new() + fn components(&self) -> Components { + ComponentsBuilder::new() .schema_from::() .schema_from::() .schema_from::() @@ -62,11 +70,6 @@ impl StorageApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() - .build(); - - OpenApiBuilder::new() - .paths(paths) - .components(Some(components)) .build() } } diff --git a/rust/agama-server/src/web/docs/users.rs b/rust/agama-server/src/web/docs/users.rs index da76dc3af..5826334f5 100644 --- a/rust/agama-server/src/web/docs/users.rs +++ b/rust/agama-server/src/web/docs/users.rs @@ -1,18 +1,26 @@ -use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; +use utoipa::openapi::{ComponentsBuilder, Paths, PathsBuilder}; + +use super::ApiDocBuilder; pub struct UsersApiDocBuilder; -impl UsersApiDocBuilder { - pub fn build() -> OpenApi { - let paths = PathsBuilder::new() +impl ApiDocBuilder for UsersApiDocBuilder { + fn title(&self) -> String { + "Users HTTP API".to_string() + } + + fn paths(&self) -> Paths { + PathsBuilder::new() .path_from::() .path_from::() .path_from::() .path_from::() .path_from::() - .build(); + .build() + } - let components = ComponentsBuilder::new() + fn components(&self) -> utoipa::openapi::Components { + ComponentsBuilder::new() .schema_from::() .schema_from::() .schema_from::() @@ -22,11 +30,6 @@ impl UsersApiDocBuilder { .description(Some("Additional user information (unused)".to_string())) .build(), ) - .build(); - - OpenApiBuilder::new() - .paths(paths) - .components(Some(components)) .build() } } diff --git a/rust/xtask/Cargo.toml b/rust/xtask/Cargo.toml index ae9740772..e32411ebd 100644 --- a/rust/xtask/Cargo.toml +++ b/rust/xtask/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] agama-cli = { path = "../agama-cli" } +agama-server = { path = "../agama-server" } clap = { version = "4.5.19", default-features = false } clap-markdown = "0.1.4" clap_complete = "4.5.32" diff --git a/rust/xtask/src/main.rs b/rust/xtask/src/main.rs index ac8f09dfa..b7077130d 100644 --- a/rust/xtask/src/main.rs +++ b/rust/xtask/src/main.rs @@ -1,9 +1,14 @@ use std::{env, path::PathBuf}; mod tasks { - use std::{fs::File, io::Write}; + use std::{fs::File, io::Write, path::Path}; use agama_cli::Cli; + use agama_server::web::docs::{ + ApiDocBuilder, L10nApiDocBuilder, ManagerApiDocBuilder, MiscApiDocBuilder, + NetworkApiDocBuilder, QuestionsApiDocBuilder, SoftwareApiDocBuilder, StorageApiDocBuilder, + UsersApiDocBuilder, + }; use clap::CommandFactory; use clap_complete::aot; use clap_markdown::MarkdownOptions; @@ -19,7 +24,7 @@ mod tasks { clap_complete::generate_to(aot::Fish, &mut cmd, "agama", &out_dir)?; clap_complete::generate_to(aot::Zsh, &mut cmd, "agama", &out_dir)?; - println!("Generate shell completions at {}", out_dir.display()); + println!("Generate shell completions at {}.", out_dir.display()); Ok(()) } @@ -36,7 +41,7 @@ mod tasks { let mut file = File::create(&filename)?; file.write_all(markdown.as_bytes())?; - println!("Generate Markdown documentation at {}", filename.display()); + println!("Generate Markdown documentation at {}.", filename.display()); Ok(()) } @@ -47,7 +52,36 @@ mod tasks { let cmd = Cli::command(); clap_mangen::generate_to(cmd, &out_dir)?; - println!("Generate manpages documentation at {}", out_dir.display()); + println!("Generate manpages documentation at {}.", out_dir.display()); + Ok(()) + } + + /// Generate Agama's OpenAPI specification. + pub fn generate_openapi() -> std::io::Result<()> { + let out_dir = create_output_dir("openapi")?; + + write_openapi(L10nApiDocBuilder {}, out_dir.join("l10n.json"))?; + write_openapi(ManagerApiDocBuilder {}, out_dir.join("manager.json"))?; + write_openapi(NetworkApiDocBuilder {}, out_dir.join("network.json"))?; + write_openapi(SoftwareApiDocBuilder {}, out_dir.join("software.json"))?; + write_openapi(StorageApiDocBuilder {}, out_dir.join("storage.json"))?; + write_openapi(UsersApiDocBuilder {}, out_dir.join("users.json"))?; + write_openapi(QuestionsApiDocBuilder {}, out_dir.join("questions.json"))?; + write_openapi(MiscApiDocBuilder {}, out_dir.join("misc.json"))?; + println!( + "Generate the OpenAPI specification at {}.", + out_dir.display() + ); + Ok(()) + } + + fn write_openapi>(builder: T, path: P) -> std::io::Result<()> + where + T: ApiDocBuilder, + { + let openapi = builder.build().to_pretty_json()?; + let mut file = File::create(path)?; + file.write_all(openapi.as_bytes())?; Ok(()) } } @@ -71,6 +105,7 @@ fn main() -> std::io::Result<()> { "completions" => tasks::generate_completions(), "markdown" => tasks::generate_markdown(), "manpages" => tasks::generate_manpages(), + "openapi" => tasks::generate_openapi(), other => { eprintln!("Unknown task '{}'", other); std::process::exit(1); From 1aba851e364e0f1115f6af508127d18b7ac94157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 18 Oct 2024 17:05:32 +0100 Subject: [PATCH 5/7] feat(rust): package the OpenAPI specification --- rust/package/agama.spec | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/rust/package/agama.spec b/rust/package/agama.spec index b260d3477..5c6655c47 100644 --- a/rust/package/agama.spec +++ b/rust/package/agama.spec @@ -107,6 +107,14 @@ BuildArch: noarch %description -n agama-cli-zsh-completion Zsh command-line completion support for %{name}-cli. +%package -n agama-openapi +Summary: Agama's OpenAPI Specification + +%description -n agama-openapi +The OpenAPI Specification (OAS) allows describing an HTTP API in an standard and +language-agnostic way. This package contains the specification for Agama's HTTP +API. + %prep %autosetup -a1 -n agama # Remove exec bits to prevent an issue in fedora shebang checking. Uncomment only if required. @@ -117,6 +125,7 @@ Zsh command-line completion support for %{name}-cli. cargo run --package xtask -- manpages gzip out/man/* cargo run --package xtask -- completions +cargo run --package xtask -- openapi %install install -D -d -m 0755 %{buildroot}%{_bindir} @@ -140,6 +149,10 @@ install -Dm644 %{_builddir}/agama/out/shell/%{name}.bash %{buildroot}%{_datadir} install -Dm644 %{_builddir}/agama/out/shell/_%{name} %{buildroot}%{_datadir}/zsh/site-functions/_%{name} install -Dm644 %{_builddir}/agama/out/shell/%{name}.fish %{buildroot}%{_datadir}/fish/vendor_completions.d/%{name}.fish +# install OpenAPI specification +mkdir -p %{buildroot}%{_datadir}/agama/openapi +install -m 0644 %{_builddir}/agama/out/openapi/* %{buildroot}%{_datadir}/agama/openapi + %check PATH=$PWD/share/bin:$PATH %ifarch aarch64 @@ -188,4 +201,9 @@ echo $PATH %dir %{_datadir}/zsh %{_datadir}/zsh/* +%files -n agama-openapi +%dir %{_datadir}/agama +%dir %{_datadir}/agama/openapi +%{_datadir}/agama/openapi/*.json + %changelog From a8884563a718cd6861a97e9e4aaa0b31c3f116f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 22 Oct 2024 09:55:37 +0100 Subject: [PATCH 6/7] ci: check the OpenAPI specification --- .github/workflows/ci-rust.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml index 45a8efe23..8b053d623 100644 --- a/.github/workflows/ci-rust.yml +++ b/.github/workflows/ci-rust.yml @@ -76,6 +76,7 @@ jobs: openssl-3 pam-devel python-langtable-data + python3-openapi_spec_validator timezone xkeyboard-config @@ -101,6 +102,11 @@ jobs: - name: Run the tests run: cargo tarpaulin --out xml -- --nocapture + - name: Generate and validate the OpenAPI specification + run: | + cargo xtask openapi + openapi-spec-validator out/openapi/* + # send the code coverage for the Rust part to the coveralls.io - name: Coveralls GitHub Action uses: coverallsapp/github-action@v2 From 780ba9332e0ee8289634ca906ace04f58ff08364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 22 Oct 2024 10:49:38 +0100 Subject: [PATCH 7/7] docs(rust): update changes file --- rust/package/agama.changes | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 9c73676ac..5e6e29a1b 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Tue Oct 22 09:46:41 UTC 2024 - Imobach Gonzalez Sosa + +- Improve OpenAPI specification generation (gh#agama-project/agama#1564): + - Add a lot of missing elements to make the specification valid. + - Use a xtask to generate the OpenAPI specification at build time. + - Ship the specification in a separate package (agama-openapi). + ------------------------------------------------------------------- Wed Oct 16 15:07:33 UTC 2024 - Imobach Gonzalez Sosa