diff --git a/rust/agama-lib/src/progress.rs b/rust/agama-lib/src/progress.rs index 8def7b25d3..2b43468a8d 100644 --- a/rust/agama-lib/src/progress.rs +++ b/rust/agama-lib/src/progress.rs @@ -73,7 +73,7 @@ use tokio_stream::{StreamExt, StreamMap}; use zbus::{proxy::PropertyStream, Connection}; /// Represents the progress for an Agama service. -#[derive(Clone, Default, Debug, Serialize)] +#[derive(Clone, Default, Debug, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct Progress { /// Current step diff --git a/rust/agama-server/src/web/common.rs b/rust/agama-server/src/web/common.rs index ebb2536399..332a798a48 100644 --- a/rust/agama-server/src/web/common.rs +++ b/rust/agama-server/src/web/common.rs @@ -94,9 +94,9 @@ struct ServiceStatusState<'a> { proxy: ServiceStatusProxy<'a>, } -#[derive(Clone, Serialize)] -struct ServiceStatus { - /// Current service status. +#[derive(Clone, Serialize, utoipa::ToSchema)] +pub struct ServiceStatus { + /// Current service status (0 = idle, 1 = busy). current: u32, } @@ -189,7 +189,7 @@ struct ProgressState<'a> { } /// Information about the current progress sequence. -#[derive(Clone, Default, Serialize)] +#[derive(Clone, Default, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct ProgressSequence { /// Sequence steps if known in advance @@ -319,7 +319,7 @@ struct IssuesState<'a> { proxy: IssuesProxy<'a>, } -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, utoipa::ToSchema)] pub struct Issue { description: String, details: Option, diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index ce9b8d240e..65eae4f79a 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::{Components, InfoBuilder, OpenApiBuilder, Paths}; +use utoipa::openapi::{Components, Info, InfoBuilder, OpenApi, OpenApiBuilder, Paths}; mod network; pub use network::NetworkApiDocBuilder; @@ -36,6 +36,7 @@ mod users; pub use users::UsersApiDocBuilder; mod misc; pub use misc::MiscApiDocBuilder; +pub mod common; pub trait ApiDocBuilder { fn title(&self) -> String { @@ -46,16 +47,27 @@ pub trait ApiDocBuilder { fn components(&self) -> Components; - fn build(&self) -> utoipa::openapi::OpenApi { - let info = InfoBuilder::new() + fn info(&self) -> Info { + InfoBuilder::new() .title(self.title()) .version("0.1.0") - .build(); + .build() + } + + fn nested(&self) -> Option { + None + } - OpenApiBuilder::new() - .info(info) + fn build(&self) -> utoipa::openapi::OpenApi { + let mut api = OpenApiBuilder::new() + .info(self.info()) .paths(self.paths()) .components(Some(self.components())) - .build() + .build(); + + if let Some(nested) = self.nested() { + api.merge(nested); + } + api } } diff --git a/rust/agama-server/src/web/docs/common.rs b/rust/agama-server/src/web/docs/common.rs new file mode 100644 index 0000000000..7deeb64166 --- /dev/null +++ b/rust/agama-server/src/web/docs/common.rs @@ -0,0 +1,207 @@ +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + +//! This module implements builders for the generation of OpenAPI documentation for the common APIs +//! (e.g., issues, service status or progress). + +use super::ApiDocBuilder; +use crate::web::common::{Issue, ServiceStatus}; +use agama_lib::progress::Progress; +use utoipa::openapi::{ + path::OperationBuilder, schema::RefBuilder, ArrayBuilder, Components, ComponentsBuilder, + ContentBuilder, HttpMethod, PathItem, Paths, PathsBuilder, ResponseBuilder, ResponsesBuilder, +}; + +/// Implements a builder for the issues API documentation. +pub struct IssuesApiDocBuilder { + paths: Vec<(String, PathItem)>, +} + +impl IssuesApiDocBuilder { + pub fn new() -> Self { + Self { paths: vec![] } + } + + /// Adds a new issues API path. + /// + /// * `path`: path of the API. + /// * `summary`: summary to be included in the OpenAPI documentation. + /// * `operation_id`: operation ID of the API. + pub fn add(self, path: &str, summary: &str, operation_id: &str) -> Self { + let mut paths = self.paths; + paths.push((path.to_string(), Self::issues_path(summary, operation_id))); + Self { paths, ..self } + } + + fn issues_path(summary: &'_ str, operation_id: &'_ str) -> PathItem { + PathItem::new( + HttpMethod::Get, + OperationBuilder::new() + .summary(Some(summary)) + .operation_id(Some(operation_id)) + .responses( + ResponsesBuilder::new().response( + "200", + ResponseBuilder::new() + .description("List of found issues") + .content( + "application/json", + ContentBuilder::new() + .schema(Some( + ArrayBuilder::new().items(RefBuilder::new().ref_location( + "#/components/schemas/Issue".to_string(), + )), + )) + .build(), + ), + ), + ), + ) + } +} + +impl ApiDocBuilder for IssuesApiDocBuilder { + fn title(&self) -> String { + "Issues HTTP API".to_string() + } + + fn paths(&self) -> Paths { + let mut paths_builder = PathsBuilder::new(); + for (path, item) in self.paths.iter() { + paths_builder = paths_builder.path(path, item.clone()); + } + paths_builder.build() + } + + fn components(&self) -> Components { + ComponentsBuilder::new().schema_from::().build() + } +} + +/// Implements a builder for the service status API documentation. +pub struct ServiceStatusApiDocBuilder { + path: String, +} + +impl ServiceStatusApiDocBuilder { + /// Creates a new builder. + /// + /// * `path`: path of the API (e.g., "/api/storage/status"). + pub fn new(path: &str) -> Self { + Self { + path: path.to_string(), + } + } +} + +impl ApiDocBuilder for ServiceStatusApiDocBuilder { + fn title(&self) -> String { + "Services status HTTP API".to_string() + } + + fn paths(&self) -> Paths { + let path_item = PathItem::new( + HttpMethod::Get, + OperationBuilder::new() + .summary(Some("Service status".to_string())) + .operation_id(Some("status")) + .responses( + ResponsesBuilder::new().response( + "200", + ResponseBuilder::new() + .description("Current service status") + .content( + "application/json", + ContentBuilder::new() + .schema(Some(RefBuilder::new().ref_location( + "#/components/schemas/ServiceStatus".to_string(), + ))) + .build(), + ), + ), + ), + ); + + PathsBuilder::new() + .path(self.path.to_string(), path_item) + .build() + } + + fn components(&self) -> Components { + ComponentsBuilder::new() + .schema_from::() + .build() + } +} + +/// Implements a builder for the progress API documentation. +pub struct ProgressApiDocBuilder { + path: String, +} + +impl ProgressApiDocBuilder { + /// Creates a new builder. + /// + /// * `path`: path of the API (e.g., "/api/storage/progress"). + pub fn new(path: &str) -> Self { + Self { + path: path.to_string(), + } + } +} + +impl ApiDocBuilder for ProgressApiDocBuilder { + fn title(&self) -> String { + "Progress HTTP API".to_string() + } + + fn paths(&self) -> Paths { + let path_item = + PathItem::new( + HttpMethod::Get, + OperationBuilder::new() + .summary(Some("Service progress".to_string())) + .operation_id(Some("progress")) + .responses( + ResponsesBuilder::new().response( + "200", + ResponseBuilder::new() + .description("Current operation progress") + .content( + "application/json", + ContentBuilder::new() + .schema(Some(RefBuilder::new().ref_location( + "#/components/schemas/Progress".to_string(), + ))) + .build(), + ), + ), + ), + ); + + PathsBuilder::new() + .path(self.path.to_string(), path_item) + .build() + } + + fn components(&self) -> Components { + ComponentsBuilder::new().schema_from::().build() + } +} diff --git a/rust/agama-server/src/web/docs/issues.rs b/rust/agama-server/src/web/docs/issues.rs deleted file mode 100644 index 201aeea35b..0000000000 --- a/rust/agama-server/src/web/docs/issues.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) [2024] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// 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::{Components, ComponentsBuilder, Paths, PathsBuilder}; - -use super::ApiDocBuilder; - -pub struct IssuesApiDocBuilder; - -impl ApiDocBuilder for IssuesApiDocBuilder { - fn title(&self) -> String { - "Issues 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/manager.rs b/rust/agama-server/src/web/docs/manager.rs index 57e1ae5c1e..203ec8ac1a 100644 --- a/rust/agama-server/src/web/docs/manager.rs +++ b/rust/agama-server/src/web/docs/manager.rs @@ -18,9 +18,12 @@ // 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, PathsBuilder}; +use utoipa::openapi::{ComponentsBuilder, OpenApi, PathsBuilder}; -use super::ApiDocBuilder; +use super::{ + common::{ProgressApiDocBuilder, ServiceStatusApiDocBuilder}, + ApiDocBuilder, +}; pub struct ManagerApiDocBuilder; @@ -47,4 +50,11 @@ impl ApiDocBuilder for ManagerApiDocBuilder { .schema_from::() .build() } + + fn nested(&self) -> Option { + let mut status = ServiceStatusApiDocBuilder::new("/api/storage/status").build(); + let progress = ProgressApiDocBuilder::new("/api/storage/progress").build(); + status.merge(progress); + Some(status) + } } diff --git a/rust/agama-server/src/web/docs/software.rs b/rust/agama-server/src/web/docs/software.rs index 2b52a31c2e..0a7ea0cb44 100644 --- a/rust/agama-server/src/web/docs/software.rs +++ b/rust/agama-server/src/web/docs/software.rs @@ -18,9 +18,12 @@ // 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::{Components, ComponentsBuilder, Paths, PathsBuilder}; +use utoipa::openapi::{Components, ComponentsBuilder, OpenApi, Paths, PathsBuilder}; -use super::ApiDocBuilder; +use super::{ + common::{IssuesApiDocBuilder, ServiceStatusApiDocBuilder}, + ApiDocBuilder, +}; pub struct SoftwareApiDocBuilder; @@ -37,6 +40,14 @@ impl ApiDocBuilder for SoftwareApiDocBuilder { .path_from::() .path_from::() .path_from::() + // .path( + // "/api/software/issues/software", + // issues_path("List of software-related issues", "software_issues"), + // ) + // .path( + // "/api/software/issues/product", + // issues_path("List of product-related issues", "product_issues"), + // ) .build() } @@ -50,6 +61,25 @@ impl ApiDocBuilder for SoftwareApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() .build() } + + fn nested(&self) -> Option { + let mut issues = IssuesApiDocBuilder::new() + .add( + "/api/software/issues/software", + "List of software-related issues", + "software_issues", + ) + .add( + "/api/product/issues/product", + "List of product-related issues", + "product_issues", + ) + .build(); + let status = ServiceStatusApiDocBuilder::new("/api/storage/status").build(); + issues.merge(status); + Some(issues) + } } diff --git a/rust/agama-server/src/web/docs/storage.rs b/rust/agama-server/src/web/docs/storage.rs index 441eafa945..9e0159fedc 100644 --- a/rust/agama-server/src/web/docs/storage.rs +++ b/rust/agama-server/src/web/docs/storage.rs @@ -18,9 +18,12 @@ // 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::{Components, ComponentsBuilder, Paths, PathsBuilder}; +use utoipa::openapi::{Components, ComponentsBuilder, OpenApi, Paths, PathsBuilder}; -use super::ApiDocBuilder; +use super::{ + common::{IssuesApiDocBuilder, ProgressApiDocBuilder, ServiceStatusApiDocBuilder}, + ApiDocBuilder, +}; pub struct StorageApiDocBuilder; @@ -72,12 +75,10 @@ impl ApiDocBuilder for StorageApiDocBuilder { fn components(&self) -> 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::() @@ -108,10 +109,28 @@ impl ApiDocBuilder for StorageApiDocBuilder { .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() } + + fn nested(&self) -> Option { + let mut issues = IssuesApiDocBuilder::new() + .add( + "/api/storage/issues", + "List of storage-related issues", + "storage_issues", + ) + .build(); + let status = ServiceStatusApiDocBuilder::new("/api/storage/status").build(); + let progress = ProgressApiDocBuilder::new("/api/storage/progress").build(); + issues.merge(status); + issues.merge(progress); + Some(issues) + } } diff --git a/rust/agama-server/src/web/docs/users.rs b/rust/agama-server/src/web/docs/users.rs index 7daae29609..42034d58a9 100644 --- a/rust/agama-server/src/web/docs/users.rs +++ b/rust/agama-server/src/web/docs/users.rs @@ -18,9 +18,12 @@ // 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, Paths, PathsBuilder}; +use utoipa::openapi::{ComponentsBuilder, OpenApi, Paths, PathsBuilder}; -use super::ApiDocBuilder; +use super::{ + common::{IssuesApiDocBuilder, ServiceStatusApiDocBuilder}, + ApiDocBuilder, +}; pub struct UsersApiDocBuilder; @@ -44,6 +47,7 @@ impl ApiDocBuilder for UsersApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() .schema( "zbus.zvariant.OwnedValue", utoipa::openapi::ObjectBuilder::new() @@ -52,4 +56,17 @@ impl ApiDocBuilder for UsersApiDocBuilder { ) .build() } + + fn nested(&self) -> Option { + let mut issues = IssuesApiDocBuilder::new() + .add( + "/api/users/issues", + "List of user-related issues", + "user_issues", + ) + .build(); + let status = ServiceStatusApiDocBuilder::new("/api/storage/status").build(); + issues.merge(status); + Some(issues) + } }