1 //! Parser example for INI files.
2
3 use std::{
4 collections::HashMap,
5 env, fmt,
6 fs::File,
7 io::{self, Read},
8 };
9
10 use combine::{parser::char::space, stream::position, *};
11
12 #[cfg(feature = "std")]
13 use combine::stream::easy;
14
15 #[cfg(feature = "std")]
16 use combine::stream::position::SourcePosition;
17
18 enum Error<E> {
19 Io(io::Error),
20 Parse(E),
21 }
22
23 impl<E> fmt::Display for Error<E>
24 where
25 E: fmt::Display,
26 {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match *self {
29 Error::Io(ref err) => write!(f, "{}", err),
30 Error::Parse(ref err) => write!(f, "{}", err),
31 }
32 }
33 }
34
35 #[derive(PartialEq, Debug)]
36 pub struct Ini {
37 pub global: HashMap<String, String>,
38 pub sections: HashMap<String, HashMap<String, String>>,
39 }
40
property<Input>() -> impl Parser<Input, Output = (String, String)> where Input: Stream<Token = char>,41 fn property<Input>() -> impl Parser<Input, Output = (String, String)>
42 where
43 Input: Stream<Token = char>,
44 {
45 (
46 many1(satisfy(|c| c != '=' && c != '[' && c != ';')),
47 token('='),
48 many1(satisfy(|c| c != '\n' && c != ';')),
49 )
50 .map(|(key, _, value)| (key, value))
51 .message("while parsing property")
52 }
53
whitespace<Input>() -> impl Parser<Input> where Input: Stream<Token = char>,54 fn whitespace<Input>() -> impl Parser<Input>
55 where
56 Input: Stream<Token = char>,
57 {
58 let comment = (token(';'), skip_many(satisfy(|c| c != '\n'))).map(|_| ());
59 // Wrap the `spaces().or(comment)` in `skip_many` so that it skips alternating whitespace and
60 // comments
61 skip_many(skip_many1(space()).or(comment))
62 }
63
properties<Input>() -> impl Parser<Input, Output = HashMap<String, String>> where Input: Stream<Token = char>,64 fn properties<Input>() -> impl Parser<Input, Output = HashMap<String, String>>
65 where
66 Input: Stream<Token = char>,
67 {
68 // After each property we skip any whitespace that followed it
69 many(property().skip(whitespace()))
70 }
71
section<Input>() -> impl Parser<Input, Output = (String, HashMap<String, String>)> where Input: Stream<Token = char>,72 fn section<Input>() -> impl Parser<Input, Output = (String, HashMap<String, String>)>
73 where
74 Input: Stream<Token = char>,
75 {
76 (
77 between(token('['), token(']'), many(satisfy(|c| c != ']'))),
78 whitespace(),
79 properties(),
80 )
81 .map(|(name, _, properties)| (name, properties))
82 .message("while parsing section")
83 }
84
ini<Input>() -> impl Parser<Input, Output = Ini> where Input: Stream<Token = char>,85 fn ini<Input>() -> impl Parser<Input, Output = Ini>
86 where
87 Input: Stream<Token = char>,
88 {
89 (whitespace(), properties(), many(section()))
90 .map(|(_, global, sections)| Ini { global, sections })
91 }
92
93 #[test]
ini_ok()94 fn ini_ok() {
95 let text = r#"
96 language=rust
97
98 [section]
99 name=combine; Comment
100 type=LL(1)
101
102 "#;
103 let mut expected = Ini {
104 global: HashMap::new(),
105 sections: HashMap::new(),
106 };
107 expected
108 .global
109 .insert(String::from("language"), String::from("rust"));
110
111 let mut section = HashMap::new();
112 section.insert(String::from("name"), String::from("combine"));
113 section.insert(String::from("type"), String::from("LL(1)"));
114 expected.sections.insert(String::from("section"), section);
115
116 let result = ini().parse(text).map(|t| t.0);
117 assert_eq!(result, Ok(expected));
118 }
119
120 #[cfg(feature = "std")]
121 #[test]
ini_error()122 fn ini_error() {
123 let text = "[error";
124 let result = ini().easy_parse(position::Stream::new(text)).map(|t| t.0);
125 assert_eq!(
126 result,
127 Err(easy::Errors {
128 position: SourcePosition { line: 1, column: 7 },
129 errors: vec![
130 easy::Error::end_of_input(),
131 easy::Error::Expected(']'.into()),
132 easy::Error::Message("while parsing section".into()),
133 ],
134 })
135 );
136 }
137
main()138 fn main() {
139 let result = match env::args().nth(1) {
140 Some(file) => File::open(file).map_err(Error::Io).and_then(main_),
141 None => main_(io::stdin()),
142 };
143 match result {
144 Ok(_) => println!("OK"),
145 Err(err) => println!("{}", err),
146 }
147 }
148
149 #[cfg(feature = "std")]
main_<R>(mut read: R) -> Result<(), Error<easy::Errors<char, String, SourcePosition>>> where R: Read,150 fn main_<R>(mut read: R) -> Result<(), Error<easy::Errors<char, String, SourcePosition>>>
151 where
152 R: Read,
153 {
154 let mut text = String::new();
155 read.read_to_string(&mut text).map_err(Error::Io)?;
156 ini()
157 .easy_parse(position::Stream::new(&*text))
158 .map_err(|err| Error::Parse(err.map_range(|s| s.to_string())))?;
159 Ok(())
160 }
161
162 #[cfg(not(feature = "std"))]
main_<R>(mut read: R) -> Result<(), Error<::combine::error::StringStreamError>> where R: Read,163 fn main_<R>(mut read: R) -> Result<(), Error<::combine::error::StringStreamError>>
164 where
165 R: Read,
166 {
167 let mut text = String::new();
168 read.read_to_string(&mut text).map_err(Error::Io)?;
169 ini()
170 .parse(position::Stream::new(&*text))
171 .map_err(Error::Parse)?;
172 Ok(())
173 }
174