uri_pattern_matcher/
lib.rs

1//! This crate can be used to parse URIs like the ones we can found in OpenApi spec for paths (/foo/{bar}).
2//! Once the pattern is parsed, you can check if any string matches against it. You can also compare two patterns to find the more specific.
3//!
4//! For now it doesn't handle any other pattern than {pattern}. Feel free to open an issue if you have a need for a specific usecase.
5//! Can probably be used for paths on filesystems as well if one can find a usecase for this.
6//!
7//! # Example
8//!
9//! Here are examples for the common usages of this crate:
10//!
11//! ```rust
12//! # use uri_pattern_matcher::UriPattern;
13//! let pattern: UriPattern = "/api/{resource}/{id}/details".into();
14//! assert!(pattern.is_match("/api/resource/id1/details"));
15//! assert!(pattern.is_match("/api/customer/John/details"));
16//! ```
17//!
18//! ```rust
19//! # use uri_pattern_matcher::UriPattern;
20//! let pattern: UriPattern = "/api/{foo}/{bar}/zzz".into();
21//! let pattern2: UriPattern = "/api/{foo}/bar/{zzz}".into();
22//! assert_ne!(pattern, pattern2);
23//! assert!(pattern > pattern2);
24//! ```
25//!
26//! We are also able to combine all of this using Iterators.
27//! Here we'll retrieve the most specific pattern matching our candidate string:
28//! ```rust
29//! # use uri_pattern_matcher::UriPattern;
30//! let patterns: Vec<UriPattern> = vec![
31//!     "/api/{foo}/bar/{zzz}".into(),
32//!     "/api/{foo}/{bar}/zzz".into(),
33//!     "/{api}/{foo}/foo/{zzz}".into()
34//!     ];
35//! let candidate = "/api/resource/bar/zzz";
36//! let best_match = patterns.iter()
37//!            .filter(|p| p.is_match(candidate))
38//!            .max();
39//! assert_eq!(best_match.unwrap(), &UriPattern::from("/api/{foo}/{bar}/zzz"));
40//! ```
41mod uri_pattern_score;
42mod pattern_part;
43
44use core::cmp::Ordering;
45use crate::pattern_part::PatternPart;
46use crate::uri_pattern_score::UriPatternScore;
47
48/// Struct used to parse strings as patterns - Check if an incoming string matches a pattern - Pattern Comparison
49#[derive(Debug, Clone)]
50pub struct UriPattern<'a> {
51    value: &'a str,
52    pub(crate) parts: Vec<PatternPart<'a>>,
53}
54
55impl<'a> From<&'a str> for UriPattern<'a> {
56    fn from(pattern: &'a str) -> Self {
57        let parts = pattern.split('/').map(|part| part.into()).collect();
58        Self { value : pattern, parts }
59    }
60}
61
62impl UriPattern<'_> {
63    /// Method used to check if a candidate string matches against the pattern
64    /// # Example
65    ///
66    /// ```rust
67    /// use uri_pattern_matcher::UriPattern;
68    ///
69    /// let pattern: UriPattern = "/api/{resource}/{id}/details".into();
70    /// assert!(pattern.is_match("/api/resource/id1/details"));
71    /// assert!(pattern.is_match("/api/customer/John/details"));
72    /// ```
73    pub fn is_match(&self, candidate: &str) -> bool {
74        !candidate.split('/').enumerate().map(|(key, value)| {
75            match self.parts[key] {
76                PatternPart::Joker => true,
77                PatternPart::Value(s) => if s == value {true} else {false},
78            }
79        })
80            .collect::<Vec<bool>>()
81            .contains(&false)
82    }
83}
84
85impl PartialEq for UriPattern<'_> {
86    fn eq(&self, other: &Self) -> bool {
87        let score: UriPatternScore = self.into();
88        let other_score: UriPatternScore = other.into();
89        score == other_score
90    }
91}
92
93impl PartialOrd for UriPattern<'_> {
94    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
95        let score: UriPatternScore = self.into();
96        let other_score: UriPatternScore = other.into();
97        score.partial_cmp(&other_score)
98    }
99}
100
101impl Ord for UriPattern<'_> {
102    fn cmp(&self, other: &Self) -> Ordering {
103        let score: UriPatternScore = self.into();
104        let other_score: UriPatternScore = other.into();
105        score.cmp(&other_score)
106    }
107}
108
109impl Eq for UriPattern<'_> {}
110
111
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn parsing_works() {
119        let pattern: UriPattern = "/a/{b}/{c}/d".into();
120        assert!(pattern.is_match("/a/resource/test/d"));
121    }
122
123    #[test]
124    fn non_equality_works() {
125        let pattern: UriPattern = "/a/{b}/{c}/d".into();
126        let pattern2: UriPattern = "/a/{b}/c/{d}".into();
127        assert_ne!(pattern, pattern2);
128    }
129
130    #[test]
131    fn equality_works() {
132        let pattern: UriPattern = "/a/{b}/{c}/d".into();
133        let pattern2: UriPattern = "/api/{resource}/{id}/details".into();
134        assert_eq!(pattern, pattern2);
135    }
136
137    #[test]
138    fn inequality_works() {
139        let pattern: UriPattern = "/a/{b}/{c}/d".into();
140        let pattern2: UriPattern = "/a/{b}/c/{d}".into();
141        assert!(pattern > pattern2);
142    }
143
144    #[test]
145    fn best_match_with_ord() {
146        let patterns: Vec<UriPattern> = vec![
147            "/api/{foo}/bar/{zzz}".into(),
148            "/api/{foo}/{bar}/zzz".into(),
149            "/{api}/{foo}/foo/{zzz}".into()
150        ];
151        let candidate = "/api/resource/bar/zzz";
152        let best_match = patterns.iter()
153            .filter(|p| p.is_match(candidate))
154            .max();
155        assert_eq!(best_match.unwrap(), &UriPattern::from("/api/{foo}/{bar}/zzz"));
156    }
157}