1#![deny(missing_docs)]
2
3use 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#[derive(Debug, Error)]
20pub enum Error {
21 #[error("Failed to open the configuration file")]
23 OpeningFile,
24 #[error("Subheader found without a corresponding header")]
26 SubheaderWithoutHeader,
27 #[error("Multiple values found for key: {0}")]
29 MultipleKeys(Box<str>),
30 #[error("Input issues")]
32 Input,
33}
34
35impl Error {
36 pub fn print_message(&self, path: &Path) {
38 use Error::*;
39 match self {
40 OpeningFile => eprintln!("Opening file \"{}\" failed", path.display()),
41 SubheaderWithoutHeader => {
42 eprintln!(
43 "File \"{}\" has a subheader without a header",
44 path.display()
45 )
46 }
47 MultipleKeys(key) => {
48 eprintln!("Path \"{}\" has a duplicate of key {key}", path.display())
49 }
50 Input => eprintln!("Input error while reading \"{}\"", path.display()),
51 }
52 }
53}
54
55pub fn parse_config(path: &Path) -> Result<IndexMap<Box<str>, Box<str>>, Error> {
57 let Ok(file) = File::open(path) else {
58 return Err(Error::OpeningFile);
59 };
60 let reader = BufReader::new(file);
61
62 let mut map = IndexMap::new();
63
64 let mut key_path = Vec::new();
65 let mut prefix = String::new();
66
67 for line in reader.lines() {
68 let Ok(line) = line else {
69 return Err(Error::Input);
70 };
71 if let Some(success) = parse_header(&mut key_path, &line) {
72 let Ok(changes) = success else {
73 return Err(Error::SubheaderWithoutHeader);
74 };
75
76 changes.apply();
77 prefix.clear();
78 for header in &key_path {
79 prefix.push_str(header);
80 prefix.push(':');
81 }
82 continue;
83 }
84
85 let line = line.trim();
86 if line.is_empty() {
87 continue;
88 }
89
90 let (key, value) = line
91 .split_once(char::is_whitespace)
92 .map_or_else(|| (line, ""), |(key, value)| (key, value.trim_start()));
93
94 let full_key = format!("{prefix}{key}").into();
95 if map.contains_key(&full_key) {
96 return Err(Error::MultipleKeys(full_key));
97 }
98 map.insert(full_key, value.into());
99 }
100
101 Ok(map)
102}
103
104#[cfg(test)]
105mod tests {
106 #![allow(clippy::unwrap_used)]
107
108 use super::{Error, parse_config};
109 use std::fs::write;
110
111 fn parse(name: &str, content: &str) -> Result<super::IndexMap<Box<str>, Box<str>>, Error> {
112 let path = std::env::temp_dir().join(name);
113 write(&path, content).unwrap();
114 let result = parse_config(&path);
115 let _ = std::fs::remove_file(path);
116 result
117 }
118
119 #[test]
120 fn flattens_nested_headers() {
121 let map = parse(
122 "header_config_nested.hc",
123 "root value\n\n# A\n\nkey 1\n\n# B\n\nkey 2\n\n## Sub\n\ndeep 3\nempty\n",
124 )
125 .unwrap();
126
127 assert_eq!(map.get("root").map(AsRef::as_ref), Some("value"));
128 assert_eq!(map.get("A:key").map(AsRef::as_ref), Some("1"));
129 assert_eq!(map.get("B:key").map(AsRef::as_ref), Some("2"));
130 assert_eq!(map.get("B:Sub:deep").map(AsRef::as_ref), Some("3"));
131 assert_eq!(map.get("B:Sub:empty").map(AsRef::as_ref), Some(""));
132 }
133
134 #[test]
135 fn rejects_duplicate_keys() {
136 let result = parse("header_config_dup.hc", "# A\n\nkey 1\nkey 2\n");
137 assert!(matches!(result, Err(Error::MultipleKeys(key)) if &*key == "A:key"));
138 }
139
140 #[test]
141 fn rejects_orphan_subheader() {
142 let result = parse("header_config_orphan.hc", "## Sub\n\nkey 1\n");
143 assert!(matches!(result, Err(Error::SubheaderWithoutHeader)));
144 }
145}