mod utils;
use etcetera::app_strategy;
use utils::*;
use etcetera::{
AppStrategy, AppStrategyArgs, app_strategy::choose_app_strategy,
app_strategy::choose_native_strategy,
};
use lazy_static::lazy_static;
use serde::{Serialize, de::DeserializeOwned};
use std::fs::{self, File, OpenOptions, Permissions};
use std::io::{ErrorKind::NotFound, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use thiserror::Error;
#[cfg(feature = "toml_conf")]
use toml::{
de::Error as TomlDeErr, from_str as toml_from_str, ser::Error as TomlSerErr,
to_string_pretty as toml_to_string_pretty,
};
#[cfg(feature = "basic_toml_conf")]
use basic_toml::{
Error as TomlDeErr, Error as TomlSerErr, from_str as toml_from_str,
to_string as toml_to_string_pretty,
};
#[cfg(not(any(
feature = "toml_conf",
feature = "basic_toml_conf",
feature = "yaml_conf",
feature = "ron_conf"
)))]
compile_error!(
"Exactly one config language feature must be enabled to use \
confy. Please enable one of either the `toml_conf`, `yaml_conf`, \
, `ron_conf` or `toml_basic_conf` features."
);
#[cfg(any(
all(feature = "toml_conf", feature = "basic_toml_conf"),
all(
any(feature = "toml_conf", feature = "basic_toml_conf"),
feature = "yaml_conf"
),
all(
any(feature = "toml_conf", feature = "basic_toml_conf"),
feature = "ron_conf"
),
all(feature = "ron_conf", feature = "yaml_conf"),
))]
compile_error!(
"Exactly one config language feature must be enabled to compile \
confy. Please disable one of either the `toml_conf`, `basic_toml_conf`, `yaml_conf`, or `ron_conf` features. \
NOTE: `toml_conf` is a default feature, so disabling it might mean switching off \
default features for confy in your Cargo.toml"
);
#[cfg(any(feature = "toml_conf", feature = "basic_toml_conf"))]
const EXTENSION: &str = "toml";
#[cfg(feature = "yaml_conf")]
const EXTENSION: &str = "yml";
#[cfg(feature = "ron_conf")]
const EXTENSION: &str = "ron";
lazy_static! {
static ref STRATEGY: Mutex<ConfigStrategy> = Mutex::new(ConfigStrategy::App);
}
#[derive(Debug, Error)]
pub enum ConfyError {
#[cfg(any(feature = "toml_conf", feature = "basic_toml_conf"))]
#[error("Bad TOML data")]
BadTomlData(#[source] TomlDeErr),
#[cfg(feature = "yaml_conf")]
#[error("Bad YAML data")]
BadYamlData(#[source] serde_yaml::Error),
#[cfg(feature = "ron_conf")]
#[error("Bad RON data")]
BadRonData(#[source] ron::error::SpannedError),
#[error("Failed to create directory")]
DirectoryCreationFailed(#[source] std::io::Error),
#[error("Failed to load configuration file")]
GeneralLoadError(#[source] std::io::Error),
#[error("Bad configuration directory: {0}")]
BadConfigDirectory(String),
#[cfg(any(feature = "toml_conf", feature = "basic_toml_conf"))]
#[error("Failed to serialize configuration data into TOML")]
SerializeTomlError(#[source] TomlSerErr),
#[cfg(feature = "yaml_conf")]
#[error("Failed to serialize configuration data into YAML")]
SerializeYamlError(#[source] serde_yaml::Error),
#[cfg(feature = "ron_conf")]
#[error("Failed to serialize configuration data into RON")]
SerializeRonError(#[source] ron::error::Error),
#[error("Failed to write configuration file")]
WriteConfigurationFileError(#[source] std::io::Error),
#[error("Failed to read configuration file")]
ReadConfigurationFileError(#[source] std::io::Error),
#[error("Failed to open configuration file")]
OpenConfigurationFileError(#[source] std::io::Error),
#[error("Failed to set configuration file permissions")]
SetPermissionsFileError(#[source] std::io::Error),
}
pub enum ConfigStrategy {
App,
Native,
}
pub fn change_config_strategy(changer: ConfigStrategy) {
*STRATEGY
.lock()
.expect("Error getting lock on Config Strategy") = changer;
}
enum InternalStrategy {
App(app_strategy::Xdg),
NativeMac(app_strategy::Apple),
NativeUnix(app_strategy::Unix),
NativeWindows(app_strategy::Windows),
}
impl AppStrategy for InternalStrategy {
fn home_dir(&self) -> &Path {
unimplemented!()
}
fn config_dir(&self) -> PathBuf {
match self {
InternalStrategy::App(xdg) => xdg.config_dir(),
InternalStrategy::NativeMac(mac) => mac.config_dir(),
InternalStrategy::NativeUnix(unix) => unix.config_dir(),
InternalStrategy::NativeWindows(windows) => windows.config_dir(),
}
}
fn data_dir(&self) -> PathBuf {
unimplemented!()
}
fn cache_dir(&self) -> PathBuf {
unimplemented!()
}
fn state_dir(&self) -> Option<PathBuf> {
unimplemented!()
}
fn runtime_dir(&self) -> Option<PathBuf> {
unimplemented!()
}
}
impl From<app_strategy::Xdg> for InternalStrategy {
fn from(value: app_strategy::Xdg) -> Self {
InternalStrategy::App(value)
}
}
impl From<app_strategy::Apple> for InternalStrategy {
fn from(value: app_strategy::Apple) -> Self {
InternalStrategy::NativeMac(value)
}
}
impl From<app_strategy::Unix> for InternalStrategy {
fn from(value: app_strategy::Unix) -> Self {
InternalStrategy::NativeUnix(value)
}
}
impl From<app_strategy::Windows> for InternalStrategy {
fn from(value: app_strategy::Windows) -> Self {
InternalStrategy::NativeWindows(value)
}
}
pub fn load<'a, T: Serialize + DeserializeOwned + Default>(
app_name: &str,
config_name: impl Into<Option<&'a str>>,
) -> Result<T, ConfyError> {
get_configuration_file_path(app_name, config_name).and_then(load_path)
}
pub fn load_path<T: Serialize + DeserializeOwned + Default>(
path: impl AsRef<Path>,
) -> Result<T, ConfyError> {
match File::open(&path) {
Ok(mut cfg) => {
let cfg_string = cfg
.get_string()
.map_err(ConfyError::ReadConfigurationFileError)?;
#[cfg(any(feature = "toml_conf", feature = "basic_toml_conf"))]
{
let cfg_data = toml_from_str(&cfg_string);
cfg_data.map_err(ConfyError::BadTomlData)
}
#[cfg(feature = "yaml_conf")]
{
let cfg_data = serde_yaml::from_str(&cfg_string);
cfg_data.map_err(ConfyError::BadYamlData)
}
#[cfg(feature = "ron_conf")]
{
let cfg_data = ron::from_str(&cfg_string);
cfg_data.map_err(ConfyError::BadRonData)
}
}
Err(ref e) if e.kind() == NotFound => {
if let Some(parent) = path.as_ref().parent() {
fs::create_dir_all(parent).map_err(ConfyError::DirectoryCreationFailed)?;
}
let cfg = T::default();
store_path(path, &cfg)?;
Ok(cfg)
}
Err(e) => Err(ConfyError::GeneralLoadError(e)),
}
}
pub fn load_or_else<T, F>(path: impl AsRef<Path>, op: F) -> Result<T, ConfyError>
where
T: DeserializeOwned + Serialize,
F: FnOnce() -> T,
{
let path_ref = path.as_ref();
let load_value = || {
let cfg = op();
if let Some(parent) = path.as_ref().parent() {
fs::create_dir_all(parent).map_err(ConfyError::DirectoryCreationFailed)?;
}
store_path(path_ref, &cfg)?;
Ok(cfg)
};
match File::open(path_ref) {
Ok(mut cfg) => {
let mut load_from_file = || {
let cfg_string = cfg
.get_string()
.map_err(ConfyError::ReadConfigurationFileError)?;
#[cfg(any(feature = "toml_conf", feature = "basic_toml_conf"))]
{
let cfg_data = toml_from_str(&cfg_string);
cfg_data.map_err(ConfyError::BadTomlData)
}
#[cfg(feature = "yaml_conf")]
{
let cfg_data = serde_yaml::from_str(&cfg_string);
cfg_data.map_err(ConfyError::BadYamlData)
}
#[cfg(feature = "ron_conf")]
{
let cfg_data = ron::from_str(&cfg_string);
cfg_data.map_err(ConfyError::BadRonData)
}
};
load_from_file().or_else(|_| load_value())
}
Err(ref e) if e.kind() == NotFound => load_value(),
Err(e) => Err(ConfyError::GeneralLoadError(e)),
}
}
pub fn store<'a, T: Serialize>(
app_name: &str,
config_name: impl Into<Option<&'a str>>,
cfg: T,
) -> Result<(), ConfyError> {
let path = get_configuration_file_path(app_name, config_name)?;
store_path(path, cfg)
}
pub fn store_perms<'a, T: Serialize>(
app_name: &str,
config_name: impl Into<Option<&'a str>>,
cfg: T,
perms: Permissions,
) -> Result<(), ConfyError> {
let path = get_configuration_file_path(app_name, config_name)?;
store_path_perms(path, cfg, perms)
}
pub fn store_path<T: Serialize>(path: impl AsRef<Path>, cfg: T) -> Result<(), ConfyError> {
do_store(path.as_ref(), cfg, None)
}
pub fn store_path_perms<T: Serialize>(
path: impl AsRef<Path>,
cfg: T,
perms: Permissions,
) -> Result<(), ConfyError> {
do_store(path.as_ref(), cfg, Some(perms))
}
fn do_store<T: Serialize>(
path: &Path,
cfg: T,
perms: Option<Permissions>,
) -> Result<(), ConfyError> {
let config_dir = path
.parent()
.ok_or_else(|| ConfyError::BadConfigDirectory(format!("{path:?} is a root or prefix")))?;
fs::create_dir_all(config_dir).map_err(ConfyError::DirectoryCreationFailed)?;
let s;
#[cfg(any(feature = "toml_conf", feature = "basic_toml_conf"))]
{
s = toml_to_string_pretty(&cfg).map_err(ConfyError::SerializeTomlError)?;
}
#[cfg(feature = "yaml_conf")]
{
s = serde_yaml::to_string(&cfg).map_err(ConfyError::SerializeYamlError)?;
}
#[cfg(feature = "ron_conf")]
{
let pretty_cfg = ron::ser::PrettyConfig::default();
s = ron::ser::to_string_pretty(&cfg, pretty_cfg).map_err(ConfyError::SerializeRonError)?;
}
let mut f = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(ConfyError::OpenConfigurationFileError)?;
if let Some(p) = perms {
f.set_permissions(p)
.map_err(ConfyError::SetPermissionsFileError)?;
}
f.write_all(s.as_bytes())
.map_err(ConfyError::WriteConfigurationFileError)?;
Ok(())
}
pub fn get_configuration_file_path<'a>(
app_name: &str,
config_name: impl Into<Option<&'a str>>,
) -> Result<PathBuf, ConfyError> {
let config_name = config_name.into().unwrap_or("default-config");
let project: InternalStrategy = match *STRATEGY
.lock()
.expect("Error getting lock on config strategy")
{
ConfigStrategy::App => choose_app_strategy(AppStrategyArgs {
top_level_domain: "rs".to_string(),
author: "".to_string(),
app_name: app_name.to_string(),
})
.map_err(|e| {
ConfyError::BadConfigDirectory(format!("could not determine home directory path: {e}"))
})?
.into(),
ConfigStrategy::Native => choose_native_strategy(AppStrategyArgs {
top_level_domain: "rs".to_string(),
author: "".to_string(),
app_name: app_name.to_string(),
})
.map_err(|e| {
ConfyError::BadConfigDirectory(format!("could not determine home directory path: {e}"))
})?
.into(),
};
let mut path = project.config_dir();
path.push(format!("{config_name}.{EXTENSION}"));
Ok(path)
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Serializer;
use serde_derive::{Deserialize, Serialize};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[derive(PartialEq, Default, Debug, Serialize, Deserialize)]
struct ExampleConfig {
name: String,
count: usize,
}
fn with_config_path(test_fn: fn(&Path)) {
let config_dir = tempfile::tempdir().expect("creating test fixture failed");
let config_path = config_dir
.path()
.join("example-app")
.join("example-config")
.with_extension(EXTENSION);
test_fn(&config_path);
config_dir.close().expect("removing test fixture failed");
}
#[test]
fn load_path_works() {
with_config_path(|path| {
let config: ExampleConfig = load_path(path).expect("load_path failed");
assert_eq!(config, ExampleConfig::default());
})
}
#[test]
fn load_or_else_works() {
with_config_path(|path| {
let the_value = || ExampleConfig {
name: "a".to_string(),
count: 5,
};
let config: ExampleConfig = load_or_else(path, the_value).expect("load_or_else failed");
assert_eq!(config, the_value());
});
with_config_path(|path| {
fs::create_dir_all(path.parent().unwrap()).unwrap();
let mut file = File::create(path).expect("creating file failed");
file.write("some normal text".as_bytes())
.expect("write to file failed");
drop(file);
let the_value = || ExampleConfig {
name: "a".to_string(),
count: 5,
};
let config: ExampleConfig = load_or_else(path, the_value).expect("load_or_else failed");
assert_eq!(config, the_value());
})
}
#[test]
fn test_store_path() {
with_config_path(|path| {
let config: ExampleConfig = ExampleConfig {
name: "Test".to_string(),
count: 42,
};
store_path(path, &config).expect("store_path failed");
let loaded = load_path(path).expect("load_path failed");
assert_eq!(config, loaded);
})
}
#[test]
fn test_store_path_native() {
change_config_strategy(ConfigStrategy::Native);
with_config_path(|path| {
let config: ExampleConfig = ExampleConfig {
name: "Test".to_string(),
count: 42,
};
let file_path = get_configuration_file_path("example-app", "example-config").unwrap();
if cfg!(target_os = "macos") {
assert_eq!(
file_path,
Path::new(&format!(
"{}/Library/Preferences/rs.example-app/example-config.toml",
std::env::home_dir().unwrap().display()
)),
);
} else if cfg!(target_os = "linux") {
assert_eq!(
file_path,
Path::new(&format!(
"{}/.config/example-app/example-config.toml",
std::env::home_dir().unwrap().display()
))
);
} else {
assert_eq!(
file_path,
Path::new(&format!(
"{}\\AppData\\Roaming\\example-app\\config\\example-config.toml",
std::env::home_dir().unwrap().display()
)),
);
}
store_path(path, &config).expect("store_path failed");
let loaded = load_path(path).expect("load_path failed");
assert_eq!(config, loaded);
})
}
#[test]
fn test_store_path_change() {
change_config_strategy(ConfigStrategy::Native);
with_config_path(|path| {
let config: ExampleConfig = ExampleConfig {
name: "Test".to_string(),
count: 42,
};
let file_path = get_configuration_file_path("example-app", "example-config").unwrap();
if cfg!(target_os = "macos") {
assert_eq!(
file_path,
Path::new(&format!(
"{}/Library/Preferences/rs.example-app/example-config.toml",
std::env::home_dir().unwrap().display()
)),
);
} else if cfg!(target_os = "linux") {
assert_eq!(
file_path,
Path::new(&format!(
"{}/.config/example-app/example-config.toml",
std::env::home_dir().unwrap().display()
))
);
} else {
assert_eq!(
file_path,
Path::new(&format!(
"{}\\AppData\\Roaming\\example-app\\config\\example-config.toml",
std::env::home_dir().unwrap().display()
)),
);
}
change_config_strategy(ConfigStrategy::App);
let file_path = get_configuration_file_path("example-app", "example-config").unwrap();
if cfg!(target_os = "macos") {
assert_eq!(
file_path,
Path::new(&format!(
"{}/.config/example-app/example-config.toml",
std::env::home_dir().unwrap().display()
)),
);
} else if cfg!(target_os = "linux") {
assert_eq!(
file_path,
Path::new(&format!(
"{}/.config/example-app/example-config.toml",
std::env::home_dir().unwrap().display()
))
);
} else {
assert_eq!(
file_path,
Path::new(&format!(
"{}\\AppData\\Roaming\\example-app\\config\\example-config.toml",
std::env::home_dir().unwrap().display()
)),
);
}
store_path(path, &config).expect("store_path failed");
let loaded = load_path(path).expect("load_path failed");
assert_eq!(config, loaded);
})
}
#[test]
#[cfg(unix)]
fn test_store_path_perms() {
with_config_path(|path| {
let config: ExampleConfig = ExampleConfig {
name: "Secret".to_string(),
count: 16549,
};
store_path_perms(path, &config, Permissions::from_mode(0o600))
.expect("store_path_perms failed");
let loaded = load_path(path).expect("load_path failed");
assert_eq!(config, loaded);
})
}
#[test]
fn test_store_path_perms_readonly() {
with_config_path(|path| {
let config: ExampleConfig = ExampleConfig {
name: "Soon read-only".to_string(),
count: 27115,
};
store_path(path, &config).expect("store_path failed");
let metadata = fs::metadata(path).expect("reading metadata failed");
let mut permissions = metadata.permissions();
permissions.set_readonly(true);
store_path_perms(path, &config, permissions).expect("store_path_perms failed");
assert!(
fs::metadata(path)
.expect("reading metadata failed")
.permissions()
.readonly()
);
})
}
#[test]
fn test_store_path_root_error() {
let err = store_path(PathBuf::from("/"), &ExampleConfig::default())
.expect_err("store_path should fail");
assert_eq!(
err.to_string(),
r#"Bad configuration directory: "/" is a root or prefix"#,
)
}
struct CannotSerialize;
impl Serialize for CannotSerialize {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::Error;
Err(S::Error::custom("cannot serialize CannotSerialize"))
}
}
#[test]
fn test_store_path_atomic() -> Result<(), ConfyError> {
let tmp = tempfile::NamedTempFile::new().expect("Failed to create NamedTempFile");
let path = tmp.path();
let message = "Hello world!";
{
let mut f = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(ConfyError::OpenConfigurationFileError)?;
f.write_all(message.as_bytes())
.map_err(ConfyError::WriteConfigurationFileError)?;
f.flush().map_err(ConfyError::WriteConfigurationFileError)?;
}
let store_result = store_path(path, CannotSerialize);
assert!(matches!(store_result, Err(_)));
let buf = {
let mut f = OpenOptions::new()
.read(true)
.open(path)
.map_err(ConfyError::OpenConfigurationFileError)?;
let mut buf = String::new();
use std::io::Read;
f.read_to_string(&mut buf)
.map_err(ConfyError::ReadConfigurationFileError)?;
buf
};
assert_eq!(buf, message);
Ok(())
}
#[test]
fn test_change_struct_name() -> Result<(), ConfyError> {
with_config_path(|path| {
#[derive(PartialEq, Default, Debug, Serialize, Deserialize)]
struct AnotherExampleConfig {
name: String,
count: usize,
}
store_path(path, &ExampleConfig::default()).expect("store_path failed");
let _: AnotherExampleConfig = load_path(path).expect("load_path failed");
});
Ok(())
}
}