Skip to main content

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_unchecked(method, pattern, |_, _, _| Response::new());
246        self
247    }
248
249    /// The registered routes as `(method, pattern)`, in registration order.
250    pub fn route_entries(&self) -> Vec<crate::router::RouteInfo> {
251        self.router.route_entries()
252    }
253
254    /// Register a typed handler (18.28.0): a function with extractor
255    /// arguments annotated `#[get("/path")]` & co. The method and pattern
256    /// come from the attribute, which checked the pattern's parameters
257    /// against the function's `Path` argument at compile time.
258    ///
259    /// ```rust,ignore
260    /// use rust_web_server::handler::{Json, Path, State};
261    ///
262    /// #[get("/posts/{id}")]
263    /// fn show_post(Path(id): Path<u64>, State(db): State<Db>) -> Result<Json<Post>, AppError> {
264    ///     db.find(id).map(Json).ok_or_else(|| AppError::NotFound(format!("post {id}")))
265    /// }
266    ///
267    /// let app = App::with_state(db).route(show_post);
268    /// ```
269    pub fn route<H: crate::handler::Handler<S>>(mut self, handler: H) -> Self {
270        let state = Arc::clone(&self.state);
271        self.router = self.router.add(H::METHOD, H::PATTERN, move |request, params, connection| {
272            handler.handle(request, params, connection, &state)
273        });
274        self
275    }
276
277    /// Register routes under `prefix` that share guards and middleware
278    /// (18.35.0, #94); see [`group`](crate::group).
279    pub fn group(mut self, prefix: &str, build: impl FnOnce(crate::group::Group<S>) -> crate::group::Group<S>) -> Self {
280        let routes = build(crate::group::Group::new(prefix)).finish();
281        self.add_group_routes(routes);
282        self
283    }
284
285    /// Register the conventional routes of a resource at `path` (18.35.0,
286    /// #94): `index`, `new_form`, `create`, `show`, `edit`, `update`,
287    /// `destroy`; see [`group`](crate::group).
288    pub fn resource(mut self, path: &str, build: impl FnOnce(crate::group::Resource<S>) -> crate::group::Resource<S>) -> Self {
289        let routes = crate::group::Group::new("").resource(path, build).finish();
290        self.add_group_routes(routes);
291        self
292    }
293
294    /// Name the route registered last, for
295    /// [`url_for`](crate::router::url_for) (18.35.0, #94).
296    pub fn name(self, name: &str) -> Self {
297        if let Some(pattern) = self.router.last_pattern() {
298            crate::router::name_route(name, &pattern);
299        }
300        self
301    }
302
303    fn add_group_routes(&mut self, routes: Vec<crate::group::GroupRoute<crate::group::SyncHandler<S>>>) {
304        let mut router = std::mem::replace(&mut self.router, Router::new());
305        for route in routes {
306            if let Some(name) = &route.name {
307                crate::router::name_route(name, &route.pattern);
308            }
309            let state = Arc::clone(&self.state);
310            let handler = route.handler;
311            router = router.add(route.method, &route.pattern, move |request, params, connection| handler(request, params, connection, &state));
312        }
313        self.router = router;
314    }
315
316    /// Routes that never run because an earlier one with the same method
317    /// matches every path they do (18.28.0). See
318    /// [`Router::route_conflicts`](crate::router::Router::route_conflicts);
319    /// each is also printed once to stderr when it's registered.
320    pub fn route_conflicts(&self) -> Vec<crate::router::RouteConflict> {
321        self.router.route_conflicts()
322    }
323
324    /// Attach an MCP server to this application. Requests that do not match
325    /// the MCP endpoint (`POST /mcp`) are forwarded to `self`, so all
326    /// previously registered routes remain active.
327    ///
328    /// ```rust,no_run
329    /// # extern crate rws_server as rust_web_server;
330    /// use rust_web_server::app::App;
331    /// use rust_web_server::mcp::{McpContent, extract_arg};
332    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
333    /// use rust_web_server::core::New;
334    ///
335    /// struct Db { url: String }
336    ///
337    /// let app = App::with_state(Db { url: "postgres://localhost/mydb".to_string() })
338    ///     .get("/api/users", |_req, _params, _conn, _db| {
339    ///         let mut r = Response::new();
340    ///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
341    ///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
342    ///         r
343    ///     })
344    ///     .mcp("my-server", "1.0")
345    ///     .tool("list_users", "List all users", "{}", |_| {
346    ///         Ok(McpContent::json(r#"[{"id":1,"name":"Alice"}]"#))
347    ///     });
348    /// ```
349    pub fn mcp(self, name: impl Into<String>, version: impl Into<String>) -> crate::mcp::McpServer {
350        crate::mcp::McpServer::new(name, version).wrap(self)
351    }
352
353    /// Wrap this application in a middleware layer.
354    ///
355    /// Enables fluent composition:
356    ///
357    /// ```rust,no_run
358    /// # extern crate rws_server as rust_web_server;
359    /// use rust_web_server::app::App;
360    /// use rust_web_server::core::New;
361    /// use rust_web_server::middleware::RateLimitLayer;
362    /// use rust_web_server::response::Response;
363    ///
364    /// let app = App::with_state(())
365    ///     .get("/ping", |_, _, _, _| Response::new())
366    ///     .wrap(RateLimitLayer);
367    /// ```
368    pub fn wrap<M: Middleware + 'static>(self, layer: M) -> WithMiddleware<AppWithState<S>> {
369        WithMiddleware::new(self).wrap(layer)
370    }
371
372    /// Add `GET /openapi.json` (a generated OpenAPI 3.1 document covering
373    /// every route registered so far) and `GET /docs` (Swagger UI, loaded
374    /// from a CDN, pointed at `/openapi.json`).
375    ///
376    /// Call this *after* registering your routes — routes added afterward
377    /// still work but won't appear in the generated spec, since it's built
378    /// once at this call rather than read dynamically per request.
379    ///
380    /// Requires the `openapi` feature.
381    ///
382    /// ```rust,no_run
383    /// # extern crate rws_server as rust_web_server;
384    /// use rust_web_server::app::App;
385    /// use rust_web_server::openapi::OpenApiConfig;
386    /// use rust_web_server::response::Response;
387    /// use rust_web_server::core::New;
388    ///
389    /// let app = App::with_state(())
390    ///     .get("/users", |_req, _params, _conn, _state| Response::new())
391    ///     .get("/users/:id", |_req, _params, _conn, _state| Response::new())
392    ///     .openapi(OpenApiConfig::new("My API", "1.0.0"));
393    /// ```
394    /// This app's OpenAPI document as JSON (what [`openapi`](Self::openapi)
395    /// serves), e.g. for [`McpServer::openapi_tools`](crate::mcp::McpServer::openapi_tools).
396    #[cfg(feature = "openapi")]
397    pub fn openapi_spec(&self, config: &crate::openapi::OpenApiConfig) -> String {
398        crate::openapi::build_spec(config, &self.route_entries())
399    }
400
401    #[cfg(feature = "openapi")]
402    pub fn openapi(self, config: crate::openapi::OpenApiConfig) -> Self {
403        let spec_json = Arc::new(crate::openapi::build_spec(&config, &self.route_entries()));
404        let html = Arc::new(crate::openapi::swagger_ui_html("/openapi.json"));
405
406        let spec_for_route = Arc::clone(&spec_json);
407        self.get("/openapi.json", move |_req, _params, _conn, _state| {
408            let mut r = Response::new();
409            r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
410            r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
411            r.content_range_list = vec![Range::get_content_range(
412                spec_for_route.as_bytes().to_vec(),
413                MimeType::APPLICATION_JSON.to_string(),
414            )];
415            r
416        })
417        .get("/docs", move |_req, _params, _conn, _state| {
418            let mut r = Response::new();
419            r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
420            r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
421            r.content_range_list = vec![Range::get_content_range(
422                html.as_bytes().to_vec(),
423                MimeType::TEXT_HTML.to_string(),
424            )];
425            r
426        })
427    }
428}
429
430impl<S: Send + Sync + 'static> Application for AppWithState<S> {
431    fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
432        if let Some(response) = self.router.handle(request, connection) {
433            return Ok(response);
434        }
435        self.fallback_app().execute(request, connection)
436    }
437
438    /// Route handlers are user code and may block: only requests no route
439    /// matches, falling through to the built-in `App`, run inline.
440    fn runs_inline(&self, request: &Request, connection: &ConnectionInfo) -> bool {
441        !self.router.matches(request, connection) && App::is_inline_request(request)
442    }
443
444    fn streams_body(&self, request: &Request, connection: &ConnectionInfo) -> bool {
445        self.streaming.matches(request, connection)
446    }
447
448    fn registered_routes(&self) -> Vec<crate::router::RouteInfo> {
449        self.router.route_entries()
450    }
451}