#![warn(missing_docs)]
pub enum CallbackKind<'a> {
Section(&'a str),
Directive(Option<&'a str>, &'a str, Option<&'a str>),
}
pub struct Callback<'a> {
pub filename: Option<&'a str>,
pub line: &'a str,
pub line_number: usize,
pub kind: CallbackKind<'a>,
}
pub trait Ini {
type Err;
fn callback(&mut self, cb: Callback) -> Result<(), Self::Err>;
fn parse_str(&mut self, ini: &str) -> Result<(), Self::Err> {
self.parse(None, ini)
}
fn parse(&mut self, filename: Option<&str>, ini: &str) -> Result<(), Self::Err> {
self.parse_with_section(None, filename, ini).map(|_| ())
}
fn parse_with_section<'a>(
&mut self,
mut section: Option<&'a str>,
filename: Option<&str>,
ini: &'a str,
) -> Result<Option<&'a str>, Self::Err> {
for (line_number, line) in ini.lines().enumerate() {
section = self.parse_line(filename, line, line_number, section)?
}
Ok(section)
}
fn parse_line<'a>(
&mut self,
filename: Option<&str>,
line: &'a str,
line_number: usize,
mut section: Option<&'a str>,
) -> Result<Option<&'a str>, Self::Err> {
let line = line.trim();
let kind;
let line_number = line_number + 1;
if line.is_empty() || line.starts_with('#') {
return Ok(section);
}
if line.starts_with('[') && line.ends_with(']') {
let header = &line[1..line.len() - 1];
kind = CallbackKind::Section(header);
section = Some(header);
} else {
let pair = split_pair(line);
kind = CallbackKind::Directive(section, pair.0, pair.1)
}
let data = Callback {
filename,
line,
line_number,
kind,
};
self.callback(data)?;
Ok(section)
}
}
fn split_pair(s: &str) -> (&str, Option<&str>) {
let mut split = s.splitn(2, '=');
(
split.next().unwrap().trim_end(),
split.next().map(|s| s.trim_start()),
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[derive(Default)]
struct Config {
cake: bool,
amount: u32,
lie: bool,
include_value: bool,
}
impl Ini for Config {
type Err = String;
fn callback(&mut self, cb: Callback) -> Result<(), Self::Err> {
let include = "include_value";
match cb.kind {
CallbackKind::Section(section) => assert_eq!(section, "nom"),
CallbackKind::Directive(section, key, value) => {
assert_eq!(section, Some("nom"));
match key {
"include" => {
self.parse_with_section(section, cb.filename, include)?;
}
"include_value" => self.include_value = true,
"cake" => self.cake = true,
"amount" => self.amount = value.unwrap().parse().unwrap(),
"lie" => self.lie = value.unwrap().parse().unwrap(),
_ => panic!("that's not cake"),
}
}
}
Ok(())
}
}
impl FromStr for Config {
type Err = <Config as Ini>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut config = Config::default();
config.parse_str(s).map(|_| config)
}
}
#[test]
fn cake() {
let ini = "
[nom]
cake
amount = 23
include
lie = true
#comment";
let config: Config = ini.parse().unwrap();
assert_eq!(config.cake, true);
assert_eq!(config.amount, 23);
assert_eq!(config.lie, true);
assert_eq!(config.include_value, true);
}
#[test]
fn comment() {
let mut config = Config::default();
config.parse_str("#cake").unwrap();
assert_eq!(config.cake, false);
}
#[test]
#[should_panic]
fn no_cake() {
let mut config = Config::default();
config
.parse_str(
"[nom]
not a cake",
)
.unwrap();
}
#[test]
#[should_panic]
fn no_section() {
let mut config = Config::default();
config.parse_str("cake").unwrap();
}
}