#![doc = include_str!("../README.md")]
#![warn(clippy::nursery, clippy::cargo)]
pub mod error;
#[cfg(feature = "xml")]
use std::io::BufReader;
use std::{
ffi::OsStr,
fs::{File, OpenOptions},
io::Write,
path::Path,
};
pub use error::Result;
use error::{Error, TomlError};
use serde::{de::DeserializeOwned, Serialize};
#[cfg(feature = "toml_crate")]
use toml_crate as toml;
#[derive(Debug, Clone, Copy)]
pub enum ConfigFormat {
Json,
Toml,
Xml,
Yaml,
}
impl ConfigFormat {
pub fn from_extension(extension: &str) -> Option<Self> {
match extension.to_lowercase().as_str() {
#[cfg(feature = "json")]
"json" => Some(Self::Json),
#[cfg(feature = "toml_crate")]
"toml" => Some(Self::Toml),
#[cfg(feature = "xml")]
"xml" => Some(Self::Xml),
#[cfg(feature = "yaml")]
"yaml" | "yml" => Some(Self::Yaml),
_ => None,
}
}
pub fn from_path(path: &Path) -> Option<Self> {
Self::from_extension(path.extension().and_then(OsStr::to_str)?)
}
}
pub trait LoadConfigFile {
fn load_with_specific_format(path: impl AsRef<Path>, config_type: ConfigFormat) -> Result<Self>
where
Self: Sized;
fn load(path: impl AsRef<Path>) -> Result<Self>
where
Self: Sized,
{
let path = path.as_ref();
let config_type = ConfigFormat::from_path(path).ok_or(Error::UnsupportedFormat)?;
Self::load_with_specific_format(path, config_type)
}
fn load_or_default(path: impl AsRef<Path>) -> Result<Self>
where
Self: Sized + Default,
{
match Self::load(path) {
Err(error::Error::FileAccess(e)) if e.kind() == std::io::ErrorKind::NotFound => {
Ok(Self::default())
}
other => other,
}
}
}
impl<C: DeserializeOwned> LoadConfigFile for C {
fn load_with_specific_format(path: impl AsRef<Path>, config_type: ConfigFormat) -> Result<Self>
where
Self: Sized,
{
let path = path.as_ref();
match config_type {
#[cfg(feature = "json")]
ConfigFormat::Json => serde_json::from_reader(open_file(path)?).map_err(Error::Json),
#[cfg(feature = "toml_crate")]
ConfigFormat::Toml => Ok(toml::from_str(
std::fs::read_to_string(path)
.map_err(Error::FileAccess)?
.as_str(),
)
.map_err(TomlError::DeserializationError)?),
#[cfg(feature = "xml")]
ConfigFormat::Xml => Ok(quick_xml::de::from_reader(BufReader::new(open_file(
path,
)?))?),
#[cfg(feature = "yaml")]
ConfigFormat::Yaml => serde_yml::from_reader(open_file(path)?).map_err(Error::Yaml),
#[allow(unreachable_patterns)]
_ => Err(Error::UnsupportedFormat),
}
}
}
pub trait StoreConfigFile {
fn store_with_specific_format(
self,
path: impl AsRef<Path>,
config_type: ConfigFormat,
) -> Result<()>;
fn store(self, path: impl AsRef<Path>) -> Result<()>
where
Self: Sized,
{
let path = path.as_ref();
let config_type = ConfigFormat::from_path(path).ok_or(Error::UnsupportedFormat)?;
self.store_with_specific_format(path, config_type)
}
fn store_without_overwrite(self, path: impl AsRef<Path>) -> Result<()>
where
Self: Sized,
{
if path.as_ref().exists() {
return Err(Error::FileExists);
}
self.store(path)
}
}
impl<C: Serialize> StoreConfigFile for C {
fn store_with_specific_format(
self,
path: impl AsRef<Path>,
config_type: ConfigFormat,
) -> Result<()> {
let path = path.as_ref();
match config_type {
#[cfg(feature = "json")]
ConfigFormat::Json => {
serde_json::to_writer_pretty(open_write_file(path)?, &self).map_err(Error::Json)
}
#[cfg(feature = "toml_crate")]
ConfigFormat::Toml => {
open_write_file(path)?.write_all(
toml::to_string_pretty(&self)
.map_err(TomlError::SerializationError)?
.as_bytes(),
)?;
Ok(())
}
#[cfg(feature = "xml")]
ConfigFormat::Xml => Ok(std::fs::write(path, quick_xml::se::to_string(&self)?)?),
#[cfg(feature = "yaml")]
ConfigFormat::Yaml => {
serde_yml::to_writer(open_write_file(path)?, &self).map_err(Error::Yaml)
}
#[allow(unreachable_patterns)]
_ => Err(Error::UnsupportedFormat),
}
}
}
#[allow(unused)]
fn open_file(path: &Path) -> Result<File> {
File::open(path).map_err(Error::FileAccess)
}
#[allow(unused)]
fn open_write_file(path: &Path) -> Result<File> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(Error::FileAccess)
}
#[cfg(test)]
mod test {
use std::env::temp_dir;
use serde::Deserialize;
use super::*;
#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
struct TestConfig {
host: String,
port: u64,
tags: Vec<String>,
inner: TestConfigInner,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
struct TestConfigInner {
answer: u8,
}
impl TestConfig {
#[allow(unused)]
fn example() -> Self {
Self {
host: "example.com".to_string(),
port: 443,
tags: vec!["example".to_string(), "test".to_string()],
inner: TestConfigInner { answer: 42 },
}
}
}
fn test_read_with_extension(extension: &str) {
let config = TestConfig::load(format!("testdata/config.{extension}"));
assert_eq!(config.unwrap(), TestConfig::example());
}
fn test_write_with_extension(extension: &str) {
let mut temp = temp_dir().join("config");
temp.set_extension(extension);
TestConfig::example().store(dbg!(&temp)).unwrap();
assert!(temp.is_file());
dbg!(std::fs::read_to_string(&temp).unwrap());
assert_eq!(TestConfig::example(), TestConfig::load(&temp).unwrap());
std::fs::remove_file(temp).unwrap();
}
#[test]
fn test_unknown() {
let config = TestConfig::load("/tmp/foobar");
assert!(matches!(config, Err(Error::UnsupportedFormat)));
}
#[test]
#[cfg(feature = "toml_crate")]
fn test_file_not_found() {
let config = TestConfig::load("/tmp/foobar.toml");
assert!(matches!(config, Err(Error::FileAccess(_))));
}
#[test]
#[cfg(feature = "json")]
fn test_json() {
test_read_with_extension("json");
test_write_with_extension("json");
}
#[test]
#[cfg(feature = "toml_crate")]
fn test_toml() {
test_read_with_extension("toml");
test_write_with_extension("toml");
}
#[test]
#[cfg(feature = "xml")]
fn test_xml() {
test_read_with_extension("xml");
test_write_with_extension("xml");
}
#[test]
#[cfg(feature = "yaml")]
fn test_yaml() {
test_read_with_extension("yml");
test_write_with_extension("yaml");
}
#[test]
#[cfg(feature = "toml_crate")]
fn test_store_without_overwrite() {
let temp = temp_dir().join("test_store_without_overwrite.toml");
std::fs::File::create(&temp).unwrap();
assert!(TestConfig::example()
.store_without_overwrite(dbg!(&temp))
.is_err());
std::fs::remove_file(temp).unwrap();
}
#[test]
#[cfg(feature = "toml_crate")]
fn test_store_load_with_specific_format() {
let temp = temp_dir().join("test_store_load_with_specific_format.toml");
std::fs::File::create(&temp).unwrap();
TestConfig::example()
.store_with_specific_format(dbg!(&temp), ConfigFormat::Yaml)
.unwrap();
assert!(TestConfig::load(&temp).is_err());
assert!(TestConfig::load_with_specific_format(&temp, ConfigFormat::Yaml).is_ok());
std::fs::remove_file(temp).unwrap();
}
}