180 lines
4.7 KiB
Rust
180 lines
4.7 KiB
Rust
//! 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(())
|
|
}
|