| //! Parser example for INI files. |
| |
| use std::{ |
| collections::HashMap, |
| env, fmt, |
| fs::File, |
| io::{self, Read}, |
| }; |
| |
| use combine::{parser::char::space, stream::position, *}; |
| |
| #[cfg(feature = "std")] |
| use combine::stream::easy; |
| |
| #[cfg(feature = "std")] |
| use combine::stream::position::SourcePosition; |
| |
| enum Error<E> { |
| Io(io::Error), |
| Parse(E), |
| } |
| |
| impl<E> fmt::Display for Error<E> |
| where |
| E: fmt::Display, |
| { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| match *self { |
| Error::Io(ref err) => write!(f, "{}", err), |
| Error::Parse(ref err) => write!(f, "{}", err), |
| } |
| } |
| } |
| |
| #[derive(PartialEq, Debug)] |
| pub struct Ini { |
| pub global: HashMap<String, String>, |
| pub sections: HashMap<String, HashMap<String, String>>, |
| } |
| |
| fn property<Input>() -> impl Parser<Input, Output = (String, String)> |
| where |
| Input: Stream<Token = char>, |
| // Necessary due to rust-lang/rust#24159 |
| Input::Error: ParseError<Input::Token, Input::Range, Input::Position>, |
| { |
| ( |
| many1(satisfy(|c| c != '=' && c != '[' && c != ';')), |
| token('='), |
| many1(satisfy(|c| c != '\n' && c != ';')), |
| ) |
| .map(|(key, _, value)| (key, value)) |
| .message("while parsing property") |
| } |
| |
| fn whitespace<Input>() -> impl Parser<Input> |
| where |
| Input: Stream<Token = char>, |
| Input::Error: ParseError<Input::Token, Input::Range, Input::Position>, |
| { |
| let comment = (token(';'), skip_many(satisfy(|c| c != '\n'))).map(|_| ()); |
| // Wrap the `spaces().or(comment)` in `skip_many` so that it skips alternating whitespace and |
| // comments |
| skip_many(skip_many1(space()).or(comment)) |
| } |
| |
| fn properties<Input>() -> impl Parser<Input, Output = HashMap<String, String>> |
| where |
| Input: Stream<Token = char>, |
| Input::Error: ParseError<Input::Token, Input::Range, Input::Position>, |
| { |
| // After each property we skip any whitespace that followed it |
| many(property().skip(whitespace())) |
| } |
| |
| fn section<Input>() -> impl Parser<Input, Output = (String, HashMap<String, String>)> |
| where |
| Input: Stream<Token = char>, |
| Input::Error: ParseError<Input::Token, Input::Range, Input::Position>, |
| { |
| ( |
| between(token('['), token(']'), many(satisfy(|c| c != ']'))), |
| whitespace(), |
| properties(), |
| ) |
| .map(|(name, _, properties)| (name, properties)) |
| .message("while parsing section") |
| } |
| |
| fn ini<Input>() -> impl Parser<Input, Output = Ini> |
| where |
| Input: Stream<Token = char>, |
| Input::Error: ParseError<Input::Token, Input::Range, Input::Position>, |
| { |
| (whitespace(), properties(), many(section())) |
| .map(|(_, global, sections)| Ini { global, sections }) |
| } |
| |
| #[test] |
| fn ini_ok() { |
| let text = r#" |
| language=rust |
| |
| [section] |
| name=combine; Comment |
| type=LL(1) |
| |
| "#; |
| let mut expected = Ini { |
| global: HashMap::new(), |
| sections: HashMap::new(), |
| }; |
| expected |
| .global |
| .insert(String::from("language"), String::from("rust")); |
| |
| let mut section = HashMap::new(); |
| section.insert(String::from("name"), String::from("combine")); |
| section.insert(String::from("type"), String::from("LL(1)")); |
| expected.sections.insert(String::from("section"), section); |
| |
| let result = ini().parse(text).map(|t| t.0); |
| assert_eq!(result, Ok(expected)); |
| } |
| |
| #[cfg(feature = "std")] |
| #[test] |
| fn ini_error() { |
| let text = "[error"; |
| let result = ini().easy_parse(position::Stream::new(text)).map(|t| t.0); |
| assert_eq!( |
| result, |
| Err(easy::Errors { |
| position: SourcePosition { line: 1, column: 7 }, |
| errors: vec![ |
| easy::Error::end_of_input(), |
| easy::Error::Expected(']'.into()), |
| easy::Error::Message("while parsing section".into()), |
| ], |
| }) |
| ); |
| } |
| |
| fn main() { |
| let result = match env::args().nth(1) { |
| Some(file) => File::open(file).map_err(Error::Io).and_then(main_), |
| None => main_(io::stdin()), |
| }; |
| match result { |
| Ok(_) => println!("OK"), |
| Err(err) => println!("{}", err), |
| } |
| } |
| |
| #[cfg(feature = "std")] |
| fn main_<R>(mut read: R) -> Result<(), Error<easy::Errors<char, String, SourcePosition>>> |
| where |
| R: Read, |
| { |
| let mut text = String::new(); |
| read.read_to_string(&mut text).map_err(Error::Io)?; |
| ini() |
| .easy_parse(position::Stream::new(&*text)) |
| .map_err(|err| Error::Parse(err.map_range(|s| s.to_string())))?; |
| Ok(()) |
| } |
| |
| #[cfg(not(feature = "std"))] |
| fn main_<R>(mut read: R) -> Result<(), Error<::combine::error::StringStreamError>> |
| where |
| R: Read, |
| { |
| let mut text = String::new(); |
| read.read_to_string(&mut text).map_err(Error::Io)?; |
| ini() |
| .parse(position::Stream::new(&*text)) |
| .map_err(Error::Parse)?; |
| Ok(()) |
| } |