Skip to main content

rws_server/async_state/
mod.rs

1//! Async-capable state-aware application — requires the `http2` feature (tokio).
2//!
3//! [`AsyncAppWithState<S>`] is the async counterpart to [`AppWithState<S>`]:
4//! handlers are `async fn` closures that can `await` database queries, HTTP
5//! clients, or any other async I/O without blocking the OS thread.
6//!
7//! The sync [`Application`] bridge works in any calling context: when inside
8//! an existing tokio runtime (HTTP/2 / HTTP/3), it spawns a scoped OS thread
9//! with its own single-threaded runtime; when called from the HTTP/1.1
10//! thread-pool (no runtime), it creates a temporary single-threaded runtime.
11//!
12//! Unmatched routes fall through to the built-in [`App`] controller chain.
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! # extern crate rws_server as rust_web_server;
18//! use std::sync::Arc;
19//! use rust_web_server::async_state::AsyncAppWithState;
20//! use rust_web_server::core::New;
21//! use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
22//! use rust_web_server::range::Range;
23//! use rust_web_server::mime_type::MimeType;
24//! use rust_web_server::router::PathParams;
25//! use rust_web_server::request::Request;
26//! use rust_web_server::server::ConnectionInfo;
27//!
28//! struct AppState {
29//!     greeting: String,
30//! }
31//!
32//! let app = AsyncAppWithState::new(AppState { greeting: "Hello".to_string() })
33//!     .get("/greet/:name", |_req, params, _conn, state| async move {
34//!         let name = params.get("name").unwrap_or("world");
35//!         let body = format!("{}, {}!", state.greeting, name);
36//!         let mut r = Response::new();
37//!         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
38//!         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
39//!         r.content_range_list = vec![
40//!             Range::get_content_range(body.into_bytes(), MimeType::TEXT_PLAIN.to_string())
41//!         ];
42//!         r
43//!     });
44//! ```
45//!
46//! [`AppWithState<S>`]: crate::state::AppWithState
47
48#[cfg(test)]
49mod tests;
50
51use std::future::Future;
52use std::pin::Pin;
53use std::sync::Arc;
54
55use crate::app::App;
56use crate::application::Application;
57use crate::core::New;
58use crate::request::Request;
59use crate::response::Response;
60use crate::router::matcher::{self, Segment};
61use crate::router::PathParams;
62use crate::server::ConnectionInfo;
63#[cfg(feature = "openapi")]
64use crate::mime_type::MimeType;
65#[cfg(feature = "openapi")]
66use crate::range::Range;
67#[cfg(feature = "openapi")]
68use crate::response::STATUS_CODE_REASON_PHRASE;
69
70type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
71
72type AsyncHandlerFn<S> = Arc<
73    dyn Fn(Request, PathParams, ConnectionInfo, Arc<S>) -> BoxFuture<Response> + Send + Sync,
74>;
75
76// ── AsyncRoute ────────────────────────────────────────────────────────────────
77
78#[derive(Clone)]
79struct AsyncRoute<S> {
80    method: String,
81    segments: Vec<Segment>,
82    handler: AsyncHandlerFn<S>,
83}
84
85// ── AsyncAppWithState ─────────────────────────────────────────────────────────
86
87/// An [`Application`] whose route handlers are `async` functions.
88///
89/// State is stored as `Arc<S>` and passed by value (cheap clone) to each
90/// handler invocation. Handlers receive owned `Request`, `PathParams`, and
91/// `ConnectionInfo` values so the returned future is `'static`.
92#[derive(Clone)]
93pub struct AsyncAppWithState<S> {
94    state: Arc<S>,
95    routes: Vec<AsyncRoute<S>>,
96    /// When `Some`, the fallback `App` is pinned to this config (see
97    /// [`App::with_config`]); when `None`, the fallback reads
98    /// `RWS_CONFIG_*` env vars per request via `App::new()`, same as `App`'s
99    /// own default.
100    config: Option<Arc<crate::server_config::ServerConfig>>,
101}
102
103impl<S: Send + Sync + 'static> AsyncAppWithState<S> {
104    /// Create a new `AsyncAppWithState` wrapping `state`.
105    pub fn new(state: S) -> Self {
106        AsyncAppWithState { state: Arc::new(state), routes: Vec::new(), config: None }
107    }
108
109    /// Pin the fallback [`App`] (used for any request this app's own routes
110    /// don't match) to an explicit [`crate::server_config::ServerConfig`],
111    /// instead of reading `RWS_CONFIG_*` environment variables per request.
112    ///
113    /// Mirrors [`App::with_config`] / [`crate::state::AppWithState::with_config`].
114    pub fn with_config(mut self, config: crate::server_config::ServerConfig) -> Self {
115        self.config = Some(Arc::new(config));
116        self
117    }
118
119    /// Return a reference to the shared state.
120    pub fn state(&self) -> &S {
121        &self.state
122    }
123
124    /// Return a snapshot of all registered routes as `(method, pattern)` pairs.
125    pub fn route_entries(&self) -> Vec<crate::router::RouteInfo> {
126        self.routes
127            .iter()
128            .map(|r| crate::router::RouteInfo {
129                method: r.method.clone(),
130                pattern: matcher::segments_to_pattern(&r.segments),
131            })
132            .collect()
133    }
134
135    /// The fallback `App` for requests this app's own routes don't match —
136    /// pinned to `self.config` if set, otherwise `App::new()`'s default
137    /// per-request env read.
138    fn fallback_app(&self) -> App {
139        match &self.config {
140            Some(c) => App::with_config((**c).clone()),
141            None => App::new(),
142        }
143    }
144
145    fn add<F, Fut>(mut self, method: &str, pattern: &str, handler: F) -> Self
146    where
147        F: Fn(Request, PathParams, ConnectionInfo, Arc<S>) -> Fut + Send + Sync + 'static,
148        Fut: Future<Output = Response> + Send + 'static,
149    {
150        self.routes.push(AsyncRoute {
151            method: method.to_string(),
152            segments: matcher::parse_pattern(pattern),
153            handler: Arc::new(move |req, params, conn, state| Box::pin(handler(req, params, conn, state))),
154        });
155        if let Some((last, earlier)) = self.routes.split_last() {
156            let earlier = earlier.iter().map(|r| (r.method.as_str(), r.segments.as_slice()));
157            if let Some(conflict) = crate::router::conflict_with(earlier, &last.method, &last.segments) {
158                conflict.warn_once();
159            }
160        }
161        self
162    }
163
164    /// Routes that never run because an earlier one with the same method
165    /// matches every path they do (18.28.0). See
166    /// [`Router::route_conflicts`](crate::router::Router::route_conflicts);
167    /// each is also printed once to stderr when it's registered.
168    pub fn route_conflicts(&self) -> Vec<crate::router::RouteConflict> {
169        crate::router::conflicts(self.routes.iter().map(|r| (r.method.as_str(), r.segments.as_slice())))
170    }
171
172    /// Register a typed handler (18.28.0): an `async fn` (or a sync
173    /// function) with extractor arguments annotated `#[get("/path")]` &
174    /// co.; the method and pattern come from the attribute.
175    ///
176    /// ```rust,ignore
177    /// #[get("/posts/{id}")]
178    /// async fn show_post(Path(id): Path<i64>, State(pool): State<DbPool>) -> Result<Json<Post>, AppError> {
179    ///     // … .await …
180    /// }
181    ///
182    /// let app = App::with_async_state(pool).route(show_post);
183    /// ```
184    pub fn route<H: crate::handler::AsyncHandler<S>>(self, handler: H) -> Self {
185        let handler = Arc::new(handler);
186        self.add(H::METHOD, H::PATTERN, move |request, params, connection, state| {
187            handler.handle_async(request, params, connection, state)
188        })
189    }
190
191    /// Register an async `GET` handler for `pattern`.
192    pub fn get<F, Fut>(self, pattern: &str, handler: F) -> Self
193    where
194        F: Fn(Request, PathParams, ConnectionInfo, Arc<S>) -> Fut + Send + Sync + 'static,
195        Fut: Future<Output = Response> + Send + 'static,
196    {
197        self.add("GET", pattern, handler)
198    }
199
200    /// Register an async `POST` handler for `pattern`.
201    pub fn post<F, Fut>(self, pattern: &str, handler: F) -> Self
202    where
203        F: Fn(Request, PathParams, ConnectionInfo, Arc<S>) -> Fut + Send + Sync + 'static,
204        Fut: Future<Output = Response> + Send + 'static,
205    {
206        self.add("POST", pattern, handler)
207    }
208
209    /// Register an async `PUT` handler for `pattern`.
210    pub fn put<F, Fut>(self, pattern: &str, handler: F) -> Self
211    where
212        F: Fn(Request, PathParams, ConnectionInfo, Arc<S>) -> Fut + Send + Sync + 'static,
213        Fut: Future<Output = Response> + Send + 'static,
214    {
215        self.add("PUT", pattern, handler)
216    }
217
218    /// Register an async `PATCH` handler for `pattern`.
219    pub fn patch<F, Fut>(self, pattern: &str, handler: F) -> Self
220    where
221        F: Fn(Request, PathParams, ConnectionInfo, Arc<S>) -> Fut + Send + Sync + 'static,
222        Fut: Future<Output = Response> + Send + 'static,
223    {
224        self.add("PATCH", pattern, handler)
225    }
226
227    /// Register an async `DELETE` handler for `pattern`.
228    pub fn delete<F, Fut>(self, pattern: &str, handler: F) -> Self
229    where
230        F: Fn(Request, PathParams, ConnectionInfo, Arc<S>) -> Fut + Send + Sync + 'static,
231        Fut: Future<Output = Response> + Send + 'static,
232    {
233        self.add("DELETE", pattern, handler)
234    }
235
236    /// Add `GET /openapi.json` (a generated OpenAPI 3.1 document covering
237    /// every route registered so far) and `GET /docs` (Swagger UI, loaded
238    /// from a CDN, pointed at `/openapi.json`).
239    ///
240    /// Call this *after* registering your routes — routes added afterward
241    /// still work but won't appear in the generated spec, since it's built
242    /// once at this call rather than read dynamically per request.
243    ///
244    /// Requires the `openapi` feature (and `http2`, since `AsyncAppWithState`
245    /// already requires it).
246    #[cfg(feature = "openapi")]
247    pub fn openapi(self, config: crate::openapi::OpenApiConfig) -> Self {
248        let spec_json = Arc::new(crate::openapi::build_spec(&config, &self.route_entries()));
249        let html = Arc::new(crate::openapi::swagger_ui_html("/openapi.json"));
250
251        let spec_for_route = Arc::clone(&spec_json);
252        self.get("/openapi.json", move |_req, _params, _conn, _state| {
253            let spec_for_route = Arc::clone(&spec_for_route);
254            async move {
255                let mut r = Response::new();
256                r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
257                r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
258                r.content_range_list = vec![Range::get_content_range(
259                    spec_for_route.as_bytes().to_vec(),
260                    MimeType::APPLICATION_JSON.to_string(),
261                )];
262                r
263            }
264        })
265        .get("/docs", move |_req, _params, _conn, _state| {
266            let html = Arc::clone(&html);
267            async move {
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.content_range_list = vec![Range::get_content_range(
272                    html.as_bytes().to_vec(),
273                    MimeType::TEXT_HTML.to_string(),
274                )];
275                r
276            }
277        })
278    }
279
280    async fn execute_async(
281        &self,
282        request: &Request,
283        connection: &ConnectionInfo,
284    ) -> Result<Response, String> {
285        let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
286        let path_segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
287
288        for route in &self.routes {
289            if route.method != request.method {
290                continue;
291            }
292            if let Some(params_map) = matcher::try_match(&route.segments, &path_segs) {
293                let params = PathParams::from_map(params_map);
294                let fut = (route.handler)(
295                    request.clone(),
296                    params,
297                    connection.clone(),
298                    Arc::clone(&self.state),
299                );
300                return Ok(fut.await);
301            }
302        }
303
304        self.fallback_app().execute(request, connection)
305    }
306}
307
308impl<S: Send + Sync + 'static> Application for AsyncAppWithState<S> {
309    fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
310        let request = request.clone();
311        let connection = connection.clone();
312        crate::async_bridge::block_on_isolated(|| self.execute_async(&request, &connection))
313    }
314
315    fn registered_routes(&self) -> Vec<crate::router::RouteInfo> {
316        self.route_entries()
317    }
318}