yew_router_nested/matcher/
mod.rs

1//! Module for matching route strings based on tokens generated from the yew_router_route_parser
2//! crate.
3
4mod matcher_impl;
5mod util;
6
7use nom::IResult;
8use std::collections::HashSet;
9use yew_router_route_parser::{parse_str_and_optimize_tokens, PrettyParseError};
10
11pub use yew_router_route_parser::{CaptureVariant, Captures, MatcherToken};
12
13/// Attempts to match routes, transform the route to Component props and render that Component.
14#[derive(Debug, PartialEq, Clone)]
15pub struct RouteMatcher {
16    /// Tokens used to determine how the matcher will match a route string.
17    pub tokens: Vec<MatcherToken>,
18    /// Settings
19    pub settings: MatcherSettings,
20}
21
22/// Settings used for the matcher.
23#[derive(Debug, PartialEq, Clone, Copy)]
24pub struct MatcherSettings {
25    /// All literal matches do not care about case.
26    pub case_insensitive: bool,
27}
28
29impl Default for MatcherSettings {
30    fn default() -> Self {
31        MatcherSettings {
32            case_insensitive: false,
33        }
34    }
35}
36
37impl RouteMatcher {
38    /// Attempt to create a RouteMatcher from a "matcher string".
39    pub fn try_from(i: &str) -> Result<Self, PrettyParseError> {
40        let settings = MatcherSettings::default();
41        Self::new(i, settings)
42    }
43
44    /// Creates a new Matcher with settings.
45    pub fn new(i: &str, settings: MatcherSettings) -> Result<Self, PrettyParseError> {
46        Ok(RouteMatcher {
47            tokens: parse_str_and_optimize_tokens(
48                i,
49                yew_router_route_parser::FieldNamingScheme::Unnamed, // The most permissive scheme
50            )?, /* TODO this field type should be a superset of Named, but it would be better to source this from settings, and make sure that the macro generates settings as such. */
51            settings,
52        })
53    }
54
55    /// Match a route string, collecting the results into a map.
56    pub fn capture_route_into_map<'a, 'b: 'a>(
57        &'b self,
58        i: &'a str,
59    ) -> IResult<&'a str, Captures<'a>> {
60        matcher_impl::match_into_map(&self.tokens, &self.settings)(i)
61    }
62
63    /// Match a route string, collecting the results into a vector.
64    pub fn capture_route_into_vec<'a, 'b: 'a>(
65        &'b self,
66        i: &'a str,
67    ) -> IResult<&'a str, Vec<String>> {
68        matcher_impl::match_into_vec(&self.tokens, &self.settings)(i)
69    }
70
71    /// Gets a set of all names that will be captured.
72    /// This is useful in determining if a given struct will be able to be populated by a given path
73    /// matcher before being given a concrete path to match.
74    pub fn capture_names(&self) -> HashSet<&str> {
75        fn capture_names_impl(tokens: &[MatcherToken]) -> HashSet<&str> {
76            tokens
77                .iter()
78                .fold(HashSet::new(), |mut acc: HashSet<&str>, token| {
79                    match token {
80                        MatcherToken::Exact(_) | MatcherToken::End => {}
81                        MatcherToken::Capture(capture) => match &capture {
82                            CaptureVariant::ManyNamed(name)
83                            | CaptureVariant::Named(name)
84                            | CaptureVariant::NumberedNamed { name, .. } => {
85                                acc.insert(&name);
86                            }
87                            CaptureVariant::Unnamed
88                            | CaptureVariant::ManyUnnamed
89                            | CaptureVariant::NumberedUnnamed { .. } => {}
90                        },
91                    }
92                    acc
93                })
94        }
95        capture_names_impl(&self.tokens)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use yew_router_route_parser::{
103        convert_tokens,
104        parser::{RefCaptureVariant, RouteParserToken},
105    };
106
107    impl<'a> From<Vec<RouteParserToken<'a>>> for RouteMatcher {
108        fn from(tokens: Vec<RouteParserToken<'a>>) -> Self {
109            let settings = MatcherSettings::default();
110            RouteMatcher {
111                tokens: convert_tokens(&tokens),
112                settings,
113            }
114        }
115    }
116
117    #[test]
118    fn basic_separator() {
119        let tokens = vec![RouteParserToken::Separator];
120        let path_matcher = RouteMatcher::from(tokens);
121        path_matcher
122            .capture_route_into_map("/")
123            .expect("should parse");
124    }
125
126    #[test]
127    fn multiple_tokens() {
128        let tokens = vec![
129            RouteParserToken::Separator,
130            RouteParserToken::Exact("lorem"),
131            RouteParserToken::Separator,
132        ];
133
134        let path_matcher = RouteMatcher::from(tokens);
135        path_matcher
136            .capture_route_into_map("/lorem/")
137            .expect("should parse");
138    }
139
140    #[test]
141    fn simple_capture() {
142        let tokens = vec![
143            RouteParserToken::Separator,
144            RouteParserToken::Capture(RefCaptureVariant::Named("lorem")),
145            RouteParserToken::Separator,
146        ];
147        let path_matcher = RouteMatcher::from(tokens);
148        let (_, matches) = path_matcher
149            .capture_route_into_map("/ipsum/")
150            .expect("should parse");
151        assert_eq!(matches["lorem"], "ipsum".to_string())
152    }
153
154    #[test]
155    fn simple_capture_with_no_trailing_separator() {
156        let tokens = vec![
157            RouteParserToken::Separator,
158            RouteParserToken::Capture(RefCaptureVariant::Named("lorem")),
159        ];
160        let path_matcher = RouteMatcher::from(tokens);
161        let (_, matches) = path_matcher
162            .capture_route_into_map("/ipsum")
163            .expect("should parse");
164        assert_eq!(matches["lorem"], "ipsum".to_string())
165    }
166
167    #[test]
168    fn match_with_trailing_match_many() {
169        let tokens = vec![
170            RouteParserToken::Separator,
171            RouteParserToken::Exact("a"),
172            RouteParserToken::Separator,
173            RouteParserToken::Capture(RefCaptureVariant::ManyNamed("lorem")),
174        ];
175        let path_matcher = RouteMatcher::from(tokens);
176        let (_, _matches) = path_matcher
177            .capture_route_into_map("/a/")
178            .expect("should parse");
179    }
180
181    #[test]
182    fn fail_match_with_trailing_match_single() {
183        let tokens = vec![
184            RouteParserToken::Separator,
185            RouteParserToken::Exact("a"),
186            RouteParserToken::Separator,
187            RouteParserToken::Capture(RefCaptureVariant::Named("lorem")),
188        ];
189        let path_matcher = RouteMatcher::from(tokens);
190        path_matcher
191            .capture_route_into_map("/a/")
192            .expect_err("should not parse");
193    }
194
195    #[test]
196    fn match_n() {
197        let tokens = vec![
198            RouteParserToken::Separator,
199            RouteParserToken::Capture(RefCaptureVariant::NumberedNamed {
200                sections: 3,
201                name: "lorem",
202            }),
203            RouteParserToken::Separator,
204            RouteParserToken::Exact("a"),
205        ];
206        let path_matcher = RouteMatcher::from(tokens);
207        let (_, _matches) = path_matcher
208            .capture_route_into_map("/garbage1/garbage2/garbage3/a")
209            .expect("should parse");
210    }
211
212    #[test]
213    fn match_n_no_overrun() {
214        let tokens = vec![
215            RouteParserToken::Separator,
216            RouteParserToken::Capture(RefCaptureVariant::NumberedNamed {
217                sections: 3,
218                name: "lorem",
219            }),
220        ];
221        let path_matcher = RouteMatcher::from(tokens);
222        let (s, _matches) = path_matcher
223            .capture_route_into_map("/garbage1/garbage2/garbage3")
224            .expect("should parse");
225        assert_eq!(s.len(), 0)
226    }
227
228    #[test]
229    fn match_n_named() {
230        let tokens = vec![
231            RouteParserToken::Separator,
232            RouteParserToken::Capture(RefCaptureVariant::NumberedNamed {
233                sections: 3,
234                name: "captured",
235            }),
236            RouteParserToken::Separator,
237            RouteParserToken::Exact("a"),
238        ];
239        let path_matcher = RouteMatcher::from(tokens);
240        let (_, matches) = path_matcher
241            .capture_route_into_map("/garbage1/garbage2/garbage3/a")
242            .expect("should parse");
243        assert_eq!(
244            matches["captured"],
245            "garbage1/garbage2/garbage3".to_string()
246        )
247    }
248
249    #[test]
250    fn match_many() {
251        let tokens = vec![
252            RouteParserToken::Separator,
253            RouteParserToken::Capture(RefCaptureVariant::ManyNamed("lorem")),
254            RouteParserToken::Separator,
255            RouteParserToken::Exact("a"),
256        ];
257        let path_matcher = RouteMatcher::from(tokens);
258        let (_, _matches) = path_matcher
259            .capture_route_into_map("/garbage1/garbage2/garbage3/a")
260            .expect("should parse");
261    }
262
263    #[test]
264    fn match_many_named() {
265        let tokens = vec![
266            RouteParserToken::Separator,
267            RouteParserToken::Capture(RefCaptureVariant::ManyNamed("captured")),
268            RouteParserToken::Separator,
269            RouteParserToken::Exact("a"),
270        ];
271        let path_matcher = RouteMatcher::from(tokens);
272        let (_, matches) = path_matcher
273            .capture_route_into_map("/garbage1/garbage2/garbage3/a")
274            .expect("should parse");
275        assert_eq!(
276            matches["captured"],
277            "garbage1/garbage2/garbage3".to_string()
278        )
279    }
280}