1use std::{
2 env, fs,
3 path::{Path, PathBuf},
4};
5
6use toml::Table;
7
8use crate::SettingsError;
9
10pub struct ConfigPathOptions<'a> {
11 pub app: &'a str,
12 pub path: Option<&'a str>,
13 pub default_file: Option<&'a str>,
14}
15
16pub fn resolve_config_file(options: &ConfigPathOptions) -> Result<Option<PathBuf>, SettingsError> {
34 if let Ok(env_var) = env::var(format!("{}_CONFIG", options.app.to_uppercase())) {
36 return Ok(Some(PathBuf::from(env_var)));
37 }
38
39 if let Some(path) = options.path {
41 return Ok(Some(Path::new(path).to_path_buf()));
42 }
43
44 let Some(default_file) = options.default_file else {
47 return Ok(None);
48 };
49
50 let Some(project_dirs) = directories::ProjectDirs::from("", "", options.app) else {
52 return Err(SettingsError::NoConfigDir);
53 };
54
55 Ok(Some(project_dirs.config_dir().join(default_file)))
56}
57
58pub fn load_toml_file(path: Option<&Path>) -> Result<Option<toml::Table>, SettingsError> {
69 match path {
70 Some(file) => {
71 let raw_content = match fs::read_to_string(file) {
72 Ok(content) => content,
73 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
74 Err(e) => {
75 return Err(SettingsError::ConfigRead {
76 path: file.to_path_buf(),
77 source: e,
78 });
79 }
80 };
81
82 let parsed_content =
83 raw_content
84 .parse::<Table>()
85 .map_err(|e| SettingsError::ConfigParse {
86 path: file.to_path_buf(),
87 source: e,
88 })?;
89
90 Ok(Some(parsed_content))
91 }
92 None => Ok(None),
93 }
94}
95
96#[cfg(test)]
97mod test {
98 use std::io::Write;
99
100 use std::assert_matches;
101 use toml::Value;
102
103 use super::*;
104
105 #[test]
106 fn test_resolve_config_file_from_configured_path() {
107 let tmp = tempfile::NamedTempFile::new().unwrap();
108 let tmp_path = tmp.into_temp_path();
109 let tmp_path_str = tmp_path.to_str().unwrap().to_string();
110
111 let options = ConfigPathOptions {
112 app: "test_app",
113 path: Some(&tmp_path_str),
114 default_file: None,
115 };
116
117 let config_file = resolve_config_file(&options).unwrap().unwrap();
118 assert_eq!(config_file, PathBuf::from(tmp_path_str))
119 }
120
121 #[test]
122 fn test_resolve_config_file_from_env_var() {
123 let config_path = "~/.config/test_app/test.toml";
124 let options = ConfigPathOptions {
125 app: "test_app",
126 path: None,
127 default_file: None,
128 };
129
130 temp_env::with_var("TEST_APP_CONFIG", Some(config_path), || {
131 let config_file = resolve_config_file(&options);
132 assert!(config_file.is_ok());
133
134 let config_file = config_file.unwrap().unwrap();
135 assert_eq!(config_file.to_str().unwrap(), config_path)
136 });
137 }
138
139 #[test]
140 fn test_resolve_config_file_returns_none_without_path_or_default() {
141 let options = ConfigPathOptions {
142 app: "test_app_without_path_or_default",
143 path: None,
144 default_file: None,
145 };
146
147 let config_file = resolve_config_file(&options).unwrap();
148 assert!(config_file.is_none())
149 }
150
151 #[test]
152 fn test_load_toml_file_returns_table() {
153 let content = "foo = 'bar'";
154 let mut tmp = tempfile::NamedTempFile::new().unwrap();
155 tmp.write_all(content.as_bytes()).unwrap();
156
157 let parsed = load_toml_file(Some(tmp.path())).unwrap().unwrap();
158 assert_eq!(parsed["foo"], Value::from("bar"))
159 }
160
161 #[test]
162 fn test_load_toml_file_returns_none_for_missing_file() {
163 let parsed = load_toml_file(Some(Path::new("not/real/path"))).unwrap();
164 assert!(parsed.is_none())
165 }
166
167 #[test]
168 fn test_load_toml_file_returns_none_when_given_none() {
169 let parsed = load_toml_file(None).unwrap();
170 assert!(parsed.is_none())
171 }
172
173 #[test]
174 fn test_load_toml_file_returns_settingserror_for_invalid_toml() {
175 let content = "aaabbbccc";
176 let mut tmp = tempfile::NamedTempFile::new().unwrap();
177 tmp.write_all(content.as_bytes()).unwrap();
178
179 let err = load_toml_file(Some(tmp.path())).unwrap_err();
180 assert_matches!(err, SettingsError::ConfigParse { .. })
181 }
182}