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#[derive(Debug, Error)]
89pub enum ParamError {
90	#[error("Missing required parameter: {0}")]
91	MissingParameter(String),
92
93	#[error("{}", .0.format_error())]
94	InvalidParameter(Box<ParamErrorContext>),
95
96	#[error("{}", .0.format_error())]
97	ParseError(Box<ParamErrorContext>),
98
99	#[error("{}", .0.format_error())]
100	DeserializationError(Box<ParamErrorContext>),
101
102	#[error("{}", .0.format_error())]
103	UrlEncodingError(Box<ParamErrorContext>),
104
105	#[error("Request body error: {0}")]
106	BodyError(String),
107
108	#[error("Payload too large: {0}")]
109	PayloadTooLarge(String),
110
111	#[cfg(feature = "validation")]
112	#[error("{}", .0.format_error())]
113	ValidationError(Box<ParamErrorContext>),
114}
115
116impl ParamError {
117	/// Create a deserialization error from serde_json::Error
118	pub fn json_deserialization<T>(err: serde_json::Error, raw_value: Option<String>) -> Self {
119		let field_name = extract_field_from_serde_error(&err);
120		let mut ctx = ParamErrorContext::new(ParamType::Json, err.to_string())
121			.with_expected_type::<T>()
122			.with_source(Box::new(err));
123
124		if let Some(field) = field_name {
125			ctx = ctx.with_field(field);
126		}
127
128		if let Some(raw) = raw_value {
129			ctx = ctx.with_raw_value(raw);
130		}
131
132		ParamError::DeserializationError(Box::new(ctx))
133	}
134
135	/// Create a URL encoding error
136	pub fn url_encoding<T>(
137		param_type: ParamType,
138		err: serde_urlencoded::de::Error,
139		raw_value: Option<String>,
140	) -> Self {
141		let field_name = extract_field_from_urlencoded_error(&err);
142		let mut ctx = ParamErrorContext::new(param_type, 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::UrlEncodingError(Box::new(ctx))
155	}
156
157	/// Create an invalid parameter error
158	pub fn invalid<T>(param_type: ParamType, message: impl Into<String>) -> Self {
159		let ctx = ParamErrorContext::new(param_type, message).with_expected_type::<T>();
160		ParamError::InvalidParameter(Box::new(ctx))
161	}
162
163	/// Create a parse error
164	pub fn parse<T>(
165		param_type: ParamType,
166		message: impl Into<String>,
167		source: Box<dyn std::error::Error + Send + Sync>,
168	) -> Self {
169		let ctx = ParamErrorContext::new(param_type, message)
170			.with_expected_type::<T>()
171			.with_source(source);
172		ParamError::ParseError(Box::new(ctx))
173	}
174
175	/// Get the error context if available
176	pub fn context(&self) -> Option<&ParamErrorContext> {
177		match self {
178			ParamError::InvalidParameter(ctx) => Some(ctx),
179			ParamError::ParseError(ctx) => Some(ctx),
180			ParamError::DeserializationError(ctx) => Some(ctx),
181			ParamError::UrlEncodingError(ctx) => Some(ctx),
182			#[cfg(feature = "validation")]
183			ParamError::ValidationError(ctx) => Some(ctx),
184			_ => None,
185		}
186	}
187
188	/// Format the error as multiple lines for detailed logging
189	pub fn format_multiline(&self, include_raw_value: bool) -> String {
190		match self.context() {
191			Some(ctx) => ctx.format_multiline(include_raw_value),
192			None => format!("  {}", self),
193		}
194	}
195}
196
197impl From<ParamError> for CoreError {
198	fn from(err: ParamError) -> Self {
199		// Use structured context if available, otherwise fall back to generic validation error
200		match err.context() {
201			Some(ctx) => CoreError::ParamValidation(Box::new(ctx.clone())),
202			None => CoreError::Validation(err.to_string()),
203		}
204	}
205}
206
207pub type ParamResult<T> = std::result::Result<T, ParamError>;
208
209/// Context for parameter extraction
210pub struct ParamContext {
211	/// Path parameters extracted from the URL
212	pub path_params: std::collections::HashMap<String, String>,
213	/// Header name registry keyed by value type
214	header_names: HashMap<TypeId, &'static str>,
215	/// Cookie name registry keyed by value type
216	cookie_names: HashMap<TypeId, &'static str>,
217}
218
219impl ParamContext {
220	/// Create a new empty ParamContext
221	///
222	/// # Examples
223	///
224	/// ```
225	/// use reinhardt_di::params::ParamContext;
226	///
227	/// let ctx = ParamContext::new();
228	/// assert_eq!(ctx.path_params.len(), 0);
229	/// ```
230	pub fn new() -> Self {
231		Self {
232			path_params: std::collections::HashMap::new(),
233			header_names: HashMap::new(),
234			cookie_names: HashMap::new(),
235		}
236	}
237	/// Create a ParamContext with pre-populated path parameters
238	///
239	/// # Examples
240	///
241	/// ```
242	/// use reinhardt_di::params::ParamContext;
243	/// use std::collections::HashMap;
244	///
245	/// let mut params = HashMap::new();
246	/// params.insert("id".to_string(), "42".to_string());
247	/// params.insert("name".to_string(), "test".to_string());
248	///
249	/// let ctx = ParamContext::with_path_params(params);
250	/// assert_eq!(ctx.get_path_param("id"), Some("42"));
251	/// assert_eq!(ctx.get_path_param("name"), Some("test"));
252	/// ```
253	pub fn with_path_params(path_params: std::collections::HashMap<String, String>) -> Self {
254		Self {
255			path_params,
256			header_names: HashMap::new(),
257			cookie_names: HashMap::new(),
258		}
259	}
260	/// Get a path parameter by name
261	///
262	/// Returns `None` if the parameter doesn't exist.
263	///
264	/// # Examples
265	///
266	/// ```
267	/// use reinhardt_di::params::ParamContext;
268	/// use std::collections::HashMap;
269	///
270	/// let mut params = HashMap::new();
271	/// params.insert("user_id".to_string(), "123".to_string());
272	///
273	/// let ctx = ParamContext::with_path_params(params);
274	/// assert_eq!(ctx.get_path_param("user_id"), Some("123"));
275	/// assert_eq!(ctx.get_path_param("missing"), None);
276	/// ```
277	pub fn get_path_param(&self, name: &str) -> Option<&str> {
278		self.path_params.get(name).map(|s| s.as_str())
279	}
280
281	/// Register a header name for the given value type `T`
282	pub fn set_header_name<T: 'static>(&mut self, name: &'static str) {
283		self.header_names.insert(TypeId::of::<T>(), name);
284	}
285
286	/// Get a registered header name for value type `T`
287	pub fn get_header_name<T: 'static>(&self) -> Option<&'static str> {
288		self.header_names.get(&TypeId::of::<T>()).copied()
289	}
290
291	/// Register a cookie name for the given value type `T`
292	pub fn set_cookie_name<T: 'static>(&mut self, name: &'static str) {
293		self.cookie_names.insert(TypeId::of::<T>(), name);
294	}
295
296	/// Get a registered cookie name for value type `T`
297	pub fn get_cookie_name<T: 'static>(&self) -> Option<&'static str> {
298		self.cookie_names.get(&TypeId::of::<T>()).copied()
299	}
300}
301
302impl Default for ParamContext {
303	fn default() -> Self {
304		Self::new()
305	}
306}