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