rama_http/matcher/path/
mod.rs

1use crate::service::web::response::IntoResponse;
2use crate::{Request, StatusCode};
3use rama_core::{Context, context::Extensions};
4use std::collections::HashMap;
5
6mod de;
7
8#[derive(Debug, Clone, Default)]
9/// parameters that are inserted in the [`Context`],
10/// in case the [`PathMatcher`] found a match for the given [`Request`].
11pub struct UriParams {
12    params: Option<HashMap<String, String>>,
13    glob: Option<String>,
14}
15
16impl UriParams {
17    fn insert(&mut self, name: String, value: String) {
18        self.params
19            .get_or_insert_with(HashMap::new)
20            .insert(name, value);
21    }
22
23    /// Some str slice will be returned in case a param could be found for the given name.
24    pub fn get(&self, name: impl AsRef<str>) -> Option<&str> {
25        self.params
26            .as_ref()
27            .and_then(|params| params.get(name.as_ref()))
28            .map(String::as_str)
29    }
30
31    fn append_glob(&mut self, value: &str) {
32        match self.glob {
33            Some(ref mut glob) => {
34                glob.push('/');
35                glob.push_str(value);
36            }
37            None => self.glob = Some(format!("/{}", value)),
38        }
39    }
40
41    /// Some str slice will be returned in case a glob value was captured
42    /// for the last part of the Path that was matched on.
43    pub fn glob(&self) -> Option<&str> {
44        self.glob.as_deref()
45    }
46
47    /// Deserialize the [`UriParams`] into a given type.
48    pub fn deserialize<T>(&self) -> Result<T, UriParamsDeserializeError>
49    where
50        T: serde::de::DeserializeOwned,
51    {
52        match self.params {
53            Some(ref params) => {
54                let params: Vec<_> = params
55                    .iter()
56                    .map(|(k, v)| (k.as_str(), v.as_str()))
57                    .collect();
58                let deserializer = de::PathDeserializer::new(&params);
59                T::deserialize(deserializer)
60            }
61            None => Err(de::PathDeserializationError::new(de::ErrorKind::NoParams)),
62        }
63        .map_err(UriParamsDeserializeError)
64    }
65
66    /// Extend the [`UriParams`] with the given iterator.
67    pub fn extend<I, K, V>(&mut self, iter: I) -> &mut Self
68    where
69        I: IntoIterator<Item = (K, V)>,
70        K: Into<String>,
71        V: Into<String>,
72    {
73        let params = self.params.get_or_insert_with(HashMap::new);
74        for (k, v) in iter {
75            params.insert(k.into(), v.into());
76        }
77        self
78    }
79
80    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
81        self.params
82            .as_ref()
83            .map(|params| params.iter().map(|(k, v)| (k.as_str(), v.as_str())))
84            .into_iter()
85            .flatten()
86    }
87}
88
89impl<'a> FromIterator<(&'a str, &'a str)> for UriParams {
90    fn from_iter<T: IntoIterator<Item = (&'a str, &'a str)>>(iter: T) -> Self {
91        let mut params = UriParams::default();
92        for (k, v) in iter {
93            params.insert(k.to_owned(), v.to_owned());
94        }
95        params
96    }
97}
98
99#[derive(Debug)]
100/// Error that can occur during the deserialization of the [`UriParams`].
101///
102/// See [`UriParams::deserialize`] for more information.
103pub struct UriParamsDeserializeError(de::PathDeserializationError);
104
105impl UriParamsDeserializeError {
106    /// Get the response body text used for this rejection.
107    pub fn body_text(&self) -> String {
108        use de::ErrorKind;
109        match self.0.kind {
110            ErrorKind::Message(_)
111            | ErrorKind::NoParams
112            | ErrorKind::ParseError { .. }
113            | ErrorKind::ParseErrorAtIndex { .. }
114            | ErrorKind::ParseErrorAtKey { .. } => format!("Invalid URL: {}", self.0.kind),
115            ErrorKind::WrongNumberOfParameters { .. } | ErrorKind::UnsupportedType { .. } => {
116                self.0.kind.to_string()
117            }
118        }
119    }
120
121    /// Get the status code used for this rejection.
122    pub fn status(&self) -> StatusCode {
123        use de::ErrorKind;
124        match self.0.kind {
125            ErrorKind::Message(_)
126            | ErrorKind::NoParams
127            | ErrorKind::ParseError { .. }
128            | ErrorKind::ParseErrorAtIndex { .. }
129            | ErrorKind::ParseErrorAtKey { .. } => StatusCode::BAD_REQUEST,
130            ErrorKind::WrongNumberOfParameters { .. } | ErrorKind::UnsupportedType { .. } => {
131                StatusCode::INTERNAL_SERVER_ERROR
132            }
133        }
134    }
135}
136
137impl std::fmt::Display for UriParamsDeserializeError {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        self.0.fmt(f)
140    }
141}
142
143impl std::error::Error for UriParamsDeserializeError {}
144
145impl IntoResponse for UriParamsDeserializeError {
146    fn into_response(self) -> crate::Response {
147        crate::utils::macros::log_http_rejection!(
148            rejection_type = UriParamsDeserializeError,
149            body_text = self.body_text(),
150            status = self.status(),
151        );
152        (self.status(), self.body_text()).into_response()
153    }
154}
155
156#[derive(Debug, Clone)]
157enum PathFragment {
158    Literal(String),
159    Param(String),
160    Glob,
161}
162
163#[derive(Debug, Clone)]
164enum PathMatcherKind {
165    Literal(String),
166    FragmentList(Vec<PathFragment>),
167}
168
169#[derive(Debug, Clone)]
170/// Matcher based on the URI path.
171pub struct PathMatcher {
172    kind: PathMatcherKind,
173}
174
175impl PathMatcher {
176    /// Create a new [`PathMatcher`] for the given path.
177    pub fn new(path: impl AsRef<str>) -> Self {
178        let path = path.as_ref();
179        let path = path.trim().trim_matches('/');
180
181        if !path.contains([':', '*', '{', '}']) {
182            return Self {
183                kind: PathMatcherKind::Literal(path.to_lowercase()),
184            };
185        }
186
187        let path_parts: Vec<_> = path.split('/').filter(|s| !s.is_empty()).collect();
188        let fragment_length = path_parts.len();
189        if fragment_length == 1 && path_parts[0].is_empty() {
190            return Self {
191                kind: PathMatcherKind::FragmentList(vec![PathFragment::Glob]),
192            };
193        }
194
195        let fragments: Vec<PathFragment> = path_parts
196            .into_iter()
197            .enumerate()
198            .filter_map(|(index, s)| {
199                if s.is_empty() {
200                    return None;
201                }
202                if s.starts_with(':') {
203                    Some(PathFragment::Param(
204                        s.trim_start_matches(':').to_lowercase(),
205                    ))
206                } else if s.starts_with('{') && s.ends_with('}') && s.len() > 2 {
207                    let param_name = s[1..s.len() - 1].to_lowercase();
208                    Some(PathFragment::Param(param_name))
209                } else if s == "*" && index == fragment_length - 1 {
210                    Some(PathFragment::Glob)
211                } else {
212                    Some(PathFragment::Literal(s.to_lowercase()))
213                }
214            })
215            .collect();
216
217        Self {
218            kind: PathMatcherKind::FragmentList(fragments),
219        }
220    }
221
222    pub(crate) fn matches_path(&self, path: &str) -> Option<UriParams> {
223        let path = path.trim().trim_matches('/');
224        match &self.kind {
225            PathMatcherKind::Literal(literal) => {
226                if literal.eq_ignore_ascii_case(path) {
227                    Some(UriParams::default())
228                } else {
229                    None
230                }
231            }
232            PathMatcherKind::FragmentList(fragments) => {
233                let fragments_iter = fragments.iter().map(Some).chain(std::iter::repeat(None));
234                let mut params = UriParams::default();
235                for (segment, fragment) in path
236                    .split('/')
237                    .map(Some)
238                    .chain(std::iter::repeat(None))
239                    .zip(fragments_iter)
240                {
241                    match (segment, fragment) {
242                        (Some(segment), Some(fragment)) => match fragment {
243                            PathFragment::Literal(literal) => {
244                                if !literal.eq_ignore_ascii_case(segment) {
245                                    return None;
246                                }
247                            }
248                            PathFragment::Param(name) => {
249                                if segment.is_empty() {
250                                    return None;
251                                }
252                                let segment = percent_encoding::percent_decode(segment.as_bytes())
253                                    .decode_utf8()
254                                    .map(|s| s.to_string())
255                                    .unwrap_or_else(|_| segment.to_owned());
256                                params.insert(name.to_owned(), segment);
257                            }
258                            PathFragment::Glob => {
259                                params.append_glob(segment);
260                            }
261                        },
262                        (None, None) => {
263                            break;
264                        }
265                        (Some(segment), None) => {
266                            params.glob()?;
267                            params.append_glob(segment);
268                        }
269                        _ => {
270                            return None;
271                        }
272                    }
273                }
274
275                Some(params)
276            }
277        }
278    }
279}
280
281impl<State, Body> rama_core::matcher::Matcher<State, Request<Body>> for PathMatcher {
282    fn matches(
283        &self,
284        ext: Option<&mut Extensions>,
285        _ctx: &Context<State>,
286        req: &Request<Body>,
287    ) -> bool {
288        match self.matches_path(req.uri().path()) {
289            None => false,
290            Some(params) => {
291                if let Some(ext) = ext {
292                    ext.insert(params);
293                }
294                true
295            }
296        }
297    }
298}
299
300#[cfg(test)]
301mod test {
302    use super::*;
303
304    #[test]
305    fn test_path_matcher_match_path() {
306        struct TestCase {
307            path: &'static str,
308            matcher_path: &'static str,
309            result: Option<UriParams>,
310        }
311
312        impl TestCase {
313            fn some(path: &'static str, matcher_path: &'static str, result: UriParams) -> Self {
314                Self {
315                    path,
316                    matcher_path,
317                    result: Some(result),
318                }
319            }
320
321            fn none(path: &'static str, matcher_path: &'static str) -> Self {
322                Self {
323                    path,
324                    matcher_path,
325                    result: None,
326                }
327            }
328        }
329
330        let test_cases = vec![
331            TestCase::some("/", "/", UriParams::default()),
332            TestCase::some("", "/", UriParams::default()),
333            TestCase::some("/", "", UriParams::default()),
334            TestCase::some("", "", UriParams::default()),
335            TestCase::some("/foo", "/foo", UriParams::default()),
336            TestCase::some("/foo", "//foo//", UriParams::default()),
337            TestCase::some("/*foo", "/*foo", UriParams::default()),
338            TestCase::some("/foo/*bar/baz", "/foo/*bar/baz", UriParams::default()),
339            TestCase::none("/foo/*bar/baz", "/foo/*bar"),
340            TestCase::none("/", "/:foo"),
341            TestCase::some(
342                "/",
343                "/*",
344                UriParams {
345                    glob: Some("/".to_owned()),
346                    ..UriParams::default()
347                },
348            ),
349            TestCase::none("/", "//:foo"),
350            TestCase::none("", "/:foo"),
351            TestCase::none("/foo", "/bar"),
352            TestCase::some(
353                "/person/glen%20dc/age",
354                "/person/:name/age",
355                UriParams {
356                    params: Some({
357                        let mut params = HashMap::new();
358                        params.insert("name".to_owned(), "glen dc".to_owned());
359                        params
360                    }),
361                    ..UriParams::default()
362                },
363            ),
364            TestCase::none("/foo", "/bar"),
365            TestCase::some("/foo", "foo", UriParams::default()),
366            TestCase::some("/foo/bar/", "foo/bar", UriParams::default()),
367            TestCase::none("/foo/bar/", "foo/baz"),
368            TestCase::some("/foo/bar/", "/foo/bar", UriParams::default()),
369            TestCase::some("/foo/bar", "/foo/bar", UriParams::default()),
370            TestCase::some("/foo/bar", "foo/bar", UriParams::default()),
371            TestCase::some("/book/oxford-dictionary/author", "/book/:title/author", {
372                let mut params = UriParams::default();
373                params.insert("title".to_owned(), "oxford-dictionary".to_owned());
374                params
375            }),
376            TestCase::some("/book/oxford-dictionary/author", "/book/{title}/author", {
377                let mut params = UriParams::default();
378                params.insert("title".to_owned(), "oxford-dictionary".to_owned());
379                params
380            }),
381            TestCase::some(
382                "/book/oxford-dictionary/author/0",
383                "/book/:title/author/:index",
384                {
385                    let mut params = UriParams::default();
386                    params.insert("title".to_owned(), "oxford-dictionary".to_owned());
387                    params.insert("index".to_owned(), "0".to_owned());
388                    params
389                },
390            ),
391            TestCase::some(
392                "/book/oxford-dictionary/author/1",
393                "/book/{title}/author/{index}",
394                {
395                    let mut params = UriParams::default();
396                    params.insert("title".to_owned(), "oxford-dictionary".to_owned());
397                    params.insert("index".to_owned(), "1".to_owned());
398                    params
399                },
400            ),
401            TestCase::none("/book/oxford-dictionary", "/book/:title/author"),
402            TestCase::none(
403                "/book/oxford-dictionary/author/birthdate",
404                "/book/:title/author",
405            ),
406            TestCase::none("oxford-dictionary/author", "/book/:title/author"),
407            TestCase::none("/foo", "/"),
408            TestCase::none("/foo", "/*f"),
409            TestCase::some(
410                "/foo",
411                "/*",
412                UriParams {
413                    glob: Some("/foo".to_owned()),
414                    ..UriParams::default()
415                },
416            ),
417            TestCase::some(
418                "/assets/css/reset.css",
419                "/assets/*",
420                UriParams {
421                    glob: Some("/css/reset.css".to_owned()),
422                    ..UriParams::default()
423                },
424            ),
425            TestCase::some("/assets/eu/css/reset.css", "/assets/:local/*", {
426                let mut params = UriParams::default();
427                params.insert("local".to_owned(), "eu".to_owned());
428                params.glob = Some("/css/reset.css".to_owned());
429                params
430            }),
431            TestCase::some("/assets/eu/css/reset.css", "/assets/:local/css/*", {
432                let mut params = UriParams::default();
433                params.insert("local".to_owned(), "eu".to_owned());
434                params.glob = Some("/reset.css".to_owned());
435                params
436            }),
437        ];
438        for test_case in test_cases.into_iter() {
439            let matcher = PathMatcher::new(test_case.matcher_path);
440            let result = matcher.matches_path(test_case.path);
441            match (result.as_ref(), test_case.result.as_ref()) {
442                (None, None) => (),
443                (Some(result), Some(expected_result)) => {
444                    assert_eq!(
445                        result.params,
446                        expected_result.params,
447                        "unexpected result params: ({}).matcher({}) => {:?} != {:?}",
448                        test_case.matcher_path,
449                        test_case.path,
450                        result.params,
451                        expected_result.params,
452                    );
453                    assert_eq!(
454                        result.glob, expected_result.glob,
455                        "unexpected result glob: ({}).matcher({}) => {:?} != {:?}",
456                        test_case.matcher_path, test_case.path, result.glob, expected_result.glob,
457                    );
458                }
459                _ => {
460                    panic!(
461                        "unexpected result: ({}).matcher({}) => {:?} != {:?}",
462                        test_case.matcher_path, test_case.path, result, test_case.result
463                    )
464                }
465            }
466        }
467    }
468
469    #[test]
470    fn test_deserialize_uri_params() {
471        let params = UriParams {
472            params: Some({
473                let mut params = HashMap::new();
474                params.insert("name".to_owned(), "glen dc".to_owned());
475                params.insert("age".to_owned(), "42".to_owned());
476                params
477            }),
478            glob: Some("/age".to_owned()),
479        };
480
481        #[derive(serde::Deserialize)]
482        struct Person {
483            name: String,
484            age: u8,
485        }
486
487        let person: Person = params.deserialize().unwrap();
488        assert_eq!(person.name, "glen dc");
489        assert_eq!(person.age, 42);
490    }
491}