#![deny(missing_docs)]
use header_parsing::parse_header;
use indexmap::IndexMap;
use thiserror::Error;
use std::{
fs::File,
io::{BufRead, BufReader},
path::Path,
};
#[derive(Debug, Error)]
pub enum Error {
#[error("Failed to open the configuration file")]
OpeningFile,
#[error("Subheader found without a corresponding header")]
SubheaderWithoutHeader,
#[error("Multiple values found for key: {0}")]
MultipleKeys(Box<str>),
#[error("Input issues")]
Input,
}
impl Error {
pub fn print_message(&self, path: &Path) {
use Error::*;
match self {
OpeningFile => eprintln!("Opening file \"{}\" failed", path.display()),
SubheaderWithoutHeader => {
eprintln!(
"File \"{}\" has a subheader without a header",
path.display()
)
}
MultipleKeys(key) => {
eprintln!("Path \"{}\" has a duplicate of key {key}", path.display())
}
Input => eprintln!("Input error while reading \"{}\"", path.display()),
}
}
}
pub fn parse_config(path: &Path) -> Result<IndexMap<Box<str>, Box<str>>, Error> {
let Ok(file) = File::open(path) else {
return Err(Error::OpeningFile);
};
let reader = BufReader::new(file);
let mut map = IndexMap::new();
let mut key_path = Vec::new();
let mut prefix = String::new();
for line in reader.lines() {
let Ok(line) = line else {
return Err(Error::Input);
};
if let Some(success) = parse_header(&mut key_path, &line) {
let Ok(changes) = success else {
return Err(Error::SubheaderWithoutHeader);
};
changes.apply();
prefix.clear();
for header in &key_path {
prefix.push_str(header);
prefix.push(':');
}
continue;
}
let line = line.trim();
if line.is_empty() {
continue;
}
let (key, value) = line
.split_once(char::is_whitespace)
.map_or_else(|| (line, ""), |(key, value)| (key, value.trim_start()));
let full_key = format!("{prefix}{key}").into();
if map.contains_key(&full_key) {
return Err(Error::MultipleKeys(full_key));
}
map.insert(full_key, value.into());
}
Ok(map)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::{Error, parse_config};
use std::fs::write;
fn parse(name: &str, content: &str) -> Result<super::IndexMap<Box<str>, Box<str>>, Error> {
let path = std::env::temp_dir().join(name);
write(&path, content).unwrap();
let result = parse_config(&path);
let _ = std::fs::remove_file(path);
result
}
#[test]
fn flattens_nested_headers() {
let map = parse(
"header_config_nested.hc",
"root value\n\n# A\n\nkey 1\n\n# B\n\nkey 2\n\n## Sub\n\ndeep 3\nempty\n",
)
.unwrap();
assert_eq!(map.get("root").map(AsRef::as_ref), Some("value"));
assert_eq!(map.get("A:key").map(AsRef::as_ref), Some("1"));
assert_eq!(map.get("B:key").map(AsRef::as_ref), Some("2"));
assert_eq!(map.get("B:Sub:deep").map(AsRef::as_ref), Some("3"));
assert_eq!(map.get("B:Sub:empty").map(AsRef::as_ref), Some(""));
}
#[test]
fn rejects_duplicate_keys() {
let result = parse("header_config_dup.hc", "# A\n\nkey 1\nkey 2\n");
assert!(matches!(result, Err(Error::MultipleKeys(key)) if &*key == "A:key"));
}
#[test]
fn rejects_orphan_subheader() {
let result = parse("header_config_orphan.hc", "## Sub\n\nkey 1\n");
assert!(matches!(result, Err(Error::SubheaderWithoutHeader)));
}
}