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