#![deny(missing_docs)]
#![allow(clippy::tabs_in_doc_comments)]
use std::env::var;
use std::fs::{create_dir_all, remove_dir, remove_file, File};
use std::path::PathBuf;
use std::{error, io, result};
use serde::{de::DeserializeOwned, Serialize};
const MSG_NO_SYSTEM_CONFIG_DIR: &str = "no system config directory detected";
pub type Error = Box<dyn error::Error>;
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug, PartialEq, Clone, Default)]
pub enum Format {
#[default]
#[cfg(feature = "json")]
Json,
#[cfg(feature = "yaml")]
Yaml,
#[cfg(feature = "pickle")]
Pickle,
#[cfg(feature = "ini")]
Ini,
#[cfg(feature = "toml")]
Toml,
}
impl Format {
pub fn default_name(&self) -> String {
format!("config.{:?}", self).to_lowercase()
}
}
#[derive(Debug, PartialEq, Clone, Default)]
pub enum Location {
#[default]
Auto,
Path(PathBuf),
File(PathBuf),
Dir(PathBuf),
}
#[derive(Debug, PartialEq, Clone)]
pub struct Abserde {
pub app: String,
pub location: Location,
pub format: Format,
}
impl Abserde {
fn config_path(&self) -> Result<PathBuf> {
let system_config_dir = dirs::config_dir()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, MSG_NO_SYSTEM_CONFIG_DIR))?;
Ok(match &self.location {
Location::Auto => system_config_dir
.join(&self.app)
.join(&self.format.default_name()),
Location::Path(path) => path.clone(),
Location::Dir(dir) => dir.join(&self.format.default_name()),
Location::File(file) => system_config_dir.join(&self.app).join(file),
})
}
pub fn delete(&self) -> Result<()> {
let config_path = self.config_path()?;
remove_file(&config_path)?;
match &self.location {
Location::Dir(_) => {}
_ => {
let config_dir = config_path.parent().ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, MSG_NO_SYSTEM_CONFIG_DIR)
})?;
_ = remove_dir(config_dir);
}
}
Ok(())
}
}
impl Default for Abserde {
fn default() -> Self {
Self {
app: var("CARGO_PKG_NAME").unwrap_or_else(|_| env!("CARGO_PKG_NAME").to_string()),
location: Default::default(),
format: Default::default(),
}
}
}
pub trait Config {
type T;
fn load_config(abserde: &Abserde) -> Result<Self::T>;
fn save_config(&self, abserde: &Abserde) -> Result<()>;
}
impl<T> Config for T
where
T: Serialize,
T: DeserializeOwned,
{
type T = T;
fn load_config(abserde: &Abserde) -> Result<Self::T> {
let config_path = abserde.config_path()?;
Ok(match abserde.format {
#[cfg(feature = "json")]
Format::Json => {
let file = File::open(config_path)?;
serde_json::from_reader(io::BufReader::new(file))?
}
#[cfg(feature = "yaml")]
Format::Yaml => {
let file = File::open(config_path)?;
serde_yaml::from_reader(io::BufReader::new(file))?
}
#[cfg(feature = "pickle")]
Format::Pickle => {
let file = File::open(config_path)?;
serde_pickle::from_reader(io::BufReader::new(file), serde_pickle::DeOptions::new())?
}
#[cfg(feature = "ini")]
Format::Ini => {
let file = File::open(config_path)?;
serde_ini::from_read(io::BufReader::new(file))?
}
#[cfg(feature = "toml")]
Format::Toml => {
use io::Read;
let mut file = File::open(config_path)?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;
toml::from_str(&buf)?
}
})
}
fn save_config(&self, abserde: &Abserde) -> Result<()> {
let config_path = abserde.config_path()?;
let config_dir = config_path
.parent()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, MSG_NO_SYSTEM_CONFIG_DIR))?;
create_dir_all(config_dir)?;
match abserde.format {
#[cfg(feature = "json")]
Format::Json => {
serde_json::to_writer(File::create(&config_path)?, self)?;
}
#[cfg(feature = "yaml")]
Format::Yaml => {
serde_yaml::to_writer(File::create(&config_path)?, self)?;
}
#[cfg(feature = "pickle")]
Format::Pickle => {
serde_pickle::to_writer(
&mut File::create(&config_path)?,
self,
serde_pickle::SerOptions::new(),
)?;
}
#[cfg(feature = "ini")]
Format::Ini => {
serde_ini::to_writer(File::create(&config_path)?, self)?;
}
#[cfg(feature = "toml")]
Format::Toml => {
use io::Write;
write!(File::create(&config_path)?, "{}", toml::to_string(self)?)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::fmt::Debug;
use fake::{Dummy, Fake, Faker};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serial_test::serial;
use tempfile::{NamedTempFile, TempDir};
use crate::{Abserde, Config, Format, Location};
const APP_NAME: &str = "rust_prefs_test";
#[derive(Serialize, Deserialize, Debug, Default, Dummy, PartialEq)]
struct TestConfigSimple {
string_val: String,
i8_val: i8,
i16_val: i16,
i32_val: i32,
u8_val: u8,
u16_val: u16,
u32_val: u32,
f32_val: f32,
}
#[derive(Serialize, Deserialize, Debug, Default, Dummy, PartialEq)]
struct TestConfigComplex {
string_val: String,
i8_val: i8,
i16_val: i16,
i32_val: i32,
i64_val: i64,
i128_val: i128,
u8_val: u8,
u16_val: u16,
u32_val: u32,
u64_val: u64,
u128_val: u128,
f32_val: f32,
f64_val: f64,
vec_1_val: Vec<i64>,
vec_2_val: Vec<(String, i8, i8, i32, i64, String, String, String)>,
vec_3_val: Vec<(f32, f32, f32, f64, f64)>,
hash_map_1_val: HashMap<String, String>,
hash_map_2_val: HashMap<i16, i64>,
hash_map_3_val: HashMap<i16, Vec<(String, i32, u8, HashMap<String, i32>)>>,
hash_map_4_val: HashMap<String, (f64, f32, i8)>,
}
fn test_save_load_delete<T>(abserde: &Abserde)
where
T: Serialize,
T: DeserializeOwned,
T: Dummy<Faker>,
T: PartialEq,
T: Debug,
{
let test_config_saved: T = Faker.fake();
test_config_saved.save_config(&abserde).unwrap();
let test_config_loaded = T::load_config(&abserde).unwrap();
assert_eq!(test_config_saved, test_config_loaded);
abserde.delete().unwrap();
}
#[test]
#[serial]
fn test_auto() {
test_save_load_delete::<TestConfigSimple>(&Abserde::default());
}
#[cfg(feature = "json")]
#[test]
#[serial]
fn test_json_auto() {
test_save_load_delete::<TestConfigComplex>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Auto,
format: Format::Json,
});
}
#[cfg(feature = "json")]
#[test]
fn test_json_path() {
let tmp_file = NamedTempFile::new().unwrap();
test_save_load_delete::<TestConfigComplex>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Path(tmp_file.path().into()),
format: Format::Json,
});
}
#[cfg(feature = "json")]
#[test]
#[serial]
fn test_json_file() {
test_save_load_delete::<TestConfigComplex>(&Abserde {
app: APP_NAME.to_string(),
location: Location::File("custom_file.json".into()),
format: Format::Json,
});
}
#[cfg(feature = "json")]
#[test]
fn test_json_dir() {
let tmp_dir = TempDir::new().unwrap();
test_save_load_delete::<TestConfigComplex>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Dir(tmp_dir.path().into()),
format: Format::Json,
});
}
#[cfg(feature = "yaml")]
#[test]
#[serial]
fn test_yaml_auto() {
test_save_load_delete::<TestConfigComplex>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Auto,
format: Format::Yaml,
});
}
#[cfg(feature = "yaml")]
#[test]
fn test_yaml_path() {
let tmp_file = NamedTempFile::new().unwrap();
test_save_load_delete::<TestConfigComplex>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Path(tmp_file.path().into()),
format: Format::Yaml,
});
}
#[cfg(feature = "yaml")]
#[test]
#[serial]
fn test_yaml_file() {
test_save_load_delete::<TestConfigComplex>(&Abserde {
app: APP_NAME.to_string(),
location: Location::File("custom_file.yaml".into()),
format: Format::Yaml,
});
}
#[cfg(feature = "yaml")]
#[test]
fn test_yaml_dir() {
let tmp_dir = TempDir::new().unwrap();
test_save_load_delete::<TestConfigComplex>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Dir(tmp_dir.path().into()),
format: Format::Yaml,
});
}
#[cfg(feature = "pickle")]
#[test]
#[serial]
fn test_pickle_auto() {
test_save_load_delete::<TestConfigSimple>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Auto,
format: Format::Pickle,
});
}
#[cfg(feature = "pickle")]
#[test]
fn test_pickle_path() {
let tmp_file = NamedTempFile::new().unwrap();
test_save_load_delete::<TestConfigSimple>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Path(tmp_file.path().into()),
format: Format::Pickle,
});
}
#[cfg(feature = "pickle")]
#[test]
#[serial]
fn test_pickle_file() {
test_save_load_delete::<TestConfigSimple>(&Abserde {
app: APP_NAME.to_string(),
location: Location::File("custom_file.pickle".into()),
format: Format::Pickle,
});
}
#[cfg(feature = "pickle")]
#[test]
fn test_pickle_dir() {
let tmp_dir = TempDir::new().unwrap();
test_save_load_delete::<TestConfigSimple>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Dir(tmp_dir.path().into()),
format: Format::Pickle,
});
}
#[cfg(feature = "ini")]
#[test]
#[serial]
fn test_ini_auto() {
test_save_load_delete::<TestConfigSimple>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Auto,
format: Format::Ini,
});
}
#[cfg(feature = "ini")]
#[test]
fn test_ini_path() {
let tmp_file = NamedTempFile::new().unwrap();
test_save_load_delete::<TestConfigSimple>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Path(tmp_file.path().into()),
format: Format::Ini,
});
}
#[cfg(feature = "ini")]
#[test]
#[serial]
fn test_ini_file() {
test_save_load_delete::<TestConfigSimple>(&Abserde {
app: APP_NAME.to_string(),
location: Location::File("custom_file.ini".into()),
format: Format::Ini,
});
}
#[cfg(feature = "ini")]
#[test]
fn test_ini_dir() {
let tmp_dir = TempDir::new().unwrap();
test_save_load_delete::<TestConfigSimple>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Dir(tmp_dir.path().into()),
format: Format::Ini,
});
}
#[cfg(feature = "toml")]
#[test]
#[serial]
fn test_toml_auto() {
test_save_load_delete::<TestConfigSimple>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Auto,
format: Format::Toml,
});
}
#[cfg(feature = "toml")]
#[test]
fn test_toml_path() {
let tmp_file = NamedTempFile::new().unwrap();
test_save_load_delete::<TestConfigSimple>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Path(tmp_file.path().into()),
format: Format::Toml,
});
}
#[cfg(feature = "toml")]
#[test]
#[serial]
fn test_toml_file() {
test_save_load_delete::<TestConfigSimple>(&Abserde {
app: APP_NAME.to_string(),
location: Location::File("custom_file.toml".into()),
format: Format::Toml,
});
}
#[cfg(feature = "toml")]
#[test]
fn test_toml_dir() {
let tmp_dir = TempDir::new().unwrap();
test_save_load_delete::<TestConfigSimple>(&Abserde {
app: APP_NAME.to_string(),
location: Location::Dir(tmp_dir.path().into()),
format: Format::Toml,
});
}
}