yew_router_route_parser/
core.rs

1use crate::{
2    error::{ExpectedToken, ParserErrorReason},
3    parser::{CaptureOrExact, RefCaptureVariant, RouteParserToken},
4    ParseError,
5};
6use nom::{
7    branch::alt,
8    bytes::complete::{tag, take_till1},
9    character::{
10        complete::{char, digit1},
11        is_digit,
12    },
13    combinator::{map, map_parser},
14    error::ErrorKind,
15    sequence::{delimited, separated_pair},
16    IResult,
17};
18
19/// Indicates if the parser is working to create a matcher for a datastructure with named or unnamed fields.
20#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Ord, PartialOrd)]
21pub enum FieldNamingScheme {
22    /// For Thing { field: String }
23    Named,
24    /// for Thing(String)
25    Unnamed,
26    /// for Thing
27    Unit,
28}
29
30pub fn get_slash(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
31    map(char('/'), |_: char| RouteParserToken::Separator)(i)
32        .map_err(|_: nom::Err<()>| nom::Err::Error(ParseError::expected(ExpectedToken::Separator)))
33}
34
35pub fn get_question(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
36    map(char('?'), |_: char| RouteParserToken::QueryBegin)(i)
37        .map_err(|_: nom::Err<()>| nom::Err::Error(ParseError::expected(ExpectedToken::QueryBegin)))
38}
39
40pub fn get_and(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
41    map(char('&'), |_: char| RouteParserToken::QuerySeparator)(i).map_err(|_: nom::Err<()>| {
42        nom::Err::Error(ParseError::expected(ExpectedToken::QuerySeparator))
43    })
44}
45
46/// Returns a FragmentBegin variant if the next character is '\#'.
47pub fn get_hash(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
48    map(char('#'), |_: char| RouteParserToken::FragmentBegin)(i).map_err(|_: nom::Err<()>| {
49        nom::Err::Error(ParseError::expected(ExpectedToken::FragmentBegin))
50    })
51}
52
53/// Returns an End variant if the next character is a '!`.
54pub fn get_end(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
55    map(char('!'), |_: char| RouteParserToken::End)(i)
56        .map_err(|_: nom::Err<()>| nom::Err::Error(ParseError::expected(ExpectedToken::End)))
57}
58
59/// Returns an End variant if the next character is a '!`.
60fn get_open_bracket(i: &str) -> IResult<&str, (), ParseError> {
61    map(char('{'), |_: char| ())(i).map_err(|_: nom::Err<()>| {
62        nom::Err::Error(ParseError::expected(ExpectedToken::OpenBracket))
63    })
64}
65
66fn get_close_bracket(i: &str) -> IResult<&str, (), ParseError> {
67    map(char('}'), |_: char| ())(i).map_err(|_: nom::Err<()>| {
68        nom::Err::Error(ParseError::expected(ExpectedToken::CloseBracket))
69    })
70}
71
72fn get_eq(i: &str) -> IResult<&str, (), ParseError> {
73    map(char('='), |_: char| ())(i)
74        .map_err(|_: nom::Err<()>| nom::Err::Error(ParseError::expected(ExpectedToken::Equals)))
75}
76
77fn get_star(i: &str) -> IResult<&str, (), ParseError> {
78    map(char('*'), |_: char| ())(i)
79        .map_err(|_: nom::Err<()>| nom::Err::Error(ParseError::expected(ExpectedToken::Star)))
80}
81
82fn get_colon(i: &str) -> IResult<&str, (), ParseError> {
83    map(char(':'), |_: char| ())(i)
84        .map_err(|_: nom::Err<()>| nom::Err::Error(ParseError::expected(ExpectedToken::Colon)))
85}
86
87fn rust_ident(i: &str) -> IResult<&str, &str, ParseError> {
88    let invalid_ident_chars = r##" \|/{[]()?+=-!@#$%^&*~`'";:"##;
89    // Detect an ident by first reading until a } is found,
90    // then validating the captured section against invalid characters that can't be in rust idents.
91    map_parser(take_till1(move |c| c == '}'), move |i: &str| {
92        match take_till1::<_, _, ()>(|c| invalid_ident_chars.contains(c))(i) {
93            Ok((remain, got)) => {
94                // Detects if the first character is a digit.
95                if !got.is_empty() && got.starts_with(|c: char| is_digit(c as u8)) {
96                    Err(nom::Err::Failure(ParseError {
97                        reason: Some(ParserErrorReason::BadRustIdent(got.chars().next().unwrap())),
98                        expected: vec![ExpectedToken::Ident],
99                        offset: 1,
100                    }))
101                } else if !remain.is_empty() {
102                    Err(nom::Err::Failure(ParseError {
103                        reason: Some(ParserErrorReason::BadRustIdent(
104                            remain.chars().next().unwrap(),
105                        )),
106                        expected: vec![ExpectedToken::CloseBracket, ExpectedToken::Ident],
107                        offset: got.len() + 1,
108                    }))
109                } else {
110                    Ok((i, i))
111                }
112            }
113            Err(_) => Ok((i, i)),
114        }
115    })(i)
116}
117
118/// Matches escaped items
119fn escaped_item_impl(i: &str) -> IResult<&str, &str> {
120    map(alt((tag("!!"), tag("{{"), tag("}}"))), |s| match s {
121        "!!" => "!",
122        "}}" => "}",
123        "{{" => "{",
124        _ => unreachable!(),
125    })(i)
126}
127
128/// Matches "".
129pub fn nothing(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
130    if i.is_empty() {
131        Ok((i, RouteParserToken::Nothing))
132    } else {
133        Err(nom::Err::Error(ParseError {
134            reason: None, // This should never actually report an error.
135            expected: vec![],
136            offset: 0,
137        }))
138    }
139}
140
141/// The provided string of special characters will be used to terminate this parser.
142///
143/// Due to escaped character parser, the list of special characters MUST contain the characters:
144/// "!{}" within it.
145fn exact_impl(special_chars: &'static str) -> impl Fn(&str) -> IResult<&str, &str, ParseError> {
146    // Detect either an exact ident, or an escaped item.
147    // At higher levels, this can be called multiple times in a row,
148    // and that results of those multiple parse attempts will be stitched together into one literal.
149    move |i: &str| {
150        alt((
151            take_till1(move |c| special_chars.contains(c)),
152            escaped_item_impl,
153        ))(i)
154        .map_err(|x: nom::Err<(&str, ErrorKind)>| {
155            let s = match x {
156                nom::Err::Error((s, _)) | nom::Err::Failure((s, _)) => s,
157                nom::Err::Incomplete(_) => panic!(),
158            };
159            nom::Err::Error(ParseError {
160                reason: Some(ParserErrorReason::BadLiteral),
161                expected: vec![ExpectedToken::Literal],
162                offset: 1 + i.len() - s.len(),
163            })
164        })
165    }
166}
167
168const SPECIAL_CHARS: &str = r##"/?&#={}!"##;
169const FRAGMENT_SPECIAL_CHARS: &str = r##"{}!"##;
170
171pub fn exact(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
172    map(exact_impl(SPECIAL_CHARS), RouteParserToken::Exact)(i)
173}
174
175/// More permissive exact matchers
176pub fn fragment_exact(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
177    map(exact_impl(FRAGMENT_SPECIAL_CHARS), RouteParserToken::Exact)(i)
178}
179
180pub fn capture<'a>(
181    field_naming_scheme: FieldNamingScheme,
182) -> impl Fn(&'a str) -> IResult<&'a str, RouteParserToken<'a>, ParseError> {
183    map(capture_impl(field_naming_scheme), RouteParserToken::Capture)
184}
185
186fn capture_single_impl<'a>(
187    field_naming_scheme: FieldNamingScheme,
188) -> impl Fn(&'a str) -> IResult<&'a str, RefCaptureVariant<'a>, ParseError> {
189    move |i: &str| match field_naming_scheme {
190        FieldNamingScheme::Named => delimited(
191            get_open_bracket,
192            named::single_capture_impl,
193            get_close_bracket,
194        )(i),
195        FieldNamingScheme::Unnamed => delimited(
196            get_open_bracket,
197            alt((named::single_capture_impl, unnamed::single_capture_impl)),
198            get_close_bracket,
199        )(i),
200        FieldNamingScheme::Unit => {
201            println!("Unit encountered, erroring in capture single");
202            Err(nom::Err::Failure(ParseError {
203                reason: Some(ParserErrorReason::CapturesInUnit),
204                expected: vec![],
205                offset: 0,
206            }))
207        }
208    }
209}
210
211/// Captures {ident}, {*:ident}, {<number>:ident}
212///
213/// Depending on the provided field naming, it may also match {}, {*}, and {<number>} for unnamed fields, or none at all for units.
214fn capture_impl<'a>(
215    field_naming_scheme: FieldNamingScheme,
216) -> impl Fn(&'a str) -> IResult<&'a str, RefCaptureVariant, ParseError> {
217    move |i: &str| match field_naming_scheme {
218        FieldNamingScheme::Named => {
219            let inner = alt((
220                named::many_capture_impl,
221                named::numbered_capture_impl,
222                named::single_capture_impl,
223            ));
224            delimited(get_open_bracket, inner, get_close_bracket)(i)
225        }
226        FieldNamingScheme::Unnamed => {
227            let inner = alt((
228                named::many_capture_impl,
229                unnamed::many_capture_impl,
230                named::numbered_capture_impl,
231                unnamed::numbered_capture_impl,
232                named::single_capture_impl,
233                unnamed::single_capture_impl,
234            ));
235            delimited(get_open_bracket, inner, get_close_bracket)(i)
236        }
237        FieldNamingScheme::Unit => Err(nom::Err::Error(ParseError {
238            reason: Some(ParserErrorReason::CapturesInUnit),
239            expected: vec![],
240            offset: 0,
241        })),
242    }
243}
244
245mod named {
246    use super::*;
247    pub fn single_capture_impl(i: &str) -> IResult<&str, RefCaptureVariant, ParseError> {
248        map(rust_ident, |key| RefCaptureVariant::Named(key))(i)
249    }
250
251    pub fn many_capture_impl(i: &str) -> IResult<&str, RefCaptureVariant, ParseError> {
252        map(
253            separated_pair(get_star, get_colon, rust_ident),
254            |(_, key)| RefCaptureVariant::ManyNamed(key),
255        )(i)
256    }
257
258    pub fn numbered_capture_impl(i: &str) -> IResult<&str, RefCaptureVariant, ParseError> {
259        map(
260            separated_pair(digit1, get_colon, rust_ident),
261            |(number, key)| RefCaptureVariant::NumberedNamed {
262                sections: number.parse().unwrap(),
263                name: key,
264            },
265        )(i)
266    }
267}
268
269mod unnamed {
270    use super::*;
271
272    /// #Note
273    /// because this always succeeds, try this last
274    pub fn single_capture_impl(i: &str) -> IResult<&str, RefCaptureVariant, ParseError> {
275        Ok((i, RefCaptureVariant::Unnamed))
276    }
277
278    pub fn many_capture_impl(i: &str) -> IResult<&str, RefCaptureVariant, ParseError> {
279        map(get_star, |_| RefCaptureVariant::ManyUnnamed)(i)
280    }
281
282    pub fn numbered_capture_impl(i: &str) -> IResult<&str, RefCaptureVariant, ParseError> {
283        map(digit1, |number: &str| RefCaptureVariant::NumberedUnnamed {
284            sections: number.parse().unwrap(),
285        })(i)
286    }
287}
288
289/// Gets a capture or exact, mapping it to the CaptureOrExact enum - to provide a limited subset.
290fn cap_or_exact<'a>(
291    field_naming_scheme: FieldNamingScheme,
292) -> impl Fn(&'a str) -> IResult<&'a str, CaptureOrExact<'a>, ParseError> {
293    move |i: &str| {
294        alt((
295            map(
296                capture_single_impl(field_naming_scheme),
297                CaptureOrExact::Capture,
298            ),
299            map(exact_impl(SPECIAL_CHARS), CaptureOrExact::Exact),
300        ))(i)
301    }
302}
303
304/// Matches a query
305pub fn query<'a>(
306    field_naming_scheme: FieldNamingScheme,
307) -> impl Fn(&'a str) -> IResult<&'a str, RouteParserToken<'a>, ParseError> {
308    move |i: &str| {
309        map(
310            separated_pair(
311                exact_impl(SPECIAL_CHARS),
312                get_eq,
313                cap_or_exact(field_naming_scheme),
314            ),
315            |(ident, capture_or_exact)| RouteParserToken::Query {
316                ident,
317                capture_or_exact,
318            },
319        )(i)
320    }
321}
322
323#[cfg(test)]
324mod test {
325    use super::*;
326
327    #[test]
328    fn lit() {
329        let x = exact("hello").expect("Should parse");
330        assert_eq!(x.1, RouteParserToken::Exact("hello"))
331    }
332
333    #[test]
334    fn cap_or_exact_match_lit() {
335        cap_or_exact(FieldNamingScheme::Named)("lorem").expect("Should parse");
336    }
337    #[test]
338    fn cap_or_exact_match_cap() {
339        cap_or_exact(FieldNamingScheme::Named)("{lorem}").expect("Should parse");
340    }
341
342    #[test]
343    fn query_section_exact() {
344        query(FieldNamingScheme::Named)("lorem=ipsum").expect("should parse");
345    }
346
347    #[test]
348    fn query_section_capture_named() {
349        query(FieldNamingScheme::Named)("lorem={ipsum}").expect("should parse");
350    }
351    #[test]
352    fn query_section_capture_named_fails_without_key() {
353        query(FieldNamingScheme::Named)("lorem={}").expect_err("should not parse");
354    }
355    #[test]
356    fn query_section_capture_unnamed_succeeds_without_key() {
357        query(FieldNamingScheme::Unnamed)("lorem={}").expect("should parse");
358    }
359
360    #[test]
361    fn non_leading_numbers_in_ident() {
362        rust_ident("hello5").expect("sholud parse");
363    }
364    #[test]
365    fn leading_numbers_in_ident_fails() {
366        rust_ident("5hello").expect_err("sholud not parse");
367    }
368}