use std::collections::HashMap;
use std::fmt::Display;
use std::io::Error as StdError;
use std::str::FromStr;
use std::sync::Arc;
use thiserror::Error as ThisError;
#[cfg(feature = "json")]
mod json;
#[cfg(feature = "toml")]
mod toml;
#[cfg(feature = "yaml")]
mod yaml;
#[cfg(test)]
mod tests;
#[cfg(feature = "json")]
pub use json::JsonParser;
#[cfg(feature = "toml")]
pub use toml::TomlParser;
#[cfg(feature = "yaml")]
pub use yaml::YmlParser;
#[derive(Debug, ThisError)]
pub enum ParserError {
#[error("'{0}' is not a valid file format")]
InvalidFileFormat(String),
#[error("Missing required field: {0}")]
MissingField(String),
#[error("Type mismatch in field '{field}', expected {expected}")]
TypeMismatch { field: String, expected: String },
#[error("This file has no file extension")]
MissingFileExtension,
#[error("Nested configuration error in {section}: {source}")]
NestedError {
section: String,
#[source]
source: Box<ParserError>,
},
#[error("{0:#}")]
Io(#[from] StdError),
#[cfg(feature = "toml")]
#[error("TOML parsing error: {0}")]
TomlError(#[from] toml_edit::TomlError),
#[cfg(feature = "json")]
#[error("JSON parsing error: {0}")]
JsonError(#[from] jzon::Error),
#[cfg(feature = "yaml")]
#[error("YAML parsing error: {0}")]
YmlError(#[from] yaml_rust2::ScanError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FileFormat {
#[cfg(feature = "yaml")]
Yml,
#[cfg(feature = "json")]
Json,
#[cfg(feature = "toml")]
Toml,
}
impl FromStr for FileFormat {
type Err = ParserError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s
.to_lowercase()
.as_str()
{
#[cfg(feature = "yaml")]
"yml" | "yaml" => Ok(FileFormat::Yml),
#[cfg(feature = "json")]
"json" => Ok(FileFormat::Json),
#[cfg(feature = "toml")]
"toml" => Ok(FileFormat::Toml),
_ => Err(ParserError::InvalidFileFormat(s.into())),
}
}
}
impl Display for FileFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(feature = "yaml")]
FileFormat::Yml => write!(f, "yaml"),
#[cfg(feature = "json")]
FileFormat::Json => write!(f, "json"),
#[cfg(feature = "toml")]
FileFormat::Toml => write!(f, "toml"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConfigValue {
Value(String),
Section(HashMap<String, ConfigValue>),
Array(Vec<ConfigValue>),
}
pub trait Parser: Send + Sync {
fn extensions(&self) -> &'static [&'static str];
fn format(&self) -> FileFormat {
match self.extensions()[0] {
#[cfg(feature = "yaml")]
"yml" | "yaml" => FileFormat::Yml,
#[cfg(feature = "json")]
"json" => FileFormat::Json,
#[cfg(feature = "toml")]
"toml" => FileFormat::Toml,
_ => panic!("Unsupported file format"),
}
}
fn load(&self, path: &str) -> Result<ConfigValue, ParserError>;
}
pub fn get_parser(ext: &str) -> Result<Arc<dyn Parser>, ParserError> {
match ext {
#[cfg(feature = "yaml")]
"yml" | "yaml" => Ok(Arc::new(crate::parser::yaml::YmlParser)),
#[cfg(feature = "json")]
"json" => Ok(Arc::new(crate::parser::json::JsonParser)),
#[cfg(feature = "toml")]
"toml" => Ok(Arc::new(crate::parser::toml::TomlParser)),
_ => Err(ParserError::InvalidFileFormat(ext.into())),
}
}
pub fn get_file_extension(path: &str) -> Result<String, ParserError> {
let ext = std::path::Path::new(path)
.extension()
.and_then(|s| s.to_str())
.ok_or(ParserError::MissingFileExtension)?;
Ok(ext.into())
}
pub trait FromConfigValue {
fn from_config_value(value: &ConfigValue) -> Result<Self, ParserError>
where
Self: Sized;
}
macro_rules! impl_from_config_value {
($($t:ty),* $(,)?) => {$(
impl FromConfigValue for $t {
fn from_config_value(value: &ConfigValue) -> Result<Self, ParserError> {
match value {
ConfigValue::Value(s) => parse_value::<$t>(s),
_ => Err(ParserError::TypeMismatch {
field: "Expected a scalar value".into(),
expected: stringify!($t).into(),
}),
}
}
}
)*};
}
fn parse_value<T: FromStr>(s: &str) -> Result<T, ParserError> {
s.parse::<T>()
.map_err(|_| ParserError::TypeMismatch {
field: "Failed to parse value".into(),
expected: stringify!(T).into(),
})
}
impl_from_config_value!(
String, bool, i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, usize, isize, f32, f64, char
);
impl<T> FromConfigValue for Vec<T>
where
T: FromConfigValue,
{
fn from_config_value(value: &ConfigValue) -> Result<Self, ParserError> {
match value {
ConfigValue::Array(items) => items
.iter()
.enumerate()
.map(|(i, item)| {
T::from_config_value(item).map_err(|e| ParserError::NestedError {
section: format!("[{}]", i),
source: Box::new(e),
})
})
.collect(),
_ => Err(ParserError::TypeMismatch {
field: "".into(),
expected: "array".into(),
}),
}
}
}