use crate::source::Source;
use crate::item::{SourceLocation, StringItem, Value};
use crate::confpath::ConfPath;
use crate::Config;
use std::io::{Read, BufRead, BufReader};
use std::path::Path;
use std::fs::File;
use std::ffi::OsString;
use std::collections::HashMap;
use std::rc::Rc;
use std::fmt;
#[derive(Debug)]
pub enum Error {
NoPreviousKey(Rc<TextSourceLocation>),
MissingKeyValueDelimiter(Rc<TextSourceLocation>),
IoError(std::io::Error),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::NoPreviousKey(location) => write!(f, "No previous key in {}", location),
Error::MissingKeyValueDelimiter(location) => write!(f, "Missing value for key in {}", location),
Error::IoError(error) => write!(f, "I/O error: {}", error),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::IoError(source) => Some(source),
_ => None
}
}
}
impl From<std::io::Error> for Error {
fn from(io_error: std::io::Error) -> Self {
Error::IoError(io_error)
}
}
#[derive(Debug)]
pub struct TextSourceLocation {
source_name: String,
line_start: usize,
line_end: usize
}
impl TextSourceLocation {
fn new(source_name: &str, line_start: usize, line_end: usize) -> Rc<Self> {
Rc::new(Self {
source_name: source_name.to_owned(),
line_start,
line_end
})
}
}
impl fmt::Display for TextSourceLocation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.line_start == self.line_end {
write!(f, "conf:{}:{}", self.source_name, self.line_start)
} else {
write!(f, "conf:{}:{}-{}", self.source_name, self.line_start, self.line_end)
}
}
}
impl SourceLocation for TextSourceLocation {}
struct CurrentValue<'a> {
value: String,
source_name: &'a str,
line_start: usize,
line_end: usize
}
pub struct ConfigText {
items: HashMap<ConfPath, StringItem>
}
impl ConfigText {
fn put_value(&mut self, key: &Option<ConfPath>, value: &mut Option<CurrentValue>) {
if let Some(key) = key {
if let Some(value) = value.take() {
self.items.entry(key.clone()).or_insert_with(|| StringItem::new(key.clone())).push(Value::new(value.value, TextSourceLocation::new(value.source_name, value.line_start, value.line_end)));
} else {
unreachable!("Logic error: put_value must not be called without a current value.");
}
}
}
fn find_start_of_comment(s: &str) -> Option<usize> {
let mut chars = s.chars();
let mut pos = 0;
while let Some(c) = chars.next() {
match c {
'#' => return Some(pos),
'\\' => { chars.next(); pos+=1; },
_ => ()
}
pos+=1;
}
None
}
pub fn new(conf_source: impl Read, source_name: &str) -> Result<Box<Self>, Error> {
Self::with_path(conf_source, source_name, &ConfPath::default())
}
pub fn with_path(conf_source: impl Read, source_name: &str, path_root: &ConfPath) -> Result<Box<Self>, Error> {
let mut conf = Self {
items: HashMap::default()
};
let reader = BufReader::new(conf_source);
let mut current_key: Option<ConfPath> = None;
let mut current_value: Option<CurrentValue> = None; let mut current_section = path_root.clone();
let mut line_no: usize = 1;
for read_line in reader.lines() {
let mut line = read_line?;
if let Some(pos) = Self::find_start_of_comment(&line) {
line.truncate(pos);
}
let trimed = line.trim();
if trimed.is_empty() {
conf.put_value(¤t_key, &mut current_value);
current_key = None;
} else if trimed.starts_with('[') && trimed.ends_with(']') {
conf.put_value(¤t_key, &mut current_value);
current_section=path_root.push_all(trimed.trim()[1..trimed.len()-1].split('.'));
current_key = None;
} else if trimed.starts_with('|') {
if current_key.is_some() {
let mut current_value_mut = current_value.take().unwrap();
if !current_value_mut.value.is_empty() { current_value_mut.value.push('\n'); }
current_value_mut.value.push_str(&line.trim_start()[1..]);
current_value_mut.line_end = line_no;
current_value = Some(current_value_mut);
} else {
return Err(Error::NoPreviousKey(TextSourceLocation::new(source_name, line_no, line_no)));
}
} else {
conf.put_value(¤t_key, &mut current_value);
if let [key, value] = line.splitn(2, '=').collect::<Vec<&str>>()[..] {
let key = key.trim();
if !key.is_empty() {
current_key = Some(current_section.push_all(key.trim().split('.')));
}
if current_key.is_none() {
return Err(Error::NoPreviousKey(TextSourceLocation::new(source_name, line_no, line_no)));
}
current_value = Some(CurrentValue {
value: value.to_owned(),
source_name,
line_start: line_no,
line_end: line_no
});
} else {
return Err(Error::MissingKeyValueDelimiter(TextSourceLocation::new(source_name, line_no, line_no)));
}
}
line_no+=1;
}
if current_value.is_some() {
conf.put_value(¤t_key, &mut current_value);
}
Ok(Box::new(conf))
}
}
impl Source for ConfigText {
fn get(&self, key: ConfPath) -> Option<StringItem> {
self.items.get(&key).cloned()
}
}
pub fn stack_config(config: &mut Config, config_path: Option<&mut ConfPath>, file_name: &OsString, paths: &[&Path]) -> Result<(), Error>{
let file_name = Path::new(file_name);
for &path in paths {
let this_path = path.join(file_name);
if let Ok(config_file) = File::open(&this_path) {
if let Some(config_path) = &config_path {
config.add_source(ConfigText::with_path(config_file, &this_path.to_string_lossy(), *config_path)?);
} else {
config.add_source(ConfigText::new(config_file, &this_path.to_string_lossy())?);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::source::Source;
use crate::item::StringItem;
use crate::item::ValueExtractor;
use crate::error::ConfigError;
fn assert_item(items: StringItem, template: &[&str]) {
let items: Vec<String> = Ok(Result::<StringItem, ConfigError>::Ok(items).unwrap()).values(..).unwrap();
let mut items_iter = items.iter();
let mut tmpl_iter = template.iter();
while let (Some(a), Some(b)) = (items_iter.next(), tmpl_iter.next()) {
assert_eq!(a, *b);
}
}
#[test]
fn parsing() {
let config_file = r#"
key1=value1
key2=value2.1
key2=value2.2
key3=value3.1
|value3.2
|value3.3
key4=value4.1
=value4.2
key5=value5
test2.key6=value6
[test1]
key1=value1
=value2
[comments] # My comment
key1=value # comment
key2=value#comment
key3=value\#nocomment
key4=value\#nocomment # comment
"#;
let conf = ConfigText::new(config_file.as_bytes(), "myfile").unwrap();
assert_item(conf.get(ConfPath::from(["key1"])).unwrap(), &["value1"]);
assert_item(conf.get(ConfPath::from(["key2"])).unwrap(), &["value2.1", "value2.2"]);
assert_item(conf.get(ConfPath::from(["key3"])).unwrap(), &["value3.1\nvalue3.2\nvalue3.3"]);
assert_item(conf.get(ConfPath::from(["key4"])).unwrap(), &["value4.1", "value4.2"]);
assert_item(conf.get(ConfPath::from(["key5"])).unwrap(), &["value5"]);
assert_item(conf.get(ConfPath::from(["test2", "key6"])).unwrap(), &["value6"]);
assert_item(conf.get(ConfPath::from(["test1", "key1"])).unwrap(), &["value1", "value2"]);
assert_item(conf.get(ConfPath::from(["comments", "key1"])).unwrap(), &["value "]);
assert_item(conf.get(ConfPath::from(["comments", "key2"])).unwrap(), &["value"]);
assert_item(conf.get(ConfPath::from(["comments", "key3"])).unwrap(), &["value\\#nocomment"]);
assert_item(conf.get(ConfPath::from(["comments", "key4"])).unwrap(), &["value\\#nocomment "]);
}
#[test]
#[should_panic(expected = "NoPreviousKey(TextSourceLocation { source_name: \"myfile\", line_start: 2, line_end: 2 })")]
fn prase_error_dangling_cont() {
let config_file = r#"
|Continuation without value.
"#;
let _ = ConfigText::new(config_file.as_bytes(), "myfile").unwrap();
}
#[test]
#[should_panic(expected = "MissingKeyValueDelimiter(TextSourceLocation { source_name: \"myfile\", line_start: 2, line_end: 2 })")]
fn prase_error_dangling_key() {
let config_file = r#"
Key without value
"#;
let _ = ConfigText::new(config_file.as_bytes(), "myfile").unwrap();
}
#[test]
#[should_panic(expected = "NoPreviousKey(TextSourceLocation { source_name: \"myfile\", line_start: 2, line_end: 2 })")]
fn prase_error_dangling_value() {
let config_file = r#"
=Add without key
"#;
let _ = ConfigText::new(config_file.as_bytes(), "myfile").unwrap();
}
#[test]
fn stack() {
let paths: [&Path; 3] = [
&Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata").join("p1"),
&Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata").join("p_non"),
&Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata").join("p2")
];
let mut config = Config::default();
stack_config(&mut config, None, &OsString::from("test.conf"), &paths[..]).unwrap();
assert_eq!((config.get(ConfPath::from(["key_p1"])).value() as Result<String, ConfigError>).unwrap(), "p1");
assert_eq!((config.get(ConfPath::from(["key_p2"])).value() as Result<String, ConfigError>).unwrap(), "p2");
assert_eq!((config.get(ConfPath::from(["key_p1_p2"])).value() as Result<String, ConfigError>).unwrap(), "p1");
}
#[test]
fn stack_with_path() {
let paths: [&Path; 3] = [
&Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata").join("p1"),
&Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata").join("p_non"),
&Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata").join("p2")
];
let mut config = Config::default();
let mut cp = ConfPath::default();
stack_config(&mut config, Some(&mut cp), &OsString::from("test.conf"), &paths[..]).unwrap();
let mut key_names: Vec<_> = cp.children().map(|c| String::from(c.tail_component_name().unwrap())).collect();
key_names.sort();
assert_eq!(key_names.len(), 3);
assert_eq!(key_names[0], "key_p1");
assert_eq!(key_names[1], "key_p1_p2");
assert_eq!(key_names[2], "key_p2");
}
}