nido-svc-common 0.1.0-alpha.1

Shared health, error, OpenAPI, SSE, and middleware primitives for nido-*-svc crates
Documentation
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Hand-rolled minimal OpenAPI 3.1.0 spec builder.
//!
//! utoipa migration is deferred per axum-utoipa-sse-sota-2026 research:
//! start hand-rolled, migrate per-service once DTOs are annotated.

use axum::{routing::MethodRouter, Json};
use serde_json::{json, Map, Value};

/// A single route entry for the spec.
#[derive(Debug, Clone)]
pub struct RouteSpec {
    pub path: String,
    pub method: String,
    pub summary: String,
    pub operation_id: String,
}

/// Minimal OpenAPI 3.1.0 JSON spec builder.
#[derive(Debug, Clone)]
pub struct OpenApiSpec {
    pub service_name: String,
    pub version: String,
    pub routes: Vec<RouteSpec>,
}

impl OpenApiSpec {
    pub fn new(service_name: impl Into<String>, version: impl Into<String>) -> Self {
        Self {
            service_name: service_name.into(),
            version: version.into(),
            routes: Vec::new(),
        }
    }

    pub fn route(
        mut self,
        path: impl Into<String>,
        method: impl Into<String>,
        summary: impl Into<String>,
        operation_id: impl Into<String>,
    ) -> Self {
        self.routes.push(RouteSpec {
            path: path.into(),
            method: method.into(),
            summary: summary.into(),
            operation_id: operation_id.into(),
        });
        self
    }

    /// Build the OpenAPI JSON value.
    pub fn build_json(&self) -> Value {
        let mut paths: Map<String, Value> = Map::new();
        for r in &self.routes {
            let method_key = r.method.to_lowercase();
            let entry = paths.entry(r.path.clone()).or_insert_with(|| json!({}));
            if let Value::Object(ref mut map) = entry {
                map.insert(
                    method_key,
                    json!({
                        "summary": r.summary,
                        "operationId": r.operation_id,
                        "responses": { "200": { "description": "OK" } }
                    }),
                );
            }
        }

        json!({
            "openapi": "3.1.0",
            "info": {
                "title": self.service_name,
                "version": self.version,
            },
            "paths": paths,
        })
    }

    /// Return an axum `MethodRouter` that serves the spec as JSON from
    /// `GET /openapi.json`.  Merge this into your router:
    ///
    /// ```rust,no_run
    /// # use nido_svc_common::openapi::OpenApiSpec;
    /// # use axum::Router;
    /// # use axum::routing::get;
    /// let spec = OpenApiSpec::new("my-svc", "0.1.0");
    /// let router = Router::new()
    ///     .route("/openapi.json", spec.build_handler());
    /// ```
    pub fn build_handler(self) -> MethodRouter {
        let json_val = self.build_json();
        axum::routing::get(move || {
            let v = json_val.clone();
            async move { Json(v) }
        })
    }
}