#![deny(missing_docs)]
#![warn(rust_2018_idioms)]
#![doc(html_root_url = "https://docs.rs/config-file/0.2.3/")]
use serde::de::DeserializeOwned;
use std::{ffi::OsStr, fs::File, path::Path};
use thiserror::Error;
#[cfg(feature = "toml")]
use toml_crate as toml;
pub trait FromConfigFile {
fn from_config_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigFileError>
where
Self: Sized;
}
impl<C: DeserializeOwned> FromConfigFile for C {
fn from_config_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigFileError>
where
Self: Sized,
{
let path = path.as_ref();
let extension = path
.extension()
.and_then(OsStr::to_str)
.map(|extension| extension.to_lowercase());
match extension.as_deref() {
#[cfg(feature = "json")]
Some("json") => {
serde_json::from_reader(open_file(path)?).map_err(ConfigFileError::Json)
}
#[cfg(feature = "toml")]
Some("toml") => toml::from_str(
std::fs::read_to_string(path)
.map_err(ConfigFileError::FileAccess)?
.as_str(),
)
.map_err(ConfigFileError::Toml),
#[cfg(feature = "xml")]
Some("xml") => {
serde_xml_rs::from_reader(open_file(path)?).map_err(ConfigFileError::Xml)
}
#[cfg(feature = "yaml")]
Some("yaml") | Some("yml") => {
serde_yaml::from_reader(open_file(path)?).map_err(ConfigFileError::Yaml)
}
_ => Err(ConfigFileError::UnsupportedFormat),
}
}
}
#[allow(unused)]
fn open_file(path: &Path) -> Result<File, ConfigFileError> {
File::open(path).map_err(ConfigFileError::FileAccess)
}
#[derive(Error, Debug)]
pub enum ConfigFileError {
#[error("couldn't read config file")]
FileAccess(#[from] std::io::Error),
#[cfg(feature = "json")]
#[error("couldn't parse JSON file")]
Json(#[from] serde_json::Error),
#[cfg(feature = "toml")]
#[error("couldn't parse TOML file")]
Toml(#[from] toml::de::Error),
#[cfg(feature = "xml")]
#[error("couldn't parse XML file")]
Xml(#[from] serde_xml_rs::Error),
#[cfg(feature = "yaml")]
#[error("couldn't parse YAML file")]
Yaml(#[from] serde_yaml::Error),
#[error("don't know how to parse file")]
UnsupportedFormat,
}
#[cfg(test)]
mod test {
use super::*;
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq)]
struct TestConfig {
host: String,
port: u64,
tags: Vec<String>,
inner: TestConfigInner,
}
#[derive(Debug, Deserialize, PartialEq)]
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 },
}
}
}
#[test]
fn test_unknown() {
let config = TestConfig::from_config_file("/tmp/foobar");
assert!(matches!(config, Err(ConfigFileError::UnsupportedFormat)));
}
#[test]
#[cfg(feature = "toml")]
fn test_file_not_found() {
let config = TestConfig::from_config_file("/tmp/foobar.toml");
assert!(matches!(config, Err(ConfigFileError::FileAccess(_))));
}
#[test]
#[cfg(feature = "json")]
fn test_json() {
let config = TestConfig::from_config_file("testdata/config.json");
assert_eq!(config.unwrap(), TestConfig::example());
}
#[test]
#[cfg(feature = "toml")]
fn test_toml() {
let config = TestConfig::from_config_file("testdata/config.toml");
assert_eq!(config.unwrap(), TestConfig::example());
}
#[test]
#[cfg(feature = "xml")]
fn test_xml() {
let config = TestConfig::from_config_file("testdata/config.xml");
assert_eq!(config.unwrap(), TestConfig::example());
}
#[test]
#[cfg(feature = "yaml")]
fn test_yaml() {
let config = TestConfig::from_config_file("testdata/config.yml");
assert_eq!(config.unwrap(), TestConfig::example());
}
}