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}