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 request lacks valid authentication for an extractor that
123	/// requires it. Maps to `CoreError::Authentication` and therefore to
124	/// HTTP 401 once propagated through the handler macro. Use this when
125	/// an authenticated session, token, or identity is missing rather
126	/// than when a request parameter is malformed (see #4446).
127	#[error("Authentication required: {0}")]
128	Authentication(String),
129
130	/// The extractor failed for a reason that is neither a malformed
131	/// request nor a missing identity (e.g. a misconfigured DI scope, a
132	/// broken provider, or another infrastructure-level failure). Maps
133	/// to `CoreError::Internal` so the handler returns HTTP 500 rather
134	/// than masking the failure as a 4xx response.
135	#[error("Internal extractor error: {0}")]
136	Internal(String),
137
138	/// The parameter failed validation constraints.
139	#[cfg(feature = "validation")]
140	#[error("{}", .0.format_error())]
141	ValidationError(Box<ParamErrorContext>),
142
143	/// Struct-level validation failed after successful extraction.
144	///
145	/// Contains structured per-field errors from `Validate::validate()`.
146	/// Unlike `ValidationError` (which uses `ParamErrorContext` for single-field
147	/// constraint violations), this variant carries a full `ValidationErrors`
148	/// map for multi-field struct validation.
149	#[cfg(feature = "validation")]
150	#[error("Validation failed: {0:?}")]
151	ValidationFailed(Box<reinhardt_core::validators::ValidationErrors>),
152}
153
154impl ParamError {
155	/// Create a deserialization error from serde_json::Error
156	pub fn json_deserialization<T>(err: serde_json::Error, raw_value: Option<String>) -> Self {
157		let field_name = extract_field_from_serde_error(&err);
158		let mut ctx = ParamErrorContext::new(ParamType::Json, err.to_string())
159			.with_expected_type::<T>()
160			.with_source(Box::new(err));
161
162		if let Some(field) = field_name {
163			ctx = ctx.with_field(field);
164		}
165
166		if let Some(raw) = raw_value {
167			ctx = ctx.with_raw_value(raw);
168		}
169
170		ParamError::DeserializationError(Box::new(ctx))
171	}
172
173	/// Create a URL encoding error
174	pub fn url_encoding<T>(
175		param_type: ParamType,
176		err: serde_urlencoded::de::Error,
177		raw_value: Option<String>,
178	) -> Self {
179		let field_name = extract_field_from_urlencoded_error(&err);
180		let mut ctx = ParamErrorContext::new(param_type, err.to_string())
181			.with_expected_type::<T>()
182			.with_source(Box::new(err));
183
184		if let Some(field) = field_name {
185			ctx = ctx.with_field(field);
186		}
187
188		if let Some(raw) = raw_value {
189			ctx = ctx.with_raw_value(raw);
190		}
191
192		ParamError::UrlEncodingError(Box::new(ctx))
193	}
194
195	/// Create an invalid parameter error
196	pub fn invalid<T>(param_type: ParamType, message: impl Into<String>) -> Self {
197		let ctx = ParamErrorContext::new(param_type, message).with_expected_type::<T>();
198		ParamError::InvalidParameter(Box::new(ctx))
199	}
200
201	/// Create a parse error
202	pub fn parse<T>(
203		param_type: ParamType,
204		message: impl Into<String>,
205		source: Box<dyn std::error::Error + Send + Sync>,
206	) -> Self {
207		let ctx = ParamErrorContext::new(param_type, message)
208			.with_expected_type::<T>()
209			.with_source(source);
210		ParamError::ParseError(Box::new(ctx))
211	}
212
213	/// Get the error context if available
214	pub fn context(&self) -> Option<&ParamErrorContext> {
215		match self {
216			ParamError::InvalidParameter(ctx) => Some(ctx),
217			ParamError::ParseError(ctx) => Some(ctx),
218			ParamError::DeserializationError(ctx) => Some(ctx),
219			ParamError::UrlEncodingError(ctx) => Some(ctx),
220			#[cfg(feature = "validation")]
221			ParamError::ValidationError(ctx) => Some(ctx),
222			_ => None,
223		}
224	}
225
226	/// Format the error as multiple lines for detailed logging
227	pub fn format_multiline(&self, include_raw_value: bool) -> String {
228		match self.context() {
229			Some(ctx) => ctx.format_multiline(include_raw_value),
230			None => format!("  {}", self),
231		}
232	}
233}
234
235impl From<ParamError> for CoreError {
236	fn from(err: ParamError) -> Self {
237		// Preserve authentication semantics: `Authentication` MUST surface
238		// as `CoreError::Authentication` so the handler returns HTTP 401
239		// rather than the 400 implied by `Validation`/`ParamValidation`.
240		// `Internal` similarly MUST surface as `CoreError::Internal` so a
241		// genuine misconfiguration (e.g. a corrupted DI scope) is not
242		// masked as a 4xx response. See #4446.
243		let err = match err {
244			ParamError::Authentication(msg) => return CoreError::Authentication(msg),
245			ParamError::Internal(msg) => return CoreError::Internal(msg),
246			other => other,
247		};
248		// Use structured context if available, otherwise fall back to generic validation error
249		match err.context() {
250			Some(ctx) => CoreError::ParamValidation(Box::new(ctx.clone())),
251			None => CoreError::Validation(err.to_string()),
252		}
253	}
254}
255
256/// A specialized `Result` type for parameter extraction operations.
257pub type ParamResult<T> = std::result::Result<T, ParamError>;
258
259/// Context for parameter extraction
260pub struct ParamContext {
261	/// Path parameters extracted from the URL.
262	///
263	/// Stored in URL pattern declaration order so that tuple extractors such
264	/// as `Path<(T1, T2)>` can rely on positional ordering matching the URL
265	/// pattern. See issue #4013.
266	pub path_params: reinhardt_http::PathParams,
267	/// Header name registry keyed by value type
268	header_names: HashMap<TypeId, &'static str>,
269	/// Cookie name registry keyed by value type
270	cookie_names: HashMap<TypeId, &'static str>,
271}
272
273impl ParamContext {
274	/// Create a new empty ParamContext
275	///
276	/// # Examples
277	///
278	/// ```
279	/// use reinhardt_di::params::ParamContext;
280	///
281	/// let ctx = ParamContext::new();
282	/// assert_eq!(ctx.path_params.len(), 0);
283	/// ```
284	pub fn new() -> Self {
285		Self {
286			path_params: reinhardt_http::PathParams::new(),
287			header_names: HashMap::new(),
288			cookie_names: HashMap::new(),
289		}
290	}
291	/// Create a ParamContext with pre-populated path parameters.
292	///
293	/// Accepts anything convertible into [`reinhardt_http::PathParams`],
294	/// including a `HashMap<String, String>` (note: converting from a
295	/// `HashMap` does not preserve ordering — supply a
296	/// `Vec<(String, String)>` or a `PathParams` directly when ordering
297	/// matters, as it does for tuple extractors). See issue #4013.
298	///
299	/// # Examples
300	///
301	/// ```
302	/// use reinhardt_di::params::ParamContext;
303	/// use reinhardt_http::PathParams;
304	///
305	/// let mut params = PathParams::new();
306	/// params.insert("id", "42");
307	/// params.insert("name", "test");
308	///
309	/// let ctx = ParamContext::with_path_params(params);
310	/// assert_eq!(ctx.get_path_param("id"), Some("42"));
311	/// assert_eq!(ctx.get_path_param("name"), Some("test"));
312	/// ```
313	pub fn with_path_params(path_params: impl Into<reinhardt_http::PathParams>) -> Self {
314		Self {
315			path_params: path_params.into(),
316			header_names: HashMap::new(),
317			cookie_names: HashMap::new(),
318		}
319	}
320	/// Get a path parameter by name
321	///
322	/// Returns `None` if the parameter doesn't exist.
323	///
324	/// # Examples
325	///
326	/// ```
327	/// use reinhardt_di::params::ParamContext;
328	/// use std::collections::HashMap;
329	///
330	/// let mut params = HashMap::new();
331	/// params.insert("user_id".to_string(), "123".to_string());
332	///
333	/// let ctx = ParamContext::with_path_params(params);
334	/// assert_eq!(ctx.get_path_param("user_id"), Some("123"));
335	/// assert_eq!(ctx.get_path_param("missing"), None);
336	/// ```
337	pub fn get_path_param(&self, name: &str) -> Option<&str> {
338		self.path_params.get(name).map(|s| s.as_str())
339	}
340
341	/// Register a header name for the given value type `T`
342	pub fn set_header_name<T: 'static>(&mut self, name: &'static str) {
343		self.header_names.insert(TypeId::of::<T>(), name);
344	}
345
346	/// Get a registered header name for value type `T`
347	pub fn get_header_name<T: 'static>(&self) -> Option<&'static str> {
348		self.header_names.get(&TypeId::of::<T>()).copied()
349	}
350
351	/// Register a cookie name for the given value type `T`
352	pub fn set_cookie_name<T: 'static>(&mut self, name: &'static str) {
353		self.cookie_names.insert(TypeId::of::<T>(), name);
354	}
355
356	/// Get a registered cookie name for value type `T`
357	pub fn get_cookie_name<T: 'static>(&self) -> Option<&'static str> {
358		self.cookie_names.get(&TypeId::of::<T>()).copied()
359	}
360}
361
362impl Default for ParamContext {
363	fn default() -> Self {
364		Self::new()
365	}
366}