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