//! 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 { Io(io::Error), Parse(E), } impl fmt::Display for Error 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, pub sections: HashMap>, } fn property() -> impl Parser where Input: Stream, // Necessary due to rust-lang/rust#24159 Input::Error: ParseError, { ( many1(satisfy(|c| c != '=' && c != '[' && c != ';')), token('='), many1(satisfy(|c| c != '\n' && c != ';')), ) .map(|(key, _, value)| (key, value)) .message("while parsing property") } fn whitespace() -> impl Parser where Input: Stream, Input::Error: ParseError, { 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() -> impl Parser> where Input: Stream, Input::Error: ParseError, { // After each property we skip any whitespace that followed it many(property().skip(whitespace())) } fn section() -> impl Parser)> where Input: Stream, Input::Error: ParseError, { ( between(token('['), token(']'), many(satisfy(|c| c != ']'))), whitespace(), properties(), ) .map(|(name, _, properties)| (name, properties)) .message("while parsing section") } fn ini() -> impl Parser where Input: Stream, Input::Error: ParseError, { (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_(mut read: R) -> Result<(), Error>> 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_(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(()) }