use crate::modules::confy::utils::CheckedStringRead;
use directories_next::ProjectDirs;
use serde::{de::DeserializeOwned, Serialize};
use std::fs::{self, File, OpenOptions};
use std::io::{ErrorKind::NotFound, Write};
use std::path::{Path, PathBuf};
use thiserror::Error;
#[cfg(not(any(feature = "toml_conf", feature = "yaml_conf")))]
compile_error!(
"Exactly one config language feature must be enabled to use \
confy. Please enable one of either the `toml_conf` or `yaml_conf` \
features."
);
#[cfg(all(feature = "toml_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` or `yaml_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(feature = "toml_conf")]
const EXTENSION: &str = "toml";
#[cfg(feature = "yaml_conf")]
const EXTENSION: &str = "yml";
#[derive(Debug, Error)]
pub enum ConfyError {
#[cfg(feature = "toml_conf")]
#[error("Bad TOML data")]
BadTomlData(#[source] toml::de::Error),
#[cfg(feature = "yaml_conf")]
#[error("Bad YAML data")]
BadYamlData(#[source] serde_yaml::Error),
#[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(feature = "toml_conf")]
#[error("Failed to serialize configuration data into TOML")]
SerializeTomlError(#[source] toml::ser::Error),
#[cfg(feature = "yaml_conf")]
#[error("Failed to serialize configuration data into YAML")]
SerializeYamlError(#[source] serde_yaml::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),
}
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(feature = "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)
}
}
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 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_path<T: Serialize>(path: impl AsRef<Path>, cfg: T) -> Result<(), ConfyError> {
let path = path.as_ref();
let config_dir = path
.parent()
.ok_or_else(|| ConfyError::BadConfigDirectory(format!("{:?} is a root or prefix", path)))?;
fs::create_dir_all(config_dir).map_err(ConfyError::DirectoryCreationFailed)?;
let s;
#[cfg(feature = "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)?;
}
let mut f = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(ConfyError::OpenConfigurationFileError)?;
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 = ProjectDirs::from("rs", "", app_name)
.ok_or_else(|| ConfyError::BadConfigDirectory("could not determine home directory path".to_string()))?;
let config_dir_str = get_configuration_directory_str(&project)?;
let path = [config_dir_str, &format!("{}.{}", config_name, EXTENSION)]
.iter()
.collect();
Ok(path)
}
fn get_configuration_directory_str(project: &ProjectDirs) -> Result<&str, ConfyError> {
let path = project.config_dir();
path.to_str()
.ok_or_else(|| ConfyError::BadConfigDirectory(format!("{:?} is not valid Unicode", path)))
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Serializer;
use serde_derive::{Deserialize, Serialize};
#[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 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_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(())
}
}