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>, Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,41 fn property<Input>() -> impl Parser<Input, Output = (String, String)>
42 where
43 Input: Stream<Token = char>,
44 // Necessary due to rust-lang/rust#24159
45 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
46 {
47 (
48 many1(satisfy(|c| c != '=' && c != '[' && c != ';')),
49 token('='),
50 many1(satisfy(|c| c != '\n' && c != ';')),
51 )
52 .map(|(key, _, value)| (key, value))
53 .message("while parsing property")
54 }
55
whitespace<Input>() -> impl Parser<Input> where Input: Stream<Token = char>, Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,56 fn whitespace<Input>() -> impl Parser<Input>
57 where
58 Input: Stream<Token = char>,
59 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
60 {
61 let comment = (token(';'), skip_many(satisfy(|c| c != '\n'))).map(|_| ());
62 // Wrap the `spaces().or(comment)` in `skip_many` so that it skips alternating whitespace and
63 // comments
64 skip_many(skip_many1(space()).or(comment))
65 }
66
properties<Input>() -> impl Parser<Input, Output = HashMap<String, String>> where Input: Stream<Token = char>, Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,67 fn properties<Input>() -> impl Parser<Input, Output = HashMap<String, String>>
68 where
69 Input: Stream<Token = char>,
70 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
71 {
72 // After each property we skip any whitespace that followed it
73 many(property().skip(whitespace()))
74 }
75
section<Input>() -> impl Parser<Input, Output = (String, HashMap<String, String>)> where Input: Stream<Token = char>, Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,76 fn section<Input>() -> impl Parser<Input, Output = (String, HashMap<String, String>)>
77 where
78 Input: Stream<Token = char>,
79 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
80 {
81 (
82 between(token('['), token(']'), many(satisfy(|c| c != ']'))),
83 whitespace(),
84 properties(),
85 )
86 .map(|(name, _, properties)| (name, properties))
87 .message("while parsing section")
88 }
89
ini<Input>() -> impl Parser<Input, Output = Ini> where Input: Stream<Token = char>, Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,90 fn ini<Input>() -> impl Parser<Input, Output = Ini>
91 where
92 Input: Stream<Token = char>,
93 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
94 {
95 (whitespace(), properties(), many(section()))
96 .map(|(_, global, sections)| Ini { global, sections })
97 }
98
99 #[test]
ini_ok()100 fn ini_ok() {
101 let text = r#"
102 language=rust
103
104 [section]
105 name=combine; Comment
106 type=LL(1)
107
108 "#;
109 let mut expected = Ini {
110 global: HashMap::new(),
111 sections: HashMap::new(),
112 };
113 expected
114 .global
115 .insert(String::from("language"), String::from("rust"));
116
117 let mut section = HashMap::new();
118 section.insert(String::from("name"), String::from("combine"));
119 section.insert(String::from("type"), String::from("LL(1)"));
120 expected.sections.insert(String::from("section"), section);
121
122 let result = ini().parse(text).map(|t| t.0);
123 assert_eq!(result, Ok(expected));
124 }
125
126 #[cfg(feature = "std")]
127 #[test]
ini_error()128 fn ini_error() {
129 let text = "[error";
130 let result = ini().easy_parse(position::Stream::new(text)).map(|t| t.0);
131 assert_eq!(
132 result,
133 Err(easy::Errors {
134 position: SourcePosition { line: 1, column: 7 },
135 errors: vec![
136 easy::Error::end_of_input(),
137 easy::Error::Expected(']'.into()),
138 easy::Error::Message("while parsing section".into()),
139 ],
140 })
141 );
142 }
143
main()144 fn main() {
145 let result = match env::args().nth(1) {
146 Some(file) => File::open(file).map_err(Error::Io).and_then(main_),
147 None => main_(io::stdin()),
148 };
149 match result {
150 Ok(_) => println!("OK"),
151 Err(err) => println!("{}", err),
152 }
153 }
154
155 #[cfg(feature = "std")]
main_<R>(mut read: R) -> Result<(), Error<easy::Errors<char, String, SourcePosition>>> where R: Read,156 fn main_<R>(mut read: R) -> Result<(), Error<easy::Errors<char, String, SourcePosition>>>
157 where
158 R: Read,
159 {
160 let mut text = String::new();
161 read.read_to_string(&mut text).map_err(Error::Io)?;
162 ini()
163 .easy_parse(position::Stream::new(&*text))
164 .map_err(|err| Error::Parse(err.map_range(|s| s.to_string())))?;
165 Ok(())
166 }
167
168 #[cfg(not(feature = "std"))]
main_<R>(mut read: R) -> Result<(), Error<::combine::error::StringStreamError>> where R: Read,169 fn main_<R>(mut read: R) -> Result<(), Error<::combine::error::StringStreamError>>
170 where
171 R: Read,
172 {
173 let mut text = String::new();
174 read.read_to_string(&mut text).map_err(Error::Io)?;
175 ini()
176 .parse(position::Stream::new(&*text))
177 .map_err(Error::Parse)?;
178 Ok(())
179 }
180