From 3374b029bea44fed6ad5a46a7a05c269ce61af58 Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 26 Oct 2026 21:43:42 +0100 Subject: replace utoipa by static openapi.yaml --- src/api/mod.rs | 59 ++++++++++--------------------------- src/api/peers.rs | 40 +++++-------------------- src/api/store.rs | 74 +++-------------------------------------------- src/state/peer.rs | 2 +- src/transport/firewall.rs | 3 +- src/transport/traffic.rs | 7 ++--- 6 files changed, 32 insertions(+), 153 deletions(-) (limited to 'src') diff --git a/src/api/mod.rs b/src/api/mod.rs index bf7b06b..20237e1 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,4 +1,4 @@ -//! HTTP API: router, OpenAPI document and shared response helpers. +//! HTTP API: router, the API description and shared response helpers. pub(crate) mod auth; mod debug; @@ -24,7 +24,6 @@ use axum::{ use serde_json::{Value as JsonValue, json}; use sha2::{Digest, Sha256}; use tracing::{error, warn}; -use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; use crate::api::auth::{AuthState, AuthenticatedUser, basic_auth}; @@ -32,8 +31,8 @@ use crate::document::meta; use crate::document::store::StoreError; use crate::state::AppState; -/// Builds the complete HTTP router. Everything except Swagger, `/health` and `/ready` is -/// authenticated. +/// Builds the complete HTTP router. Everything except the API description (`/swagger-ui`, +/// `/openapi.yaml`), `/health` and `/ready` is authenticated. pub(crate) fn router(state: AppState, auth_state: AuthState, debug_api: bool) -> Router { let mut api: Router = Router::new() .route("/store", get(store::get_store_root)) @@ -65,7 +64,11 @@ pub(crate) fn router(state: AppState, auth_state: AuthState, debug_api: bool) -> } api.layer(middleware::from_fn_with_state(auth_state, basic_auth)) - .merge(SwaggerUi::new("/swagger-ui").url("/openapi.json", ApiDoc::openapi())) + // The hand-written spec (`docs/openapi.yaml`), and the Swagger UI showing it. + .merge( + SwaggerUi::new("/swagger-ui").config(utoipa_swagger_ui::Config::from("/openapi.yaml")), + ) + .route("/openapi.yaml", get(openapi_spec)) .route("/health", get(health::health)) .route("/ready", get(health::ready)) // Outermost, so authentication failures get the error body too. @@ -125,6 +128,14 @@ async fn json_errors(request: Request, next: Next) -> Response { Response::from_parts(parts, Body::from(body)) } +/// `GET /openapi.yaml`: the API description (`docs/openapi.yaml`, built in). +async fn openapi_spec() -> impl IntoResponse { + ( + [(CONTENT_TYPE, HeaderValue::from_static("application/yaml"))], + include_str!("../../docs/openapi.yaml"), + ) +} + /// `GET /admin/config`: the effective configuration with each value's source; /// imported values are left out. async fn get_config(State(state): State) -> Json { @@ -231,41 +242,3 @@ fn response_for_value( Ok(response) } - -// --- openapi spec ------------------------------------------------------------ -// TODO spec is mostly out of sync - -#[derive(OpenApi)] -#[openapi( - paths( - store::get_store_root, - store::get_store_entry, - store::put_store_entry, - store::delete_store_entry, - peers::list_peers, - peers::delete_peer, - ), - components( - schemas( - peers::PeerStatus, - peers::PeerListResponse, - peers::PeerLivenessStatus, - peers::EpochInfo, - crate::transport::traffic::PeerTraffic, - crate::transport::traffic::ByKind, - crate::transport::traffic::Counter, - crate::state::Incidents, - peers::ThroughputInfo, - ) - ), - tags( - (name = "Store", description = "Key-value store operations"), - (name = "Peers", description = "Peer discovery and retirement") - ), - info( - title = "Automerge Replication API", - version = "1.0.0", - description = "HTTP API for a replicated Automerge key-value store" - ) -)] -struct ApiDoc; diff --git a/src/api/peers.rs b/src/api/peers.rs index eb522b0..9af24c0 100644 --- a/src/api/peers.rs +++ b/src/api/peers.rs @@ -8,7 +8,6 @@ use axum::{ use serde::{Deserialize, Serialize}; use tokio::time::Instant; use tracing::warn; -use utoipa::ToSchema; use super::store_status; use crate::document::replica::Epoch; @@ -18,14 +17,14 @@ use crate::state::{AppState, HelloState, Incidents, Liveness}; use crate::swarm::peer::forgotten_path; use crate::transport::traffic::PeerTraffic; -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Serialize)] #[serde(rename_all = "lowercase")] pub(crate) enum PeerLivenessStatus { Active, Dormant, } -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct EpochInfo { generation: u64, @@ -42,7 +41,7 @@ impl From for EpochInfo { } } -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PeerStatus { public_key: String, @@ -75,7 +74,7 @@ pub(crate) struct PeerStatus { sync_throughput: Option, } -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ThroughputInfo { bytes_per_sec: f64, @@ -84,7 +83,7 @@ pub(crate) struct ThroughputInfo { secs_ago: u64, } -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PeerListResponse { /// Our own alias (`--alias`). @@ -102,18 +101,7 @@ pub(crate) struct DeletePeerQuery { force: Option, } -#[utoipa::path( - get, - path = "/peers", - tag = "Peers", - responses( - ( - status = 200, - description = "Lists known peers and their local replication status", - body = PeerListResponse - ) - ) -)] +/// `GET /peers` (`docs/openapi.yaml`). pub(crate) async fn list_peers( State(state): State, ) -> Result, StatusCode> { @@ -182,21 +170,7 @@ pub(crate) async fn list_peers( })) } -#[utoipa::path( - delete, - path = "/peers/{peer}", - tag = "Peers", - params( - ("peer" = String, Path, description = "Peer public key"), - ("force" = Option, Query, description = "`true` or `alive`: also forget a peer that is currently alive; it returns on its next message") - ), - responses( - (status = 204, description = "Swarm-wide forget recorded"), - (status = 400, description = "Not a public key, or the local node's own key"), - (status = 409, description = "Peer is alive; pass force=alive"), - (status = 503, description = "Node is not ready") - ) -)] +/// `DELETE /peers/{pk}`: forgets a peer swarm-wide. pub(crate) async fn delete_peer( State(state): State, Path(peer): Path, diff --git a/src/api/store.rs b/src/api/store.rs index c8a072f..a05bc23 100644 --- a/src/api/store.rs +++ b/src/api/store.rs @@ -48,20 +48,7 @@ fn conflicts_response( response_for_value(&body, None) // always JSON } -#[utoipa::path( - get, - path = "/store", - tag = "Store", - params( - ("conflicts" = Option, Query, description = "Wrap the value and list every conflict resolved on the way"), - ("heads" = Option, Query, description = "Return the document heads at the time of the read in x-continuum-heads"), - ), - responses( - (status = 200, description = "Returns the complete store as JSON or text, depending on Accept"), - (status = 503, description = "Node is not ready"), - (status = 500, description = "Failed to read or serialize the document") - ) -)] +/// `GET /store`: the whole store (`docs/openapi.yaml`). pub(crate) async fn get_store_root( State(state): State, headers: HeaderMap, @@ -96,23 +83,7 @@ pub(crate) async fn get_store_root( mark_source(response, source, heads) } -#[utoipa::path( - get, - path = "/store/{key}", - tag = "Store", - params( - ("key" = String, Path, description = "Slash-separated store path, for example settings/theme"), - ("conflicts" = Option, Query, description = "Wrap the value and list every conflict resolved on the way"), - ("heads" = Option, Query, description = "Return the document heads at the time of the read in x-continuum-heads"), - ), - responses( - (status = 200, description = "Returns the value as JSON or text, depending on Accept"), - (status = 400, description = "Invalid store path"), - (status = 404, description = "Value does not exist"), - (status = 500, description = "Failed to read or serialize the value"), - (status = 503, description = "Node is not ready (and not rejoining with a stale view)") - ) -)] +/// `GET /store/{key}`. pub(crate) async fn get_store_entry( State(state): State, Path(key): Path, @@ -181,28 +152,7 @@ fn mark_source( Ok(response) } -#[utoipa::path( - put, - path = "/store/{key}", - tag = "Store", - params( - ("key" = String, Path, description = "Slash-separated store path") - ), - request_body( - content = String, - description = "Value to write. Use application/json for JSON values or text/plain for strings.", - content_type = "application/json" - ), - responses( - (status = 200, description = "Value written successfully"), - (status = 400, description = "Invalid path, malformed JSON, invalid UTF-8, or unsupported JSON value"), - (status = 403, description = "Protocol data (_continuum) and the user isn't a protocol writer, or epoch metadata"), - (status = 409, description = "Path conflicts with an existing non-map value"), - (status = 415, description = "Unsupported Content-Type"), - (status = 500, description = "Internal error or synchronization notification failure"), - (status = 503, description = "Node is not ready") - ) -)] +/// `PUT /store/{key}`. pub(crate) async fn put_store_entry( State(state): State, Extension(user): Extension, @@ -244,23 +194,7 @@ pub(crate) async fn put_store_entry( response_for_value(&value, headers.get(ACCEPT)) } -#[utoipa::path( - delete, - path = "/store/{key}", - tag = "Store", - params( - ("key" = String, Path, description = "Slash-separated store path"), - ("prune" = Option, Query, description = "Remove empty ancestor maps after deletion") - ), - responses( - (status = 204, description = "Value deleted successfully"), - (status = 400, description = "Invalid store path"), - (status = 403, description = "Protocol data (_continuum) and the user isn't a protocol writer, or epoch metadata"), - (status = 404, description = "Value does not exist"), - (status = 500, description = "Deletion or synchronization notification failed"), - (status = 503, description = "Node is not ready") - ) -)] +/// `DELETE /store/{key}`. #[instrument(skip(state), fields(path, prune = query.prune))] pub(crate) async fn delete_store_entry( State(state): State, diff --git a/src/state/peer.rs b/src/state/peer.rs index 944b886..344bcdb 100644 --- a/src/state/peer.rs +++ b/src/state/peer.rs @@ -134,7 +134,7 @@ pub(crate) struct PeerState { /// Trouble with a peer, counted since it was first listed. Lost keepalives and refused /// sends are in the traffic counters (`transport::traffic`): every PONG answers a PING. -#[derive(Debug, Default, Clone, Copy, serde::Serialize, utoipa::ToSchema)] +#[derive(Debug, Default, Clone, Copy, serde::Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct Incidents { /// Our sync messages left unanswered until the reply timeout (then sent again). diff --git a/src/transport/firewall.rs b/src/transport/firewall.rs index 23d91fe..037e739 100644 --- a/src/transport/firewall.rs +++ b/src/transport/firewall.rs @@ -4,13 +4,12 @@ use std::sync::{Arc, RwLock, RwLockReadGuard}; use anyhow::Result; use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; use crate::locks::RwLockExt; use crate::transport::EncodedMessage; use crate::transport::{MessageHandler, ParsedMessage, Transport}; -#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct FirewallRules { /// Messages from these peers are dropped before the application sees them. #[serde(default)] diff --git a/src/transport/traffic.rs b/src/transport/traffic.rs index 78c5fee..7d245ac 100644 --- a/src/transport/traffic.rs +++ b/src/transport/traffic.rs @@ -14,12 +14,11 @@ use std::sync::{Arc, Mutex, MutexGuard}; use anyhow::Result; use async_trait::async_trait; use serde::Serialize; -use utoipa::ToSchema; use crate::locks::MutexExt; use crate::transport::{EncodedMessage, MessageHandler, MessageKind, ParsedMessage, Transport}; -#[derive(Debug, Default, Clone, Copy, Serialize, ToSchema)] +#[derive(Debug, Default, Clone, Copy, Serialize)] pub struct Counter { pub messages: u64, pub bytes: u64, @@ -32,7 +31,7 @@ impl Counter { } } -#[derive(Debug, Default, Clone, Copy, Serialize, ToSchema)] +#[derive(Debug, Default, Clone, Copy, Serialize)] #[serde(rename_all = "camelCase")] pub struct ByKind { pub hello: Counter, @@ -56,7 +55,7 @@ impl ByKind { } } -#[derive(Debug, Default, Clone, Copy, Serialize, ToSchema)] +#[derive(Debug, Default, Clone, Copy, Serialize)] #[serde(rename_all = "camelCase")] pub struct PeerTraffic { /// Handed to Mycelium (it accepted them). -- cgit v1.3.1