use nom::{
call, char,
character::complete::{multispace0, space0},
combinator::all_consuming,
do_parse, is_not, named, opt,
};
use std::error;
use std::fmt;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Read};
use std::path::Path;
#[derive(Debug)]
pub struct IniHandlerError {}
impl fmt::Display for IniHandlerError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "handler failure")
}
}
impl error::Error for IniHandlerError {}
#[derive(Debug)]
pub enum IniError<HandlerError: fmt::Debug + error::Error> {
InvalidLine(String),
Handler(HandlerError),
Io(io::Error),
}
impl<HandlerError: fmt::Debug + error::Error> fmt::Display for IniError<HandlerError> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IniError::InvalidLine(line) => write!(f, "invalid line: {}", line),
IniError::Handler(err) => write!(f, "handler error: {:?}", err),
IniError::Io(err) => write!(f, "io error: {:?}", err),
}
}
}
impl<HandlerError: fmt::Debug + error::Error> error::Error for IniError<HandlerError> {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
IniError::InvalidLine(_) => None,
IniError::Handler(err) => err.source(),
IniError::Io(err) => err.source(),
}
}
}
pub trait IniHandler {
type Error: fmt::Debug;
fn section(&mut self, name: &str) -> Result<(), Self::Error>;
fn option(&mut self, key: &str, value: &str) -> Result<(), Self::Error>;
fn comment(&mut self, _: &str) -> Result<(), Self::Error> {
Ok(())
}
}
named!(parse_comment<&str,&str>,
do_parse!(
space0
>> char!(';')
>> space0
>> ("")
)
);
named!(parse_section<&str, &str>,
do_parse!(
char!('[')
>> opt!(space0)
>> name: is_not!(" \t\r\n]")
>> opt!(space0)
>> char!(']')
>> multispace0
>> (name)
)
);
named!(parse_option<&str, &str>,
do_parse!(
key: is_not!(" ;=")
>> opt!(space0)
>> char!('=')
>> opt!(space0)
>> (key)
)
);
named!(parse_blank<&str,&str>,
call!(multispace0)
);
macro_rules! map_herror {
($res:expr) => {
$res.map_err(|err| IniError::Handler(err))
};
}
pub struct IniParser<'a, Error: fmt::Debug + error::Error> {
handler: &'a mut dyn IniHandler<Error = Error>,
}
impl<'a, Error: fmt::Debug + error::Error> IniParser<'a, Error> {
pub fn new(handler: &'a mut dyn IniHandler<Error = Error>) -> IniParser<'a, Error> {
IniParser { handler }
}
fn parse_ini_line(&mut self, line: &str) -> Result<(), IniError<Error>> {
match parse_comment(line) {
Ok((comment, _)) => map_herror!(self.handler.comment(comment)),
Err(_) => match all_consuming(parse_section)(line) {
Ok((_, name)) => map_herror!(self.handler.section(name)),
Err(_) => match parse_option(line) {
Ok((value, key)) => map_herror!(self.handler.option(key, value)),
Err(_) => match all_consuming(parse_blank)(line) {
Ok(_) => Ok(()),
Err(_) => Err(IniError::InvalidLine(line.to_string())),
},
},
},
}
}
pub fn parse_buffered<B: BufRead>(&mut self, input: B) -> Result<(), IniError<Error>> {
for res in input.lines() {
match res {
Ok(line) => self.parse_ini_line(line.trim_end())?,
Err(err) => return Err(IniError::Io(err)),
}
}
Ok(())
}
pub fn parse<R: Read>(&mut self, input: R) -> Result<(), IniError<Error>> {
let mut reader = BufReader::new(input);
self.parse_buffered(&mut reader)
}
pub fn parse_file<P>(&mut self, path: P) -> Result<(), IniError<Error>>
where
P: AsRef<Path>,
{
let file = File::open(path).map_err(|err| IniError::Io(err))?;
self.parse(file)
}
}
#[cfg(test)]
mod tests {
use super::{
all_consuming, parse_blank, parse_comment, parse_option, parse_section, IniHandler,
IniParser,
};
use std::error;
use std::fmt;
use std::io::{self, Seek, Write};
#[test]
fn parse_sections() {
for line in &["[one]\n", "[ one ] "] {
let (_, name) = all_consuming(parse_section)(line).unwrap();
assert_eq!("one", name);
}
for line in &["[one\n", "name = value"] {
let res = all_consuming(parse_section)(line);
assert!(res.is_err(), "parsing should have failed for: {}", line);
}
}
#[test]
fn parse_options() {
for line in &["name = test", "name = one two three "] {
let (value, key) = parse_option(line).unwrap();
assert_eq!("name", key);
assert!(value.len() > 0);
}
}
#[test]
fn parse_blank_lines() {
for line in &["\n", " \t \n"] {
all_consuming(parse_blank)(line).unwrap();
}
}
#[test]
fn parse_comments() {
for line in &["; comment", " ; comment"] {
let (comment, _) = parse_comment(line).unwrap();
assert_eq!("comment", comment);
}
}
#[derive(Debug)]
enum ConfigError {
InvalidSection,
InvalidOption,
}
impl fmt::Display for ConfigError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::InvalidSection => write!(fmt, "invalid section"),
ConfigError::InvalidOption => write!(fmt, "invalid option"),
}
}
}
impl error::Error for ConfigError {}
enum ConfigSection {
Default,
Logging,
}
struct Config {
section: ConfigSection,
pub name: Option<String>,
pub level: Option<String>,
pub has_comments: bool,
}
impl Config {
fn new() -> Config {
Config {
section: ConfigSection::Default,
name: None,
level: None,
has_comments: false,
}
}
}
impl IniHandler for Config {
type Error = ConfigError;
fn section(&mut self, name: &str) -> Result<(), Self::Error> {
match name {
"logging" => {
self.section = ConfigSection::Logging;
Ok(())
}
_ => Err(ConfigError::InvalidSection),
}
}
fn option(&mut self, key: &str, value: &str) -> Result<(), Self::Error> {
match self.section {
ConfigSection::Default if key == "name" => self.name = Some(value.to_string()),
ConfigSection::Logging if key == "level" => self.level = Some(value.to_string()),
_ => return Err(ConfigError::InvalidOption),
}
Ok(())
}
fn comment(&mut self, _: &str) -> Result<(), Self::Error> {
self.has_comments = true;
Ok(())
}
}
const VALID_INI: &str = "name = test suite
; logging section
[logging]
level = error
";
#[test]
fn parse_valid_ini() -> io::Result<()> {
let mut buf = io::Cursor::new(Vec::<u8>::new());
writeln!(buf, "{}", VALID_INI)?;
buf.seek(io::SeekFrom::Start(0))?;
let mut handler = Config::new();
let mut parser = IniParser::new(&mut handler);
parser.parse(buf).unwrap();
assert_eq!(Some("test suite".to_string()), handler.name);
assert!(handler.has_comments);
Ok(())
}
const INVALID_SECTION: &str = "name = test suite
[unknown]
level = error
";
#[test]
fn parse_invalid_section() -> io::Result<()> {
let mut buf = io::Cursor::new(Vec::<u8>::new());
writeln!(buf, "{}", INVALID_SECTION)?;
buf.seek(io::SeekFrom::Start(0))?;
let mut handler = Config::new();
let mut parser = IniParser::new(&mut handler);
assert!(parser.parse(buf).is_err());
Ok(())
}
}