torch_web/
extractors.rs

1//! # Extractors
2//!
3//! Extractors allow you to declaratively parse requests and extract the data you need.
4//! They provide a type-safe way to access request components like path parameters,
5//! query strings, JSON bodies, headers, and application state.
6//!
7//! ## Example
8//!
9//! ```rust,no_run
10//! use torch_web::{App, extractors::*};
11//! use serde::Deserialize;
12//!
13//! #[derive(Deserialize)]
14//! struct User {
15//!     name: String,
16//!     email: String,
17//! }
18//!
19//! async fn create_user(
20//!     Path(user_id): Path<u32>,
21//!     Query(params): Query<std::collections::HashMap<String, String>>,
22//!     Json(user): Json<User>,
23//! ) -> Response {
24//!     // user_id, params, and user are all type-safe and validated
25//!     Response::ok().json(&user).unwrap()
26//! }
27//! ```
28
29use std::collections::HashMap;
30use std::convert::Infallible;
31use std::future::Future;
32use std::pin::Pin;
33use crate::{Request, Response};
34use http::{HeaderMap, StatusCode};
35
36/// Trait for extracting data from the complete request
37pub trait FromRequest: Sized {
38    /// The error type returned when extraction fails
39    type Error: IntoResponse + Send + Sync;
40
41    /// Extract data from the request
42    fn from_request(
43        req: Request,
44    ) -> Pin<Box<dyn Future<Output = Result<(Self, Request), Self::Error>> + Send + 'static>>;
45}
46
47/// Trait for extracting data from request parts (without consuming the body)
48pub trait FromRequestParts: Sized {
49    /// The error type returned when extraction fails
50    type Error: IntoResponse + Send + Sync;
51
52    /// Extract data from request parts
53    fn from_request_parts(
54        req: &mut Request,
55    ) -> Pin<Box<dyn Future<Output = Result<Self, Self::Error>> + Send + 'static>>;
56}
57
58/// Error type for extraction failures
59#[derive(Debug)]
60pub enum ExtractionError {
61    /// Missing path parameter
62    MissingPathParam(String),
63    /// Invalid path parameter format
64    InvalidPathParam(String),
65    /// Invalid query parameter format
66    InvalidQuery(String),
67    /// Invalid JSON body
68    InvalidJson(String),
69    /// Missing required header
70    MissingHeader(String),
71    /// Invalid header value
72    InvalidHeader(String),
73    /// Missing application state
74    MissingState(String),
75    /// Invalid form data
76    InvalidForm(String),
77    /// Invalid cookie
78    InvalidCookie(String),
79    /// Content too large
80    ContentTooLarge(String),
81    /// Unsupported media type
82    UnsupportedMediaType(String),
83    /// Custom error
84    Custom(String),
85}
86
87impl std::fmt::Display for ExtractionError {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        match self {
90            ExtractionError::MissingPathParam(msg) => write!(f, "Missing path parameter: {}", msg),
91            ExtractionError::InvalidPathParam(msg) => write!(f, "Invalid path parameter: {}", msg),
92            ExtractionError::InvalidQuery(msg) => write!(f, "Invalid query parameter: {}", msg),
93            ExtractionError::InvalidJson(msg) => write!(f, "Invalid JSON body: {}", msg),
94            ExtractionError::MissingHeader(msg) => write!(f, "Missing header: {}", msg),
95            ExtractionError::InvalidHeader(msg) => write!(f, "Invalid header: {}", msg),
96            ExtractionError::MissingState(msg) => write!(f, "Missing application state: {}", msg),
97            ExtractionError::InvalidForm(msg) => write!(f, "Invalid form data: {}", msg),
98            ExtractionError::InvalidCookie(msg) => write!(f, "Invalid cookie: {}", msg),
99            ExtractionError::ContentTooLarge(msg) => write!(f, "Content too large: {}", msg),
100            ExtractionError::UnsupportedMediaType(msg) => write!(f, "Unsupported media type: {}", msg),
101            ExtractionError::Custom(msg) => write!(f, "{}", msg),
102        }
103    }
104}
105
106impl std::error::Error for ExtractionError {}
107
108// Ensure ExtractionError is Send + Sync for async compatibility
109unsafe impl Send for ExtractionError {}
110unsafe impl Sync for ExtractionError {}
111
112/// Convert extraction errors into HTTP responses
113pub trait IntoResponse {
114    fn into_response(self) -> Response;
115}
116
117impl IntoResponse for ExtractionError {
118    fn into_response(self) -> Response {
119        let status = match self {
120            ExtractionError::MissingPathParam(_) | ExtractionError::InvalidPathParam(_) => {
121                StatusCode::BAD_REQUEST
122            }
123            ExtractionError::InvalidQuery(_) => StatusCode::BAD_REQUEST,
124            ExtractionError::InvalidJson(_) => StatusCode::BAD_REQUEST,
125            ExtractionError::MissingHeader(_) | ExtractionError::InvalidHeader(_) => {
126                StatusCode::BAD_REQUEST
127            }
128            ExtractionError::MissingState(_) => StatusCode::INTERNAL_SERVER_ERROR,
129            ExtractionError::InvalidForm(_) => StatusCode::BAD_REQUEST,
130            ExtractionError::InvalidCookie(_) => StatusCode::BAD_REQUEST,
131            ExtractionError::ContentTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE,
132            ExtractionError::UnsupportedMediaType(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
133            ExtractionError::Custom(_) => StatusCode::BAD_REQUEST,
134        };
135
136        Response::with_status(status).body(self.to_string())
137    }
138}
139
140impl IntoResponse for Infallible {
141    fn into_response(self) -> Response {
142        match self {}
143    }
144}
145
146impl IntoResponse for Response {
147    fn into_response(self) -> Response {
148        self
149    }
150}
151
152impl IntoResponse for &'static str {
153    fn into_response(self) -> Response {
154        Response::ok().body(self)
155    }
156}
157
158impl IntoResponse for String {
159    fn into_response(self) -> Response {
160        Response::ok().body(self)
161    }
162}
163
164impl IntoResponse for StatusCode {
165    fn into_response(self) -> Response {
166        Response::with_status(self)
167    }
168}
169
170impl<T> IntoResponse for (StatusCode, T)
171where
172    T: IntoResponse,
173{
174    fn into_response(self) -> Response {
175        let mut response = self.1.into_response();
176        *response.status_code_mut() = self.0;
177        response
178    }
179}
180
181// Re-export common types for convenience
182pub use path::Path;
183pub use query::{Query, SerdeQuery};
184pub use headers::Headers;
185pub use state::State;
186pub use form::{Form, SerdeForm};
187pub use cookies::{Cookies, SessionCookie, CookieBuilder, SameSite, get_cookie, get_required_cookie};
188
189#[cfg(feature = "json")]
190pub use json::Json;
191
192// Module declarations
193mod path;
194mod query;
195mod headers;
196pub mod state;
197mod form;
198mod cookies;
199
200#[cfg(feature = "json")]
201mod json;