1#![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
6pub mod config;
8pub mod error;
10pub mod extractor;
12pub mod handler;
14pub mod middleware;
15#[cfg(feature = "openapi")]
16pub mod openapi;
17
18pub use axum;
19pub use spring::async_trait;
20use spring::signal;
21pub use spring_macros::middlewares;
24pub use spring_macros::nest;
25
26pub 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
59pub use axum::routing::MethodFilter;
61
62#[cfg(not(feature = "openapi"))]
64pub type Router = axum::Router;
65pub 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#[cfg(feature = "openapi")]
100pub type Routers = Vec<aide::axum::ApiRouter>;
101#[cfg(not(feature = "openapi"))]
102pub type Routers = Vec<axum::Router>;
103
104#[cfg(feature = "openapi")]
106type OpenApiTransformer = fn(TransformOpenApi) -> TransformOpenApi;
107
108pub trait WebConfigurator {
110 fn add_router(&mut self, router: Router) -> &mut Self;
112
113 #[cfg(feature = "openapi")]
115 fn openapi(&mut self, openapi: OpenApi) -> &mut Self;
116
117 #[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 #[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#[derive(Clone)]
150pub struct AppState {
151 pub app: Arc<App>,
153}
154
155pub 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 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 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 #[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 let router = router.layer(Extension(AppState { app }));
214
215 tracing::info!("axum server started");
216 if config.connect_info {
217 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}