spring_web/
lib.rs

1//! [![spring-rs](https://img.shields.io/github/stars/spring-rs/spring-rs)](https://spring-rs.github.io/docs/plugins/spring-web)
2#![doc = include_str!("../README.md")]
3#![doc(html_favicon_url = "https://spring-rs.github.io/favicon.ico")]
4#![doc(html_logo_url = "https://spring-rs.github.io/logo.svg")]
5
6/// spring-web config
7pub mod config;
8/// spring-web defined error
9pub mod error;
10/// axum extract
11pub mod extractor;
12/// axum route handler
13pub mod handler;
14pub mod middleware;
15#[cfg(feature = "openapi")]
16pub mod openapi;
17
18pub use axum;
19pub use spring::async_trait;
20use spring::signal;
21/////////////////web-macros/////////////////////
22/// To use these Procedural Macros, you need to add `spring-web` dependency
23pub use spring_macros::middlewares;
24pub use spring_macros::nest;
25
26// route macros
27pub use spring_macros::delete;
28pub use spring_macros::get;
29pub use spring_macros::head;
30pub use spring_macros::options;
31pub use spring_macros::patch;
32pub use spring_macros::post;
33pub use spring_macros::put;
34pub use spring_macros::route;
35pub use spring_macros::routes;
36pub use spring_macros::trace;
37
38#[cfg(feature = "openapi")]
39pub use spring_macros::api_route;
40#[cfg(feature = "openapi")]
41pub use spring_macros::api_routes;
42#[cfg(feature = "openapi")]
43pub use spring_macros::delete_api;
44#[cfg(feature = "openapi")]
45pub use spring_macros::get_api;
46#[cfg(feature = "openapi")]
47pub use spring_macros::head_api;
48#[cfg(feature = "openapi")]
49pub use spring_macros::options_api;
50#[cfg(feature = "openapi")]
51pub use spring_macros::patch_api;
52#[cfg(feature = "openapi")]
53pub use spring_macros::post_api;
54#[cfg(feature = "openapi")]
55pub use spring_macros::put_api;
56#[cfg(feature = "openapi")]
57pub use spring_macros::trace_api;
58
59/// axum::routing::MethodFilter re-export
60pub use axum::routing::MethodFilter;
61
62/// Router with AppState
63#[cfg(not(feature = "openapi"))]
64pub type Router = axum::Router;
65/// MethodRouter with AppState
66pub use axum::routing::MethodRouter;
67
68#[cfg(feature = "openapi")]
69pub use aide;
70#[cfg(feature = "openapi")]
71pub use aide::openapi::OpenApi;
72#[cfg(feature = "openapi")]
73pub type Router = aide::axum::ApiRouter;
74#[cfg(feature = "openapi")]
75pub use aide::axum::routing::ApiMethodRouter;
76
77#[cfg(feature = "openapi")]
78use aide::transform::TransformOpenApi;
79
80use anyhow::Context;
81use axum::Extension;
82use config::ServerConfig;
83use config::WebConfig;
84use spring::plugin::component::ComponentRef;
85use spring::plugin::ComponentRegistry;
86use spring::plugin::MutableComponentRegistry;
87use spring::{
88    app::{App, AppBuilder},
89    config::ConfigRegistry,
90    error::Result,
91    plugin::Plugin,
92};
93use std::{net::SocketAddr, ops::Deref, sync::Arc};
94
95#[cfg(feature = "openapi")]
96use crate::config::OpenApiConfig;
97
98/// Routers collection
99#[cfg(feature = "openapi")]
100pub type Routers = Vec<aide::axum::ApiRouter>;
101#[cfg(not(feature = "openapi"))]
102pub type Routers = Vec<axum::Router>;
103
104/// OpenAPI
105#[cfg(feature = "openapi")]
106type OpenApiTransformer = fn(TransformOpenApi) -> TransformOpenApi;
107
108/// Web Configurator
109pub trait WebConfigurator {
110    /// add route to app registry
111    fn add_router(&mut self, router: Router) -> &mut Self;
112
113    /// Initialize OpenAPI Documents
114    #[cfg(feature = "openapi")]
115    fn openapi(&mut self, openapi: OpenApi) -> &mut Self;
116
117    /// Defining OpenAPI Documents
118    #[cfg(feature = "openapi")]
119    fn api_docs(&mut self, api_docs: OpenApiTransformer) -> &mut Self;
120}
121
122impl WebConfigurator for AppBuilder {
123    fn add_router(&mut self, router: Router) -> &mut Self {
124        if let Some(routers) = self.get_component_ref::<Routers>() {
125            unsafe {
126                let raw_ptr = ComponentRef::into_raw(routers);
127                let routers = &mut *(raw_ptr as *mut Routers);
128                routers.push(router);
129            }
130            self
131        } else {
132            self.add_component(vec![router])
133        }
134    }
135
136    /// Initialize OpenAPI Documents
137    #[cfg(feature = "openapi")]
138    fn openapi(&mut self, openapi: OpenApi) -> &mut Self {
139        self.add_component(openapi)
140    }
141
142    #[cfg(feature = "openapi")]
143    fn api_docs(&mut self, api_docs: OpenApiTransformer) -> &mut Self {
144        self.add_component(api_docs)
145    }
146}
147
148/// State of App
149#[derive(Clone)]
150pub struct AppState {
151    /// App Registry Ref
152    pub app: Arc<App>,
153}
154
155/// Web Plugin Definition
156pub struct WebPlugin;
157
158#[async_trait]
159impl Plugin for WebPlugin {
160    async fn build(&self, app: &mut AppBuilder) {
161        let config = app
162            .get_config::<WebConfig>()
163            .expect("web plugin config load failed");
164
165        // 1. collect router
166        let routers = app.get_component_ref::<Routers>();
167        let mut router: Router = match routers {
168            Some(rs) => {
169                let mut router = Router::new();
170                for r in rs.deref().iter() {
171                    router = router.merge(r.to_owned());
172                }
173                router
174            }
175            None => Router::new(),
176        };
177        if let Some(middlewares) = config.middlewares {
178            router = crate::middleware::apply_middleware(router, middlewares);
179        }
180
181        app.add_component(router);
182
183        let server_conf = config.server;
184        #[cfg(feature = "openapi")]
185        {
186            let openapi_conf = config.openapi;
187            app.add_component(openapi_conf.clone());
188        }
189
190        app.add_scheduler(move |app: Arc<App>| Box::new(Self::schedule(app, server_conf)));
191    }
192}
193
194impl WebPlugin {
195    async fn schedule(app: Arc<App>, config: ServerConfig) -> Result<String> {
196        let router = app.get_expect_component::<Router>();
197
198        // 2. bind tcp listener
199        let addr = SocketAddr::from((config.binding, config.port));
200        let listener = tokio::net::TcpListener::bind(addr)
201            .await
202            .with_context(|| format!("bind tcp listener failed:{addr}"))?;
203        tracing::info!("bind tcp listener: {addr}");
204
205        // 3. openapi
206        #[cfg(feature = "openapi")]
207        let router = {
208            let openapi_conf = app.get_expect_component::<OpenApiConfig>();
209            finish_openapi(&app, router, openapi_conf)
210        };
211
212        // 4. axum server
213        let router = router.layer(Extension(AppState { app }));
214
215        tracing::info!("axum server started");
216        if config.connect_info {
217            // with client connect info
218            let service = router.into_make_service_with_connect_info::<SocketAddr>();
219            let server = axum::serve(listener, service);
220            if config.graceful {
221                server
222                    .with_graceful_shutdown(signal::shutdown_signal())
223                    .await
224            } else {
225                server.await
226            }
227        } else {
228            let service = router.into_make_service();
229            let server = axum::serve(listener, service);
230            if config.graceful {
231                server
232                    .with_graceful_shutdown(signal::shutdown_signal())
233                    .await
234            } else {
235                server.await
236            }
237        }
238        .context("start axum server failed")?;
239
240        Ok("axum schedule finished".to_string())
241    }
242}
243
244#[cfg(feature = "openapi")]
245pub fn enable_openapi() {
246    aide::generate::on_error(|error| {
247        tracing::error!("{error}");
248    });
249    aide::generate::extract_schemas(false);
250}
251
252#[cfg(feature = "openapi")]
253fn finish_openapi(
254    app: &App,
255    router: aide::axum::ApiRouter,
256    openapi_conf: OpenApiConfig,
257) -> axum::Router {
258    let router = router.nest_api_service(&openapi_conf.doc_prefix, docs_routes(&openapi_conf));
259
260    let mut api = app.get_component::<OpenApi>().unwrap_or_else(|| OpenApi {
261        info: openapi_conf.info,
262        ..Default::default()
263    });
264
265    let router = if let Some(api_docs) = app.get_component::<OpenApiTransformer>() {
266        router.finish_api_with(&mut api, api_docs)
267    } else {
268        router.finish_api(&mut api)
269    };
270
271    router.layer(Extension(Arc::new(api)))
272}
273
274#[cfg(feature = "openapi")]
275pub fn docs_routes(OpenApiConfig { doc_prefix, info }: &OpenApiConfig) -> aide::axum::ApiRouter {
276    let router = aide::axum::ApiRouter::new();
277    let _openapi_path = &format!("{doc_prefix}/openapi.json");
278    let _doc_title = &info.title;
279
280    #[cfg(feature = "openapi-scalar")]
281    let router = router.route(
282        "/scalar",
283        aide::scalar::Scalar::new(_openapi_path)
284            .with_title(_doc_title)
285            .axum_route(),
286    );
287    #[cfg(feature = "openapi-redoc")]
288    let router = router.route(
289        "/redoc",
290        aide::redoc::Redoc::new(_openapi_path)
291            .with_title(_doc_title)
292            .axum_route(),
293    );
294    #[cfg(feature = "openapi-swagger")]
295    let router = router.route(
296        "/swagger",
297        aide::swagger::Swagger::new(_openapi_path)
298            .with_title(_doc_title)
299            .axum_route(),
300    );
301
302    router.route("/openapi.json", axum::routing::get(serve_docs))
303}
304
305#[cfg(feature = "openapi")]
306async fn serve_docs(Extension(api): Extension<Arc<OpenApi>>) -> impl aide::axum::IntoApiResponse {
307    axum::response::IntoResponse::into_response(axum::Json(api.as_ref()))
308}
309
310#[cfg(feature = "openapi")]
311pub fn default_transform<'a>(
312    path_item: aide::transform::TransformPathItem<'a>,
313) -> aide::transform::TransformPathItem<'a> {
314    path_item
315}