pkg_rs/
config.rs

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        // Store the children documents in a HashMap
71        let mut config = HashMap::new();
72
73        // Helper function to get and clone children documents
74        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(); // Clone to get owned KdlDocument
85
86            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                    // Linux/MacOS
98                    Path::new(&home_dir).join(stripped)
99                } else if let Some(home_dir) = env::var_os("USERPROFILE") {
100                    // Windows
101                    Path::new(&home_dir).join(stripped)
102                } else {
103                    PathBuf::from(path)
104                }
105            } else {
106                PathBuf::from(path)
107            }
108        }
109
110        // Helper function to get string values from nodes
111        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}