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 /// Return a snapshot of all registered routes as `(method, pattern)` pairs.
192 /// Hand requests to `method` `pattern` (same pattern syntax as the route)
193 /// their body as a stream instead of reading it into memory first
194 /// (#31): the handler reads `request.body` with `std::io::Read` while
195 /// the upload is still arriving.
196 ///
197 /// ```rust,no_run
198 /// use std::io::Read;
199 /// use rust_web_server::app::App;
200 /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
201 ///
202 /// let app = App::with_state(())
203 /// .put("/upload/:name", |request, params, _conn, _state| {
204 /// let mut file = std::fs::File::create(format!("/tmp/{}", params.get("name").unwrap())).unwrap();
205 /// let copied = std::io::copy(&mut &request.body, &mut file);
206 /// Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, &format!("{copied:?}"))
207 /// })
208 /// .stream_body("PUT", "/upload/:name");
209 /// ```
210 pub fn stream_body(mut self, method: &str, pattern: &str) -> Self {
211 self.streaming = self.streaming.add(method, pattern, |_, _, _| Response::new());
212 self
213 }
214
215 pub fn route_entries(&self) -> Vec<crate::router::RouteInfo> {
216 self.router.route_entries()
217 }
218
219 /// Attach an MCP server to this application. Requests that do not match
220 /// the MCP endpoint (`POST /mcp`) are forwarded to `self`, so all
221 /// previously registered routes remain active.
222 ///
223 /// ```rust,no_run
224 /// use rust_web_server::app::App;
225 /// use rust_web_server::mcp::{McpContent, extract_arg};
226 /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
227 /// use rust_web_server::core::New;
228 ///
229 /// struct Db { url: String }
230 ///
231 /// let app = App::with_state(Db { url: "postgres://localhost/mydb".to_string() })
232 /// .get("/api/users", |_req, _params, _conn, _db| {
233 /// let mut r = Response::new();
234 /// r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
235 /// r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
236 /// r
237 /// })
238 /// .mcp("my-server", "1.0")
239 /// .tool("list_users", "List all users", "{}", |_| {
240 /// Ok(McpContent::json(r#"[{"id":1,"name":"Alice"}]"#))
241 /// });
242 /// ```
243 pub fn mcp(self, name: impl Into<String>, version: impl Into<String>) -> crate::mcp::McpServer {
244 crate::mcp::McpServer::new(name, version).wrap(self)
245 }
246
247 /// Wrap this application in a middleware layer.
248 ///
249 /// Enables fluent composition:
250 ///
251 /// ```rust,no_run
252 /// use rust_web_server::app::App;
253 /// use rust_web_server::core::New;
254 /// use rust_web_server::middleware::RateLimitLayer;
255 /// use rust_web_server::response::Response;
256 ///
257 /// let app = App::with_state(())
258 /// .get("/ping", |_, _, _, _| Response::new())
259 /// .wrap(RateLimitLayer);
260 /// ```
261 pub fn wrap<M: Middleware + 'static>(self, layer: M) -> WithMiddleware<AppWithState<S>> {
262 WithMiddleware::new(self).wrap(layer)
263 }
264
265 /// Add `GET /openapi.json` (a generated OpenAPI 3.0 document covering
266 /// every route registered so far) and `GET /docs` (Swagger UI, loaded
267 /// from a CDN, pointed at `/openapi.json`).
268 ///
269 /// Call this *after* registering your routes — routes added afterward
270 /// still work but won't appear in the generated spec, since it's built
271 /// once at this call rather than read dynamically per request.
272 ///
273 /// Requires the `openapi` feature.
274 ///
275 /// ```rust,no_run
276 /// use rust_web_server::app::App;
277 /// use rust_web_server::openapi::OpenApiConfig;
278 /// use rust_web_server::response::Response;
279 /// use rust_web_server::core::New;
280 ///
281 /// let app = App::with_state(())
282 /// .get("/users", |_req, _params, _conn, _state| Response::new())
283 /// .get("/users/:id", |_req, _params, _conn, _state| Response::new())
284 /// .openapi(OpenApiConfig::new("My API", "1.0.0"));
285 /// ```
286 #[cfg(feature = "openapi")]
287 pub fn openapi(self, config: crate::openapi::OpenApiConfig) -> Self {
288 let spec_json = Arc::new(crate::openapi::build_spec(&config, &self.route_entries()));
289 let html = Arc::new(crate::openapi::swagger_ui_html("/openapi.json"));
290
291 let spec_for_route = Arc::clone(&spec_json);
292 self.get("/openapi.json", move |_req, _params, _conn, _state| {
293 let mut r = Response::new();
294 r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
295 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
296 r.content_range_list = vec![Range::get_content_range(
297 spec_for_route.as_bytes().to_vec(),
298 MimeType::APPLICATION_JSON.to_string(),
299 )];
300 r
301 })
302 .get("/docs", move |_req, _params, _conn, _state| {
303 let mut r = Response::new();
304 r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
305 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
306 r.content_range_list = vec![Range::get_content_range(
307 html.as_bytes().to_vec(),
308 MimeType::TEXT_HTML.to_string(),
309 )];
310 r
311 })
312 }
313}
314
315impl<S: Send + Sync + 'static> Application for AppWithState<S> {
316 fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
317 if let Some(response) = self.router.handle(request, connection) {
318 return Ok(response);
319 }
320 self.fallback_app().execute(request, connection)
321 }
322
323 /// Route handlers are user code and may block: only requests no route
324 /// matches, falling through to the built-in `App`, run inline.
325 fn runs_inline(&self, request: &Request, connection: &ConnectionInfo) -> bool {
326 !self.router.matches(request, connection) && App::is_inline_request(request)
327 }
328
329 fn streams_body(&self, request: &Request, connection: &ConnectionInfo) -> bool {
330 self.streaming.matches(request, connection)
331 }
332}