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    /// Routes whose body the handler reads as a stream ([`Self::stream_body`]);
81    /// matched only, never dispatched.
82    streaming: Router,
83    /// When `Some`, the fallback `App` is pinned to this config (see
84    /// [`App::with_config`]); when `None`, the fallback reads
85    /// `RWS_CONFIG_*` env vars per request via `App::new()`, same as `App`'s
86    /// own default.
87    config: Option<Arc<ServerConfig>>,
88}
89
90impl<S: Send + Sync + 'static> AppWithState<S> {
91    /// Create a new `AppWithState` wrapping `state`.
92    ///
93    /// `state` is stored behind an `Arc` so it can be shared across threads
94    /// without cloning. Register routes with the builder methods.
95    pub fn new(state: S) -> Self {
96        AppWithState {
97            state: Arc::new(state),
98            router: Router::new(),
99            streaming: Router::new(),
100            config: None,
101        }
102    }
103
104    /// Pin the fallback [`App`] (used for any request this app's own routes
105    /// don't match) to an explicit [`ServerConfig`], instead of reading
106    /// `RWS_CONFIG_*` environment variables per request.
107    ///
108    /// Mirrors [`App::with_config`] — same rationale: safe for parallel
109    /// tests (no `test_env::lock()` needed) and lets multiple
110    /// differently-configured instances coexist in one process.
111    pub fn with_config(mut self, config: ServerConfig) -> Self {
112        self.config = Some(Arc::new(config));
113        self
114    }
115
116    /// Return a reference to the shared state.
117    pub fn state(&self) -> &S {
118        &self.state
119    }
120
121    /// The fallback `App` for requests this app's own routes don't match —
122    /// pinned to `self.config` if set, otherwise `App::new()`'s default
123    /// per-request env read.
124    fn fallback_app(&self) -> App {
125        match &self.config {
126            Some(c) => App::with_shared_config(Arc::clone(c)),
127            None => App::new(),
128        }
129    }
130
131    /// Register a `GET` handler for `pattern`.
132    pub fn get<F>(mut self, pattern: &str, handler: F) -> Self
133    where
134        F: Fn(&Request, &PathParams, &ConnectionInfo, &S) -> Response + Send + Sync + 'static,
135    {
136        let state = Arc::clone(&self.state);
137        self.router = self.router.get(pattern, move |req, params, conn| {
138            handler(req, params, conn, &state)
139        });
140        self
141    }
142
143    /// Register a `POST` handler for `pattern`.
144    pub fn post<F>(mut self, pattern: &str, handler: F) -> Self
145    where
146        F: Fn(&Request, &PathParams, &ConnectionInfo, &S) -> Response + Send + Sync + 'static,
147    {
148        let state = Arc::clone(&self.state);
149        self.router = self.router.post(pattern, move |req, params, conn| {
150            handler(req, params, conn, &state)
151        });
152        self
153    }
154
155    /// Register a `PUT` handler for `pattern`.
156    pub fn put<F>(mut self, pattern: &str, handler: F) -> Self
157    where
158        F: Fn(&Request, &PathParams, &ConnectionInfo, &S) -> Response + Send + Sync + 'static,
159    {
160        let state = Arc::clone(&self.state);
161        self.router = self.router.put(pattern, move |req, params, conn| {
162            handler(req, params, conn, &state)
163        });
164        self
165    }
166
167    /// Register a `PATCH` handler for `pattern`.
168    pub fn patch<F>(mut self, pattern: &str, handler: F) -> Self
169    where
170        F: Fn(&Request, &PathParams, &ConnectionInfo, &S) -> Response + Send + Sync + 'static,
171    {
172        let state = Arc::clone(&self.state);
173        self.router = self.router.patch(pattern, move |req, params, conn| {
174            handler(req, params, conn, &state)
175        });
176        self
177    }
178
179    /// Register a `DELETE` handler for `pattern`.
180    pub fn delete<F>(mut self, pattern: &str, handler: F) -> Self
181    where
182        F: Fn(&Request, &PathParams, &ConnectionInfo, &S) -> Response + Send + Sync + 'static,
183    {
184        let state = Arc::clone(&self.state);
185        self.router = self.router.delete(pattern, move |req, params, conn| {
186            handler(req, params, conn, &state)
187        });
188        self
189    }
190
191    /// Serve WebSocket connections at `pattern`: an upgrade request's
192    /// connection goes to `handler` with the route's parameters and the
193    /// state, anything else gets `426 Upgrade Required`. The handler runs
194    /// on a thread of its own for the connection's life.
195    ///
196    /// ```rust,no_run
197    /// use std::sync::Mutex;
198    /// use rust_web_server::app::App;
199    /// use rust_web_server::websocket::Message;
200    ///
201    /// let app = App::with_state(Mutex::new(0u64)).websocket("/count", |mut ws, _params, state| {
202    ///     while let Ok(Some(Message::Text(_))) = ws.recv() {
203    ///         let n = { let mut n = state.lock().unwrap(); *n += 1; *n };
204    ///         let _ = ws.send_text(&n.to_string());
205    ///     }
206    /// });
207    /// ```
208    #[cfg(not(target_arch = "wasm32"))]
209    pub fn websocket<F>(mut self, pattern: &str, handler: F) -> Self
210    where
211        F: Fn(crate::websocket::WebSocketConnection, PathParams, Arc<S>) + Send + Sync + 'static,
212    {
213        let state = Arc::clone(&self.state);
214        let handler = Arc::new(handler);
215        self.router = self.router.get(pattern, move |req, params, _conn| {
216            let (handler, params, state) = (Arc::clone(&handler), params.clone(), Arc::clone(&state));
217            crate::websocket::WebSocket::upgrade(req, move |ws| handler(ws, params, state))
218        });
219        self
220    }
221
222    /// Return a snapshot of all registered routes as `(method, pattern)` pairs.
223    /// Hand requests to `method` `pattern` (same pattern syntax as the route)
224    /// their body as a stream instead of reading it into memory first
225    /// (#31): the handler reads `request.body` with `std::io::Read` while
226    /// the upload is still arriving.
227    ///
228    /// ```rust,no_run
229    /// use std::io::Read;
230    /// use rust_web_server::app::App;
231    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
232    ///
233    /// let app = App::with_state(())
234    ///     .put("/upload/:name", |request, params, _conn, _state| {
235    ///         let mut file = std::fs::File::create(format!("/tmp/{}", params.get("name").unwrap())).unwrap();
236    ///         let copied = std::io::copy(&mut &request.body, &mut file);
237    ///         Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, &format!("{copied:?}"))
238    ///     })
239    ///     .stream_body("PUT", "/upload/:name");
240    /// ```
241    pub fn stream_body(mut self, method: &str, pattern: &str) -> Self {
242        self.streaming = self.streaming.add(method, pattern, |_, _, _| Response::new());
243        self
244    }
245
246    pub fn route_entries(&self) -> Vec<crate::router::RouteInfo> {
247        self.router.route_entries()
248    }
249
250    /// Attach an MCP server to this application. Requests that do not match
251    /// the MCP endpoint (`POST /mcp`) are forwarded to `self`, so all
252    /// previously registered routes remain active.
253    ///
254    /// ```rust,no_run
255    /// use rust_web_server::app::App;
256    /// use rust_web_server::mcp::{McpContent, extract_arg};
257    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
258    /// use rust_web_server::core::New;
259    ///
260    /// struct Db { url: String }
261    ///
262    /// let app = App::with_state(Db { url: "postgres://localhost/mydb".to_string() })
263    ///     .get("/api/users", |_req, _params, _conn, _db| {
264    ///         let mut r = Response::new();
265    ///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
266    ///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
267    ///         r
268    ///     })
269    ///     .mcp("my-server", "1.0")
270    ///     .tool("list_users", "List all users", "{}", |_| {
271    ///         Ok(McpContent::json(r#"[{"id":1,"name":"Alice"}]"#))
272    ///     });
273    /// ```
274    pub fn mcp(self, name: impl Into<String>, version: impl Into<String>) -> crate::mcp::McpServer {
275        crate::mcp::McpServer::new(name, version).wrap(self)
276    }
277
278    /// Wrap this application in a middleware layer.
279    ///
280    /// Enables fluent composition:
281    ///
282    /// ```rust,no_run
283    /// use rust_web_server::app::App;
284    /// use rust_web_server::core::New;
285    /// use rust_web_server::middleware::RateLimitLayer;
286    /// use rust_web_server::response::Response;
287    ///
288    /// let app = App::with_state(())
289    ///     .get("/ping", |_, _, _, _| Response::new())
290    ///     .wrap(RateLimitLayer);
291    /// ```
292    pub fn wrap<M: Middleware + 'static>(self, layer: M) -> WithMiddleware<AppWithState<S>> {
293        WithMiddleware::new(self).wrap(layer)
294    }
295
296    /// Add `GET /openapi.json` (a generated OpenAPI 3.1 document covering
297    /// every route registered so far) and `GET /docs` (Swagger UI, loaded
298    /// from a CDN, pointed at `/openapi.json`).
299    ///
300    /// Call this *after* registering your routes — routes added afterward
301    /// still work but won't appear in the generated spec, since it's built
302    /// once at this call rather than read dynamically per request.
303    ///
304    /// Requires the `openapi` feature.
305    ///
306    /// ```rust,no_run
307    /// use rust_web_server::app::App;
308    /// use rust_web_server::openapi::OpenApiConfig;
309    /// use rust_web_server::response::Response;
310    /// use rust_web_server::core::New;
311    ///
312    /// let app = App::with_state(())
313    ///     .get("/users", |_req, _params, _conn, _state| Response::new())
314    ///     .get("/users/:id", |_req, _params, _conn, _state| Response::new())
315    ///     .openapi(OpenApiConfig::new("My API", "1.0.0"));
316    /// ```
317    #[cfg(feature = "openapi")]
318    pub fn openapi(self, config: crate::openapi::OpenApiConfig) -> Self {
319        let spec_json = Arc::new(crate::openapi::build_spec(&config, &self.route_entries()));
320        let html = Arc::new(crate::openapi::swagger_ui_html("/openapi.json"));
321
322        let spec_for_route = Arc::clone(&spec_json);
323        self.get("/openapi.json", move |_req, _params, _conn, _state| {
324            let mut r = Response::new();
325            r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
326            r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
327            r.content_range_list = vec![Range::get_content_range(
328                spec_for_route.as_bytes().to_vec(),
329                MimeType::APPLICATION_JSON.to_string(),
330            )];
331            r
332        })
333        .get("/docs", move |_req, _params, _conn, _state| {
334            let mut r = Response::new();
335            r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
336            r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
337            r.content_range_list = vec![Range::get_content_range(
338                html.as_bytes().to_vec(),
339                MimeType::TEXT_HTML.to_string(),
340            )];
341            r
342        })
343    }
344}
345
346impl<S: Send + Sync + 'static> Application for AppWithState<S> {
347    fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
348        if let Some(response) = self.router.handle(request, connection) {
349            return Ok(response);
350        }
351        self.fallback_app().execute(request, connection)
352    }
353
354    /// Route handlers are user code and may block: only requests no route
355    /// matches, falling through to the built-in `App`, run inline.
356    fn runs_inline(&self, request: &Request, connection: &ConnectionInfo) -> bool {
357        !self.router.matches(request, connection) && App::is_inline_request(request)
358    }
359
360    fn streams_body(&self, request: &Request, connection: &ConnectionInfo) -> bool {
361        self.streaming.matches(request, connection)
362    }
363}