spring_utoipa/
lib.rs

1#![feature(type_alias_impl_trait)]
2
3pub mod config;
4
5pub use utoipa;
6pub use utoipauto::{utoipa_ignore, utoipauto};
7
8use config::UtoipaConfig;
9use spring::async_trait;
10use spring::config::ConfigRegistry;
11use spring::plugin::{MutableComponentRegistry, Plugin};
12use spring::{app::AppBuilder, plugin::ComponentRegistry};
13use spring_web::{Router, WebConfigurator};
14use utoipa::openapi::OpenApi;
15
16#[cfg(not(any(feature = "rapidoc", feature = "swagger-ui")))]
17use spring_web::axum::{Json, routing};
18#[cfg(feature = "rapidoc")]
19use utoipa_rapidoc::RapiDoc;
20#[cfg(feature = "redoc")]
21use utoipa_redoc::{Redoc, Servable};
22#[cfg(feature = "scalar")]
23use utoipa_scalar::{Scalar, Servable};
24#[cfg(feature = "swagger-ui")]
25use utoipa_swagger_ui::SwaggerUi;
26
27pub trait UtoipaConfigurator {
28    fn with_openapi(&mut self, openapi: OpenApi) -> &mut Self;
29}
30impl UtoipaConfigurator for AppBuilder {
31    fn with_openapi(&mut self, openapi: OpenApi) -> &mut Self {
32        if let Some(_wrapper) = self.get_component_ref::<OpenApi>() {
33            panic!("Error adding OpenApi: OpenApi was already added in application")
34        } else {
35            self.add_component(openapi)
36        }
37    }
38}
39
40pub struct UtoipaPlugin;
41
42#[async_trait]
43impl Plugin for UtoipaPlugin {
44    async fn build(&self, app: &mut AppBuilder) {
45        let config = app
46            .get_config::<UtoipaConfig>()
47            .expect("utoipa plugin config load failed");
48
49        let openapi = app
50            .get_component::<OpenApi>()
51            .expect("Expected openapi to be configured");
52
53        let mut router = Router::new();
54
55        #[cfg(any(
56            all(feature = "rapidoc", feature = "redoc"),
57            all(feature = "rapidoc", feature = "scalar"),
58            all(feature = "rapidoc", feature = "swagger-ui"),
59            all(feature = "redoc", feature = "scalar"),
60            all(feature = "redoc", feature = "swagger-ui"),
61            all(feature = "scalar", feature = "swagger-ui"),
62        ))]
63        panic!("Only one OpenApi visualizer can be used!");
64
65        #[cfg(not(any(feature = "rapidoc", feature = "swagger-ui")))]
66        {
67            let openapi = openapi.clone();
68            router = router.route(&config.path, routing::get(|| async move { Json(openapi) }));
69        }
70
71        #[cfg(any(
72            feature = "rapidoc",
73            feature = "redoc",
74            feature = "scalar",
75            feature = "swagger-ui"
76        ))]
77        assert_ne!(
78            config.path, config.visualizer_path,
79            "openapi.json path shouldn't collide with OpenAPI visualizer path"
80        );
81
82        #[cfg(feature = "rapidoc")]
83        {
84            router = router
85                .merge(RapiDoc::with_openapi(config.path, openapi).path(config.visualizer_path));
86        }
87
88        #[cfg(feature = "redoc")]
89        {
90            router = router.merge(Redoc::with_url(config.visualizer_path, openapi));
91        }
92
93        #[cfg(feature = "scalar")]
94        {
95            router = router.merge(Scalar::with_url(config.visualizer_path, openapi));
96        }
97
98        #[cfg(feature = "swagger-ui")]
99        {
100            assert_ne!(
101                config.visualizer_path, "",
102                "Swagger-UI shouldn't be mounted on root!"
103            );
104            assert_ne!(
105                config.visualizer_path, "/",
106                "Swagger-UI shouldn't be mounted on root!"
107            );
108            router = router.merge(SwaggerUi::new(config.visualizer_path).url(config.path, openapi));
109        }
110
111        app.add_router(router);
112    }
113}