Skip to main content

rws_server/handler/
path.rs

1//! [`Path<T>`]: route parameters as typed values.
2
3use std::fmt::Display;
4use std::str::FromStr;
5
6use super::{rejection, FromRequestParts, RequestParts};
7use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
8use crate::router::PathParams;
9
10/// The route's parameters, parsed: `Path(id): Path<u64>` for one,
11/// `Path((id, slug)): Path<(u64, String)>` for several, or a
12/// `#[derive(FromPath)]` struct whose fields are named after them.
13///
14/// Values are percent-decoded before parsing (`/tags/a%20b` → `"a b"`;
15/// [`PathParams`] keeps them raw). A value that doesn't parse answers
16/// `400 Bad Request`.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Path<T>(pub T);
19
20impl<T> std::ops::Deref for Path<T> {
21    type Target = T;
22    fn deref(&self) -> &T {
23        &self.0
24    }
25}
26
27impl<S, T: FromPath> FromRequestParts<S> for Path<T> {
28    fn from_request_parts(parts: &RequestParts<'_, S>) -> Result<Self, Response> {
29        T::from_path(parts.params, parts.path_names)
30            .map(Path)
31            .map_err(|detail| rejection(parts.request, STATUS_CODE_REASON_PHRASE.n400_bad_request, &detail))
32    }
33}
34
35/// A type [`Path`] can build from route parameters.
36///
37/// Implemented for the standard scalars (one parameter), tuples of up to
38/// six `FromStr` values (one parameter each, in order), and structs with
39/// `#[derive(FromPath)]` (one parameter per field, by name). A type of your
40/// own with `FromStr` works as a one-tuple: `Path((slug,)): Path<(Slug,)>`.
41pub trait FromPath: Sized {
42    /// How many parameters this type takes.
43    const ARITY: usize;
44    /// The parameters by name (a struct's fields), or empty when they are
45    /// taken in order. The route macros check these against the pattern.
46    const NAMES: &'static [&'static str] = &[];
47    /// Build the value. `names` are the route parameters to take, in order
48    /// (`ARITY` of them); an error is the rejection's detail.
49    fn from_path(params: &PathParams, names: &[&str]) -> Result<Self, String>;
50}
51
52/// One parameter, percent-decoded and parsed.
53pub fn parse_param<T>(params: &PathParams, name: &str) -> Result<T, String>
54where
55    T: FromStr,
56    T::Err: Display,
57{
58    let raw = params.get(name).ok_or_else(|| format!("the route has no path parameter `{name}`"))?;
59    let decoded = percent_decode(raw);
60    decoded.parse::<T>().map_err(|e| format!("path parameter `{name}`: {e}"))
61}
62
63/// `%HH` as a byte, everything else as it is (`+` stays a plus); invalid
64/// UTF-8 becomes U+FFFD.
65fn percent_decode(raw: &str) -> String {
66    if !raw.contains('%') {
67        return raw.to_string();
68    }
69    let b = raw.as_bytes();
70    let mut out = Vec::with_capacity(b.len());
71    let mut i = 0;
72    let hex = |c: u8| (c as char).to_digit(16);
73    while i < b.len() {
74        if b[i] == b'%' {
75            if let (Some(h), Some(l)) = (b.get(i + 1).and_then(|c| hex(*c)), b.get(i + 2).and_then(|c| hex(*c))) {
76                out.push((h * 16 + l) as u8);
77                i += 3;
78                continue;
79            }
80        }
81        out.push(b[i]);
82        i += 1;
83    }
84    String::from_utf8_lossy(&out).into_owned()
85}
86
87/// Whether `T` takes exactly the route parameters `route` — evaluated in a
88/// `const` by the route macros, so a mismatch is a compile error.
89pub const fn path_fits<T: FromPath>(route: &[&str]) -> bool {
90    if T::NAMES.is_empty() {
91        return T::ARITY == route.len();
92    }
93    if T::NAMES.len() != route.len() {
94        return false;
95    }
96    let mut i = 0;
97    while i < T::NAMES.len() {
98        if !contains(route, T::NAMES[i]) {
99            return false;
100        }
101        i += 1;
102    }
103    true
104}
105
106const fn contains(list: &[&str], name: &str) -> bool {
107    let mut i = 0;
108    while i < list.len() {
109        if str_eq(list[i], name) {
110            return true;
111        }
112        i += 1;
113    }
114    false
115}
116
117const fn str_eq(a: &str, b: &str) -> bool {
118    let (a, b) = (a.as_bytes(), b.as_bytes());
119    if a.len() != b.len() {
120        return false;
121    }
122    let mut i = 0;
123    while i < a.len() {
124        if a[i] != b[i] {
125            return false;
126        }
127        i += 1;
128    }
129    true
130}
131
132fn one<T>(params: &PathParams, names: &[&str]) -> Result<T, String>
133where
134    T: FromStr,
135    T::Err: Display,
136{
137    match names {
138        [name] => parse_param(params, name),
139        _ => Err(format!("expected 1 path parameter, the route passes {}", names.len())),
140    }
141}
142
143macro_rules! scalar_from_path {
144    ($($t:ty),*) => {$(
145        impl FromPath for $t {
146            const ARITY: usize = 1;
147            fn from_path(params: &PathParams, names: &[&str]) -> Result<Self, String> {
148                one(params, names)
149            }
150        }
151    )*};
152}
153
154scalar_from_path!(
155    u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, bool, char, String,
156    std::net::IpAddr, std::net::Ipv4Addr, std::net::Ipv6Addr
157);
158
159macro_rules! tuple_from_path {
160    ($n:expr; $($t:ident $i:tt),+) => {
161        impl<$($t),+> FromPath for ($($t,)+)
162        where $($t: FromStr, $t::Err: Display),+
163        {
164            const ARITY: usize = $n;
165            fn from_path(params: &PathParams, names: &[&str]) -> Result<Self, String> {
166                if names.len() != $n {
167                    return Err(format!("expected {} path parameters, the route passes {}", $n, names.len()));
168                }
169                Ok(($(parse_param::<$t>(params, names[$i])?,)+))
170            }
171        }
172    };
173}
174
175tuple_from_path!(1; A 0);
176tuple_from_path!(2; A 0, B 1);
177tuple_from_path!(3; A 0, B 1, C 2);
178tuple_from_path!(4; A 0, B 1, C 2, D 3);
179tuple_from_path!(5; A 0, B 1, C 2, D 3, E 4);
180tuple_from_path!(6; A 0, B 1, C 2, D 3, E 4, F 5);