rws_server/handler/mod.rs
1//! Typed handlers (18.28.0, #86): extractors as function arguments, any
2//! [`IntoResponse`] as the return value, the route's method and pattern
3//! taken from the `#[get]`/`#[post]`/… attribute.
4//!
5//! ```rust,ignore
6//! use rust_web_server::app::App;
7//! use rust_web_server::error::AppError;
8//! use rust_web_server::get;
9//! use rust_web_server::handler::{Json, Path, State};
10//!
11//! struct Db;
12//!
13//! #[get("/posts/{id}")]
14//! fn show_post(Path(id): Path<u64>, State(db): State<Db>) -> Result<Json<String>, AppError> {
15//! let _ = db;
16//! Ok(Json(format!("post {id}")))
17//! }
18//!
19//! let app = App::with_state(Db).route(show_post);
20//! ```
21//!
22//! The attribute turns `show_post` into a unit struct implementing
23//! [`Handler`] (and [`AsyncHandler`]); the function as written stays callable
24//! as `show_post::call(…)`. Registration —
25//! [`AppWithState::route`](crate::state::AppWithState::route),
26//! [`Router::route`](crate::router::Router::route),
27//! `AsyncAppWithState::route` — reads the method and pattern from
28//! [`RouteSpec`], so the pattern the macro checked is the one that's served.
29//!
30//! The macro checks at compile time that the pattern's parameters and the
31//! [`Path`] argument agree: a route with parameters needs a `Path` (or
32//! `&PathParams`) argument, a `Path` needs parameters, a tuple has one
33//! element per parameter, names bound as `Path(id)` / `Path((id, slug))`
34//! are parameters of the route, and a `#[derive(FromPath)]` struct's
35//! fields are exactly its parameters.
36//!
37//! Each argument is extracted in order; the first that fails answers the
38//! request with its rejection (problem details, like rws's own errors):
39//!
40//! | Argument | Rejection |
41//! |---|---|
42//! | [`Path<T>`] | `400` when a parameter doesn't parse |
43//! | `Query<T>` (`serde`) | `400` when the query string doesn't fit `T` |
44//! | `Form<T>` (`serde`) | `415` without an urlencoded `Content-Type`, `400` for a body that isn't UTF-8, `422` when the fields don't fit `T` |
45//! | `Json<T>` (`serde`) | `400` (as `json::Json`'s `FromRequest`) |
46//! | [`BodyText`] | `400` for a body that isn't UTF-8 |
47//! | [`State<S>`], `&S`, [`Body`], [`RequestHeaders`], [`RequestId`], `&Request`, `&ConnectionInfo`, `&PathParams` | never |
48//! | any other [`FromRequest`] type (`Validated<T>`, your own) | its own |
49//!
50//! Classic handlers — `fn(&Request, &PathParams, &ConnectionInfo[, &S])` —
51//! keep the old behaviour: the attribute only adds a doc comment and they
52//! are registered with `.get(pattern, handler)` as before.
53
54#[cfg(test)]
55mod tests;
56mod path;
57#[cfg(feature = "serde")]
58mod urlencoded;
59
60use std::future::Future;
61use std::pin::Pin;
62use std::sync::Arc;
63
64use crate::extract::FromRequest;
65use crate::header::Header;
66use crate::request::Request;
67use crate::response::{Response, StatusCodeReasonPhrase};
68use crate::router::PathParams;
69use crate::server::ConnectionInfo;
70
71pub use crate::error::IntoResponse;
72pub use crate::extract::{Body, BodyText, RequestHeaders, RequestId};
73#[cfg(feature = "serde")]
74pub use crate::json::Json;
75pub use path::{FromPath, Path};
76
77/// What an extractor sees: the request, the route's parameters, the
78/// connection and the app's state.
79pub struct RequestParts<'a, S> {
80 pub request: &'a Request,
81 pub params: &'a PathParams,
82 pub connection: &'a ConnectionInfo,
83 pub state: &'a Arc<S>,
84 /// The route parameters the handler's [`Path`] argument takes, in the
85 /// order it takes them (set by the route macro).
86 pub path_names: &'static [&'static str],
87}
88
89/// A typed handler argument: built from the [`RequestParts`], or a
90/// ready-to-send rejection.
91///
92/// Every [`FromRequest`] type is one, so `#[derive(FromRequest)]` structs
93/// and `Validated<T>` work as arguments too.
94pub trait FromRequestParts<S>: Sized {
95 fn from_request_parts(parts: &RequestParts<'_, S>) -> Result<Self, Response>;
96}
97
98impl<S, T: FromRequest> FromRequestParts<S> for T {
99 fn from_request_parts(parts: &RequestParts<'_, S>) -> Result<Self, Response> {
100 T::from_request(parts.request)
101 }
102}
103
104/// The app's shared state (`App::with_state(state)`): a clone of its `Arc`.
105///
106/// `State(db): State<Db>` in a handler's arguments; `&Db` works too.
107pub struct State<S>(pub Arc<S>);
108
109impl<S> Clone for State<S> {
110 fn clone(&self) -> Self {
111 State(Arc::clone(&self.0))
112 }
113}
114
115impl<S> std::ops::Deref for State<S> {
116 type Target = S;
117 fn deref(&self) -> &S {
118 &self.0
119 }
120}
121
122impl<S> FromRequestParts<S> for State<S> {
123 fn from_request_parts(parts: &RequestParts<'_, S>) -> Result<Self, Response> {
124 Ok(State(Arc::clone(parts.state)))
125 }
126}
127
128#[cfg(feature = "serde")]
129pub use urlencoded::{Form, Query};
130
131/// A route's method and pattern, generated by `#[get("/path")]` & co.
132pub trait RouteSpec {
133 const METHOD: &'static str;
134 const PATTERN: &'static str;
135}
136
137/// A typed handler for apps with state `S`, made by the route macros from a
138/// function whose arguments are extractors. Register it with `.route(h)`.
139pub trait Handler<S>: RouteSpec + Send + Sync + 'static {
140 /// Extract the arguments, call the function and turn its value into a
141 /// [`Response`] (or answer with the first argument's rejection).
142 fn handle(&self, request: &Request, params: &PathParams, connection: &ConnectionInfo, state: &Arc<S>) -> Response;
143}
144
145/// The future an [`AsyncHandler`] returns.
146pub type HandlerFuture = Pin<Box<dyn Future<Output = Response> + Send + 'static>>;
147
148/// A typed handler for `AsyncAppWithState` (`http2`): the route macros make
149/// one from an `async fn`, and from every sync typed handler too.
150pub trait AsyncHandler<S>: RouteSpec + Send + Sync + 'static {
151 fn handle_async(&self, request: Request, params: PathParams, connection: ConnectionInfo, state: Arc<S>) -> HandlerFuture;
152}
153
154/// An extractor's rejection: problem details (or text, with
155/// `error_format = "text"`) carrying the usual response headers.
156pub(crate) fn rejection(request: &Request, status: &StatusCodeReasonPhrase, detail: &str) -> Response {
157 crate::error::problem::with_headers(status, detail, detail, Some(Header::get_header_list(request)))
158}
159
160/// What the route macros' output calls. Not an API.
161#[doc(hidden)]
162pub mod __private {
163 pub use super::path::{parse_param, path_fits};
164 /// `Query`/`Form`'s deserializer on `input` (fuzz target `form`).
165 #[cfg(feature = "serde")]
166 pub use super::urlencoded::fuzz as fuzz_urlencoded;
167
168 /// The handler's return value as a response.
169 #[inline]
170 pub fn respond<R: crate::error::IntoResponse>(value: R) -> crate::response::Response {
171 value.into_response()
172 }
173}