Skip to main content

rust_web_server/state/
mod.rs

1//! Shared application state and state-aware routing.
2//!
3//! [`AppWithState<S>`] combines a typed state value (database pools, config,
4//! caches) with route registration.  Routes are tried first; requests that do
5//! not match fall through to the built-in [`App`] controller chain (static
6//! files, healthz, metrics, …).
7//!
8//! State is stored as an [`Arc<S>`] and shared across all handlers. Handlers
9//! receive an immutable `&S` reference alongside the request context.
10//!
11//! # Example
12//!
13//! ```rust,no_run
14//! use rust_web_server::state::AppWithState;
15//! use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
16//! use rust_web_server::range::Range;
17//! use rust_web_server::mime_type::MimeType;
18//! use rust_web_server::core::New;
19//!
20//! struct AppState {
21//!     greeting: String,
22//! }
23//!
24//! let app = AppWithState::new(AppState { greeting: "Hello".to_string() })
25//!     .get("/greet", |_req, _params, _conn, state| {
26//!         let mut r = Response::new();
27//!         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
28//!         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
29//!         r.content_range_list = vec![
30//!             Range::get_content_range(
31//!                 state.greeting.as_bytes().to_vec(),
32//!                 MimeType::TEXT_PLAIN.to_string(),
33//!             )
34//!         ];
35//!         r
36//!     })
37//!     .get("/users/:id", |_req, params, _conn, state| {
38//!         let id = params.get("id").unwrap_or("?");
39//!         let body = format!("{}, user {}!", state.greeting, id);
40//!         let mut r = Response::new();
41//!         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
42//!         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
43//!         r.content_range_list = vec![
44//!             Range::get_content_range(body.into_bytes(), MimeType::TEXT_PLAIN.to_string())
45//!         ];
46//!         r
47//!     });
48//! ```
49
50#[cfg(test)]
51mod tests;
52
53use std::sync::Arc;
54
55use crate::app::App;
56use crate::application::Application;
57use crate::core::New;
58use crate::middleware::{Middleware, WithMiddleware};
59use crate::request::Request;
60use crate::response::Response;
61use crate::router::{PathParams, Router};
62use crate::server::ConnectionInfo;
63use crate::server_config::ServerConfig;
64#[cfg(feature = "openapi")]
65use crate::mime_type::MimeType;
66#[cfg(feature = "openapi")]
67use crate::range::Range;
68#[cfg(feature = "openapi")]
69use crate::response::STATUS_CODE_REASON_PHRASE;
70
71/// An [`Application`] that combines user-defined state-aware routes with the
72/// built-in [`App`] controller chain as a fallback.
73///
74/// Routes are matched in registration order. The first match wins; unmatched
75/// requests are forwarded to [`App`] (static files, health probes, etc.).
76#[derive(Clone)]
77pub struct AppWithState<S> {
78    state: Arc<S>,
79    router: Router,
80    /// When `Some`, the fallback `App` is pinned to this config (see
81    /// [`App::with_config`]); when `None`, the fallback reads
82    /// `RWS_CONFIG_*` env vars per request via `App::new()`, same as `App`'s
83    /// own default.
84    config: Option<Arc<ServerConfig>>,
85}
86
87impl<S: Send + Sync + 'static> AppWithState<S> {
88    /// Create a new `AppWithState` wrapping `state`.
89    ///
90    /// `state` is stored behind an `Arc` so it can be shared across threads
91    /// without cloning. Register routes with the builder methods.
92    pub fn new(state: S) -> Self {
93        AppWithState {
94            state: Arc::new(state),
95            router: Router::new(),
96            config: None,
97        }
98    }
99
100    /// Pin the fallback [`App`] (used for any request this app's own routes
101    /// don't match) to an explicit [`ServerConfig`], instead of reading
102    /// `RWS_CONFIG_*` environment variables per request.
103    ///
104    /// Mirrors [`App::with_config`] — same rationale: safe for parallel
105    /// tests (no `test_env::lock()` needed) and lets multiple
106    /// differently-configured instances coexist in one process.
107    pub fn with_config(mut self, config: ServerConfig) -> Self {
108        self.config = Some(Arc::new(config));
109        self
110    }
111
112    /// Return a reference to the shared state.
113    pub fn state(&self) -> &S {
114        &self.state
115    }
116
117    /// The fallback `App` for requests this app's own routes don't match —
118    /// pinned to `self.config` if set, otherwise `App::new()`'s default
119    /// per-request env read.
120    fn fallback_app(&self) -> App {
121        match &self.config {
122            Some(c) => App::with_config((**c).clone()),
123            None => App::new(),
124        }
125    }
126
127    /// Register a `GET` handler for `pattern`.
128    pub fn get<F>(mut self, pattern: &str, handler: F) -> Self
129    where
130        F: Fn(&Request, &PathParams, &ConnectionInfo, &S) -> Response + Send + Sync + 'static,
131    {
132        let state = Arc::clone(&self.state);
133        self.router = self.router.get(pattern, move |req, params, conn| {
134            handler(req, params, conn, &state)
135        });
136        self
137    }
138
139    /// Register a `POST` handler for `pattern`.
140    pub fn post<F>(mut self, pattern: &str, handler: F) -> Self
141    where
142        F: Fn(&Request, &PathParams, &ConnectionInfo, &S) -> Response + Send + Sync + 'static,
143    {
144        let state = Arc::clone(&self.state);
145        self.router = self.router.post(pattern, move |req, params, conn| {
146            handler(req, params, conn, &state)
147        });
148        self
149    }
150
151    /// Register a `PUT` handler for `pattern`.
152    pub fn put<F>(mut self, pattern: &str, handler: F) -> Self
153    where
154        F: Fn(&Request, &PathParams, &ConnectionInfo, &S) -> Response + Send + Sync + 'static,
155    {
156        let state = Arc::clone(&self.state);
157        self.router = self.router.put(pattern, move |req, params, conn| {
158            handler(req, params, conn, &state)
159        });
160        self
161    }
162
163    /// Register a `PATCH` handler for `pattern`.
164    pub fn patch<F>(mut self, pattern: &str, handler: F) -> Self
165    where
166        F: Fn(&Request, &PathParams, &ConnectionInfo, &S) -> Response + Send + Sync + 'static,
167    {
168        let state = Arc::clone(&self.state);
169        self.router = self.router.patch(pattern, move |req, params, conn| {
170            handler(req, params, conn, &state)
171        });
172        self
173    }
174
175    /// Register a `DELETE` handler for `pattern`.
176    pub fn delete<F>(mut self, pattern: &str, handler: F) -> Self
177    where
178        F: Fn(&Request, &PathParams, &ConnectionInfo, &S) -> Response + Send + Sync + 'static,
179    {
180        let state = Arc::clone(&self.state);
181        self.router = self.router.delete(pattern, move |req, params, conn| {
182            handler(req, params, conn, &state)
183        });
184        self
185    }
186
187    /// Return a snapshot of all registered routes as `(method, pattern)` pairs.
188    pub fn route_entries(&self) -> Vec<crate::router::RouteInfo> {
189        self.router.route_entries()
190    }
191
192    /// Attach an MCP server to this application. Requests that do not match
193    /// the MCP endpoint (`POST /mcp`) are forwarded to `self`, so all
194    /// previously registered routes remain active.
195    ///
196    /// ```rust,no_run
197    /// use rust_web_server::app::App;
198    /// use rust_web_server::mcp::{McpContent, extract_arg};
199    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
200    /// use rust_web_server::core::New;
201    ///
202    /// struct Db { url: String }
203    ///
204    /// let app = App::with_state(Db { url: "postgres://localhost/mydb".to_string() })
205    ///     .get("/api/users", |_req, _params, _conn, _db| {
206    ///         let mut r = Response::new();
207    ///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
208    ///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
209    ///         r
210    ///     })
211    ///     .mcp("my-server", "1.0")
212    ///     .tool("list_users", "List all users", "{}", |_| {
213    ///         Ok(McpContent::json(r#"[{"id":1,"name":"Alice"}]"#))
214    ///     });
215    /// ```
216    pub fn mcp(self, name: impl Into<String>, version: impl Into<String>) -> crate::mcp::McpServer {
217        crate::mcp::McpServer::new(name, version).wrap(self)
218    }
219
220    /// Wrap this application in a middleware layer.
221    ///
222    /// Enables fluent composition:
223    ///
224    /// ```rust,no_run
225    /// use rust_web_server::app::App;
226    /// use rust_web_server::core::New;
227    /// use rust_web_server::middleware::RateLimitLayer;
228    /// use rust_web_server::response::Response;
229    ///
230    /// let app = App::with_state(())
231    ///     .get("/ping", |_, _, _, _| Response::new())
232    ///     .wrap(RateLimitLayer);
233    /// ```
234    pub fn wrap<M: Middleware + 'static>(self, layer: M) -> WithMiddleware<AppWithState<S>> {
235        WithMiddleware::new(self).wrap(layer)
236    }
237
238    /// Add `GET /openapi.json` (a generated OpenAPI 3.0 document covering
239    /// every route registered so far) and `GET /docs` (Swagger UI, loaded
240    /// from a CDN, pointed at `/openapi.json`).
241    ///
242    /// Call this *after* registering your routes — routes added afterward
243    /// still work but won't appear in the generated spec, since it's built
244    /// once at this call rather than read dynamically per request.
245    ///
246    /// Requires the `openapi` feature.
247    ///
248    /// ```rust,no_run
249    /// use rust_web_server::app::App;
250    /// use rust_web_server::openapi::OpenApiConfig;
251    /// use rust_web_server::response::Response;
252    /// use rust_web_server::core::New;
253    ///
254    /// let app = App::with_state(())
255    ///     .get("/users", |_req, _params, _conn, _state| Response::new())
256    ///     .get("/users/:id", |_req, _params, _conn, _state| Response::new())
257    ///     .openapi(OpenApiConfig::new("My API", "1.0.0"));
258    /// ```
259    #[cfg(feature = "openapi")]
260    pub fn openapi(self, config: crate::openapi::OpenApiConfig) -> Self {
261        let spec_json = Arc::new(crate::openapi::build_spec(&config, &self.route_entries()));
262        let html = Arc::new(crate::openapi::swagger_ui_html("/openapi.json"));
263
264        let spec_for_route = Arc::clone(&spec_json);
265        self.get("/openapi.json", move |_req, _params, _conn, _state| {
266            let mut r = Response::new();
267            r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
268            r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
269            r.content_range_list = vec![Range::get_content_range(
270                spec_for_route.as_bytes().to_vec(),
271                MimeType::APPLICATION_JSON.to_string(),
272            )];
273            r
274        })
275        .get("/docs", move |_req, _params, _conn, _state| {
276            let mut r = Response::new();
277            r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
278            r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
279            r.content_range_list = vec![Range::get_content_range(
280                html.as_bytes().to_vec(),
281                MimeType::TEXT_HTML.to_string(),
282            )];
283            r
284        })
285    }
286}
287
288impl<S: Send + Sync + 'static> Application for AppWithState<S> {
289    fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
290        if let Some(response) = self.router.handle(request, connection) {
291            return Ok(response);
292        }
293        self.fallback_app().execute(request, connection)
294    }
295}