pkgs/config/
read.rs

1use std::fs;
2use std::io;
3use std::path::{Path, PathBuf};
4
5use thiserror::Error;
6use toml::de::Error as TomlDeError;
7
8use super::Config;
9
10#[derive(Debug, Error)]
11pub enum ConfigError {
12    #[error(transparent)]
13    Io(#[from] io::Error),
14
15    #[error("parse error: {0}")]
16    Parse(#[from] TomlDeError),
17
18    #[error("unsupported file format: {0}")]
19    UnsupportedFileFormat(PathBuf),
20}
21
22impl Config {
23    pub fn read(path: &Path) -> Result<Self, ConfigError> {
24        let content = fs::read_to_string(path)?;
25
26        match path.extension().and_then(|s| s.to_str()) {
27            Some("toml") => Self::from_toml(&content).map_err(ConfigError::Parse),
28            _ => Err(ConfigError::UnsupportedFileFormat(path.to_path_buf())),
29        }
30    }
31
32    pub fn from_toml(content: &str) -> Result<Self, TomlDeError> {
33        toml::from_str(content)
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use indoc::indoc;
40
41    use super::*;
42    use crate::test_utils::prelude::*;
43
44    const TOML_CONTENT: &str = indoc! {r#"
45        [vars]
46        CONFIG_DIR = "${HOME}/.config"
47        DESKTOP_DIR = "${HOME}/.local/share/applications"
48        NU_AUTOLOAD = "${HOME}/.config/nu/autoload"
49
50        [packages.yazi]
51        type = "local"
52
53        [packages.yazi.maps]
54        yazi = "${CONFIG_DIR}/yazi"
55        "yazi.nu" = "${NU_AUTOLOAD}/yazi.nu"
56
57        [packages.kitty.maps]
58        kitty = "${CONFIG_DIR}/kitty"
59        "kitty.desktop" = "${DESKTOP_DIR}/kitty.desktop"
60
61        [packages."empty maps"]
62    "#};
63
64    fn expect_map(map: &BTreeMap<String, String>, key: &str, value: &str) {
65        expect_that!(map.get(key), some(eq(value)));
66    }
67
68    mod toml_parse {
69        use super::*;
70
71        #[gtest]
72        fn it_works() {
73            let config: Config = Config::from_toml(TOML_CONTENT).unwrap();
74
75            let vars = config.vars;
76            expect_eq!(
77                vars,
78                vec![
79                    ("CONFIG_DIR".into(), "${HOME}/.config".into()),
80                    (
81                        "DESKTOP_DIR".into(),
82                        "${HOME}/.local/share/applications".into()
83                    ),
84                    ("NU_AUTOLOAD".into(), "${HOME}/.config/nu/autoload".into()),
85                ]
86            );
87
88            expect_eq!(config.packages.len(), 3);
89
90            expect_eq!(config.packages["yazi"].kind, PackageType::Local);
91            expect_eq!(config.packages["yazi"].maps.len(), 2);
92            expect_map(&config.packages["yazi"].maps, "yazi", "${CONFIG_DIR}/yazi");
93            expect_map(
94                &config.packages["yazi"].maps,
95                "yazi.nu",
96                "${NU_AUTOLOAD}/yazi.nu",
97            );
98
99            expect_eq!(config.packages["kitty"].kind, PackageType::Local);
100            expect_eq!(config.packages["kitty"].maps.len(), 2);
101            expect_map(
102                &config.packages["kitty"].maps,
103                "kitty",
104                "${CONFIG_DIR}/kitty",
105            );
106            expect_map(
107                &config.packages["kitty"].maps,
108                "kitty.desktop",
109                "${DESKTOP_DIR}/kitty.desktop",
110            );
111
112            expect_eq!(config.packages["empty maps"].kind, PackageType::Local);
113            expect_that!(config.packages["empty maps"].maps, is_empty());
114        }
115    }
116
117    mod read {
118        use tempfile::NamedTempFile;
119
120        use super::*;
121
122        fn setup(suffix: &str, content: &str) -> NamedTempFile {
123            let file = NamedTempFile::with_suffix(suffix).unwrap();
124            fs::write(file.path(), content).unwrap();
125            file
126        }
127
128        #[gtest]
129        fn it_works() {
130            let file = setup(".toml", TOML_CONTENT);
131
132            let config = Config::read(file.path()).unwrap();
133            expect_eq!(config.packages.len(), 3);
134        }
135
136        #[gtest]
137        fn parse_error() {
138            let file = setup(".toml", "invalid toml content");
139
140            let err = Config::read(file.path()).unwrap_err();
141            expect_that!(err, pat!(ConfigError::Parse(_)));
142        }
143
144        #[gtest]
145        fn unsupported_file_format() {
146            let file = setup(".ini", "");
147
148            let err = Config::read(file.path()).unwrap_err();
149            expect_that!(err, pat!(ConfigError::UnsupportedFileFormat(_)));
150        }
151
152        #[gtest]
153        fn invalid_path() {
154            let file = NamedTempFile::new().unwrap();
155            let path = file.path().to_path_buf();
156            drop(file);
157
158            let err = Config::read(&path).unwrap_err();
159            expect_that!(err, pat!(ConfigError::Io(_)));
160        }
161    }
162}