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 {rmpv, socketioxide};
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 std::{net::SocketAddr, ops::Deref, sync::Arc};
103use summer::plugin::component::ComponentRef;
104use summer::plugin::ComponentRegistry;
105use summer::plugin::MutableComponentRegistry;
106use summer::{
107    app::{App, AppBuilder},
108    config::ConfigRegistry,
109    error::Result,
110    event::EventPublisher,
111    plugin::Plugin,
112};
113
114#[cfg(feature = "socket_io")]
115use config::SocketIOConfig;
116
117#[cfg(feature = "openapi")]
118use crate::config::OpenApiConfig;
119
120/// Routers collection
121#[cfg(feature = "openapi")]
122pub type Routers = Vec<aide::axum::ApiRouter>;
123#[cfg(not(feature = "openapi"))]
124pub type Routers = Vec<axum::Router>;
125
126/// Router layer function type
127///
128/// Used to add layers (middleware) to the router before the server starts.
129/// This enables plugins to dynamically register middleware layers.
130///
131/// # Example
132///
133/// ```rust,ignore
134/// use summer_web::{Router, LayerConfigurator};
135///
136/// // In your plugin's build method:
137/// app.add_router_layer(|router: Router| {
138///     router.layer(MyMiddlewareLayer::new())
139/// });
140/// ```
141pub type RouterLayer = Arc<dyn Fn(Router) -> Router + Send + Sync>;
142
143/// Collection of router layers
144pub type RouterLayers = Vec<RouterLayer>;
145
146/// Trait for adding layers to the web router
147pub trait LayerConfigurator {
148    /// Add a layer function that will be applied to the router before the server starts.
149    ///
150    /// Layers are applied in the order they are added.
151    ///
152    /// # Example
153    ///
154    /// ```rust,ignore
155    /// use summer_web::LayerConfigurator;
156    ///
157    /// app.add_router_layer(|router| {
158    ///     router.layer(MyAuthLayer::new(state))
159    /// });
160    /// ```
161    fn add_router_layer<F>(&mut self, layer: F) -> &mut Self
162    where
163        F: Fn(Router) -> Router + Send + Sync + 'static;
164}
165
166impl LayerConfigurator for AppBuilder {
167    fn add_router_layer<F>(&mut self, layer: F) -> &mut Self
168    where
169        F: Fn(Router) -> Router + Send + Sync + 'static,
170    {
171        if let Some(layers) = self.get_component_ref::<RouterLayers>() {
172            unsafe {
173                let raw_ptr = ComponentRef::into_raw(layers);
174                let layers = &mut *(raw_ptr as *mut RouterLayers);
175                layers.push(Arc::new(layer));
176            }
177            self
178        } else {
179            let layers: RouterLayers = vec![Arc::new(layer)];
180            self.add_component(layers)
181        }
182    }
183}
184
185/// OpenAPI
186#[cfg(feature = "openapi")]
187type OpenApiTransformer = fn(TransformOpenApi) -> TransformOpenApi;
188
189/// Web Configurator
190pub trait WebConfigurator {
191    /// add route to app registry
192    fn add_router(&mut self, router: Router) -> &mut Self;
193
194    /// Initialize OpenAPI Documents
195    #[cfg(feature = "openapi")]
196    fn openapi(&mut self, openapi: OpenApi) -> &mut Self;
197
198    /// Defining OpenAPI Documents
199    #[cfg(feature = "openapi")]
200    fn api_docs(&mut self, api_docs: OpenApiTransformer) -> &mut Self;
201}
202
203impl WebConfigurator for AppBuilder {
204    fn add_router(&mut self, router: Router) -> &mut Self {
205        if let Some(routers) = self.get_component_ref::<Routers>() {
206            unsafe {
207                let raw_ptr = ComponentRef::into_raw(routers);
208                let routers = &mut *(raw_ptr as *mut Routers);
209                routers.push(router);
210            }
211            self
212        } else {
213            self.add_component(vec![router])
214        }
215    }
216
217    /// Initialize OpenAPI Documents
218    #[cfg(feature = "openapi")]
219    fn openapi(&mut self, openapi: OpenApi) -> &mut Self {
220        self.add_component(openapi)
221    }
222
223    #[cfg(feature = "openapi")]
224    fn api_docs(&mut self, api_docs: OpenApiTransformer) -> &mut Self {
225        self.add_component(api_docs)
226    }
227}
228
229/// State of App
230#[derive(Clone)]
231pub struct AppState {
232    /// App Registry Ref
233    pub app: Arc<App>,
234}
235
236/// Web Plugin Definition
237pub struct WebPlugin;
238
239pub use summer::event::{ServerProtocol, ServerStartedEvent};
240
241#[async_trait]
242impl Plugin for WebPlugin {
243    async fn build(&self, app: &mut AppBuilder) {
244        let server_conf = assemble_router(app).await;
245        app.add_scheduler(move |app: Arc<App>| Box::new(Self::schedule(app, server_conf)));
246    }
247}
248
249/// Build the merged axum router from registered [`Routers`], apply configured
250/// middleware (and optional Socket.IO layer), store the resulting [`Router`] and
251/// (when the `openapi` feature is enabled) [`OpenApiConfig`] as components, and
252/// return the [`ServerConfig`] for the caller to use (e.g. binding a TCP listener
253/// or constructing a [`axum_test::TestServer`] in tests).
254///
255/// This is the build-phase half of [`WebPlugin`]; downstream test crates can call
256/// it from a custom plugin to share the exact same router assembly.
257pub async fn assemble_router(app: &mut AppBuilder) -> ServerConfig {
258    let mut config = app
259        .get_config::<WebConfig>()
260        .expect("web plugin config load failed");
261
262    config.normalize_prefixes();
263
264    #[cfg(feature = "socket_io")]
265    let socketio_config = app.get_config::<SocketIOConfig>().ok();
266
267    // 1. collect router
268    let routers = app.get_component_ref::<Routers>();
269    let mut router: Router = match routers {
270        Some(rs) => {
271            let mut router = Router::new();
272            for r in rs.deref().iter() {
273                router = router.merge(r.to_owned());
274            }
275            router
276        }
277        None => Router::new(),
278    };
279    if let Some(middlewares) = config.middlewares {
280        router = crate::middleware::apply_middleware(router, middlewares);
281    }
282
283    #[cfg(feature = "socket_io")]
284    if let Some(socketio_config) = socketio_config {
285        router = enable_socketio(socketio_config, app, router);
286    }
287
288    app.add_component(router);
289
290    let server_conf = config.server;
291    #[cfg(feature = "openapi")]
292    {
293        let openapi_conf = config.openapi;
294        app.add_component(openapi_conf.clone());
295    }
296
297    server_conf
298}
299
300/// Finalize the router stored by [`assemble_router`]: apply registered
301/// [`RouterLayers`], finish OpenAPI documents (with the `openapi` feature),
302/// inject the [`AppState`] extension, and wrap the result under `global_prefix`
303/// when non-empty.
304///
305/// Returns a fully-prepared [`axum::Router`] ready to be served (production)
306/// or wrapped in a test transport such as [`axum_test::TestServer`].
307pub fn finalize_router(app: &Arc<App>, global_prefix: &str) -> axum::Router {
308    let mut router = app.get_expect_component::<Router>();
309
310    // Apply custom router layers registered by plugins
311    // This is done after all plugins have built,
312    // ensuring plugins that depend on other plugins can still register layers
313    if let Some(layers) = app.get_component_ref::<RouterLayers>() {
314        for layer_fn in layers.deref().iter() {
315            router = layer_fn(router);
316        }
317    }
318
319    #[cfg(feature = "openapi")]
320    let router = {
321        let openapi_conf = app.get_expect_component::<OpenApiConfig>();
322        finish_openapi(app, router, openapi_conf, global_prefix)
323    };
324
325    let mut router = router.layer(Extension(AppState { app: app.clone() }));
326
327    if !global_prefix.is_empty() {
328        router = axum::Router::new().nest(global_prefix, router)
329    };
330
331    router
332}
333
334impl WebPlugin {
335    async fn schedule(app: Arc<App>, config: ServerConfig) -> Result<String> {
336        // 1. assemble final router (layers, openapi, AppState, global prefix)
337        let router = finalize_router(&app, &config.global_prefix);
338
339        // 2. bind tcp listener
340        let addr = SocketAddr::from((config.binding, config.port));
341        let listener = tokio::net::TcpListener::bind(addr)
342            .await
343            .with_context(|| format!("bind tcp listener failed:{addr}"))?;
344        tracing::info!("bind tcp listener: {addr}");
345
346        tracing::info!("axum server started");
347        // summer-nacos listens for this to register the HTTP endpoint (with grpc if both plugins run).
348        app.publish(ServerStartedEvent {
349            addr,
350            protocol: ServerProtocol::Http,
351        })
352        .await?;
353        if config.connect_info {
354            // with client connect info
355            let service = router.into_make_service_with_connect_info::<SocketAddr>();
356            let server = axum::serve(listener, service);
357            if config.graceful {
358                server
359                    .with_graceful_shutdown(signal::shutdown_signal("axum web server"))
360                    .await
361            } else {
362                server.await
363            }
364        } else {
365            let service = router.into_make_service();
366            let server = axum::serve(listener, service);
367            if config.graceful {
368                server
369                    .with_graceful_shutdown(signal::shutdown_signal("axum web server"))
370                    .await
371            } else {
372                server.await
373            }
374        }
375        .context("start axum server failed")?;
376
377        Ok("axum schedule finished".to_string())
378    }
379}
380
381#[cfg(feature = "openapi")]
382pub fn enable_openapi() {
383    aide::generate::on_error(|error| {
384        if matches!(error, aide::Error::OperationExists(..)) {
385            tracing::warn!("{error}");
386        } else {
387            tracing::error!("{error}");
388        }
389    });
390    aide::generate::extract_schemas(false);
391}
392
393#[cfg(feature = "socket_io")]
394pub fn enable_socketio(
395    socketio_config: SocketIOConfig,
396    app: &mut AppBuilder,
397    router: Router,
398) -> Router {
399    tracing::info!(
400        "Configuring SocketIO with namespace: {}",
401        socketio_config.default_namespace
402    );
403
404    let (layer, io) = socketioxide::SocketIo::builder().build_layer();
405
406    let ns_path = socketio_config.default_namespace.clone();
407    let ns_path_for_closure = ns_path.clone();
408    io.ns(ns_path, move |socket: socketioxide::extract::SocketRef| async move {
409        use summer::tracing::info;
410
411        info!(socket_id = ?socket.id, "New socket connected to namespace: {}", ns_path_for_closure);
412
413        crate::handler::auto_socketio_setup(&socket);
414    });
415
416    app.add_component(io);
417    router.layer(layer)
418}
419
420#[cfg(feature = "openapi")]
421fn finish_openapi(
422    app: &App,
423    router: aide::axum::ApiRouter,
424    openapi_conf: OpenApiConfig,
425    global_prefix: &str,
426) -> axum::Router {
427    let router = router.nest_api_service(
428        &openapi_conf.doc_prefix,
429        docs_routes(&openapi_conf, global_prefix),
430    );
431
432    let mut api = app.get_component::<OpenApi>().unwrap_or_else(|| OpenApi {
433        info: openapi_conf.info,
434        ..Default::default()
435    });
436
437    let router = if let Some(api_docs) = app.get_component::<OpenApiTransformer>() {
438        router.finish_api_with(&mut api, api_docs)
439    } else {
440        router.finish_api(&mut api)
441    };
442
443    // Prepend global_prefix to all API paths in the OpenAPI spec,
444    // since finish_api() generates paths before nest(global_prefix) is applied.
445    if !global_prefix.is_empty() {
446        if let Some(ref mut paths) = api.paths {
447            let old_paths = std::mem::take(&mut paths.paths);
448            for (path, item) in old_paths {
449                paths.paths.insert(format!("{global_prefix}{path}"), item);
450            }
451        }
452    }
453
454    router.layer(Extension(Arc::new(api)))
455}
456
457#[cfg(feature = "openapi")]
458pub fn docs_routes(
459    OpenApiConfig { doc_prefix, info }: &OpenApiConfig,
460    global_prefix: &str,
461) -> aide::axum::ApiRouter {
462    let router = aide::axum::ApiRouter::new();
463    let _openapi_path = &format!("{global_prefix}{doc_prefix}/openapi.json");
464    let _doc_title = &info.title;
465
466    #[cfg(feature = "openapi-scalar")]
467    let router = router.route(
468        "/scalar",
469        aide::scalar::Scalar::new(_openapi_path)
470            .with_title(_doc_title)
471            .axum_route(),
472    );
473    #[cfg(feature = "openapi-redoc")]
474    let router = router.route(
475        "/redoc",
476        aide::redoc::Redoc::new(_openapi_path)
477            .with_title(_doc_title)
478            .axum_route(),
479    );
480    #[cfg(feature = "openapi-swagger")]
481    let router = router.route(
482        "/swagger",
483        aide::swagger::Swagger::new(_openapi_path)
484            .with_title(_doc_title)
485            .axum_route(),
486    );
487
488    router.route("/openapi.json", axum::routing::get(serve_docs))
489}
490
491#[cfg(feature = "openapi")]
492async fn serve_docs(Extension(api): Extension<Arc<OpenApi>>) -> impl aide::axum::IntoApiResponse {
493    axum::response::IntoResponse::into_response(axum::Json(api.as_ref()))
494}
495
496#[cfg(feature = "openapi")]
497pub fn default_transform<'a>(
498    path_item: aide::transform::TransformPathItem<'a>,
499) -> aide::transform::TransformPathItem<'a> {
500    path_item
501}