choice_string/
parser.rs

1use crate::{Selection, SomeElementType};
2use nom::branch::alt;
3use nom::bytes::complete::{is_a, tag, tag_no_case};
4use nom::character::complete::{digit1, space1};
5
6use nom::combinator::{complete, cut, eof, map, map_res};
7
8use nom::multi::many1;
9use nom::sequence::tuple;
10use nom::IResult;
11
12use std::str::FromStr;
13
14/// Selects `/(none)?$/`
15fn select_none(input: &str) -> IResult<&str, Selection> {
16    fn none_literal(input: &str) -> IResult<&str, Selection> {
17        map(tuple((tag_no_case("none"), cut(eof))), |_| Selection::None)(input)
18    }
19    alt((map(eof, |_| Selection::None), none_literal))(input)
20}
21
22/// Selects `/^((([0-9]+-[0-9]+)|([0-9]+))( ;,)*)+$/`
23fn select_some(input: &str) -> IResult<&str, Selection> {
24    /// The (last) `/([0-9]+)/` part
25    fn individual_element(input: &str) -> IResult<&str, SomeElementType> {
26        map(
27            map_res(digit1, usize::from_str),
28            SomeElementType::Individual,
29        )(input)
30    }
31    /// The `/([0-9]+-[0-9]+)/` part
32    fn range_element(input: &str) -> IResult<&str, SomeElementType> {
33        map(
34            tuple((
35                map_res(digit1, usize::from_str),
36                is_a("-"),
37                map_res(cut(digit1), usize::from_str),
38            )),
39            |(start, _, end)| SomeElementType::Range(start..=end),
40        )(input)
41    }
42    /// The `/(([0-9]+-[0-9]+)|([0-9]+))/` part
43    fn some_element(input: &str) -> IResult<&str, SomeElementType> {
44        alt((range_element, individual_element))(input)
45    }
46    /// The `/( ;,)*/` part
47    fn element_separator(input: &str) -> IResult<&str, &str> {
48        alt((map(many1(alt((tag(","), tag(";"), space1))), |_| ""), eof))(input)
49    }
50    map(
51        tuple((
52            many1(map(
53                tuple((some_element, element_separator)),
54                |(etype, _rem)| etype,
55            )),
56            eof,
57        )),
58        |a| Selection::Some(a.0),
59    )(input)
60}
61
62/// Selects `/all$/`
63fn select_all(input: &str) -> IResult<&str, Selection> {
64    map(tuple((tag_no_case("all"), cut(eof))), |_| Selection::All)(input)
65}
66
67/// Parses the full selection
68fn selection(input: &str) -> IResult<&str, Selection> {
69    complete(alt((select_none, select_all, select_some)))(input)
70}
71
72/// Parses a choice string to a [`Selection`]. This does not do any de-duplicating or condensing of
73/// parsed ranges.
74pub fn parse(input: &str) -> Result<Selection, crate::Error> {
75    match selection(input) {
76        Ok((_, sel)) => Ok(sel),
77        Err(err) => {
78            if let nom::Err::Failure(error) = err {
79                Err(crate::Error::ParsingFailed(error.code))
80            } else {
81                panic!("Internal parser error");
82            }
83        }
84    }
85}
86
87#[cfg(test)]
88mod parser_tests {
89
90    use super::*;
91    use nom::error::ErrorKind;
92
93    macro_rules! does_parse {
94        ($input:literal, $name:ident) => {
95            #[test]
96            fn $name() {
97                selection($input).unwrap();
98            }
99        };
100    }
101
102    does_parse!("all", all);
103    does_parse!("none", none);
104    does_parse!("", none_no_input);
105    does_parse!("1", single_digit);
106    does_parse!("1-9", single_range);
107    does_parse!("8-2", inverse_range);
108    does_parse!("1-90", single_range_multi_digit_end);
109    does_parse!("10-90", single_range_multi_digit_start_end);
110    does_parse!("10-0", single_range_multi_digit_start);
111
112    does_parse!("1 2 3", multiple_individuals);
113    does_parse!("1-3 5-8", multiple_ranges);
114    does_parse!("1    5 8", multiple_separators);
115    does_parse!("1,,,,5,8", multiple_separators_2);
116    does_parse!("1;;;;5;;;;8", multiple_separators_3);
117    does_parse!("1;,,;5 ,;  ;8", multiple_separators_mixed);
118    does_parse!("1;5;8", separators_1);
119    does_parse!("1,5,8", separators_2);
120
121    does_parse!("1-10 15 20", mixed_elements);
122
123    #[test]
124    fn fails_broken_range_start() {
125        selection("1-").unwrap_err();
126    }
127
128    #[test]
129    fn fails_broken_range_both() {
130        selection("-").unwrap_err();
131    }
132
133    #[test]
134    fn fails_broken_range_end() {
135        selection("-5").unwrap_err();
136    }
137
138    #[test]
139    fn content_none() {
140        assert_eq!(parse("").unwrap(), Selection::None);
141    }
142
143    #[test]
144    fn content_none2() {
145        assert_eq!(parse("none").unwrap(), Selection::None);
146    }
147
148    #[test]
149    fn content_all() {
150        assert_eq!(parse("all").unwrap(), Selection::All);
151    }
152
153    #[test]
154    fn content_individual() {
155        assert_eq!(
156            parse("8").unwrap(),
157            Selection::Some(vec![SomeElementType::Individual(8)])
158        );
159    }
160
161    #[test]
162    fn content_individual_multi() {
163        assert_eq!(
164            parse("8 9 10").unwrap(),
165            Selection::Some(vec![
166                SomeElementType::Individual(8),
167                SomeElementType::Individual(9),
168                SomeElementType::Individual(10)
169            ])
170        );
171    }
172
173    #[test]
174    fn content_individual_multi_ranges_individuals() {
175        assert_eq!(
176            parse("8 9-12 4").unwrap(),
177            Selection::Some(vec![
178                SomeElementType::Individual(8),
179                SomeElementType::Range(9..=12),
180                SomeElementType::Individual(4)
181            ])
182        );
183    }
184
185    #[test]
186    fn test_error() {
187        let err = parse("1 3 5 6-8 1-;455").unwrap_err();
188        match err {
189            crate::Error::ParsingFailed(kind) => assert_eq!(kind, ErrorKind::Digit),
190            #[allow(unreachable_patterns)]
191            _ => panic!("Wrong kind"),
192        }
193    }
194}