1use std::fs::{create_dir_all, File};
2use std::path::{Path, PathBuf};
3
4pub use configr_derive::Configr;
6use snafu::{OptionExt, ResultExt};
7pub use toml;
8
9const CONFIG_FILE_NAME: &str = "config.toml";
10
11#[derive(snafu::Snafu, Debug)]
13pub enum ConfigError {
14 #[snafu(display("Unable to read configuration file from {}: {}", path.display(), source))]
16 ReadConfig { source: std::io::Error, path: PathBuf },
17 #[snafu(display("Unable to create configuration file or directory {}: {}", path.display(), source))]
19 CreateFs { source: std::io::Error, path: PathBuf },
20 #[snafu(display("Unable to parse TOML\n{}\n```\n{}```{}", path.display(), toml, source))]
22 Deserialize {
23 source: toml::de::Error,
24 path: PathBuf,
25 toml: String,
26 },
27 #[snafu(display(
30 "Unable to get config directory from OS, if you believe this is an error please file an issue on \
31 the `dirs` crate"
32 ))]
33 ConfigDir,
34}
35
36type Result<T, E = ConfigError> = std::result::Result<T, E>;
37
38pub trait Config<C>
55where
56 C: serde::ser::Serialize + serde::de::DeserializeOwned + Config<C> + Default,
57{
58 fn load(
89 app_name: &str,
90 force_user_dir: bool,
91 ) -> Result<C> {
92 let mut dir = dirs::config_dir().context(ConfigDir)?;
93 dir.push(app_name.replace(" ", "-").to_ascii_lowercase());
94 dir.push(CONFIG_FILE_NAME);
95 if !force_user_dir && !dir.exists() {
96 if let Ok(c) = if cfg!(target_family = "unix") {
97 Self::load_custom(app_name, &mut PathBuf::from("/etc"))
98 } else {
99 Self::load_custom(app_name, &mut PathBuf::from("./"))
100 } {
101 return Ok(c);
102 }
103 };
104 Self::load_specific(&dir)
105 }
106
107 fn load_custom(
124 app_name: &str,
125 config_dir: &mut PathBuf,
126 ) -> Result<C> {
127 let config_dir = {
130 config_dir.push(app_name.replace(" ", "-").to_ascii_lowercase());
131 if !config_dir.exists() {
132 create_dir_all(&config_dir).context(CreateFs { path: &config_dir })?;
133 }
134 config_dir.push(CONFIG_FILE_NAME);
135 if !config_dir.exists() {
136 let fd = File::create(&config_dir).context(CreateFs { path: &config_dir })?;
137 Self::populate_template(fd).context(CreateFs { path: &config_dir })?;
138 return Ok(C::default());
139 }
140 config_dir
141 };
142
143 Self::load_specific(&config_dir)
144 }
145
146 fn load_specific(dir: &Path) -> Result<C> {
150 let toml_content = std::fs::read_to_string(&dir).context(ReadConfig { path: &dir })?;
151
152 toml::from_str::<C>(&toml_content).context(Deserialize {
153 path: &dir,
154 toml: &toml_content,
155 })
156 }
157
158 fn populate_template(fd: File) -> std::io::Result<()> {
159 use std::io::Write;
160 let mut writer = std::io::BufWriter::new(fd);
161 writer.write_all(
162 toml::ser::to_string(&toml::Value::try_from(C::default()).unwrap())
163 .unwrap()
164 .as_bytes(),
165 )?;
166 writer.flush()?;
167 Ok(())
168 }
169}
170
171#[cfg(test)]
172mod configr_tests {
173 use configr::{Config, Configr};
174 use serde::{Deserialize, Serialize};
175
176 use crate as configr;
177
178 #[derive(Configr, Serialize, Deserialize, Default, Debug, PartialEq)]
179 struct TestConfig {
180 a: String,
181 b: String,
182 }
183
184 #[test]
185 fn read_proper_config() {
186 std::fs::create_dir("test-config2").unwrap();
187 std::fs::write("test-config2/config.toml", b"a=\"test\"\nb=\"test\"\n").unwrap();
188 let config = TestConfig::load_custom("Test Config2", &mut std::path::PathBuf::from("."));
189 assert!(if let Ok(c) = config {
191 c == TestConfig {
192 a: "test".into(),
193 b: "test".into(),
194 }
195 } else {
196 false
197 });
198
199 std::fs::remove_dir_all("test-config2").unwrap();
200 }
201
202 #[test]
203 fn serialized_config() {
204 let config = TestConfig::load_custom("Test Config1", &mut std::path::PathBuf::from("."));
205 assert!(if let Ok(c) = config {
206 c == TestConfig {
207 a: Default::default(),
208 b: Default::default(),
209 }
210 } else {
211 false
212 });
213 std::fs::remove_dir_all("test-config1").unwrap();
214 }
215}