feat(api): add health probes, OpenAPI doc, and router
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
use axum::{Json, Router, extract::State, http::StatusCode, response::IntoResponse, routing::get};
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
/// Liveness payload: the process is running.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub(crate) struct Live {
|
||||
/// Always `"ok"` when the process serves requests.
|
||||
pub status: &'static str,
|
||||
}
|
||||
|
||||
/// Readiness payload: dependencies were checked.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub(crate) struct Ready {
|
||||
/// `"ok"` when ready, `"degraded"` otherwise.
|
||||
pub status: &'static str,
|
||||
/// Whether the database responded to a ping.
|
||||
pub database: bool,
|
||||
}
|
||||
|
||||
/// Liveness probe — no dependencies checked.
|
||||
#[utoipa::path(get, path = "/health/live", responses((status = 200, body = Live)))]
|
||||
pub(crate) async fn live() -> Json<Live> {
|
||||
Json(Live { status: "ok" })
|
||||
}
|
||||
|
||||
/// Readiness probe — confirms the database answers.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/health/ready",
|
||||
responses(
|
||||
(status = 200, body = Ready, description = "Ready"),
|
||||
(status = 503, body = Ready, description = "A dependency is unavailable")
|
||||
)
|
||||
)]
|
||||
pub(crate) async fn ready(State(state): State<AppState>) -> impl IntoResponse {
|
||||
match state.db.ping().await {
|
||||
Ok(()) => (
|
||||
StatusCode::OK,
|
||||
Json(Ready {
|
||||
status: "ok",
|
||||
database: true,
|
||||
}),
|
||||
),
|
||||
Err(_) => (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(Ready {
|
||||
status: "degraded",
|
||||
database: false,
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Health routes, parameterized over [`AppState`].
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/health/live", get(live))
|
||||
.route("/health/ready", get(ready))
|
||||
}
|
||||
@@ -1 +1,24 @@
|
||||
//! HTTP API: router, handlers, and OpenAPI document.
|
||||
|
||||
mod health;
|
||||
mod openapi;
|
||||
|
||||
use axum::Router;
|
||||
use db::Db;
|
||||
|
||||
/// Shared application state passed to handlers.
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
/// Database handle for this organization.
|
||||
pub db: Db,
|
||||
/// User-facing product name (from config). Never hardcoded.
|
||||
pub app_name: String,
|
||||
}
|
||||
|
||||
/// Build the application router from shared state.
|
||||
pub fn build_app(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.merge(health::routes())
|
||||
.merge(openapi::routes())
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
use axum::{Json, Router, extract::State, routing::get};
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use crate::{AppState, health};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(health::live, health::ready),
|
||||
components(schemas(health::Live, health::Ready)),
|
||||
info(title = "Collection Management System", version = "0.0.0")
|
||||
)]
|
||||
struct ApiDoc;
|
||||
|
||||
/// Serve the OpenAPI document, overriding the title from runtime config so the
|
||||
/// product name is never hardcoded.
|
||||
async fn openapi_json(State(state): State<AppState>) -> Json<utoipa::openapi::OpenApi> {
|
||||
let mut doc = ApiDoc::openapi();
|
||||
doc.info.title = state.app_name.clone();
|
||||
Json(doc)
|
||||
}
|
||||
|
||||
/// OpenAPI routes, parameterized over [`AppState`].
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new().route("/api-docs/openapi.json", get(openapi_json))
|
||||
}
|
||||
Reference in New Issue
Block a user