use ordered_hashmap::OrderedHashMap;
use parser::{parse_line, Parsed};
use std::fmt;
use std::fs::File;
use std::io::{self, BufReader, BufWriter, Read, Write};
use std::iter::Iterator;
use std::path::Path;
use std::str::FromStr;
mod ordered_hashmap;
type Section = OrderedHashMap<String, String>;
type IniParsed = OrderedHashMap<String, Section>;
type SectionIter<'a> = ordered_hashmap::Iter<'a, String, String>;
type SectionIterMut<'a> = ordered_hashmap::IterMut<'a, String, String>;
#[derive(Debug)]
pub struct Ini {
#[doc(hidden)]
data: IniParsed,
last_section_name: String,
}
impl Ini {
pub fn new() -> Ini {
Ini {
data: IniParsed::new(),
last_section_name: String::new(),
}
}
fn from_string(string: &str) -> Ini {
let mut result = Ini::new();
for (i, line) in string.lines().enumerate() {
match parse_line(&line) {
Parsed::Section(name) => result = result.section(name),
Parsed::Value(name, value) => result = result.item(name, value),
Parsed::Error(msg) => println!("line {}: error: {}", i, msg),
_ => (),
};
}
result
}
pub fn from_file<S: AsRef<Path> + ?Sized>(path: &S) -> Result<Ini, io::Error> {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut buffer = String::new();
reader.read_to_string(&mut buffer)?;
Ok(Ini::from_string(&buffer))
}
pub fn from_buffer<S: Into<String>>(buf: S) -> Ini {
Ini::from_string(&buf.into())
}
pub fn section<S: Into<String>>(mut self, name: S) -> Self {
self.last_section_name = name.into();
self
}
pub fn item<S: Into<String>>(mut self, name: S, value: S) -> Self {
self.data
.entry(self.last_section_name.clone())
.or_insert_with(Section::new)
.insert(name.into(), value.into());
self
}
pub fn to_file<S: AsRef<Path> + ?Sized>(&self, path: &S) -> Result<(), io::Error> {
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
writer.write_all(self.to_buffer().as_bytes())?;
Ok(())
}
pub fn to_buffer(&self) -> String {
format!("{}", self)
}
fn get_raw(&self, section: &str, key: &str) -> Option<&String> {
self.data.get(section).and_then(|x| x.get(key))
}
pub fn get<T: FromStr>(&self, section: &str, key: &str) -> Option<T> {
self.get_raw(section, key).and_then(|x| x.parse().ok())
}
pub fn get_vec<T>(&self, section: &str, key: &str) -> Option<Vec<T>>
where
T: FromStr,
{
self.get_raw(section, key).and_then(|x| {
x.split(',')
.map(|s| s.trim().parse())
.collect::<Result<Vec<T>, _>>()
.ok()
})
}
pub fn iter_section(&self, section: &str) -> Option<SectionIter> {
self.data.get(section).map(|value| value.iter())
}
pub fn iter(&self) -> IniIter {
IniIter {
iter: self.data.iter(),
}
}
pub fn iter_mut(&mut self) -> IniIterMut {
IniIterMut {
iter: self.data.iter_mut(),
}
}
}
impl fmt::Display for Ini {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut buffer = String::new();
for (section, iter) in self.iter() {
buffer.push_str(&format!("[{}]\n", section));
for (key, value) in iter {
buffer.push_str(&format!("{} = {}\n", key, value));
}
buffer.push_str("\n");
}
buffer.pop();
buffer.pop();
write!(f, "{}", buffer)
}
}
impl Default for Ini {
fn default() -> Self {
Self::new()
}
}
#[doc(hidden)]
pub struct IniIter<'a> {
iter: ordered_hashmap::Iter<'a, String, Section>,
}
impl<'a> Iterator for IniIter<'a> {
type Item = (&'a String, SectionIter<'a>);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.iter
.next()
.map(|(string, section)| (string, section.iter()))
}
}
#[doc(hidden)]
pub struct IniIterMut<'a> {
iter: ordered_hashmap::IterMut<'a, String, Section>,
}
impl<'a> Iterator for IniIterMut<'a> {
type Item = (&'a String, SectionIterMut<'a>);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.iter
.next()
.map(|(string, section)| (string, section.iter_mut()))
}
}
#[cfg(test)]
mod library_test {
use super::*;
#[test]
fn bool() {
let ini = Ini::from_buffer("[string]\nabc = true");
let abc: Option<bool> = ini.get("string", "abc");
assert_eq!(abc, Some(true));
}
#[test]
fn float() {
let ini = Ini::from_string("[section]\nname=10.5");
let name: Option<f64> = ini.get("section", "name");
assert_eq!(name, Some(10.5));
}
#[test]
fn float_vec() {
let ini = Ini::from_string("[section]\nname=1.2, 3.4, 5.6");
let name: Option<Vec<f64>> = ini.get_vec("section", "name");
assert_eq!(name, Some(vec![1.2, 3.4, 5.6]));
}
#[test]
fn bad_cast() {
let ini = Ini::new().section("one").item("a", "3.14");
let a: Option<u32> = ini.get("one", "a");
assert_eq!(a, None);
}
#[test]
fn string_vec() {
let ini = Ini::from_string("[section]\nname=a, b, c");
let name: Option<Vec<String>> = ini.get_vec("section", "name");
assert_eq!(
name,
Some(vec![
String::from("a"),
String::from("b"),
String::from("c"),
])
);
}
#[test]
fn parse_error() {
let ini = Ini::from_string("[section]\nlist = 1, 2, --, 4");
let name: Option<Vec<u8>> = ini.get_vec("section", "list");
assert_eq!(name, None);
}
#[test]
fn get_or_macro() {
let ini = Ini::from_string("[section]\nlist = 1, 2, --, 4");
let with_value: Vec<u8> = ini.get_vec("section", "list").unwrap_or(vec![1, 2, 3, 4]);
assert_eq!(with_value, vec![1, 2, 3, 4]);
}
#[test]
fn ordering_iter() {
let ini = Ini::from_string("[a]\nc = 1\nb = 2\na = 3");
let keys: Vec<&String> = ini.data.get("a").unwrap().iter().map(|(k, _)| k).collect();
assert_eq!(["c", "b", "a"], keys[..]);
}
#[test]
fn ordering_keys() {
let ini = Ini::from_string("[a]\nc = 1\nb = 2\na = 3");
let keys: Vec<&String> = ini.data.get("a").unwrap().keys().collect();
assert_eq!(["c", "b", "a"], keys[..]);
}
#[test]
fn mutating() {
let mut config = Ini::new()
.section("items")
.item("a", "1")
.item("b", "2")
.item("c", "3");
for (_, item) in config.iter_mut() {
for (_, value) in item {
let v: i32 = value.parse().unwrap();
*value = format!("{}", v + 1);
}
}
let a_val: Option<u8> = config.get("items", "a");
let b_val: Option<u8> = config.get("items", "b");
let c_val: Option<u8> = config.get("items", "c");
assert_eq!(a_val, Some(2));
assert_eq!(b_val, Some(3));
assert_eq!(c_val, Some(4));
}
#[test]
fn redefine_item() {
let config = Ini::new()
.section("items")
.item("one", "3")
.item("two", "2")
.item("one", "1");
let one: Option<i32> = config.get("items", "one");
assert_eq!(one, Some(1));
}
#[test]
fn redefine_section() {
let config = Ini::new()
.section("one")
.item("a", "1")
.section("two")
.item("b", "2")
.section("one")
.item("c", "3");
let a_val: Option<i32> = config.get("one", "a");
let c_val: Option<i32> = config.get("one", "c");
assert_eq!(a_val, Some(1));
assert_eq!(c_val, Some(3));
}
}
mod parser {
#[derive(Debug)]
pub enum Parsed {
Error(String),
Empty,
Section(String),
Value(String, String),
}
pub fn parse_line(line: &str) -> Parsed {
let content = match line.split(';').next() {
Some(value) => value.trim(),
None => return Parsed::Empty,
};
if content.is_empty() {
return Parsed::Empty;
}
if content.starts_with('[') {
if content.ends_with(']') {
let section_name = content.trim_matches(|c| c == '[' || c == ']').to_owned();
return Parsed::Section(section_name);
} else {
return Parsed::Error("incorrect section syntax".to_owned());
}
} else if content.contains('=') {
let mut pair = content.splitn(2, '=').map(|s| s.trim());
let key = match pair.next() {
Some(value) => value.to_owned(),
None => return Parsed::Error("key is None".to_owned()),
};
let value = match pair.next() {
Some(value) => value.to_owned(),
None => "".to_owned(),
};
if key.is_empty() {
return Parsed::Error("empty key".to_owned());
}
return Parsed::Value(key, value);
}
Parsed::Error("incorrect syntax".to_owned())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_comment() {
match parse_line(";------") {
Parsed::Empty => assert!(true),
_ => assert!(false),
}
}
#[test]
fn test_entry() {
match parse_line("name1 = 100 ; comment") {
Parsed::Value(name, text) => {
assert_eq!(name, String::from("name1"));
assert_eq!(text, String::from("100"));
}
_ => assert!(false),
}
}
#[test]
fn test_weird_name() {
match parse_line("_.,:(){}-#@&*| = 100") {
Parsed::Value(name, text) => {
assert_eq!(name, String::from("_.,:(){}-#@&*|"));
assert_eq!(text, String::from("100"));
}
_ => assert!(false),
}
}
#[test]
fn test_text_entry() {
match parse_line("text_name = hello world!") {
Parsed::Value(name, text) => {
assert_eq!(name, String::from("text_name"));
assert_eq!(text, String::from("hello world!"));
}
_ => assert!(false),
}
}
#[test]
fn test_incorrect_token() {
match parse_line("[section = 1, 2 = value") {
Parsed::Error(_) => assert!(true),
_ => assert!(false),
}
}
#[test]
fn test_incorrect_key_value_line() {
match parse_line("= 3") {
Parsed::Error(_) => assert!(true),
_ => assert!(false),
}
}
}
}