pkgs/config/
read.rs

1use std::fs;
2use std::io;
3use std::path::{Path, PathBuf};
4
5use serde_yaml_ng::Error as YamlDeError;
6use thiserror::Error;
7use toml::de::Error as TomlDeError;
8
9use super::Config;
10
11#[derive(Debug, Error)]
12pub enum ConfigError {
13    #[error(transparent)]
14    Io(#[from] io::Error),
15
16    #[error("TOML parse error: {0}")]
17    TomlParse(#[from] TomlDeError),
18
19    #[error("YAML parse error: {0}")]
20    YamlParse(#[from] YamlDeError),
21
22    #[error("unsupported file format: {0}")]
23    UnsupportedFileFormat(PathBuf),
24}
25
26impl Config {
27    pub fn read(path: &Path) -> Result<Self, ConfigError> {
28        let content = fs::read_to_string(path)?;
29
30        match path.extension().and_then(|s| s.to_str()) {
31            Some("toml") => Self::from_toml(&content).map_err(ConfigError::TomlParse),
32            Some("yaml") | Some("yml") => Self::from_yaml(&content).map_err(ConfigError::YamlParse),
33            _ => Err(ConfigError::UnsupportedFileFormat(path.to_path_buf())),
34        }
35    }
36
37    pub fn from_toml(content: &str) -> Result<Self, TomlDeError> {
38        toml::from_str(content)
39    }
40
41    pub fn from_yaml(content: &str) -> Result<Self, YamlDeError> {
42        serde_yaml_ng::from_str(content)
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use indoc::indoc;
49
50    use super::*;
51    use crate::test_utils::prelude::*;
52
53    const TOML_CONTENT: &str = indoc! {r#"
54        [vars]
55        CONFIG_DIR = "${HOME}/.config"
56        DESKTOP_DIR = "${HOME}/.local/share/applications"
57        NU_AUTOLOAD = "${HOME}/.config/nu/autoload"
58        A_VAR = "a value with ${CONFIG} inside"
59
60        [packages.yazi]
61        kind = "local"
62
63        [packages.yazi.maps]
64        yazi = "${CONFIG_DIR}/yazi"
65        "yazi.nu" = "${NU_AUTOLOAD}/yazi.nu"
66
67        [packages.kitty.maps]
68        kitty = "${CONFIG_DIR}/kitty"
69        "kitty.desktop" = "${DESKTOP_DIR}/kitty.desktop"
70
71        [packages."empty maps"]
72    "#};
73
74    const YAML_CONTENT: &str = indoc! {r#"
75        vars:
76          CONFIG_DIR: ${HOME}/.config
77          DESKTOP_DIR: ${HOME}/.local/share/applications
78          NU_AUTOLOAD: ${HOME}/.config/nu/autoload
79          A_VAR: a value with ${CONFIG} inside
80
81        packages:
82          yazi:
83            kind: local
84            maps:
85              yazi: ${CONFIG_DIR}/yazi
86              "yazi.nu": ${NU_AUTOLOAD}/yazi.nu
87
88          kitty:
89            maps:
90              kitty: ${CONFIG_DIR}/kitty
91              kitty.desktop: ${DESKTOP_DIR}/kitty.desktop
92
93          empty maps: {}
94    "#};
95
96    mod parse {
97        use super::*;
98
99        #[gtest]
100        fn toml_parse() {
101            let config: Config = Config::from_toml(TOML_CONTENT).unwrap();
102            validate_config(config);
103        }
104
105        #[gtest]
106        fn yaml_parse() {
107            let config: Config = Config::from_yaml(YAML_CONTENT).unwrap();
108            validate_config(config);
109        }
110
111        #[gtest]
112        fn deny_unknown_fields_toml() {
113            let content = indoc! {r#"
114                [packages.yazi]
115                type = "local"
116            "#};
117
118            let err = Config::from_toml(content).unwrap_err();
119            expect_that!(err, pat!(TomlDeError { .. }));
120        }
121
122        fn validate_config(config: Config) {
123            let vars = config.vars;
124            expect_eq!(
125                vars,
126                [
127                    ("CONFIG_DIR".into(), "${HOME}/.config".into()),
128                    (
129                        "DESKTOP_DIR".into(),
130                        "${HOME}/.local/share/applications".into()
131                    ),
132                    ("NU_AUTOLOAD".into(), "${HOME}/.config/nu/autoload".into()),
133                    ("A_VAR".into(), "a value with ${CONFIG} inside".into()), // preserve order
134                ]
135            );
136
137            expect_eq!(config.packages.len(), 3);
138
139            expect_eq!(config.packages["yazi"].kind, PackageType::Local);
140            expect_eq!(
141                config.packages["yazi"].maps,
142                [
143                    ("yazi".into(), "${CONFIG_DIR}/yazi".into()),
144                    ("yazi.nu".into(), "${NU_AUTOLOAD}/yazi.nu".into())
145                ]
146            );
147
148            expect_eq!(config.packages["kitty"].kind, PackageType::Local);
149            expect_eq!(
150                config.packages["kitty"].maps,
151                [
152                    ("kitty".into(), "${CONFIG_DIR}/kitty".into()),
153                    (
154                        "kitty.desktop".into(),
155                        "${DESKTOP_DIR}/kitty.desktop".into()
156                    )
157                ]
158            );
159
160            expect_eq!(config.packages["empty maps"].kind, PackageType::Local);
161            expect_that!(config.packages["empty maps"].maps, is_empty());
162        }
163    }
164
165    mod read {
166        use tempfile::NamedTempFile;
167
168        use super::*;
169
170        fn setup(suffix: &str, content: &str) -> NamedTempFile {
171            let file = NamedTempFile::with_suffix(suffix).unwrap();
172            fs::write(file.path(), content).unwrap();
173            file
174        }
175
176        #[gtest]
177        fn read_toml() {
178            let file = setup(".toml", TOML_CONTENT);
179            let config = Config::read(file.path()).unwrap();
180            expect_eq!(config.packages.len(), 3);
181        }
182
183        #[gtest]
184        fn read_yaml() {
185            let file = setup(".yaml", YAML_CONTENT);
186            let config = Config::read(file.path()).unwrap();
187            expect_eq!(config.packages.len(), 3);
188        }
189
190        #[gtest]
191        fn read_yml() {
192            let file = setup(".yml", YAML_CONTENT);
193            let config = Config::read(file.path()).unwrap();
194            expect_eq!(config.packages.len(), 3);
195        }
196
197        #[gtest]
198        fn parse_error() {
199            let file = setup(".toml", "invalid toml content");
200
201            let err = Config::read(file.path()).unwrap_err();
202            expect_that!(err, pat!(ConfigError::TomlParse(_)));
203        }
204
205        #[gtest]
206        fn unsupported_file_format() {
207            let file = setup(".ini", "");
208
209            let err = Config::read(file.path()).unwrap_err();
210            expect_that!(err, pat!(ConfigError::UnsupportedFileFormat(_)));
211        }
212
213        #[gtest]
214        fn invalid_path() {
215            let file = NamedTempFile::new().unwrap();
216            let path = file.path().to_path_buf();
217            drop(file);
218
219            let err = Config::read(&path).unwrap_err();
220            expect_that!(err, pat!(ConfigError::Io(_)));
221        }
222    }
223}