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()), ]
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}