Skip to main content

lenso_api/
openapi.rs

1//! `OpenAPI` document assembly.
2//!
3//! Paths and component schemas are derived directly from the
4//! `#[utoipa::path]`-annotated handlers via `utoipa-axum`'s `OpenApiRouter`, so
5//! there is a single source of truth per endpoint. This module only contributes
6//! the document-level metadata (info, tags) that is not tied to any one route.
7
8use lenso_bootstrap::CompositionProfile;
9use platform_core::AppContext;
10use platform_http::{ApiOpenApiRouter, OpenApiRouter, base_router};
11use utoipa::OpenApi;
12
13/// Document-level `OpenAPI` metadata shared by every endpoint.
14///
15/// Intentionally declares no `paths` and no per-endpoint `schemas`: those are
16/// collected automatically from the annotated handlers when the router is split
17/// into its parts.
18#[derive(OpenApi)]
19#[openapi(
20    info(
21        title = "Lenso API",
22        version = "1.0.0",
23        description = "Rust-first modular monolith API contract"
24    ),
25    tags(
26        (name = "auth", description = "Auth module development session APIs"),
27        (name = "admin-runtime", description = "Read-only runtime console APIs"),
28        (name = "admin-config", description = "Editable configuration console APIs"),
29        (name = "admin-data", description = "Schema-driven admin data console APIs")
30    )
31)]
32struct ApiDoc;
33
34/// Assemble the full `OpenAPI` router: base probes, linked module routes, and
35/// admin/runtime routers, seeded with the document-level metadata.
36///
37/// Context-free: route registration and `OpenAPI` metadata never touch the
38/// database, so callers can either serve it (after `with_state` +
39/// `split_for_parts`) or extract the `OpenAPI` document alone.
40pub(crate) fn api_router() -> ApiOpenApiRouter {
41    api_router_for_profile(CompositionProfile::default())
42}
43
44pub(crate) fn api_router_for_profile(profile: CompositionProfile) -> ApiOpenApiRouter {
45    let base = OpenApiRouter::with_openapi(openapi_document_for_profile_with_composition(
46        profile,
47        &lenso_bootstrap::HostComposition::default(),
48    ))
49    .merge(base_router());
50    lenso_bootstrap::merge_linked_http_for_profile(base, profile)
51        .merge(platform_admin::router())
52        .merge(platform_admin_data::router())
53        .merge(platform_module_remote::router())
54}
55
56pub(crate) fn api_router_for_context_with_composition(
57    ctx: &AppContext,
58    composition: &lenso_bootstrap::HostComposition,
59) -> platform_core::AppResult<ApiOpenApiRouter> {
60    let profile = CompositionProfile::from_config(&ctx.config)?;
61    let base = OpenApiRouter::with_openapi(openapi_document_for_profile_with_composition(
62        profile,
63        composition,
64    ))
65    .merge(base_router());
66    Ok(
67        lenso_bootstrap::merge_linked_http_for_context_with_composition(base, ctx, composition)?
68            .merge(platform_admin::router())
69            .merge(platform_admin_data::router())
70            .merge(platform_module_remote::router()),
71    )
72}
73
74fn openapi_document_for_profile_with_composition(
75    profile: CompositionProfile,
76    composition: &lenso_bootstrap::HostComposition,
77) -> utoipa::openapi::OpenApi {
78    let mut document = ApiDoc::openapi();
79    if let Some(tags) = &mut document.tags {
80        let has_auth = profile == CompositionProfile::Demo
81            || composition
82                .linked_modules()
83                .iter()
84                .any(|module| module.module_name == "auth");
85        match profile {
86            CompositionProfile::Core => tags.retain(|tag| has_auth || tag.name != "auth"),
87            CompositionProfile::Demo => {}
88        }
89    }
90    document
91}
92
93/// The committed `OpenAPI` document, derived from the annotated handlers.
94#[must_use]
95pub fn openapi_document() -> utoipa::openapi::OpenApi {
96    api_router().to_openapi()
97}