diff --git a/rust/Cargo.lock b/rust/Cargo.lock index fea5c0dc87..854f01e033 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -3953,6 +3953,7 @@ dependencies = [ "quote", "regex", "syn 2.0.53", + "uuid", ] [[package]] diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index 0495782e13..10b0c36c10 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -44,7 +44,7 @@ pub struct WirelessSettings { pub mode: String, } -#[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 d2852cdd96..996ecb031b 100644 --- a/rust/agama-lib/src/network/types.rs +++ b/rust/agama-lib/src/network/types.rs @@ -163,7 +163,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 d7f8757141..17cb8f96fe 100644 --- a/rust/agama-lib/src/software/client.rs +++ b/rust/agama-lib/src/software/client.rs @@ -23,7 +23,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 52386a3005..4ad09d4768 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -33,7 +33,7 @@ tracing-journald = "0.3.0" tracing = "0.1.40" clap = { version = "4.5.0", features = ["derive", "wrap_help"] } tower = "0.4.13" -utoipa = { version = "4.2.0", features = ["axum_extras"] } +utoipa = { version = "4.2.0", features = ["axum_extras", "uuid"] } config = "0.14.0" rand = "0.8.5" jsonwebtoken = "9.2.0" diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index 607a3dd572..fd11d2bf28 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -26,7 +26,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"; diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-server/src/network/model.rs index 8e90c34582..2b2f05fa9b 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -782,7 +782,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] @@ -818,7 +818,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] @@ -866,7 +866,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, @@ -997,7 +997,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, @@ -1081,7 +1081,7 @@ impl TryFrom<&str> for SecurityProtocol { } } -#[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, @@ -1090,7 +1090,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, @@ -1111,7 +1111,7 @@ impl TryFrom for WEPKeyType { } } -#[derive(Debug, Default, PartialEq, Clone, Serialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub enum WEPAuthAlg { #[default] Unset, @@ -1146,7 +1146,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 @@ -1174,7 +1174,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 { @@ -1266,7 +1266,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, @@ -1281,7 +1281,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, @@ -1314,7 +1314,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 5f298f4360..e4db3bc0dd 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -464,7 +464,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 4f0de7fab4..60fc4d5afb 100644 --- a/rust/agama-server/src/storage/web.rs +++ b/rust/agama-server/src/storage/web.rs @@ -104,7 +104,8 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result>) -> 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 1623602445..df708503fd 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -1,137 +1,58 @@ -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, - 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::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::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(crate::l10n::web::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(crate::questions::web::Answer), - schemas(crate::questions::web::GenericAnswer), - schemas(crate::questions::web::GenericQuestion), - schemas(crate::questions::web::PasswordAnswer), - schemas(crate::questions::web::Question), - schemas(crate::questions::web::QuestionWithPassword), - schemas(crate::software::web::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(crate::users::web::RootConfig), - schemas(crate::users::web::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 0000000000..4a9fb68226 --- /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 0000000000..2647dd8c72 --- /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 0000000000..4195d00a57 --- /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 0000000000..a556c8a197 --- /dev/null +++ b/rust/agama-server/src/web/docs/questions.rs @@ -0,0 +1,26 @@ +use utoipa::openapi::{ComponentsBuilder, OpenApi, OpenApiBuilder, PathsBuilder}; + +pub struct QuestionsApiDocBuilder; + +impl QuestionsApiDocBuilder { + pub fn build() -> OpenApi { + let paths = PathsBuilder::new() + .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 0000000000..a49c3309f5 --- /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 0000000000..2ba842774f --- /dev/null +++ b/rust/agama-server/src/web/docs/storage.rs @@ -0,0 +1,71 @@ +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::() + .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 0000000000..be3a12f98b --- /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() + } +}