Skip to main content

reinhardt_di/
params.rs

1//! # Reinhardt Parameter Extraction
2//!
3//! FastAPI-inspired parameter extraction system.
4//!
5//! ## Features
6//!
7//! - **Path Parameters**: Extract from URL path
8//! - **Query Parameters**: Extract from query string
9//! - **Headers**: Extract from request headers
10//! - **Cookies**: Extract from cookies
11//! - **Body**: Extract from request body
12//! - **Type-safe**: Full compile-time type checking
13//!
14//! ## Example
15//!
16//! ```rust,no_run
17//! use reinhardt_di::params::{Path, Query, Json};
18//! # use serde::Deserialize;
19//! # #[derive(Deserialize)]
20//! # struct UserFilter { page: i32 }
21//! # #[derive(Deserialize)]
22//! # struct UpdateUser { name: String }
23//!
24//! // Extract path parameter
25//! let id = Path(42_i64);
26//! let user_id: i64 = id.0; // or *id
27//!
28//! // Extract query parameters
29//! # let filter_data = UserFilter { page: 1 };
30//! let filter = Query(filter_data);
31//! let page = filter.0.page;
32//!
33//! // Extract JSON body
34//! # let user_data = UpdateUser { name: "Alice".to_string() };
35//! let body = Json(user_data);
36//! let name = &body.0.name;
37//! ```
38
39pub mod body;
40pub mod cookie;
41pub mod cookie_named;
42pub(crate) mod cookie_util;
43pub mod extract;
44pub mod form;
45pub mod has_inner;
46pub mod header;
47pub mod header_named;
48pub mod json;
49#[cfg(feature = "multipart")]
50pub mod multipart;
51pub mod path;
52pub mod query;
53pub mod validation;
54
55use reinhardt_http::Error as CoreError;
56use std::any::TypeId;
57use std::collections::HashMap;
58use thiserror::Error;
59
60// Re-export Request from reinhardt-http and parameter error types from reinhardt-exception
61pub use reinhardt_core::exception::{ParamErrorContext, ParamType};
62pub use reinhardt_http::Request;
63
64// Import helper functions for parameter error extraction
65use reinhardt_core::exception::param_error::{
66	extract_field_from_serde_error, extract_field_from_urlencoded_error,
67};
68
69pub use body::Body;
70pub use cookie::{Cookie, CookieStruct};
71pub use cookie_named::{CookieName, CookieNamed, CsrfToken, SessionId};
72pub use extract::FromRequest;
73pub use form::Form;
74pub use has_inner::HasInner;
75pub use header::{Header, HeaderStruct};
76pub use header_named::{Authorization, ContentType, HeaderName, HeaderNamed};
77pub use json::Json;
78#[cfg(feature = "multipart")]
79pub use multipart::Multipart;
80pub use path::{Path, PathStruct};
81pub use query::Query;
82#[cfg(feature = "validation")]
83pub use validation::Validated;
84pub use validation::{
85	ValidatedForm, ValidatedPath, ValidatedQuery, ValidationConstraints, WithValidation,
86};
87
88// Box wrappers to reduce enum size (clippy::result_large_err mitigation)
89// ParamErrorContext contains multiple String fields which make the enum large
90/// Errors that can occur during parameter extraction from HTTP requests.
91#[derive(Debug, Error)]
92#[non_exhaustive]
93pub enum ParamError {
94	/// A required parameter was not provided.
95	#[error("Missing required parameter: {0}")]
96	MissingParameter(String),
97
98	/// The parameter value is invalid.
99	#[error("{}", .0.format_error())]
100	InvalidParameter(Box<ParamErrorContext>),
101
102	/// The parameter value could not be parsed to the expected type.
103	#[error("{}", .0.format_error())]
104	ParseError(Box<ParamErrorContext>),
105
106	/// The parameter value could not be deserialized.
107	#[error("{}", .0.format_error())]
108	DeserializationError(Box<ParamErrorContext>),
109
110	/// The URL-encoded parameter could not be decoded.
111	#[error("{}", .0.format_error())]
112	UrlEncodingError(Box<ParamErrorContext>),
113
114	/// An error occurred while reading the request body.
115	#[error("Request body error: {0}")]
116	BodyError(String),
117
118	/// The request payload exceeds the configured size limit.
119	#[error("Payload too large: {0}")]
120	PayloadTooLarge(String),
121
122	/// The parameter failed validation constraints.
123	#[cfg(feature = "validation")]
124	#[error("{}", .0.format_error())]
125	ValidationError(Box<ParamErrorContext>),
126
127	/// Struct-level validation failed after successful extraction.
128	///
129	/// Contains structured per-field errors from `Validate::validate()`.
130	/// Unlike `ValidationError` (which uses `ParamErrorContext` for single-field
131	/// constraint violations), this variant carries a full `ValidationErrors`
132	/// map for multi-field struct validation.
133	#[cfg(feature = "validation")]
134	#[error("Validation failed: {0:?}")]
135	ValidationFailed(Box<reinhardt_core::validators::ValidationErrors>),
136}
137
138impl ParamError {
139	/// Create a deserialization error from serde_json::Error
140	pub fn json_deserialization<T>(err: serde_json::Error, raw_value: Option<String>) -> Self {
141		let field_name = extract_field_from_serde_error(&err);
142		let mut ctx = ParamErrorContext::new(ParamType::Json, err.to_string())
143			.with_expected_type::<T>()
144			.with_source(Box::new(err));
145
146		if let Some(field) = field_name {
147			ctx = ctx.with_field(field);
148		}
149
150		if let Some(raw) = raw_value {
151			ctx = ctx.with_raw_value(raw);
152		}
153
154		ParamError::DeserializationError(Box::new(ctx))
155	}
156
157	/// Create a URL encoding error
158	pub fn url_encoding<T>(
159		param_type: ParamType,
160		err: serde_urlencoded::de::Error,
161		raw_value: Option<String>,
162	) -> Self {
163		let field_name = extract_field_from_urlencoded_error(&err);
164		let mut ctx = ParamErrorContext::new(param_type, err.to_string())
165			.with_expected_type::<T>()
166			.with_source(Box::new(err));
167
168		if let Some(field) = field_name {
169			ctx = ctx.with_field(field);
170		}
171
172		if let Some(raw) = raw_value {
173			ctx = ctx.with_raw_value(raw);
174		}
175
176		ParamError::UrlEncodingError(Box::new(ctx))
177	}
178
179	/// Create an invalid parameter error
180	pub fn invalid<T>(param_type: ParamType, message: impl Into<String>) -> Self {
181		let ctx = ParamErrorContext::new(param_type, message).with_expected_type::<T>();
182		ParamError::InvalidParameter(Box::new(ctx))
183	}
184
185	/// Create a parse error
186	pub fn parse<T>(
187		param_type: ParamType,
188		message: impl Into<String>,
189		source: Box<dyn std::error::Error + Send + Sync>,
190	) -> Self {
191		let ctx = ParamErrorContext::new(param_type, message)
192			.with_expected_type::<T>()
193			.with_source(source);
194		ParamError::ParseError(Box::new(ctx))
195	}
196
197	/// Get the error context if available
198	pub fn context(&self) -> Option<&ParamErrorContext> {
199		match self {
200			ParamError::InvalidParameter(ctx) => Some(ctx),
201			ParamError::ParseError(ctx) => Some(ctx),
202			ParamError::DeserializationError(ctx) => Some(ctx),
203			ParamError::UrlEncodingError(ctx) => Some(ctx),
204			#[cfg(feature = "validation")]
205			ParamError::ValidationError(ctx) => Some(ctx),
206			_ => None,
207		}
208	}
209
210	/// Format the error as multiple lines for detailed logging
211	pub fn format_multiline(&self, include_raw_value: bool) -> String {
212		match self.context() {
213			Some(ctx) => ctx.format_multiline(include_raw_value),
214			None => format!("  {}", self),
215		}
216	}
217}
218
219impl From<ParamError> for CoreError {
220	fn from(err: ParamError) -> Self {
221		// Use structured context if available, otherwise fall back to generic validation error
222		match err.context() {
223			Some(ctx) => CoreError::ParamValidation(Box::new(ctx.clone())),
224			None => CoreError::Validation(err.to_string()),
225		}
226	}
227}
228
229/// A specialized `Result` type for parameter extraction operations.
230pub type ParamResult<T> = std::result::Result<T, ParamError>;
231
232/// Context for parameter extraction
233pub struct ParamContext {
234	/// Path parameters extracted from the URL.
235	///
236	/// Stored in URL pattern declaration order so that tuple extractors such
237	/// as `Path<(T1, T2)>` can rely on positional ordering matching the URL
238	/// pattern. See issue #4013.
239	pub path_params: reinhardt_http::PathParams,
240	/// Header name registry keyed by value type
241	header_names: HashMap<TypeId, &'static str>,
242	/// Cookie name registry keyed by value type
243	cookie_names: HashMap<TypeId, &'static str>,
244}
245
246impl ParamContext {
247	/// Create a new empty ParamContext
248	///
249	/// # Examples
250	///
251	/// ```
252	/// use reinhardt_di::params::ParamContext;
253	///
254	/// let ctx = ParamContext::new();
255	/// assert_eq!(ctx.path_params.len(), 0);
256	/// ```
257	pub fn new() -> Self {
258		Self {
259			path_params: reinhardt_http::PathParams::new(),
260			header_names: HashMap::new(),
261			cookie_names: HashMap::new(),
262		}
263	}
264	/// Create a ParamContext with pre-populated path parameters.
265	///
266	/// Accepts anything convertible into [`reinhardt_http::PathParams`],
267	/// including a `HashMap<String, String>` (note: converting from a
268	/// `HashMap` does not preserve ordering — supply a
269	/// `Vec<(String, String)>` or a `PathParams` directly when ordering
270	/// matters, as it does for tuple extractors). See issue #4013.
271	///
272	/// # Examples
273	///
274	/// ```
275	/// use reinhardt_di::params::ParamContext;
276	/// use reinhardt_http::PathParams;
277	///
278	/// let mut params = PathParams::new();
279	/// params.insert("id", "42");
280	/// params.insert("name", "test");
281	///
282	/// let ctx = ParamContext::with_path_params(params);
283	/// assert_eq!(ctx.get_path_param("id"), Some("42"));
284	/// assert_eq!(ctx.get_path_param("name"), Some("test"));
285	/// ```
286	pub fn with_path_params(path_params: impl Into<reinhardt_http::PathParams>) -> Self {
287		Self {
288			path_params: path_params.into(),
289			header_names: HashMap::new(),
290			cookie_names: HashMap::new(),
291		}
292	}
293	/// Get a path parameter by name
294	///
295	/// Returns `None` if the parameter doesn't exist.
296	///
297	/// # Examples
298	///
299	/// ```
300	/// use reinhardt_di::params::ParamContext;
301	/// use std::collections::HashMap;
302	///
303	/// let mut params = HashMap::new();
304	/// params.insert("user_id".to_string(), "123".to_string());
305	///
306	/// let ctx = ParamContext::with_path_params(params);
307	/// assert_eq!(ctx.get_path_param("user_id"), Some("123"));
308	/// assert_eq!(ctx.get_path_param("missing"), None);
309	/// ```
310	pub fn get_path_param(&self, name: &str) -> Option<&str> {
311		self.path_params.get(name).map(|s| s.as_str())
312	}
313
314	/// Register a header name for the given value type `T`
315	pub fn set_header_name<T: 'static>(&mut self, name: &'static str) {
316		self.header_names.insert(TypeId::of::<T>(), name);
317	}
318
319	/// Get a registered header name for value type `T`
320	pub fn get_header_name<T: 'static>(&self) -> Option<&'static str> {
321		self.header_names.get(&TypeId::of::<T>()).copied()
322	}
323
324	/// Register a cookie name for the given value type `T`
325	pub fn set_cookie_name<T: 'static>(&mut self, name: &'static str) {
326		self.cookie_names.insert(TypeId::of::<T>(), name);
327	}
328
329	/// Get a registered cookie name for value type `T`
330	pub fn get_cookie_name<T: 'static>(&self) -> Option<&'static str> {
331		self.cookie_names.get(&TypeId::of::<T>()).copied()
332	}
333}
334
335impl Default for ParamContext {
336	fn default() -> Self {
337		Self::new()
338	}
339}