Skip to main content

tork_core/extract/
path.rs

1//! Path-parameter parsing.
2
3use std::fmt::Display;
4use std::str::FromStr;
5
6use crate::error::{Error, Result};
7use crate::extract::RequestContext;
8
9/// Parses a single captured URL segment into a typed path parameter.
10///
11/// A blanket implementation covers every type that implements [`FromStr`], so
12/// `i64`, `String`, `Uuid`, and similar types work out of the box.
13pub trait FromPathParam: Sized {
14    /// Parses `value`, the raw URL segment captured for the parameter `name`.
15    ///
16    /// `name` is used only for diagnostics; the raw value is never echoed back in
17    /// error messages.
18    fn from_path_param(name: &str, value: &str) -> Result<Self>;
19}
20
21impl<T> FromPathParam for T
22where
23    T: FromStr,
24    T::Err: Display,
25{
26    fn from_path_param(name: &str, value: &str) -> Result<Self> {
27        value
28            .parse::<T>()
29            .map_err(|_| Error::unprocessable(format!("invalid value for path parameter `{name}`")))
30    }
31}
32
33/// Resolves and parses the path parameter named `name` from the request.
34///
35/// This is generated-code support, not part of the user-facing API. A missing
36/// parameter indicates a routing bug (the route matched but the placeholder was
37/// absent), so it is reported as an internal error rather than a client error.
38#[doc(hidden)]
39pub fn __extract_path_param<T: FromPathParam>(ctx: &RequestContext, name: &str) -> Result<T> {
40    match ctx.path_param(name) {
41        Some(value) => T::from_path_param(name, value),
42        None => Err(Error::internal(format!(
43            "path parameter `{name}` was not captured by the router"
44        ))),
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use crate::body::box_body;
52    use crate::extract::PathParams;
53    use crate::state::StateMap;
54    use bytes::Bytes;
55    use http_body_util::Full;
56    use std::sync::Arc;
57
58    fn context(params: PathParams) -> RequestContext {
59        let head = http::Request::new(()).into_parts().0;
60        let body = box_body(Full::new(Bytes::new()));
61        RequestContext::new(head, params, Arc::new(StateMap::new()), body)
62    }
63
64    #[test]
65    fn missing_param_is_internal_router_error() {
66        let error =
67            __extract_path_param::<i64>(&context(PathParams::new()), "user_id").unwrap_err();
68
69        assert_eq!(error.kind(), crate::error::ErrorKind::Internal);
70        assert_eq!(
71            error.message(),
72            "path parameter `user_id` was not captured by the router"
73        );
74    }
75
76    #[test]
77    fn invalid_param_is_unprocessable() {
78        let mut params = PathParams::new();
79        params.push("user_id".to_owned(), "abc".to_owned());
80
81        let error = __extract_path_param::<i64>(&context(params), "user_id").unwrap_err();
82        assert_eq!(error.kind(), crate::error::ErrorKind::Unprocessable);
83        assert_eq!(
84            error.message(),
85            "invalid value for path parameter `user_id`"
86        );
87    }
88}