Skip to content

Commit

Permalink
fix(rust): revamp the OpenAPI specification (#1564)
Browse files Browse the repository at this point in the history
The `agama-web-server docs` command generates the OpenAPI specification
of Agama's HTTP API. However, the resulting specification is badly
broken 😢. This pull request aims to make the specification usable.

## Refactoring

Apart from being incomplete, the previous implementation had a few
problems.

* The code relies on [OpenApi
proc-macro](https://docs.rs/utoipa/latest/utoipa/derive.OpenApi.html).
This macro requires the data types to implement the [ToSchema
trait](https://docs.rs/utoipa/latest/utoipa/trait.ToSchema.html), and
due to the orphan rule, we cannot implement it for third-party types
(e.g., `IpAddr`, `IpInet`, etc.).
* We did not find a way to add the description of the common APIs (e.g.,
*progress* or *issues*) under each "scope".

For those reasons, we decided to use the builder API, which is pretty
flexible. We also took the chance to split the generation into smaller
pieces, making it easier to maintain.

## Packaging the OpenAPI documentation

Instead of generating the OpenAPI spec at runtime, we can do it at build
time and put the resulting JSON files in a separate package
(`agama-openapi`).

## Generating the `schema.d.ts`

You can generate the API in two simple steps:

```
$ cargo run -p agama-server --bin agama-web-server openapi > openapi.json
$ npx @hey-api/openapi-ts -i openapi.json -o src/client -c axios
```

## To do

* [x] Merging specifications is not the right approach because it does
not detect conflicts (e.g., network and storage devices). Perhaps it
might be a good idea to generate a separate specification for each scope
(e.g., network).
* [x] Add third-party types descriptions.
* [x] Add validation to CI.

## Future work

* Add the DASD API.
* Add the common interfaces (e.g., *progress*, *issues*, etc.)
  • Loading branch information
imobachgs authored Oct 22, 2024
2 parents 5b34e24 + 780ba93 commit 653ef75
Show file tree
Hide file tree
Showing 26 changed files with 527 additions and 202 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/ci-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
openssl-3
pam-devel
python-langtable-data
python3-openapi_spec_validator
timezone
xkeyboard-config

Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions rust/agama-lib/src/network/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NetworkConnection>,
}

#[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<String>,
Expand All @@ -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
Expand Down Expand Up @@ -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")]
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions rust/agama-lib/src/network/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>);

impl SSID {
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion rust/agama-lib/src/software/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions rust/agama-locale-data/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ flate2 = "1.0.34"
chrono-tz = "0.8.6"
regex = "1"
thiserror = "1.0.64"
utoipa = "4.2.3"
6 changes: 4 additions & 2 deletions rust/agama-locale-data/src/locale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -100,9 +100,11 @@ static KEYMAP_ID_REGEX: OnceLock<Regex> = 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<String>,
}

Expand Down
2 changes: 1 addition & 1 deletion rust/agama-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
10 changes: 0 additions & 10 deletions rust/agama-server/src/agama-web-server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -58,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.
Expand Down Expand Up @@ -379,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::openapi().to_pretty_json().unwrap());
Ok(())
}

async fn run_command(cli: Cli) -> anyhow::Result<()> {
match cli.command {
Commands::Serve(options) => serve_command(options).await,
Commands::Openapi => openapi_command(),
}
}

Expand Down
Loading

0 comments on commit 653ef75

Please sign in to comment.