header_config/
lib.rs

1#![deny(missing_docs)]
2
3/*!
4This library parses hierarchic configuration files in a markdown inspired format.
5Therefore the `parse_config` function is provided.
6**/
7
8use header_parsing::parse_header;
9use indexmap::IndexMap;
10use thiserror::Error;
11
12use std::{
13    fs::File,
14    io::{BufRead, BufReader},
15    path::Path,
16};
17
18/// Error if parsing fails.
19#[derive(Debug, Error)]
20pub enum Error {
21    /// Opening file failed.
22    #[error("Failed to open the configuration file")]
23    OpeningFile,
24    /// A subheader doesn't have a header of the right hierarchy level.
25    #[error("Subheader found without a corresponding header")]
26    SubheaderWithoutHeader,
27    /// A key appears multiple times.
28    #[error("Multiple values found for key: {0}")]
29    MultipleKeys(Box<str>),
30    /// Some input issue occured while reading the file.
31    #[error("Input issues")]
32    Input,
33}
34
35impl Error {
36    /// Prints an error message suitable for the specified path.
37    pub fn print_message(&self, path: &Path) {
38        use Error::*;
39        match self {
40            OpeningFile => eprintln!("Opening file {path:?} failed"),
41            SubheaderWithoutHeader => {
42                eprintln!("File {path:?} has a subheader without a header")
43            }
44            MultipleKeys(key) => eprintln!("Path {path:?} has a duplicate of key {key}"),
45            Input => eprintln!("Input error while reading {path:?}"),
46        }
47    }
48}
49
50/// Parses the configuration file.
51pub fn parse_config(path: &Path) -> Result<IndexMap<Box<str>, Box<str>>, Error> {
52    let Ok(file) = File::open(path) else {
53        return Err(Error::OpeningFile);
54    };
55    let reader = BufReader::new(file);
56
57    let mut map = IndexMap::new();
58
59    let mut key_path = Vec::new();
60
61    for line in reader.lines() {
62        let Ok(line) = line else {
63            return Err(Error::Input);
64        };
65        if let Some(success) = parse_header(&mut key_path, &line) {
66            let Ok(changes) = success else {
67                return Err(Error::SubheaderWithoutHeader);
68            };
69
70            changes.apply();
71            continue;
72        }
73
74        let line = line.trim();
75        if line.is_empty() {
76            continue;
77        }
78
79        let (key, value) = line
80            .split_once(char::is_whitespace)
81            .map_or_else(|| (line, ""), |(key, value)| (key, value.trim_start()));
82
83        let key_path = key_path
84            .iter()
85            .rev()
86            .fold(String::new(), |result, new| format!("{new}:{result}"));
87        let full_key = format!("{key_path}{key}").into();
88        if map.contains_key(&full_key) {
89            return Err(Error::MultipleKeys(full_key));
90        }
91        map.insert(full_key, value.into());
92    }
93
94    Ok(map)
95}