Skip to main content

reinhardt_di/params/
path.rs

1//! Path parameter extraction
2//!
3//! Extract typed values from URL path parameters.
4
5use async_trait::async_trait;
6use reinhardt_http::Request;
7use serde::de::DeserializeOwned;
8use std::fmt::{self, Debug};
9use std::ops::Deref;
10
11use super::{
12	ParamContext, ParamError, ParamErrorContext, ParamResult, ParamType, extract::FromRequest,
13};
14
15/// Extract a single value from the URL path
16///
17/// # Example
18///
19/// ```rust
20/// use reinhardt_di::params::Path;
21///
22/// let id = Path(42_i64);
23/// let user_id: i64 = id.0; // or *id
24/// assert_eq!(user_id, 42);
25/// ```
26pub struct Path<T>(pub T);
27
28impl<T> Path<T> {
29	/// Unwrap the Path and return the inner value
30	///
31	/// # Examples
32	///
33	/// ```
34	/// use reinhardt_di::params::Path;
35	///
36	/// let path = Path(42i64);
37	/// let inner = path.into_inner();
38	/// assert_eq!(inner, 42);
39	/// ```
40	pub fn into_inner(self) -> T {
41		self.0
42	}
43}
44
45impl<T> Deref for Path<T> {
46	type Target = T;
47
48	fn deref(&self) -> &Self::Target {
49		&self.0
50	}
51}
52
53impl<T: Debug> Debug for Path<T> {
54	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55		self.0.fmt(f)
56	}
57}
58
59impl<T: Clone> Clone for Path<T> {
60	fn clone(&self) -> Self {
61		Path(self.0.clone())
62	}
63}
64
65// Macro to implement FromRequest for primitive types
66// This allows extracting primitive types directly from path parameters
67macro_rules! impl_path_from_str {
68    ($($ty:ty),+ $(,)?) => {
69        $(
70            #[async_trait]
71            impl FromRequest for Path<$ty> {
72                async fn from_request(_req: &Request, ctx: &ParamContext) -> ParamResult<Self> {
73                    // For primitive types, extract the single value directly
74                    if ctx.path_params.len() != 1 {
75                        return Err(ParamError::InvalidParameter(Box::new(
76                            ParamErrorContext::new(
77                                ParamType::Path,
78                                format!(
79                                    "Expected exactly 1 path parameter for primitive type, found {}",
80                                    ctx.path_params.len()
81                                ),
82                            )
83                            .with_expected_type::<$ty>(),
84                        )));
85                    }
86
87                    let value = ctx.path_params.values().next().unwrap();
88                    value.parse::<$ty>()
89                        .map(Path)
90                        .map_err(|e| {
91                            ParamError::parse::<$ty>(
92                                ParamType::Path,
93                                format!("Failed to parse '{}' as {}: {}", value, stringify!($ty), e),
94                                Box::new(std::io::Error::new(
95                                    std::io::ErrorKind::InvalidData,
96                                    e.to_string(),
97                                )),
98                            )
99                        })
100                }
101            }
102        )+
103    };
104}
105
106// Implement for common primitive types
107impl_path_from_str!(
108	i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool
109);
110
111// Implement for Uuid when the uuid feature is enabled
112#[cfg(feature = "uuid")]
113impl_path_from_str!(uuid::Uuid);
114
115// Implementation for 2-tuple path parameters
116// This enables extracting multiple path parameters like Path<(Uuid, Uuid)>
117macro_rules! impl_path_tuple2_from_str {
118    ($($t1:ty, $t2:ty);+ $(;)?) => {
119        $(
120            #[async_trait]
121            impl FromRequest for Path<($t1, $t2)> {
122                async fn from_request(_req: &Request, ctx: &ParamContext) -> ParamResult<Self> {
123                    if ctx.path_params.len() != 2 {
124                        return Err(ParamError::InvalidParameter(Box::new(
125                            ParamErrorContext::new(
126                                ParamType::Path,
127                                format!(
128                                    "Expected exactly 2 path parameters for tuple type, found {}",
129                                    ctx.path_params.len()
130                                ),
131                            )
132                            .with_expected_type::<($t1, $t2)>(),
133                        )));
134                    }
135
136                    // Sort by key name to ensure deterministic extraction order
137                    // regardless of HashMap iteration order
138                    let mut sorted_params: Vec<_> = ctx.path_params.iter().collect();
139                    sorted_params.sort_by_key(|(k, _)| k.clone());
140                    let values: Vec<_> = sorted_params.into_iter().map(|(_, v)| v).collect();
141                    if values.len() != 2 {
142                        return Err(ParamError::InvalidParameter(Box::new(
143                            ParamErrorContext::new(
144                                ParamType::Path,
145                                "Expected exactly 2 path parameters".to_string(),
146                            )
147                            .with_expected_type::<($t1, $t2)>(),
148                        )));
149                    }
150
151                    let v1 = values[0].parse::<$t1>()
152                        .map_err(|e| {
153                            let ctx = ParamErrorContext::new(
154                                ParamType::Path,
155                                format!("Failed to parse '{}' as {}: {}", values[0], stringify!($t1), e),
156                            )
157                            .with_field("path[0]")
158                            .with_expected_type::<$t1>()
159                            .with_raw_value(values[0].as_str())
160                            .with_source(Box::new(std::io::Error::new(
161                                std::io::ErrorKind::InvalidData,
162                                e.to_string(),
163                            )));
164                            ParamError::ParseError(Box::new(ctx))
165                        })?;
166
167                    let v2 = values[1].parse::<$t2>()
168                        .map_err(|e| {
169                            let ctx = ParamErrorContext::new(
170                                ParamType::Path,
171                                format!("Failed to parse '{}' as {}: {}", values[1], stringify!($t2), e),
172                            )
173                            .with_field("path[1]")
174                            .with_expected_type::<$t2>()
175                            .with_raw_value(values[1].as_str())
176                            .with_source(Box::new(std::io::Error::new(
177                                std::io::ErrorKind::InvalidData,
178                                e.to_string(),
179                            )));
180                            ParamError::ParseError(Box::new(ctx))
181                        })?;
182
183                    Ok(Path((v1, v2)))
184                }
185            }
186        )+
187    };
188}
189
190// Common tuple combinations
191impl_path_tuple2_from_str!(
192	i64, i64;
193	String, i64;
194	i64, String;
195	String, String
196);
197
198// Uuid tuple combinations when uuid feature is enabled
199#[cfg(feature = "uuid")]
200impl_path_tuple2_from_str!(
201	uuid::Uuid, uuid::Uuid;
202	uuid::Uuid, i64;
203	i64, uuid::Uuid;
204	uuid::Uuid, String;
205	String, uuid::Uuid
206);
207
208// Special implementation for String (no parsing needed)
209#[async_trait]
210impl FromRequest for Path<String> {
211	async fn from_request(_req: &Request, ctx: &ParamContext) -> ParamResult<Self> {
212		if ctx.path_params.len() != 1 {
213			return Err(ParamError::InvalidParameter(Box::new(
214				ParamErrorContext::new(
215					ParamType::Path,
216					format!(
217						"Expected exactly 1 path parameter for String, found {}",
218						ctx.path_params.len()
219					),
220				)
221				.with_expected_type::<String>(),
222			)));
223		}
224
225		let value = ctx.path_params.values().next().unwrap().clone();
226		Ok(Path(value))
227	}
228}
229
230// Note: For complex types like enums, Vec, HashMap, etc., users should use
231// a custom deserializer or validate that the type is not suitable for path parameters.
232// We intentionally don't provide a generic DeserializeOwned impl to avoid conflicts
233// with the FromStr-based implementations above.
234
235/// PathStruct is a helper type for extracting structured path parameters
236///
237/// Use this when you need to extract multiple path parameters into a struct.
238///
239/// # Example
240///
241/// ```rust
242/// use reinhardt_di::params::PathStruct;
243/// # use serde::Deserialize;
244/// #[derive(Deserialize)]
245/// struct UserPath {
246///     user_id: i64,
247///     post_id: i64,
248/// }
249///
250/// let user_path = UserPath { user_id: 123, post_id: 456 };
251/// let path = PathStruct(user_path);
252/// let user_id = path.user_id;
253/// let post_id = path.post_id;
254/// assert_eq!(user_id, 123);
255/// assert_eq!(post_id, 456);
256/// ```
257pub struct PathStruct<T>(pub T);
258
259impl<T> PathStruct<T> {
260	/// Unwrap the PathStruct and return the inner value
261	///
262	/// # Examples
263	///
264	/// ```
265	/// use reinhardt_di::params::PathStruct;
266	/// use serde::Deserialize;
267	///
268	/// #[derive(Deserialize, Debug, PartialEq)]
269	/// struct UserPath {
270	///     user_id: i64,
271	///     post_id: i64,
272	/// }
273	///
274	/// let path = PathStruct(UserPath {
275	///     user_id: 123,
276	///     post_id: 456,
277	/// });
278	/// let inner = path.into_inner();
279	/// assert_eq!(inner.user_id, 123);
280	/// assert_eq!(inner.post_id, 456);
281	/// ```
282	pub fn into_inner(self) -> T {
283		self.0
284	}
285}
286
287impl<T> Deref for PathStruct<T> {
288	type Target = T;
289	fn deref(&self) -> &Self::Target {
290		&self.0
291	}
292}
293
294impl<T: Debug> Debug for PathStruct<T> {
295	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296		self.0.fmt(f)
297	}
298}
299
300#[async_trait]
301impl<T> FromRequest for PathStruct<T>
302where
303	T: DeserializeOwned + Send,
304{
305	async fn from_request(_req: &Request, ctx: &ParamContext) -> ParamResult<Self> {
306		// Convert path params HashMap to URL-encoded format for deserialization
307		// This enables proper type coercion from strings (e.g., "42" -> 42)
308		let encoded = serde_urlencoded::to_string(&ctx.path_params).map_err(|e| {
309			ParamError::ParseError(Box::new(
310				ParamErrorContext::new(
311					ParamType::Path,
312					format!("Failed to encode path params: {}", e),
313				)
314				.with_expected_type::<T>()
315				.with_source(Box::new(e)),
316			))
317		})?;
318
319		serde_urlencoded::from_str(&encoded)
320			.map(PathStruct)
321			.map_err(|e| ParamError::url_encoding::<T>(ParamType::Path, e, Some(encoded.clone())))
322	}
323}
324
325// Implement WithValidation trait for Path
326#[cfg(feature = "validation")]
327impl<T> super::validation::WithValidation for Path<T> {}
328
329#[cfg(test)]
330mod tests {
331	use super::*;
332	use std::collections::HashMap;
333
334	#[tokio::test]
335	async fn test_path_struct_params() {
336		use bytes::Bytes;
337		use hyper::{HeaderMap, Method, Version};
338		use serde::Deserialize;
339
340		#[derive(Debug, Deserialize, PartialEq)]
341		struct PathParams {
342			id: i64,
343		}
344
345		let mut params = HashMap::new();
346		params.insert("id".to_string(), "42".to_string());
347
348		let ctx = ParamContext::with_path_params(params);
349		let req = Request::builder()
350			.method(Method::GET)
351			.uri("/test")
352			.version(Version::HTTP_11)
353			.headers(HeaderMap::new())
354			.body(Bytes::new())
355			.build()
356			.unwrap();
357
358		let result = PathStruct::<PathParams>::from_request(&req, &ctx).await;
359		assert!(result.is_ok());
360		assert_eq!(result.unwrap().id, 42);
361	}
362
363	// Test primitive type extraction
364	#[tokio::test]
365	async fn test_path_primitive_i64() {
366		use bytes::Bytes;
367		use hyper::{HeaderMap, Method, Version};
368
369		let mut params = HashMap::new();
370		params.insert("id".to_string(), "42".to_string());
371
372		let ctx = ParamContext::with_path_params(params);
373		let req = Request::builder()
374			.method(Method::GET)
375			.uri("/test")
376			.version(Version::HTTP_11)
377			.headers(HeaderMap::new())
378			.body(Bytes::new())
379			.build()
380			.unwrap();
381
382		let result = Path::<i64>::from_request(&req, &ctx).await;
383		assert!(result.is_ok(), "Failed to extract i64: {:?}", result.err());
384		assert_eq!(*result.unwrap(), 42);
385	}
386
387	#[tokio::test]
388	async fn test_path_primitive_string() {
389		use bytes::Bytes;
390		use hyper::{HeaderMap, Method, Version};
391
392		let mut params = HashMap::new();
393		params.insert("name".to_string(), "foobar".to_string());
394
395		let ctx = ParamContext::with_path_params(params);
396		let req = Request::builder()
397			.method(Method::GET)
398			.uri("/test")
399			.version(Version::HTTP_11)
400			.headers(HeaderMap::new())
401			.body(Bytes::new())
402			.build()
403			.unwrap();
404
405		let result = Path::<String>::from_request(&req, &ctx).await;
406		assert!(
407			result.is_ok(),
408			"Failed to extract String: {:?}",
409			result.err()
410		);
411		assert_eq!(*result.unwrap(), "foobar");
412	}
413
414	#[tokio::test]
415	async fn test_path_primitive_f64() {
416		use bytes::Bytes;
417		use hyper::{HeaderMap, Method, Version};
418
419		let mut params = HashMap::new();
420		params.insert("price".to_string(), "19.99".to_string());
421
422		let ctx = ParamContext::with_path_params(params);
423		let req = Request::builder()
424			.method(Method::GET)
425			.uri("/test")
426			.version(Version::HTTP_11)
427			.headers(HeaderMap::new())
428			.body(Bytes::new())
429			.build()
430			.unwrap();
431
432		let result = Path::<f64>::from_request(&req, &ctx).await;
433		assert!(result.is_ok(), "Failed to extract f64: {:?}", result.err());
434		assert_eq!(*result.unwrap(), 19.99);
435	}
436
437	#[tokio::test]
438	async fn test_path_primitive_bool() {
439		use bytes::Bytes;
440		use hyper::{HeaderMap, Method, Version};
441
442		let mut params = HashMap::new();
443		params.insert("active".to_string(), "true".to_string());
444
445		let ctx = ParamContext::with_path_params(params);
446		let req = Request::builder()
447			.method(Method::GET)
448			.uri("/test")
449			.version(Version::HTTP_11)
450			.headers(HeaderMap::new())
451			.body(Bytes::new())
452			.build()
453			.unwrap();
454
455		let result = Path::<bool>::from_request(&req, &ctx).await;
456		assert!(result.is_ok(), "Failed to extract bool: {:?}", result.err());
457		assert!(*result.unwrap());
458	}
459
460	#[tokio::test]
461	async fn test_path_multiple_params_struct() {
462		use bytes::Bytes;
463		use hyper::{HeaderMap, Method, Version};
464		use serde::Deserialize;
465
466		#[derive(Debug, Deserialize, PartialEq)]
467		struct MultiParams {
468			user_id: i64,
469			post_id: i64,
470		}
471
472		let mut params = HashMap::new();
473		params.insert("user_id".to_string(), "123".to_string());
474		params.insert("post_id".to_string(), "456".to_string());
475
476		let ctx = ParamContext::with_path_params(params);
477		let req = Request::builder()
478			.method(Method::GET)
479			.uri("/test")
480			.version(Version::HTTP_11)
481			.headers(HeaderMap::new())
482			.body(Bytes::new())
483			.build()
484			.unwrap();
485
486		let result = PathStruct::<MultiParams>::from_request(&req, &ctx).await;
487		let params = result.unwrap();
488		assert_eq!(params.user_id, 123);
489		assert_eq!(params.post_id, 456);
490	}
491}