header-config 0.1.5

Runtime parser for hierarchical configurations using Markdown-style headers
Documentation
#![deny(missing_docs)]

/*!
This library parses hierarchic configuration files in a markdown inspired format.
Therefore the `parse_config` function is provided.
**/

use header_parsing::parse_header;
use indexmap::IndexMap;
use thiserror::Error;

use std::{
    fs::File,
    io::{BufRead, BufReader},
    path::Path,
};

/// Error if parsing fails.
#[derive(Debug, Error)]
pub enum Error {
    /// Opening file failed.
    #[error("Failed to open the configuration file")]
    OpeningFile,
    /// A subheader doesn't have a header of the right hierarchy level.
    #[error("Subheader found without a corresponding header")]
    SubheaderWithoutHeader,
    /// A key appears multiple times.
    #[error("Multiple values found for key: {0}")]
    MultipleKeys(Box<str>),
    /// Some input issue occured while reading the file.
    #[error("Input issues")]
    Input,
}

impl Error {
    /// Prints an error message suitable for the specified path.
    pub fn print_message(&self, path: &Path) {
        use Error::*;
        match self {
            OpeningFile => eprintln!("Opening file {path:?} failed"),
            SubheaderWithoutHeader => {
                eprintln!("File {path:?} has a subheader without a header")
            }
            MultipleKeys(key) => eprintln!("Path {path:?} has a duplicate of key {key}"),
            Input => eprintln!("Input error while reading {path:?}"),
        }
    }
}

/// Parses the configuration file.
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();

    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();
            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 key_path = key_path
            .iter()
            .rev()
            .fold(String::new(), |result, new| format!("{new}:{result}"));
        let full_key = format!("{key_path}{key}").into();
        if map.contains_key(&full_key) {
            return Err(Error::MultipleKeys(full_key));
        }
        map.insert(full_key, value.into());
    }

    Ok(map)
}