loco_openapi/
lib.rs

1use async_trait::async_trait;
2use axum::Router as AxumRouter;
3use loco_rs::prelude::*;
4use utoipa::openapi::OpenApi;
5use utoipa_axum::router::OpenApiRouter;
6#[cfg(feature = "redoc")]
7use utoipa_redoc::{Redoc, Servable};
8#[cfg(feature = "scalar")]
9use utoipa_scalar::{Scalar, Servable as ScalarServable};
10#[cfg(feature = "swagger")]
11use utoipa_swagger_ui::SwaggerUi;
12
13use crate::config::{get_openapi_config, set_openapi_config, InitializerConfig};
14use crate::openapi::get_merged_router;
15// Always used
16use crate::utils::set_openapi_spec;
17// Only used in feature blocks
18#[cfg(any(feature = "redoc", feature = "scalar", feature = "swagger"))]
19use crate::utils::{add_openapi_endpoints, get_openapi_spec};
20
21pub mod auth;
22pub mod config;
23pub mod openapi;
24pub mod prelude;
25pub mod utils;
26
27type RouterList = Option<Vec<OpenApiRouter<AppContext>>>;
28type InitialSpec = dyn Fn(&AppContext) -> OpenApi + Send + Sync + 'static;
29
30/// Loco initializer for `OpenAPI` with custom initial spec setup
31#[derive(Default)]
32pub struct OpenapiInitializerWithSetup {
33    /// Custom setup for the initial `OpenAPI` spec, if any
34    initial_spec: Option<Box<InitialSpec>>,
35    /// Routes to add to the `OpenAPI` spec
36    routes_setup: RouterList,
37}
38
39impl OpenapiInitializerWithSetup {
40    #[must_use]
41    pub fn new<F>(initial_spec: F, routes_setup: RouterList) -> Self
42    where
43        F: Fn(&AppContext) -> OpenApi + Send + Sync + 'static,
44    {
45        Self {
46            initial_spec: Some(Box::new(initial_spec)),
47            routes_setup,
48        }
49    }
50}
51
52#[async_trait]
53impl Initializer for OpenapiInitializerWithSetup {
54    fn name(&self) -> String {
55        "openapi".to_string()
56    }
57
58    async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result<AxumRouter> {
59        // Use the InitializerConfig wrapper
60        set_openapi_config(InitializerConfig::from(&ctx.config.initializers).into())?;
61
62        let mut api_router: OpenApiRouter<AppContext> = self
63            .initial_spec
64            .as_ref()
65            .map_or_else(OpenApiRouter::new, |custom_spec_fn| {
66                OpenApiRouter::with_openapi(custom_spec_fn(ctx))
67            });
68
69        // Merge all manually collected routes
70        if let Some(ref routes_setup) = self.routes_setup {
71            for route in routes_setup {
72                api_router = api_router.merge(route.clone());
73            }
74        }
75
76        // Merge all automatically collected routes
77        api_router = api_router.merge(get_merged_router());
78
79        // Collect the `OpenAPI` spec
80        let (_, open_api_spec) = api_router.split_for_parts();
81        set_openapi_spec(open_api_spec);
82
83        // Use `_` prefix as config might be unused if no features are enabled
84        let Some(_open_api_config) = get_openapi_config() else {
85            // No config, return original router
86            return Ok(router);
87        };
88
89        // Create a new router for UI endpoints
90        #[allow(unused_mut)]
91        let mut ui_router = AxumRouter::new();
92
93        // Serve the `OpenAPI` spec using the enabled `OpenAPI` visualizers
94        #[cfg(feature = "redoc")]
95        if let Some(config::OpenAPIType::Redoc {
96            url,
97            spec_json_url,
98            spec_yaml_url,
99        }) = get_openapi_config().and_then(|c| c.redoc.as_ref())
100        {
101            ui_router = ui_router.merge(Redoc::with_url(url, get_openapi_spec().clone()));
102            ui_router = add_openapi_endpoints(ui_router, spec_json_url, spec_yaml_url);
103        }
104
105        #[cfg(feature = "scalar")]
106        if let Some(config::OpenAPIType::Scalar {
107            url,
108            spec_json_url,
109            spec_yaml_url,
110        }) = get_openapi_config().and_then(|c| c.scalar.as_ref())
111        {
112            ui_router = ui_router.merge(Scalar::with_url(url, get_openapi_spec().clone()));
113            ui_router = add_openapi_endpoints(ui_router, spec_json_url, spec_yaml_url);
114        }
115
116        #[cfg(feature = "swagger")]
117        if let Some(config::OpenAPIType::Swagger {
118            url,
119            spec_json_url,
120            spec_yaml_url,
121        }) = get_openapi_config().and_then(|c| c.swagger.as_ref())
122        {
123            ui_router = ui_router
124                .merge(SwaggerUi::new(url).url(spec_json_url.clone(), get_openapi_spec().clone()));
125            ui_router = add_openapi_endpoints(ui_router, &None, spec_yaml_url);
126        }
127
128        // Merge the UI router with the main router
129        Ok(router.merge(ui_router))
130    }
131}