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 trait HttpStatusCode {
21 fn status_code(&self) -> axum::http::StatusCode;
22}
23
24pub use spring_macros::ProblemDetails;
25
26#[cfg(feature = "socket_io")]
27pub use { socketioxide, rmpv };
28
29pub use axum;
30pub use spring::async_trait;
31use spring::signal;
32pub use spring_macros::middlewares;
35pub use spring_macros::nest;
36
37pub use spring_macros::delete;
39pub use spring_macros::get;
40pub use spring_macros::head;
41pub use spring_macros::options;
42pub use spring_macros::patch;
43pub use spring_macros::post;
44pub use spring_macros::put;
45pub use spring_macros::route;
46pub use spring_macros::routes;
47pub use spring_macros::trace;
48
49#[cfg(feature = "socket_io")]
51pub use spring_macros::on_connection;
52#[cfg(feature = "socket_io")]
53pub use spring_macros::on_disconnect;
54#[cfg(feature = "socket_io")]
55pub use spring_macros::on_fallback;
56#[cfg(feature = "socket_io")]
57pub use spring_macros::subscribe_message;
58
59#[cfg(feature = "openapi")]
61pub use spring_macros::api_route;
62#[cfg(feature = "openapi")]
63pub use spring_macros::api_routes;
64#[cfg(feature = "openapi")]
65pub use spring_macros::delete_api;
66#[cfg(feature = "openapi")]
67pub use spring_macros::get_api;
68#[cfg(feature = "openapi")]
69pub use spring_macros::head_api;
70#[cfg(feature = "openapi")]
71pub use spring_macros::options_api;
72#[cfg(feature = "openapi")]
73pub use spring_macros::patch_api;
74#[cfg(feature = "openapi")]
75pub use spring_macros::post_api;
76#[cfg(feature = "openapi")]
77pub use spring_macros::put_api;
78#[cfg(feature = "openapi")]
79pub use spring_macros::trace_api;
80
81pub use axum::routing::MethodFilter;
83
84#[cfg(not(feature = "openapi"))]
86pub type Router = axum::Router;
87pub use axum::routing::MethodRouter;
89
90#[cfg(feature = "openapi")]
91pub use aide;
92#[cfg(feature = "openapi")]
93pub use aide::openapi::OpenApi;
94#[cfg(feature = "openapi")]
95pub type Router = aide::axum::ApiRouter;
96#[cfg(feature = "openapi")]
97pub use aide::axum::routing::ApiMethodRouter;
98
99#[cfg(feature = "openapi")]
100use aide::transform::TransformOpenApi;
101
102use anyhow::Context;
103use axum::Extension;
104use config::ServerConfig;
105use config::WebConfig;
106use spring::plugin::component::ComponentRef;
107use spring::plugin::ComponentRegistry;
108use spring::plugin::MutableComponentRegistry;
109use spring::{
110 app::{App, AppBuilder},
111 config::ConfigRegistry,
112 error::Result,
113 plugin::Plugin,
114};
115use std::{net::SocketAddr, ops::Deref, sync::Arc};
116
117#[cfg(feature = "socket_io")]
118use config::SocketIOConfig;
119
120#[cfg(feature = "openapi")]
121use crate::config::OpenApiConfig;
122
123#[cfg(feature = "openapi")]
125pub type Routers = Vec<aide::axum::ApiRouter>;
126#[cfg(not(feature = "openapi"))]
127pub type Routers = Vec<axum::Router>;
128
129#[cfg(feature = "openapi")]
131type OpenApiTransformer = fn(TransformOpenApi) -> TransformOpenApi;
132
133pub trait WebConfigurator {
135 fn add_router(&mut self, router: Router) -> &mut Self;
137
138 #[cfg(feature = "openapi")]
140 fn openapi(&mut self, openapi: OpenApi) -> &mut Self;
141
142 #[cfg(feature = "openapi")]
144 fn api_docs(&mut self, api_docs: OpenApiTransformer) -> &mut Self;
145}
146
147impl WebConfigurator for AppBuilder {
148 fn add_router(&mut self, router: Router) -> &mut Self {
149 if let Some(routers) = self.get_component_ref::<Routers>() {
150 unsafe {
151 let raw_ptr = ComponentRef::into_raw(routers);
152 let routers = &mut *(raw_ptr as *mut Routers);
153 routers.push(router);
154 }
155 self
156 } else {
157 self.add_component(vec![router])
158 }
159 }
160
161 #[cfg(feature = "openapi")]
163 fn openapi(&mut self, openapi: OpenApi) -> &mut Self {
164 self.add_component(openapi)
165 }
166
167 #[cfg(feature = "openapi")]
168 fn api_docs(&mut self, api_docs: OpenApiTransformer) -> &mut Self {
169 self.add_component(api_docs)
170 }
171}
172
173#[derive(Clone)]
175pub struct AppState {
176 pub app: Arc<App>,
178}
179
180pub struct WebPlugin;
182
183#[async_trait]
184impl Plugin for WebPlugin {
185 async fn build(&self, app: &mut AppBuilder) {
186 let config = app
187 .get_config::<WebConfig>()
188 .expect("web plugin config load failed");
189
190 #[cfg(feature = "socket_io")]
191 let socketio_config = app.get_config::<SocketIOConfig>().ok();
192
193 let routers = app.get_component_ref::<Routers>();
195 let mut router: Router = match routers {
196 Some(rs) => {
197 let mut router = Router::new();
198 for r in rs.deref().iter() {
199 router = router.merge(r.to_owned());
200 }
201 router
202 }
203 None => Router::new(),
204 };
205 if let Some(middlewares) = config.middlewares {
206 router = crate::middleware::apply_middleware(router, middlewares);
207 }
208
209 #[cfg(feature = "socket_io")]
210 if let Some(socketio_config) = socketio_config {
211 router = enable_socketio(socketio_config, app, router);
212 }
213
214 app.add_component(router);
215
216 let server_conf = config.server;
217 #[cfg(feature = "openapi")]
218 {
219 let openapi_conf = config.openapi;
220 app.add_component(openapi_conf.clone());
221 }
222
223 app.add_scheduler(move |app: Arc<App>| Box::new(Self::schedule(app, server_conf)));
224 }
225}
226
227impl WebPlugin {
228 async fn schedule(app: Arc<App>, config: ServerConfig) -> Result<String> {
229 let router = app.get_expect_component::<Router>();
230
231 let addr = SocketAddr::from((config.binding, config.port));
233 let listener = tokio::net::TcpListener::bind(addr)
234 .await
235 .with_context(|| format!("bind tcp listener failed:{addr}"))?;
236 tracing::info!("bind tcp listener: {addr}");
237
238 #[cfg(feature = "openapi")]
240 let router = {
241 let openapi_conf = app.get_expect_component::<OpenApiConfig>();
242 finish_openapi(&app, router, openapi_conf)
243 };
244
245 let router = router.layer(Extension(AppState { app }));
247
248 tracing::info!("axum server started");
249 if config.connect_info {
250 let service = router.into_make_service_with_connect_info::<SocketAddr>();
252 let server = axum::serve(listener, service);
253 if config.graceful {
254 server
255 .with_graceful_shutdown(signal::shutdown_signal("axum web server"))
256 .await
257 } else {
258 server.await
259 }
260 } else {
261 let service = router.into_make_service();
262 let server = axum::serve(listener, service);
263 if config.graceful {
264 server
265 .with_graceful_shutdown(signal::shutdown_signal("axum web server"))
266 .await
267 } else {
268 server.await
269 }
270 }
271 .context("start axum server failed")?;
272
273 Ok("axum schedule finished".to_string())
274 }
275}
276
277#[cfg(feature = "openapi")]
278pub fn enable_openapi() {
279 aide::generate::on_error(|error| {
280 tracing::error!("{error}");
281 });
282 aide::generate::extract_schemas(false);
283}
284
285#[cfg(feature = "socket_io")]
286pub fn enable_socketio(socketio_config: SocketIOConfig, app: &mut AppBuilder, router: Router) -> Router {
287 tracing::info!("Configuring SocketIO with namespace: {}", socketio_config.default_namespace);
288
289 let (layer, io) = socketioxide::SocketIo::builder()
290 .build_layer();
291
292 let ns_path = socketio_config.default_namespace.clone();
293 let ns_path_for_closure = ns_path.clone();
294 io.ns(ns_path, move |socket: socketioxide::extract::SocketRef| {
295 use spring::tracing::info;
296
297 info!(socket_id = ?socket.id, "New socket connected to namespace: {}", ns_path_for_closure);
298
299 crate::handler::auto_socketio_setup(&socket);
300 });
301
302 app.add_component(io);
303 router.layer(layer)
304}
305
306#[cfg(feature = "openapi")]
307fn finish_openapi(
308 app: &App,
309 router: aide::axum::ApiRouter,
310 openapi_conf: OpenApiConfig,
311) -> axum::Router {
312 let router = router.nest_api_service(&openapi_conf.doc_prefix, docs_routes(&openapi_conf));
313
314 let mut api = app.get_component::<OpenApi>().unwrap_or_else(|| OpenApi {
315 info: openapi_conf.info,
316 ..Default::default()
317 });
318
319 let router = if let Some(api_docs) = app.get_component::<OpenApiTransformer>() {
320 router.finish_api_with(&mut api, api_docs)
321 } else {
322 router.finish_api(&mut api)
323 };
324
325 router.layer(Extension(Arc::new(api)))
326}
327
328#[cfg(feature = "openapi")]
329pub fn docs_routes(OpenApiConfig { doc_prefix, info }: &OpenApiConfig) -> aide::axum::ApiRouter {
330 let router = aide::axum::ApiRouter::new();
331 let _openapi_path = &format!("{doc_prefix}/openapi.json");
332 let _doc_title = &info.title;
333
334 #[cfg(feature = "openapi-scalar")]
335 let router = router.route(
336 "/scalar",
337 aide::scalar::Scalar::new(_openapi_path)
338 .with_title(_doc_title)
339 .axum_route(),
340 );
341 #[cfg(feature = "openapi-redoc")]
342 let router = router.route(
343 "/redoc",
344 aide::redoc::Redoc::new(_openapi_path)
345 .with_title(_doc_title)
346 .axum_route(),
347 );
348 #[cfg(feature = "openapi-swagger")]
349 let router = router.route(
350 "/swagger",
351 aide::swagger::Swagger::new(_openapi_path)
352 .with_title(_doc_title)
353 .axum_route(),
354 );
355
356 router.route("/openapi.json", axum::routing::get(serve_docs))
357}
358
359#[cfg(feature = "openapi")]
360async fn serve_docs(Extension(api): Extension<Arc<OpenApi>>) -> impl aide::axum::IntoApiResponse {
361 axum::response::IntoResponse::into_response(axum::Json(api.as_ref()))
362}
363
364#[cfg(feature = "openapi")]
365pub fn default_transform<'a>(
366 path_item: aide::transform::TransformPathItem<'a>,
367) -> aide::transform::TransformPathItem<'a> {
368 path_item
369}