config/path/
parser.rs

1use std::str::FromStr;
2
3use winnow::ascii::digit1;
4use winnow::ascii::space0;
5use winnow::combinator::cut_err;
6use winnow::combinator::dispatch;
7use winnow::combinator::fail;
8use winnow::combinator::opt;
9use winnow::combinator::repeat;
10use winnow::combinator::seq;
11use winnow::error::ContextError;
12use winnow::error::ParseError;
13use winnow::error::StrContext;
14use winnow::error::StrContextValue;
15use winnow::prelude::*;
16use winnow::token::any;
17use winnow::token::take_while;
18
19use crate::path::Expression;
20use crate::path::Postfix;
21
22pub(crate) fn from_str(input: &str) -> Result<Expression, ParseError<&str, ContextError>> {
23    path.parse(input)
24}
25
26fn path(i: &mut &str) -> ModalResult<Expression> {
27    let root = ident.parse_next(i)?;
28    let postfix = repeat(0.., postfix).parse_next(i)?;
29    let expr = Expression { root, postfix };
30    Ok(expr)
31}
32
33fn postfix(i: &mut &str) -> ModalResult<Postfix> {
34    dispatch! {any;
35        '[' => cut_err(
36            seq!(
37                integer.map(Postfix::Index),
38                _: ']'.context(StrContext::Expected(StrContextValue::CharLiteral(']'))),
39            )
40                .map(|(i,)| i)
41                .context(StrContext::Label("subscript"))
42        ),
43        '.' => cut_err(ident.map(Postfix::Key)),
44        _ => cut_err(
45            fail
46                .context(StrContext::Label("postfix"))
47                .context(StrContext::Expected(StrContextValue::CharLiteral('[')))
48                .context(StrContext::Expected(StrContextValue::CharLiteral('.')))
49        ),
50    }
51    .parse_next(i)
52}
53
54fn ident(i: &mut &str) -> ModalResult<String> {
55    take_while(1.., ('a'..='z', 'A'..='Z', '0'..='9', '_', '-'))
56        .map(ToOwned::to_owned)
57        .context(StrContext::Label("identifier"))
58        .context(StrContext::Expected(StrContextValue::Description(
59            "ASCII alphanumeric",
60        )))
61        .context(StrContext::Expected(StrContextValue::CharLiteral('_')))
62        .context(StrContext::Expected(StrContextValue::CharLiteral('-')))
63        .parse_next(i)
64}
65
66fn integer(i: &mut &str) -> ModalResult<isize> {
67    seq!(
68        _: space0,
69        (opt('-'), digit1).take().try_map(FromStr::from_str),
70        _: space0
71    )
72    .context(StrContext::Expected(StrContextValue::Description(
73        "integer",
74    )))
75    .map(|(i,)| i)
76    .parse_next(i)
77}
78
79#[cfg(test)]
80mod test {
81    use snapbox::prelude::*;
82    use snapbox::{assert_data_eq, str};
83
84    use super::*;
85
86    #[test]
87    fn test_id() {
88        let parsed: Expression = from_str("abcd").unwrap();
89        assert_data_eq!(
90            parsed.to_debug(),
91            str![[r#"
92Expression {
93    root: "abcd",
94    postfix: [],
95}
96
97"#]]
98        );
99    }
100
101    #[test]
102    fn test_id_dash() {
103        let parsed: Expression = from_str("abcd-efgh").unwrap();
104        assert_data_eq!(
105            parsed.to_debug(),
106            str![[r#"
107Expression {
108    root: "abcd-efgh",
109    postfix: [],
110}
111
112"#]]
113        );
114    }
115
116    #[test]
117    fn test_child() {
118        let parsed: Expression = from_str("abcd.efgh").unwrap();
119        assert_data_eq!(
120            parsed.to_debug(),
121            str![[r#"
122Expression {
123    root: "abcd",
124    postfix: [
125        Key(
126            "efgh",
127        ),
128    ],
129}
130
131"#]]
132        );
133
134        let parsed: Expression = from_str("abcd.efgh.ijkl").unwrap();
135        assert_data_eq!(
136            parsed.to_debug(),
137            str![[r#"
138Expression {
139    root: "abcd",
140    postfix: [
141        Key(
142            "efgh",
143        ),
144        Key(
145            "ijkl",
146        ),
147    ],
148}
149
150"#]]
151        );
152    }
153
154    #[test]
155    fn test_subscript() {
156        let parsed: Expression = from_str("abcd[12]").unwrap();
157        assert_data_eq!(
158            parsed.to_debug(),
159            str![[r#"
160Expression {
161    root: "abcd",
162    postfix: [
163        Index(
164            12,
165        ),
166    ],
167}
168
169"#]]
170        );
171    }
172
173    #[test]
174    fn test_subscript_neg() {
175        let parsed: Expression = from_str("abcd[-1]").unwrap();
176        assert_data_eq!(
177            parsed.to_debug(),
178            str![[r#"
179Expression {
180    root: "abcd",
181    postfix: [
182        Index(
183            -1,
184        ),
185    ],
186}
187
188"#]]
189        );
190    }
191
192    #[test]
193    fn test_invalid_identifier() {
194        let err = from_str("!").unwrap_err();
195        assert_data_eq!(
196            err.to_string(),
197            str![[r#"
198!
199^
200invalid identifier
201expected ASCII alphanumeric, `_`, `-`
202"#]]
203        );
204    }
205
206    #[test]
207    fn test_invalid_child() {
208        let err = from_str("a..").unwrap_err();
209        assert_data_eq!(
210            err.to_string(),
211            str![[r#"
212a..
213  ^
214invalid identifier
215expected ASCII alphanumeric, `_`, `-`
216"#]]
217        );
218    }
219
220    #[test]
221    fn test_invalid_subscript() {
222        let err = from_str("a[b]").unwrap_err();
223        assert_data_eq!(
224            err.to_string(),
225            str![[r#"
226a[b]
227  ^
228invalid subscript
229expected integer
230"#]]
231        );
232    }
233
234    #[test]
235    fn test_incomplete_subscript() {
236        let err = from_str("a[0").unwrap_err();
237        assert_data_eq!(
238            err.to_string(),
239            str![[r#"
240a[0
241   ^
242invalid subscript
243expected `]`
244"#]]
245        );
246    }
247
248    #[test]
249    fn test_invalid_postfix() {
250        let err = from_str("a!b").unwrap_err();
251        assert_data_eq!(
252            err.to_string(),
253            str![[r#"
254a!b
255  ^
256invalid postfix
257expected `[`, `.`
258"#]]
259        );
260    }
261}