Skip to main content

summer_web/
lib.rs

1//! [![summer-rs](https://img.shields.io/github/stars/summer-rs/summer-rs)](https://summer-rs.github.io/docs/plugins/summer-web)
2#![doc = include_str!("../README.md")]
3#![doc(html_favicon_url = "https://summer-rs.github.io/favicon.ico")]
4#![doc(html_logo_url = "https://summer-rs.github.io/logo.svg")]
5
6/// summer-web config
7pub mod config;
8/// summer-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/// RFC 7807 Problem Details for HTTP APIs
18pub mod problem_details;
19
20pub use summer_macros::ProblemDetails;
21
22#[cfg(feature = "socket_io")]
23pub use { socketioxide, rmpv };
24
25pub use axum;
26pub use summer::async_trait;
27use summer::signal;
28/////////////////web-macros/////////////////////
29/// To use these Procedural Macros, you need to add `summer-web` dependency
30pub use summer_macros::middlewares;
31pub use summer_macros::nest;
32
33// route macros
34pub use summer_macros::delete;
35pub use summer_macros::get;
36pub use summer_macros::head;
37pub use summer_macros::options;
38pub use summer_macros::patch;
39pub use summer_macros::post;
40pub use summer_macros::put;
41pub use summer_macros::route;
42pub use summer_macros::routes;
43pub use summer_macros::trace;
44
45/// SocketIO macros
46#[cfg(feature = "socket_io")]
47pub use summer_macros::on_connection;
48#[cfg(feature = "socket_io")]
49pub use summer_macros::on_disconnect;
50#[cfg(feature = "socket_io")]
51pub use summer_macros::on_fallback;
52#[cfg(feature = "socket_io")]
53pub use summer_macros::subscribe_message;
54
55/// OpenAPI macros
56#[cfg(feature = "openapi")]
57pub use summer_macros::api_route;
58#[cfg(feature = "openapi")]
59pub use summer_macros::api_routes;
60#[cfg(feature = "openapi")]
61pub use summer_macros::delete_api;
62#[cfg(feature = "openapi")]
63pub use summer_macros::get_api;
64#[cfg(feature = "openapi")]
65pub use summer_macros::head_api;
66#[cfg(feature = "openapi")]
67pub use summer_macros::options_api;
68#[cfg(feature = "openapi")]
69pub use summer_macros::patch_api;
70#[cfg(feature = "openapi")]
71pub use summer_macros::post_api;
72#[cfg(feature = "openapi")]
73pub use summer_macros::put_api;
74#[cfg(feature = "openapi")]
75pub use summer_macros::trace_api;
76
77/// axum::routing::MethodFilter re-export
78pub use axum::routing::MethodFilter;
79
80/// Router with AppState
81#[cfg(not(feature = "openapi"))]
82pub type Router = axum::Router;
83/// MethodRouter with AppState
84pub 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 summer::plugin::component::ComponentRef;
103use summer::plugin::ComponentRegistry;
104use summer::plugin::MutableComponentRegistry;
105use summer::{
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/// Routers collection
120#[cfg(feature = "openapi")]
121pub type Routers = Vec<aide::axum::ApiRouter>;
122#[cfg(not(feature = "openapi"))]
123pub type Routers = Vec<axum::Router>;
124
125/// Router layer function type
126///
127/// Used to add layers (middleware) to the router before the server starts.
128/// This enables plugins to dynamically register middleware layers.
129///
130/// # Example
131///
132/// ```rust,ignore
133/// use summer_web::{Router, LayerConfigurator};
134///
135/// // In your plugin's build method:
136/// app.add_router_layer(|router: Router| {
137///     router.layer(MyMiddlewareLayer::new())
138/// });
139/// ```
140pub type RouterLayer = Arc<dyn Fn(Router) -> Router + Send + Sync>;
141
142/// Collection of router layers
143pub type RouterLayers = Vec<RouterLayer>;
144
145/// Trait for adding layers to the web router
146pub trait LayerConfigurator {
147    /// Add a layer function that will be applied to the router before the server starts.
148    ///
149    /// Layers are applied in the order they are added.
150    ///
151    /// # Example
152    ///
153    /// ```rust,ignore
154    /// use summer_web::LayerConfigurator;
155    ///
156    /// app.add_router_layer(|router| {
157    ///     router.layer(MyAuthLayer::new(state))
158    /// });
159    /// ```
160    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/// OpenAPI
185#[cfg(feature = "openapi")]
186type OpenApiTransformer = fn(TransformOpenApi) -> TransformOpenApi;
187
188/// Web Configurator
189pub trait WebConfigurator {
190    /// add route to app registry
191    fn add_router(&mut self, router: Router) -> &mut Self;
192
193    /// Initialize OpenAPI Documents
194    #[cfg(feature = "openapi")]
195    fn openapi(&mut self, openapi: OpenApi) -> &mut Self;
196
197    /// Defining OpenAPI Documents
198    #[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    /// Initialize OpenAPI Documents
217    #[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/// State of App
229#[derive(Clone)]
230pub struct AppState {
231    /// App Registry Ref
232    pub app: Arc<App>,
233}
234
235/// Web Plugin Definition
236pub struct WebPlugin;
237
238#[async_trait]
239impl Plugin for WebPlugin {
240    async fn build(&self, app: &mut AppBuilder) {
241        let mut config = app
242            .get_config::<WebConfig>()
243            .expect("web plugin config load failed");
244
245        config.normalize_prefixes();
246
247        #[cfg(feature = "socket_io")]
248        let socketio_config = app.get_config::<SocketIOConfig>().ok();
249
250        // 1. collect router
251        let routers = app.get_component_ref::<Routers>();
252        let mut router: Router = match routers {
253            Some(rs) => {
254                let mut router = Router::new();
255                for r in rs.deref().iter() {
256                    router = router.merge(r.to_owned());
257                }
258                router
259            }
260            None => Router::new(),
261        };
262        if let Some(middlewares) = config.middlewares {
263            router = crate::middleware::apply_middleware(router, middlewares);
264        }
265
266        #[cfg(feature = "socket_io")]
267        if let Some(socketio_config) = socketio_config {
268            router =  enable_socketio(socketio_config, app, router);
269        }
270
271        app.add_component(router);
272
273        let server_conf = config.server;
274        #[cfg(feature = "openapi")]
275        {
276            let openapi_conf = config.openapi;
277            app.add_component(openapi_conf.clone());
278        }
279
280        app.add_scheduler(move |app: Arc<App>| Box::new(Self::schedule(app, server_conf)));
281    }
282}
283
284impl WebPlugin {
285    async fn schedule(app: Arc<App>, config: ServerConfig) -> Result<String> {
286        let mut router = app.get_expect_component::<Router>();
287
288        // Apply custom router layers registered by plugins
289        // This is done in schedule() after all plugins have built,
290        // ensuring plugins that depend on other plugins can still register layers
291        if let Some(layers) = app.get_component_ref::<RouterLayers>() {
292            for layer_fn in layers.deref().iter() {
293                router = layer_fn(router);
294            }
295        }
296
297        // 2. bind tcp listener
298        let addr = SocketAddr::from((config.binding, config.port));
299        let listener = tokio::net::TcpListener::bind(addr)
300            .await
301            .with_context(|| format!("bind tcp listener failed:{addr}"))?;
302        tracing::info!("bind tcp listener: {addr}");
303
304        // 3. openapi
305        #[cfg(feature = "openapi")]
306        let router = {
307            let openapi_conf = app.get_expect_component::<OpenApiConfig>();
308            finish_openapi(&app, router, openapi_conf, &config.global_prefix)
309        };
310
311        // 4. axum server
312        let mut router = router.layer(Extension(AppState { app }));
313
314        if !config.global_prefix.is_empty() {
315            router = axum::Router::new().nest(&config.global_prefix, router)
316        };
317
318
319        tracing::info!("axum server started");
320        if config.connect_info {
321            // with client connect info
322            let service = router.into_make_service_with_connect_info::<SocketAddr>();
323            let server = axum::serve(listener, service);
324            if config.graceful {
325                server
326                    .with_graceful_shutdown(signal::shutdown_signal("axum web server"))
327                    .await
328            } else {
329                server.await
330            }
331        } else {
332            let service = router.into_make_service();
333            let server = axum::serve(listener, service);
334            if config.graceful {
335                server
336                    .with_graceful_shutdown(signal::shutdown_signal("axum web server"))
337                    .await
338            } else {
339                server.await
340            }
341        }
342        .context("start axum server failed")?;
343
344        Ok("axum schedule finished".to_string())
345    }
346}
347
348#[cfg(feature = "openapi")]
349pub fn enable_openapi() {
350    aide::generate::on_error(|error| {
351        tracing::error!("{error}");
352    });
353    aide::generate::extract_schemas(false);
354}
355
356#[cfg(feature = "socket_io")]
357pub fn enable_socketio(socketio_config: SocketIOConfig, app: &mut AppBuilder, router: Router) -> Router {
358    tracing::info!("Configuring SocketIO with namespace: {}", socketio_config.default_namespace);
359    
360    let (layer, io) = socketioxide::SocketIo::builder()
361        .build_layer();
362    
363    let ns_path = socketio_config.default_namespace.clone();
364    let ns_path_for_closure = ns_path.clone();
365    io.ns(ns_path, move |socket: socketioxide::extract::SocketRef| async move {
366        use summer::tracing::info;
367        
368        info!(socket_id = ?socket.id, "New socket connected to namespace: {}", ns_path_for_closure);
369        
370        crate::handler::auto_socketio_setup(&socket);
371    });
372    
373    app.add_component(io);
374    router.layer(layer)
375}
376
377#[cfg(feature = "openapi")]
378fn finish_openapi(
379    app: &App,
380    router: aide::axum::ApiRouter,
381    openapi_conf: OpenApiConfig,
382    global_prefix: &str,
383) -> axum::Router {
384    let router = router.nest_api_service(&openapi_conf.doc_prefix, docs_routes(&openapi_conf, global_prefix));
385
386    let mut api = app.get_component::<OpenApi>().unwrap_or_else(|| OpenApi {
387        info: openapi_conf.info,
388        ..Default::default()
389    });
390
391    let router = if let Some(api_docs) = app.get_component::<OpenApiTransformer>() {
392        router.finish_api_with(&mut api, api_docs)
393    } else {
394        router.finish_api(&mut api)
395    };
396
397    // Prepend global_prefix to all API paths in the OpenAPI spec,
398    // since finish_api() generates paths before nest(global_prefix) is applied.
399    if !global_prefix.is_empty() {
400        if let Some(ref mut paths) = api.paths {
401            let old_paths = std::mem::take(&mut paths.paths);
402            for (path, item) in old_paths {
403                paths.paths.insert(format!("{global_prefix}{path}"), item);
404            }
405        }
406    }
407
408    router.layer(Extension(Arc::new(api)))
409}
410
411#[cfg(feature = "openapi")]
412pub fn docs_routes(OpenApiConfig { doc_prefix, info }: &OpenApiConfig, global_prefix: &str) -> aide::axum::ApiRouter {
413    let router = aide::axum::ApiRouter::new();
414    let _openapi_path = &format!("{global_prefix}{doc_prefix}/openapi.json");
415    let _doc_title = &info.title;
416
417    #[cfg(feature = "openapi-scalar")]
418    let router = router.route(
419        "/scalar",
420        aide::scalar::Scalar::new(_openapi_path)
421            .with_title(_doc_title)
422            .axum_route(),
423    );
424    #[cfg(feature = "openapi-redoc")]
425    let router = router.route(
426        "/redoc",
427        aide::redoc::Redoc::new(_openapi_path)
428            .with_title(_doc_title)
429            .axum_route(),
430    );
431    #[cfg(feature = "openapi-swagger")]
432    let router = router.route(
433        "/swagger",
434        aide::swagger::Swagger::new(_openapi_path)
435            .with_title(_doc_title)
436            .axum_route(),
437    );
438
439    router.route("/openapi.json", axum::routing::get(serve_docs))
440}
441
442#[cfg(feature = "openapi")]
443async fn serve_docs(Extension(api): Extension<Arc<OpenApi>>) -> impl aide::axum::IntoApiResponse {
444    axum::response::IntoResponse::into_response(axum::Json(api.as_ref()))
445}
446
447#[cfg(feature = "openapi")]
448pub fn default_transform<'a>(
449    path_item: aide::transform::TransformPathItem<'a>,
450) -> aide::transform::TransformPathItem<'a> {
451    path_item
452}