summaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/mod.rs59
-rw-r--r--src/api/peers.rs40
-rw-r--r--src/api/store.rs74
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>,