diff options
| author | tv <tv@krebsco.de> | 2026-10-26 21:43:42 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-26 21:43:42 +0100 |
| commit | 3374b029bea44fed6ad5a46a7a05c269ce61af58 (patch) | |
| tree | ca9eba49e886c9dcd4416c4fe7790ca34f07f376 /src/api | |
| parent | 137b1547f847ff2402ea9e634b64ee2196a75075 (diff) | |
replace utoipa by static openapi.yaml
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/mod.rs | 59 | ||||
| -rw-r--r-- | src/api/peers.rs | 40 | ||||
| -rw-r--r-- | src/api/store.rs | 74 |
3 files changed, 27 insertions, 146 deletions
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<AppState> = 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<AppState>) -> Json<JsonValue> { @@ -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<Epoch> 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<ThroughputInfo>, } -#[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<String>, } -#[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<AppState>, ) -> Result<Json<PeerListResponse>, 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<String>, 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<AppState>, Path(peer): Path<String>, 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<bool>, Query, description = "Wrap the value and list every conflict resolved on the way"), - ("heads" = Option<bool>, 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<AppState>, 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<bool>, Query, description = "Wrap the value and list every conflict resolved on the way"), - ("heads" = Option<bool>, 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<AppState>, Path(key): Path<String>, @@ -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<AppState>, Extension(user): Extension<AuthenticatedUser>, @@ -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<bool>, 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<AppState>, |
