1use kdl::{KdlDocument, KdlError};
2use miette::{Diagnostic, IntoDiagnostic, Result, SourceSpan};
3use std::{
4 collections::HashMap,
5 env,
6 path::{Path, PathBuf},
7};
8use thiserror::Error;
9
10#[derive(Debug)]
11pub struct Config {
12 pub path: PathBuf,
13 pub source_dir: PathBuf,
14 pub bridges_set: PathBuf,
15 pub target_dir: PathBuf,
16 pub db_path: PathBuf,
17 pub load_path: PathBuf,
18}
19
20#[derive(Error, Debug, Diagnostic)]
21pub enum ConfigError {
22 #[error(transparent)]
23 #[diagnostic(code(config::io_error))]
24 IoError(#[from] std::io::Error),
25
26 #[error("Invalid KDL document")]
27 #[diagnostic(code(config::parse_error))]
28 KdlError(#[from] KdlError),
29
30 #[error("Invalid value for {0}")]
31 #[diagnostic(code(config::wrong_value))]
32 WrongValue(&'static str),
33
34 #[error("Missing required field: {0}")]
35 #[diagnostic(code(config::missing_value))]
36 MissingValue(&'static str),
37
38 #[error("Invalid path format")]
39 #[diagnostic(
40 code(config::invalid_path),
41 help("Paths must be strings and can use ~ for home directory")
42 )]
43 InvalidPath {
44 #[source_code]
45 src: String,
46 #[label("This path is invalid")]
47 bad_span: SourceSpan,
48 },
49
50 #[error("missing config file")]
51 #[diagnostic(code(config::missing_config_file))]
52 MissingConfigFile,
53}
54
55impl Config {
56 pub fn load(path: PathBuf) -> Result<Self> {
57 let config_file =
58 std::fs::read_to_string(&path).map_err(|_| ConfigError::MissingConfigFile)?;
59
60 let kdl = config_file.parse::<KdlDocument>().into_diagnostic()?;
61
62 let config_node = kdl
63 .get("config")
64 .ok_or(ConfigError::MissingValue("config"))?;
65
66 let content = config_node
67 .children()
68 .ok_or(ConfigError::MissingValue("config node is empty"))?;
69
70 let mut config = HashMap::new();
72
73 fn get_and_store_children(
75 config: &mut HashMap<String, KdlDocument>,
76 parent: &KdlDocument,
77 key: &'static str,
78 ) -> Result<(), ConfigError> {
79 let children = parent
80 .get(key)
81 .ok_or(ConfigError::MissingValue(key))?
82 .children()
83 .ok_or(ConfigError::MissingValue(key))?
84 .clone(); config.insert(key.to_string(), children);
87 Ok(())
88 }
89
90 get_and_store_children(&mut config, content, "inputs")?;
91 get_and_store_children(&mut config, content, "output")?;
92 get_and_store_children(&mut config, content, "db")?;
93
94 fn expand_home(path: &str) -> PathBuf {
95 if let Some(stripped) = path.strip_prefix("~/") {
96 if let Some(home_dir) = env::var_os("HOME") {
97 Path::new(&home_dir).join(stripped)
99 } else if let Some(home_dir) = env::var_os("USERPROFILE") {
100 Path::new(&home_dir).join(stripped)
102 } else {
103 PathBuf::from(path)
104 }
105 } else {
106 PathBuf::from(path)
107 }
108 }
109
110 fn get_node_value_as_string(
112 parent: &KdlDocument,
113 node_name: &'static str,
114 src: &str,
115 ) -> Result<PathBuf, ConfigError> {
116 let node = parent
117 .get(node_name)
118 .ok_or(ConfigError::MissingValue(node_name))?;
119
120 let value = node
121 .entries()
122 .first()
123 .ok_or(ConfigError::MissingValue(node_name))?
124 .value()
125 .as_string()
126 .ok_or_else(|| ConfigError::InvalidPath {
127 src: src.to_string(),
128 bad_span: node.span(),
129 })?
130 .to_owned();
131
132 Ok(expand_home(value.as_str()))
133 }
134
135 let src = kdl.to_string();
136
137 Ok(Self {
138 path,
139 source_dir: get_node_value_as_string(config.get("inputs").unwrap(), "path", &src)?,
140 bridges_set: get_node_value_as_string(
141 config.get("inputs").unwrap(),
142 "bridges-set",
143 &src,
144 )?,
145 target_dir: get_node_value_as_string(
146 config.get("output").unwrap(),
147 "target-dir",
148 &src,
149 )?,
150 load_path: get_node_value_as_string(config.get("output").unwrap(), "load-path", &src)?,
151 db_path: get_node_value_as_string(config.get("db").unwrap(), "path", &src)?,
152 })
153 }
154}