1use 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
15pub struct Path<T>(pub T);
27
28impl<T> Path<T> {
29 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
65macro_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 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
106impl_path_from_str!(
108 i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool
109);
110
111#[cfg(feature = "uuid")]
113impl_path_from_str!(uuid::Uuid);
114
115macro_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 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
190impl_path_tuple2_from_str!(
192 i64, i64;
193 String, i64;
194 i64, String;
195 String, String
196);
197
198#[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#[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
230pub struct PathStruct<T>(pub T);
258
259impl<T> PathStruct<T> {
260 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 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#[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 #[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}