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);
57
58impl<T> Path<T> {
59 pub fn into_inner(self) -> T {
71 self.0
72 }
73}
74
75impl<T> Deref for Path<T> {
76 type Target = T;
77
78 fn deref(&self) -> &Self::Target {
79 &self.0
80 }
81}
82
83impl<T: Debug> Debug for Path<T> {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 self.0.fmt(f)
86 }
87}
88
89impl<T: Clone> Clone for Path<T> {
90 fn clone(&self) -> Self {
91 Path(self.0.clone())
92 }
93}
94
95macro_rules! impl_path_from_str {
98 ($($ty:ty),+ $(,)?) => {
99 $(
100 #[async_trait]
101 impl FromRequest for Path<$ty> {
102 async fn from_request(_req: &Request, ctx: &ParamContext) -> ParamResult<Self> {
103 if ctx.path_params.len() != 1 {
105 return Err(ParamError::InvalidParameter(Box::new(
106 ParamErrorContext::new(
107 ParamType::Path,
108 format!(
109 "Expected exactly 1 path parameter for primitive type, found {}",
110 ctx.path_params.len()
111 ),
112 )
113 .with_expected_type::<$ty>(),
114 )));
115 }
116
117 let value = ctx.path_params.values().next().unwrap();
118 value.parse::<$ty>()
119 .map(Path)
120 .map_err(|e| {
121 ParamError::parse::<$ty>(
122 ParamType::Path,
123 format!("Failed to parse '{}' as {}: {}", value, stringify!($ty), e),
124 Box::new(std::io::Error::new(
125 std::io::ErrorKind::InvalidData,
126 e.to_string(),
127 )),
128 )
129 })
130 }
131 }
132 )+
133 };
134}
135
136impl_path_from_str!(
138 i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool
139);
140
141#[cfg(feature = "uuid")]
143impl_path_from_str!(uuid::Uuid);
144
145macro_rules! impl_path_tuple2_from_str {
148 ($($t1:ty, $t2:ty);+ $(;)?) => {
149 $(
150 #[async_trait]
151 impl FromRequest for Path<($t1, $t2)> {
152 async fn from_request(_req: &Request, ctx: &ParamContext) -> ParamResult<Self> {
153 if ctx.path_params.len() != 2 {
154 return Err(ParamError::InvalidParameter(Box::new(
155 ParamErrorContext::new(
156 ParamType::Path,
157 format!(
158 "Expected exactly 2 path parameters for tuple type, found {}",
159 ctx.path_params.len()
160 ),
161 )
162 .with_expected_type::<($t1, $t2)>(),
163 )));
164 }
165
166 let values: Vec<&String> = ctx.path_params.iter().map(|(_, v)| v).collect();
171 if values.len() != 2 {
172 return Err(ParamError::InvalidParameter(Box::new(
173 ParamErrorContext::new(
174 ParamType::Path,
175 "Expected exactly 2 path parameters".to_string(),
176 )
177 .with_expected_type::<($t1, $t2)>(),
178 )));
179 }
180
181 let v1 = values[0].parse::<$t1>()
182 .map_err(|e| {
183 let ctx = ParamErrorContext::new(
184 ParamType::Path,
185 format!("Failed to parse '{}' as {}: {}", values[0], stringify!($t1), e),
186 )
187 .with_field("path[0]")
188 .with_expected_type::<$t1>()
189 .with_raw_value(values[0].as_str())
190 .with_source(Box::new(std::io::Error::new(
191 std::io::ErrorKind::InvalidData,
192 e.to_string(),
193 )));
194 ParamError::ParseError(Box::new(ctx))
195 })?;
196
197 let v2 = values[1].parse::<$t2>()
198 .map_err(|e| {
199 let ctx = ParamErrorContext::new(
200 ParamType::Path,
201 format!("Failed to parse '{}' as {}: {}", values[1], stringify!($t2), e),
202 )
203 .with_field("path[1]")
204 .with_expected_type::<$t2>()
205 .with_raw_value(values[1].as_str())
206 .with_source(Box::new(std::io::Error::new(
207 std::io::ErrorKind::InvalidData,
208 e.to_string(),
209 )));
210 ParamError::ParseError(Box::new(ctx))
211 })?;
212
213 Ok(Path((v1, v2)))
214 }
215 }
216 )+
217 };
218}
219
220impl_path_tuple2_from_str!(
222 i64, i64;
223 String, i64;
224 i64, String;
225 String, String
226);
227
228#[cfg(feature = "uuid")]
230impl_path_tuple2_from_str!(
231 uuid::Uuid, uuid::Uuid;
232 uuid::Uuid, i64;
233 i64, uuid::Uuid;
234 uuid::Uuid, String;
235 String, uuid::Uuid
236);
237
238#[async_trait]
240impl FromRequest for Path<String> {
241 async fn from_request(_req: &Request, ctx: &ParamContext) -> ParamResult<Self> {
242 if ctx.path_params.len() != 1 {
243 return Err(ParamError::InvalidParameter(Box::new(
244 ParamErrorContext::new(
245 ParamType::Path,
246 format!(
247 "Expected exactly 1 path parameter for String, found {}",
248 ctx.path_params.len()
249 ),
250 )
251 .with_expected_type::<String>(),
252 )));
253 }
254
255 let value = ctx.path_params.values().next().unwrap().clone();
256 Ok(Path(value))
257 }
258}
259
260pub struct PathStruct<T>(pub T);
288
289impl<T> PathStruct<T> {
290 pub fn into_inner(self) -> T {
313 self.0
314 }
315}
316
317impl<T> Deref for PathStruct<T> {
318 type Target = T;
319 fn deref(&self) -> &Self::Target {
320 &self.0
321 }
322}
323
324impl<T: Debug> Debug for PathStruct<T> {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 self.0.fmt(f)
327 }
328}
329
330#[async_trait]
331impl<T> FromRequest for PathStruct<T>
332where
333 T: DeserializeOwned + Send,
334{
335 async fn from_request(_req: &Request, ctx: &ParamContext) -> ParamResult<Self> {
336 let encoded = serde_urlencoded::to_string(ctx.path_params.as_slice()).map_err(|e| {
341 ParamError::ParseError(Box::new(
342 ParamErrorContext::new(
343 ParamType::Path,
344 format!("Failed to encode path params: {}", e),
345 )
346 .with_expected_type::<T>()
347 .with_source(Box::new(e)),
348 ))
349 })?;
350
351 serde_urlencoded::from_str(&encoded)
352 .map(PathStruct)
353 .map_err(|e| ParamError::url_encoding::<T>(ParamType::Path, e, Some(encoded.clone())))
354 }
355}
356
357#[cfg(feature = "validation")]
359impl<T> super::validation::WithValidation for Path<T> {}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use std::collections::HashMap;
365
366 #[tokio::test]
367 async fn test_path_struct_params() {
368 use bytes::Bytes;
369 use hyper::{HeaderMap, Method, Version};
370 use serde::Deserialize;
371
372 #[derive(Debug, Deserialize, PartialEq)]
373 struct PathParams {
374 id: i64,
375 }
376
377 let mut params = HashMap::new();
378 params.insert("id".to_string(), "42".to_string());
379
380 let ctx = ParamContext::with_path_params(params);
381 let req = Request::builder()
382 .method(Method::GET)
383 .uri("/test")
384 .version(Version::HTTP_11)
385 .headers(HeaderMap::new())
386 .body(Bytes::new())
387 .build()
388 .unwrap();
389
390 let result = PathStruct::<PathParams>::from_request(&req, &ctx).await;
391 assert!(result.is_ok());
392 assert_eq!(result.unwrap().id, 42);
393 }
394
395 #[tokio::test]
397 async fn test_path_primitive_i64() {
398 use bytes::Bytes;
399 use hyper::{HeaderMap, Method, Version};
400
401 let mut params = HashMap::new();
402 params.insert("id".to_string(), "42".to_string());
403
404 let ctx = ParamContext::with_path_params(params);
405 let req = Request::builder()
406 .method(Method::GET)
407 .uri("/test")
408 .version(Version::HTTP_11)
409 .headers(HeaderMap::new())
410 .body(Bytes::new())
411 .build()
412 .unwrap();
413
414 let result = Path::<i64>::from_request(&req, &ctx).await;
415 assert!(result.is_ok(), "Failed to extract i64: {:?}", result.err());
416 assert_eq!(*result.unwrap(), 42);
417 }
418
419 #[tokio::test]
420 async fn test_path_primitive_string() {
421 use bytes::Bytes;
422 use hyper::{HeaderMap, Method, Version};
423
424 let mut params = HashMap::new();
425 params.insert("name".to_string(), "foobar".to_string());
426
427 let ctx = ParamContext::with_path_params(params);
428 let req = Request::builder()
429 .method(Method::GET)
430 .uri("/test")
431 .version(Version::HTTP_11)
432 .headers(HeaderMap::new())
433 .body(Bytes::new())
434 .build()
435 .unwrap();
436
437 let result = Path::<String>::from_request(&req, &ctx).await;
438 assert!(
439 result.is_ok(),
440 "Failed to extract String: {:?}",
441 result.err()
442 );
443 assert_eq!(*result.unwrap(), "foobar");
444 }
445
446 #[tokio::test]
447 async fn test_path_primitive_f64() {
448 use bytes::Bytes;
449 use hyper::{HeaderMap, Method, Version};
450
451 let mut params = HashMap::new();
452 params.insert("price".to_string(), "19.99".to_string());
453
454 let ctx = ParamContext::with_path_params(params);
455 let req = Request::builder()
456 .method(Method::GET)
457 .uri("/test")
458 .version(Version::HTTP_11)
459 .headers(HeaderMap::new())
460 .body(Bytes::new())
461 .build()
462 .unwrap();
463
464 let result = Path::<f64>::from_request(&req, &ctx).await;
465 assert!(result.is_ok(), "Failed to extract f64: {:?}", result.err());
466 assert_eq!(*result.unwrap(), 19.99);
467 }
468
469 #[tokio::test]
470 async fn test_path_primitive_bool() {
471 use bytes::Bytes;
472 use hyper::{HeaderMap, Method, Version};
473
474 let mut params = HashMap::new();
475 params.insert("active".to_string(), "true".to_string());
476
477 let ctx = ParamContext::with_path_params(params);
478 let req = Request::builder()
479 .method(Method::GET)
480 .uri("/test")
481 .version(Version::HTTP_11)
482 .headers(HeaderMap::new())
483 .body(Bytes::new())
484 .build()
485 .unwrap();
486
487 let result = Path::<bool>::from_request(&req, &ctx).await;
488 assert!(result.is_ok(), "Failed to extract bool: {:?}", result.err());
489 assert!(*result.unwrap());
490 }
491
492 #[tokio::test]
493 async fn test_path_multiple_params_struct() {
494 use bytes::Bytes;
495 use hyper::{HeaderMap, Method, Version};
496 use serde::Deserialize;
497
498 #[derive(Debug, Deserialize, PartialEq)]
499 struct MultiParams {
500 user_id: i64,
501 post_id: i64,
502 }
503
504 let mut params = HashMap::new();
505 params.insert("user_id".to_string(), "123".to_string());
506 params.insert("post_id".to_string(), "456".to_string());
507
508 let ctx = ParamContext::with_path_params(params);
509 let req = Request::builder()
510 .method(Method::GET)
511 .uri("/test")
512 .version(Version::HTTP_11)
513 .headers(HeaderMap::new())
514 .body(Bytes::new())
515 .build()
516 .unwrap();
517
518 let result = PathStruct::<MultiParams>::from_request(&req, &ctx).await;
519 let params = result.unwrap();
520 assert_eq!(params.user_id, 123);
521 assert_eq!(params.post_id, 456);
522 }
523
524 #[tokio::test]
532 async fn test_path_tuple_uses_url_pattern_order_not_alphabetical() {
533 use bytes::Bytes;
534 use hyper::{HeaderMap, Method, Version};
535 use reinhardt_http::PathParams;
536
537 let mut params = PathParams::new();
542 params.insert("org", "myslug");
543 params.insert("cluster_id", "5");
544
545 let ctx = ParamContext::with_path_params(params);
546 let req = Request::builder()
547 .method(Method::GET)
548 .uri("/orgs/myslug/clusters/5/")
549 .version(Version::HTTP_11)
550 .headers(HeaderMap::new())
551 .body(Bytes::new())
552 .build()
553 .unwrap();
554
555 let result = Path::<(String, i64)>::from_request(&req, &ctx).await;
557
558 let Path((org, cluster_id)) =
563 result.expect("tuple extraction must follow URL pattern order");
564 assert_eq!(org, "myslug");
565 assert_eq!(cluster_id, 5);
566 }
567
568 #[tokio::test]
569 async fn test_path_tuple_reverse_alphabetical_order() {
570 use bytes::Bytes;
571 use hyper::{HeaderMap, Method, Version};
572 use reinhardt_http::PathParams;
573
574 let mut params = PathParams::new();
576 params.insert("z", "first");
577 params.insert("a", "second");
578
579 let ctx = ParamContext::with_path_params(params);
580 let req = Request::builder()
581 .method(Method::GET)
582 .uri("/test")
583 .version(Version::HTTP_11)
584 .headers(HeaderMap::new())
585 .body(Bytes::new())
586 .build()
587 .unwrap();
588
589 let result = Path::<(String, String)>::from_request(&req, &ctx).await;
591
592 let Path((first, second)) = result.expect("tuple extraction must follow insertion order");
596 assert_eq!(first, "first");
597 assert_eq!(second, "second");
598 }
599}