Skip to content

Commit

Permalink
fix(openapi): add issues, progress and status routes (#1772)
Browse files Browse the repository at this point in the history
## Problem

The OpenAPI specification does not describe the common HTTP APIs (e.g., 
The specification of the common (shared) HTTP APIs the OpenAPI
description.

## Solution

Add the description of *issues*, *service status* and *progress* HTTP
APIs. Unless there are more omissions, it should close #1700.

## Testing

- *Tested manually*
  • Loading branch information
imobachgs authored Nov 25, 2024
2 parents 72170de + 1443c48 commit 9297d68
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 74 deletions.
2 changes: 1 addition & 1 deletion rust/agama-lib/src/progress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions rust/agama-server/src/web/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String>,
Expand Down
26 changes: 19 additions & 7 deletions rust/agama-server/src/web/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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<OpenApi> {
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
}
}
207 changes: 207 additions & 0 deletions rust/agama-server/src/web/docs/common.rs
Original file line number Diff line number Diff line change
@@ -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::<Issue>().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::<ServiceStatus>()
.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::<Progress>().build()
}
}
43 changes: 0 additions & 43 deletions rust/agama-server/src/web/docs/issues.rs

This file was deleted.

14 changes: 12 additions & 2 deletions rust/agama-server/src/web/docs/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -47,4 +50,11 @@ impl ApiDocBuilder for ManagerApiDocBuilder {
.schema_from::<agama_lib::logs::LogsLists>()
.build()
}

fn nested(&self) -> Option<OpenApi> {
let mut status = ServiceStatusApiDocBuilder::new("/api/storage/status").build();
let progress = ProgressApiDocBuilder::new("/api/storage/progress").build();
status.merge(progress);
Some(status)
}
}
Loading

0 comments on commit 9297d68

Please sign in to comment.