alpm_parsers/custom_ini/
parser.rs
1use std::collections::BTreeMap;
2
3use serde::Deserialize;
4use winnow::{
5 ModalResult,
6 Parser,
7 ascii::{newline, space0, till_line_ending},
8 combinator::{
9 alt,
10 cut_err,
11 eof,
12 opt,
13 preceded,
14 repeat,
15 repeat_till,
16 separated_pair,
17 terminated,
18 },
19 error::{StrContext, StrContextValue},
20 token::none_of,
21};
22
23use super::de::Error;
24
25const INVALID_KEY_NAME_SYMBOLS: [char; 3] = ['=', ' ', '\n'];
26
27#[derive(Debug, Clone, Deserialize, PartialEq)]
29#[serde(untagged)]
30pub enum Item {
31 Value(String),
32 List(Vec<String>),
33}
34
35impl Item {
36 pub fn value_or_error(&self) -> Result<&str, Error> {
37 match self {
38 Item::Value(value) => Ok(value),
39 Item::List(_) => Err(Error::InvalidState),
40 }
41 }
42}
43
44#[derive(Debug)]
46enum ParsedLine<'s> {
47 KeyValue { key: &'s str, value: &'s str },
49 Comment(&'s str),
51}
52
53fn key(input: &mut &str) -> ModalResult<()> {
55 repeat(1.., none_of(INVALID_KEY_NAME_SYMBOLS)).parse_next(input)
56}
57
58fn key_value<'s>(input: &mut &'s str) -> ModalResult<(&'s str, &'s str)> {
67 separated_pair(
68 cut_err(key.take())
69 .context(StrContext::Label("key"))
70 .context(StrContext::Expected(StrContextValue::Description(
71 "a key followed by a ` = ` delimiter.",
72 ))),
73 cut_err((" ", "=", " "))
74 .context(StrContext::Label("delimiter"))
75 .context(StrContext::Expected(StrContextValue::Description(
76 "a '=' that delimits the key value pair, surrounded by a single space.",
77 ))),
78 till_line_ending,
79 )
80 .parse_next(input)
81}
82
83fn newlines(input: &mut &str) -> ModalResult<()> {
86 repeat(0.., (newline, space0)).parse_next(input)
87}
88
89fn comment<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
91 preceded('#', till_line_ending).parse_next(input)
92}
93
94fn line<'s>(input: &mut &'s str) -> ModalResult<ParsedLine<'s>> {
96 alt((
97 terminated(comment, opt(newlines)).map(ParsedLine::Comment),
98 terminated(key_value, opt(newlines))
99 .map(|(key, value)| ParsedLine::KeyValue { key, value }),
100 ))
101 .parse_next(input)
102}
103
104fn lines<'s>(input: &mut &'s str) -> ModalResult<Vec<ParsedLine<'s>>> {
106 let (value, _terminator) = repeat_till(0.., line, eof).parse_next(input)?;
107
108 Ok(value)
109}
110
111pub fn ini_file(input: &mut &str) -> ModalResult<BTreeMap<String, Item>> {
113 let mut items: BTreeMap<String, Vec<String>> = BTreeMap::new();
114
115 let parsed_lines = preceded(newlines, lines).parse_next(input)?;
117 for parsed_line in parsed_lines {
118 match parsed_line {
119 ParsedLine::KeyValue { key, value } => {
120 let values = items.entry(key.to_string()).or_default();
121 values.push(value.to_string());
122 }
123 ParsedLine::Comment(_v) => {}
124 }
125 }
126
127 Ok(items
132 .into_iter()
133 .map(|(key, mut values)| {
134 if values.len() == 1 {
135 (key, Item::Value(values.remove(0)))
136 } else {
137 (key, Item::List(values))
138 }
139 })
140 .collect())
141}
142
143#[cfg(test)]
144mod test {
145 use testresult::TestResult;
146
147 use super::*;
148
149 static TEST_NEWLINES_INPUT: &str = "
150
151foo = bar
152
153test = nice
154
155";
156
157 #[test]
159 fn test_newlines() -> TestResult<()> {
160 let results = ini_file(&mut TEST_NEWLINES_INPUT.to_string().as_str())?;
161
162 let mut expected = BTreeMap::new();
163 expected.insert("foo".to_string(), Item::Value("bar".to_string()));
164 expected.insert("test".to_string(), Item::Value("nice".to_string()));
165
166 assert_eq!(expected, results);
167
168 Ok(())
169 }
170
171 static TEST_LISTS_INPUT: &str = "foo = bar
172
173test = very
174test = nice
175test = indeed";
176
177 #[test]
179 fn test_lists() -> TestResult<()> {
180 let results = ini_file(&mut TEST_LISTS_INPUT.to_string().as_str())?;
181
182 let mut expected = BTreeMap::new();
183 expected.insert("foo".to_string(), Item::Value("bar".to_string()));
184 expected.insert(
185 "test".to_string(),
186 Item::List(vec![
187 "very".to_string(),
188 "nice".to_string(),
189 "indeed".to_string(),
190 ]),
191 );
192
193 assert_eq!(expected, results);
194
195 Ok(())
196 }
197
198 static TEST_COMMENT_INPUT: &str = "
199# Hey
200# This is a comment
201foo = bar
202# This is another comment
203bar = baz
204# And another one";
205
206 #[test]
208 fn test_comments() -> TestResult<()> {
209 let results = ini_file(&mut TEST_COMMENT_INPUT.to_string().as_str())?;
210
211 let mut expected = BTreeMap::new();
212 expected.insert("foo".to_string(), Item::Value("bar".to_string()));
213 expected.insert("bar".to_string(), Item::Value("baz".to_string()));
214
215 assert_eq!(expected, results);
216
217 Ok(())
218 }
219}