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;
17pub mod problem_details;
19
20pub use spring_macros::ProblemDetails;
21
22#[cfg(feature = "socket_io")]
23pub use { socketioxide, rmpv };
24
25pub use axum;
26pub use spring::async_trait;
27use spring::signal;
28pub use spring_macros::middlewares;
31pub use spring_macros::nest;
32
33pub use spring_macros::delete;
35pub use spring_macros::get;
36pub use spring_macros::head;
37pub use spring_macros::options;
38pub use spring_macros::patch;
39pub use spring_macros::post;
40pub use spring_macros::put;
41pub use spring_macros::route;
42pub use spring_macros::routes;
43pub use spring_macros::trace;
44
45#[cfg(feature = "socket_io")]
47pub use spring_macros::on_connection;
48#[cfg(feature = "socket_io")]
49pub use spring_macros::on_disconnect;
50#[cfg(feature = "socket_io")]
51pub use spring_macros::on_fallback;
52#[cfg(feature = "socket_io")]
53pub use spring_macros::subscribe_message;
54
55#[cfg(feature = "openapi")]
57pub use spring_macros::api_route;
58#[cfg(feature = "openapi")]
59pub use spring_macros::api_routes;
60#[cfg(feature = "openapi")]
61pub use spring_macros::delete_api;
62#[cfg(feature = "openapi")]
63pub use spring_macros::get_api;
64#[cfg(feature = "openapi")]
65pub use spring_macros::head_api;
66#[cfg(feature = "openapi")]
67pub use spring_macros::options_api;
68#[cfg(feature = "openapi")]
69pub use spring_macros::patch_api;
70#[cfg(feature = "openapi")]
71pub use spring_macros::post_api;
72#[cfg(feature = "openapi")]
73pub use spring_macros::put_api;
74#[cfg(feature = "openapi")]
75pub use spring_macros::trace_api;
76
77pub use axum::routing::MethodFilter;
79
80#[cfg(not(feature = "openapi"))]
82pub type Router = axum::Router;
83pub use axum::routing::MethodRouter;
85
86#[cfg(feature = "openapi")]
87pub use aide;
88#[cfg(feature = "openapi")]
89pub use aide::openapi::OpenApi;
90#[cfg(feature = "openapi")]
91pub type Router = aide::axum::ApiRouter;
92#[cfg(feature = "openapi")]
93pub use aide::axum::routing::ApiMethodRouter;
94
95#[cfg(feature = "openapi")]
96use aide::transform::TransformOpenApi;
97
98use anyhow::Context;
99use axum::Extension;
100use config::ServerConfig;
101use config::WebConfig;
102use spring::plugin::component::ComponentRef;
103use spring::plugin::ComponentRegistry;
104use spring::plugin::MutableComponentRegistry;
105use spring::{
106 app::{App, AppBuilder},
107 config::ConfigRegistry,
108 error::Result,
109 plugin::Plugin,
110};
111use std::{net::SocketAddr, ops::Deref, sync::Arc};
112
113#[cfg(feature = "socket_io")]
114use config::SocketIOConfig;
115
116#[cfg(feature = "openapi")]
117use crate::config::OpenApiConfig;
118
119#[cfg(feature = "openapi")]
121pub type Routers = Vec<aide::axum::ApiRouter>;
122#[cfg(not(feature = "openapi"))]
123pub type Routers = Vec<axum::Router>;
124
125pub type RouterLayer = Arc<dyn Fn(Router) -> Router + Send + Sync>;
141
142pub type RouterLayers = Vec<RouterLayer>;
144
145pub trait LayerConfigurator {
147 fn add_router_layer<F>(&mut self, layer: F) -> &mut Self
161 where
162 F: Fn(Router) -> Router + Send + Sync + 'static;
163}
164
165impl LayerConfigurator for AppBuilder {
166 fn add_router_layer<F>(&mut self, layer: F) -> &mut Self
167 where
168 F: Fn(Router) -> Router + Send + Sync + 'static,
169 {
170 if let Some(layers) = self.get_component_ref::<RouterLayers>() {
171 unsafe {
172 let raw_ptr = ComponentRef::into_raw(layers);
173 let layers = &mut *(raw_ptr as *mut RouterLayers);
174 layers.push(Arc::new(layer));
175 }
176 self
177 } else {
178 let layers: RouterLayers = vec![Arc::new(layer)];
179 self.add_component(layers)
180 }
181 }
182}
183
184#[cfg(feature = "openapi")]
186type OpenApiTransformer = fn(TransformOpenApi) -> TransformOpenApi;
187
188pub trait WebConfigurator {
190 fn add_router(&mut self, router: Router) -> &mut Self;
192
193 #[cfg(feature = "openapi")]
195 fn openapi(&mut self, openapi: OpenApi) -> &mut Self;
196
197 #[cfg(feature = "openapi")]
199 fn api_docs(&mut self, api_docs: OpenApiTransformer) -> &mut Self;
200}
201
202impl WebConfigurator for AppBuilder {
203 fn add_router(&mut self, router: Router) -> &mut Self {
204 if let Some(routers) = self.get_component_ref::<Routers>() {
205 unsafe {
206 let raw_ptr = ComponentRef::into_raw(routers);
207 let routers = &mut *(raw_ptr as *mut Routers);
208 routers.push(router);
209 }
210 self
211 } else {
212 self.add_component(vec![router])
213 }
214 }
215
216 #[cfg(feature = "openapi")]
218 fn openapi(&mut self, openapi: OpenApi) -> &mut Self {
219 self.add_component(openapi)
220 }
221
222 #[cfg(feature = "openapi")]
223 fn api_docs(&mut self, api_docs: OpenApiTransformer) -> &mut Self {
224 self.add_component(api_docs)
225 }
226}
227
228#[derive(Clone)]
230pub struct AppState {
231 pub app: Arc<App>,
233}
234
235pub struct WebPlugin;
237
238#[async_trait]
239impl Plugin for WebPlugin {
240 async fn build(&self, app: &mut AppBuilder) {
241 let config = app
242 .get_config::<WebConfig>()
243 .expect("web plugin config load failed");
244
245 #[cfg(feature = "socket_io")]
246 let socketio_config = app.get_config::<SocketIOConfig>().ok();
247
248 let routers = app.get_component_ref::<Routers>();
250 let mut router: Router = match routers {
251 Some(rs) => {
252 let mut router = Router::new();
253 for r in rs.deref().iter() {
254 router = router.merge(r.to_owned());
255 }
256 router
257 }
258 None => Router::new(),
259 };
260 if let Some(middlewares) = config.middlewares {
261 router = crate::middleware::apply_middleware(router, middlewares);
262 }
263
264 #[cfg(feature = "socket_io")]
265 if let Some(socketio_config) = socketio_config {
266 router = enable_socketio(socketio_config, app, router);
267 }
268
269 app.add_component(router);
270
271 let server_conf = config.server;
272 #[cfg(feature = "openapi")]
273 {
274 let openapi_conf = config.openapi;
275 app.add_component(openapi_conf.clone());
276 }
277
278 app.add_scheduler(move |app: Arc<App>| Box::new(Self::schedule(app, server_conf)));
279 }
280}
281
282impl WebPlugin {
283 async fn schedule(app: Arc<App>, config: ServerConfig) -> Result<String> {
284 let mut router = app.get_expect_component::<Router>();
285
286 if let Some(layers) = app.get_component_ref::<RouterLayers>() {
290 for layer_fn in layers.deref().iter() {
291 router = layer_fn(router);
292 }
293 }
294
295 let addr = SocketAddr::from((config.binding, config.port));
297 let listener = tokio::net::TcpListener::bind(addr)
298 .await
299 .with_context(|| format!("bind tcp listener failed:{addr}"))?;
300 tracing::info!("bind tcp listener: {addr}");
301
302 #[cfg(feature = "openapi")]
304 let router = {
305 let openapi_conf = app.get_expect_component::<OpenApiConfig>();
306 finish_openapi(&app, router, openapi_conf)
307 };
308
309 let mut router = router.layer(Extension(AppState { app }));
311
312 if !config.global_prefix.is_empty() {
313 router = axum::Router::new().nest(&config.global_prefix, router)
314 };
315
316
317 tracing::info!("axum server started");
318 if config.connect_info {
319 let service = router.into_make_service_with_connect_info::<SocketAddr>();
321 let server = axum::serve(listener, service);
322 if config.graceful {
323 server
324 .with_graceful_shutdown(signal::shutdown_signal("axum web server"))
325 .await
326 } else {
327 server.await
328 }
329 } else {
330 let service = router.into_make_service();
331 let server = axum::serve(listener, service);
332 if config.graceful {
333 server
334 .with_graceful_shutdown(signal::shutdown_signal("axum web server"))
335 .await
336 } else {
337 server.await
338 }
339 }
340 .context("start axum server failed")?;
341
342 Ok("axum schedule finished".to_string())
343 }
344}
345
346#[cfg(feature = "openapi")]
347pub fn enable_openapi() {
348 aide::generate::on_error(|error| {
349 tracing::error!("{error}");
350 });
351 aide::generate::extract_schemas(false);
352}
353
354#[cfg(feature = "socket_io")]
355pub fn enable_socketio(socketio_config: SocketIOConfig, app: &mut AppBuilder, router: Router) -> Router {
356 tracing::info!("Configuring SocketIO with namespace: {}", socketio_config.default_namespace);
357
358 let (layer, io) = socketioxide::SocketIo::builder()
359 .build_layer();
360
361 let ns_path = socketio_config.default_namespace.clone();
362 let ns_path_for_closure = ns_path.clone();
363 io.ns(ns_path, move |socket: socketioxide::extract::SocketRef| {
364 use spring::tracing::info;
365
366 info!(socket_id = ?socket.id, "New socket connected to namespace: {}", ns_path_for_closure);
367
368 crate::handler::auto_socketio_setup(&socket);
369 });
370
371 app.add_component(io);
372 router.layer(layer)
373}
374
375#[cfg(feature = "openapi")]
376fn finish_openapi(
377 app: &App,
378 router: aide::axum::ApiRouter,
379 openapi_conf: OpenApiConfig,
380) -> axum::Router {
381 let router = router.nest_api_service(&openapi_conf.doc_prefix, docs_routes(&openapi_conf));
382
383 let mut api = app.get_component::<OpenApi>().unwrap_or_else(|| OpenApi {
384 info: openapi_conf.info,
385 ..Default::default()
386 });
387
388 let router = if let Some(api_docs) = app.get_component::<OpenApiTransformer>() {
389 router.finish_api_with(&mut api, api_docs)
390 } else {
391 router.finish_api(&mut api)
392 };
393
394 router.layer(Extension(Arc::new(api)))
395}
396
397#[cfg(feature = "openapi")]
398pub fn docs_routes(OpenApiConfig { doc_prefix, info }: &OpenApiConfig) -> aide::axum::ApiRouter {
399 let router = aide::axum::ApiRouter::new();
400 let _openapi_path = &format!("{doc_prefix}/openapi.json");
401 let _doc_title = &info.title;
402
403 #[cfg(feature = "openapi-scalar")]
404 let router = router.route(
405 "/scalar",
406 aide::scalar::Scalar::new(_openapi_path)
407 .with_title(_doc_title)
408 .axum_route(),
409 );
410 #[cfg(feature = "openapi-redoc")]
411 let router = router.route(
412 "/redoc",
413 aide::redoc::Redoc::new(_openapi_path)
414 .with_title(_doc_title)
415 .axum_route(),
416 );
417 #[cfg(feature = "openapi-swagger")]
418 let router = router.route(
419 "/swagger",
420 aide::swagger::Swagger::new(_openapi_path)
421 .with_title(_doc_title)
422 .axum_route(),
423 );
424
425 router.route("/openapi.json", axum::routing::get(serve_docs))
426}
427
428#[cfg(feature = "openapi")]
429async fn serve_docs(Extension(api): Extension<Arc<OpenApi>>) -> impl aide::axum::IntoApiResponse {
430 axum::response::IntoResponse::into_response(axum::Json(api.as_ref()))
431}
432
433#[cfg(feature = "openapi")]
434pub fn default_transform<'a>(
435 path_item: aide::transform::TransformPathItem<'a>,
436) -> aide::transform::TransformPathItem<'a> {
437 path_item
438}