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 header;
46pub mod header_named;
47pub mod json;
48#[cfg(feature = "multipart")]
49pub mod multipart;
50pub mod path;
51pub mod query;
52pub mod validation;
53
54use reinhardt_http::Error as CoreError;
55use std::any::TypeId;
56use std::collections::HashMap;
57use thiserror::Error;
58
59// Re-export Request from reinhardt-http and parameter error types from reinhardt-exception
60pub use reinhardt_core::exception::{ParamErrorContext, ParamType};
61pub use reinhardt_http::Request;
62
63// Import helper functions for parameter error extraction
64use reinhardt_core::exception::param_error::{
65	extract_field_from_serde_error, extract_field_from_urlencoded_error,
66};
67
68pub use body::Body;
69pub use cookie::{Cookie, CookieStruct};
70pub use cookie_named::{CookieName, CookieNamed, CsrfToken, SessionId};
71pub use extract::FromRequest;
72pub use form::Form;
73pub use header::{Header, HeaderStruct};
74pub use header_named::{Authorization, ContentType, HeaderName, HeaderNamed};
75pub use json::Json;
76#[cfg(feature = "multipart")]
77pub use multipart::Multipart;
78pub use path::{Path, PathStruct};
79pub use query::Query;
80#[cfg(feature = "validation")]
81pub use validation::Validated;
82pub use validation::{
83	ValidatedForm, ValidatedPath, ValidatedQuery, ValidationConstraints, WithValidation,
84};
85
86// Box wrappers to reduce enum size (clippy::result_large_err mitigation)
87// ParamErrorContext contains multiple String fields which make the enum large
88/// Errors that can occur during parameter extraction from HTTP requests.
89#[derive(Debug, Error)]
90pub enum ParamError {
91	/// A required parameter was not provided.
92	#[error("Missing required parameter: {0}")]
93	MissingParameter(String),
94
95	/// The parameter value is invalid.
96	#[error("{}", .0.format_error())]
97	InvalidParameter(Box<ParamErrorContext>),
98
99	/// The parameter value could not be parsed to the expected type.
100	#[error("{}", .0.format_error())]
101	ParseError(Box<ParamErrorContext>),
102
103	/// The parameter value could not be deserialized.
104	#[error("{}", .0.format_error())]
105	DeserializationError(Box<ParamErrorContext>),
106
107	/// The URL-encoded parameter could not be decoded.
108	#[error("{}", .0.format_error())]
109	UrlEncodingError(Box<ParamErrorContext>),
110
111	/// An error occurred while reading the request body.
112	#[error("Request body error: {0}")]
113	BodyError(String),
114
115	/// The request payload exceeds the configured size limit.
116	#[error("Payload too large: {0}")]
117	PayloadTooLarge(String),
118
119	/// The parameter failed validation constraints.
120	#[cfg(feature = "validation")]
121	#[error("{}", .0.format_error())]
122	ValidationError(Box<ParamErrorContext>),
123}
124
125impl ParamError {
126	/// Create a deserialization error from serde_json::Error
127	pub fn json_deserialization<T>(err: serde_json::Error, raw_value: Option<String>) -> Self {
128		let field_name = extract_field_from_serde_error(&err);
129		let mut ctx = ParamErrorContext::new(ParamType::Json, err.to_string())
130			.with_expected_type::<T>()
131			.with_source(Box::new(err));
132
133		if let Some(field) = field_name {
134			ctx = ctx.with_field(field);
135		}
136
137		if let Some(raw) = raw_value {
138			ctx = ctx.with_raw_value(raw);
139		}
140
141		ParamError::DeserializationError(Box::new(ctx))
142	}
143
144	/// Create a URL encoding error
145	pub fn url_encoding<T>(
146		param_type: ParamType,
147		err: serde_urlencoded::de::Error,
148		raw_value: Option<String>,
149	) -> Self {
150		let field_name = extract_field_from_urlencoded_error(&err);
151		let mut ctx = ParamErrorContext::new(param_type, err.to_string())
152			.with_expected_type::<T>()
153			.with_source(Box::new(err));
154
155		if let Some(field) = field_name {
156			ctx = ctx.with_field(field);
157		}
158
159		if let Some(raw) = raw_value {
160			ctx = ctx.with_raw_value(raw);
161		}
162
163		ParamError::UrlEncodingError(Box::new(ctx))
164	}
165
166	/// Create an invalid parameter error
167	pub fn invalid<T>(param_type: ParamType, message: impl Into<String>) -> Self {
168		let ctx = ParamErrorContext::new(param_type, message).with_expected_type::<T>();
169		ParamError::InvalidParameter(Box::new(ctx))
170	}
171
172	/// Create a parse error
173	pub fn parse<T>(
174		param_type: ParamType,
175		message: impl Into<String>,
176		source: Box<dyn std::error::Error + Send + Sync>,
177	) -> Self {
178		let ctx = ParamErrorContext::new(param_type, message)
179			.with_expected_type::<T>()
180			.with_source(source);
181		ParamError::ParseError(Box::new(ctx))
182	}
183
184	/// Get the error context if available
185	pub fn context(&self) -> Option<&ParamErrorContext> {
186		match self {
187			ParamError::InvalidParameter(ctx) => Some(ctx),
188			ParamError::ParseError(ctx) => Some(ctx),
189			ParamError::DeserializationError(ctx) => Some(ctx),
190			ParamError::UrlEncodingError(ctx) => Some(ctx),
191			#[cfg(feature = "validation")]
192			ParamError::ValidationError(ctx) => Some(ctx),
193			_ => None,
194		}
195	}
196
197	/// Format the error as multiple lines for detailed logging
198	pub fn format_multiline(&self, include_raw_value: bool) -> String {
199		match self.context() {
200			Some(ctx) => ctx.format_multiline(include_raw_value),
201			None => format!("  {}", self),
202		}
203	}
204}
205
206impl From<ParamError> for CoreError {
207	fn from(err: ParamError) -> Self {
208		// Use structured context if available, otherwise fall back to generic validation error
209		match err.context() {
210			Some(ctx) => CoreError::ParamValidation(Box::new(ctx.clone())),
211			None => CoreError::Validation(err.to_string()),
212		}
213	}
214}
215
216/// A specialized `Result` type for parameter extraction operations.
217pub type ParamResult<T> = std::result::Result<T, ParamError>;
218
219/// Context for parameter extraction
220pub struct ParamContext {
221	/// Path parameters extracted from the URL
222	pub path_params: std::collections::HashMap<String, String>,
223	/// Header name registry keyed by value type
224	header_names: HashMap<TypeId, &'static str>,
225	/// Cookie name registry keyed by value type
226	cookie_names: HashMap<TypeId, &'static str>,
227}
228
229impl ParamContext {
230	/// Create a new empty ParamContext
231	///
232	/// # Examples
233	///
234	/// ```
235	/// use reinhardt_di::params::ParamContext;
236	///
237	/// let ctx = ParamContext::new();
238	/// assert_eq!(ctx.path_params.len(), 0);
239	/// ```
240	pub fn new() -> Self {
241		Self {
242			path_params: std::collections::HashMap::new(),
243			header_names: HashMap::new(),
244			cookie_names: HashMap::new(),
245		}
246	}
247	/// Create a ParamContext with pre-populated path parameters
248	///
249	/// # Examples
250	///
251	/// ```
252	/// use reinhardt_di::params::ParamContext;
253	/// use std::collections::HashMap;
254	///
255	/// let mut params = HashMap::new();
256	/// params.insert("id".to_string(), "42".to_string());
257	/// params.insert("name".to_string(), "test".to_string());
258	///
259	/// let ctx = ParamContext::with_path_params(params);
260	/// assert_eq!(ctx.get_path_param("id"), Some("42"));
261	/// assert_eq!(ctx.get_path_param("name"), Some("test"));
262	/// ```
263	pub fn with_path_params(path_params: std::collections::HashMap<String, String>) -> Self {
264		Self {
265			path_params,
266			header_names: HashMap::new(),
267			cookie_names: HashMap::new(),
268		}
269	}
270	/// Get a path parameter by name
271	///
272	/// Returns `None` if the parameter doesn't exist.
273	///
274	/// # Examples
275	///
276	/// ```
277	/// use reinhardt_di::params::ParamContext;
278	/// use std::collections::HashMap;
279	///
280	/// let mut params = HashMap::new();
281	/// params.insert("user_id".to_string(), "123".to_string());
282	///
283	/// let ctx = ParamContext::with_path_params(params);
284	/// assert_eq!(ctx.get_path_param("user_id"), Some("123"));
285	/// assert_eq!(ctx.get_path_param("missing"), None);
286	/// ```
287	pub fn get_path_param(&self, name: &str) -> Option<&str> {
288		self.path_params.get(name).map(|s| s.as_str())
289	}
290
291	/// Register a header name for the given value type `T`
292	pub fn set_header_name<T: 'static>(&mut self, name: &'static str) {
293		self.header_names.insert(TypeId::of::<T>(), name);
294	}
295
296	/// Get a registered header name for value type `T`
297	pub fn get_header_name<T: 'static>(&self) -> Option<&'static str> {
298		self.header_names.get(&TypeId::of::<T>()).copied()
299	}
300
301	/// Register a cookie name for the given value type `T`
302	pub fn set_cookie_name<T: 'static>(&mut self, name: &'static str) {
303		self.cookie_names.insert(TypeId::of::<T>(), name);
304	}
305
306	/// Get a registered cookie name for value type `T`
307	pub fn get_cookie_name<T: 'static>(&self) -> Option<&'static str> {
308		self.cookie_names.get(&TypeId::of::<T>()).copied()
309	}
310}
311
312impl Default for ParamContext {
313	fn default() -> Self {
314		Self::new()
315	}
316}