configr/
lib.rs

1use std::fs::{create_dir_all, File};
2use std::path::{Path, PathBuf};
3
4/// Reexport of Attribute Macros
5pub use configr_derive::Configr;
6use snafu::{OptionExt, ResultExt};
7pub use toml;
8
9const CONFIG_FILE_NAME: &str = "config.toml";
10
11/// List of error categories
12#[derive(snafu::Snafu, Debug)]
13pub enum ConfigError {
14	/// Loading the config.toml file failed.
15	#[snafu(display("Unable to read configuration file from {}: {}", path.display(), source))]
16	ReadConfig { source: std::io::Error, path: PathBuf },
17	/// Creating the directory or file failed.
18	#[snafu(display("Unable to create configuration file or directory {}: {}", path.display(), source))]
19	CreateFs { source: std::io::Error, path: PathBuf },
20	/// TOML parsing failed in some way.
21	#[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	/// Unable to get the configuration directory, possibly because of
28	/// an unsupported OS.
29	#[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
38/// This is the main trait that you implement on your struct, either
39/// manually or using the [`Configr`][configr_derive::Configr]
40/// attribute macro
41///
42/// ```no_run
43/// use configr::{Config, Configr};
44/// #[derive(Configr, Default, serde::Serialize, serde::Deserialize)]
45/// pub struct BotConfig {
46///     bot_username: String,
47///     client_id: String,
48///     client_secret: String,
49///     channel: String,
50/// }
51///
52/// let config = BotConfig::load("bot-app", true).unwrap();
53/// ```
54pub trait Config<C>
55where
56	C: serde::ser::Serialize + serde::de::DeserializeOwned + Config<C> + Default,
57{
58	/// Load the config from the config file located in the OS
59	/// specific config directory\
60	/// This is a wrapper around
61	/// [`load_custom`][Self::load_custom] which just
62	/// takes the system configuration directory, instead of a custom
63	/// path.\
64	/// Read [`load_custom`][Self::load_custom] for more
65	/// informationg about failure and config folder structure
66	///
67	/// # Failures
68	/// this will contains the same failure possibilities as
69	/// [`load_custom`][Self::load_custom] in addtion this can
70	/// also fail due to the user configuration path not being found,
71	/// if this is the case, you should switch to using
72	/// `load_custom` or [`load_specific`][Self::load_specific] with a
73	/// custom path
74	///
75	/// # Notes
76	/// This should in almost every case be prefered over supplying
77	/// your own configuration directory.
78	///
79	/// The `force_user_dir` option makes sure the fuction always
80	/// prefers the user configuration path, compared to using /etc on
81	/// UNIX systems and besides the executable on other systems, if
82	/// the user configuration file is not found
83	///
84	/// The configuration directory is as follows\
85	/// Linux: `$XDG_CONFIG_HOME/`\
86	/// Windows: `%APPDATA%/`\
87	/// Mac OS: `$HOME/Library/Application Support/`
88	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	/// Load the config from the config file located in the app
108	/// specific config directory which is
109	/// `config_dir/app-name/config.toml`
110	///
111	/// # Notes
112	/// This should only be used in the case you are running this on a
113	/// system which you know doesn't have a configuration directory.
114	///
115	/// The app_name will be converted to lowercase-kebab-case
116	///
117	/// # Failures
118	/// This function will Error under the following circumstances\
119	/// * If the config.toml or the app-name directory could not be
120	///   created\
121	/// * If the config.toml could not be read properly\
122	/// * If the config.toml is not valid toml data
123	fn load_custom(
124		app_name: &str,
125		config_dir: &mut PathBuf,
126	) -> Result<C> {
127		// Get the location of the config file, create directories and the
128		// file itself if needed.
129		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	/// This is the completely barebones and should almost never be
147	/// used over [`load_custom`][Self::load_custom] or
148	/// [`load`][Self::load]
149	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		// expect toml parse error with correct fields but no actual values
190		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}