#![cfg_attr(docsrs, feature(doc_cfg))]
use owo_colors::*;
use serde::de::DeserializeOwned;
use std::fmt::Display;
use std::path::Path;
use std::sync::Arc;
#[cfg(feature = "env")]
use std::{env::VarError, collections::{BTreeMap, HashMap}};
#[allow(unused)]
use std::convert::Infallible;
pub mod error;
pub mod merge;
pub mod parse;
#[doc(hidden)]
pub mod util;
pub use error::Error;
#[cfg(feature = "derive")]
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
pub use metre_macros::Config;
use error::{FromPartialError, MergeError};
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
use error::FromEnvError;
pub trait Config: Sized {
type Partial: PartialConfig;
fn from_partial(partial: Self::Partial) -> Result<Self, FromPartialError>;
}
pub trait PartialConfig: DeserializeOwned + Default {
fn defaults() -> Self;
fn merge(&mut self, other: Self) -> Result<(), MergeError>;
fn list_missing_properties(&self) -> Vec<String>;
fn is_empty(&self) -> bool;
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
fn from_env_with_provider_and_optional_prefix<E: EnvProvider>(
env: &E,
prefix: Option<&str>,
) -> Result<Self, FromEnvError>;
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
fn from_env_with_provider_and_prefix<E: EnvProvider, P: AsRef<str>>(
env: &E,
prefix: P,
) -> Result<Self, FromEnvError> {
Self::from_env_with_provider_and_optional_prefix(env, Some(prefix.as_ref()))
}
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
fn from_env_with_provider<E: EnvProvider>(env: &E) -> Result<Self, FromEnvError> {
Self::from_env_with_provider_and_optional_prefix(env, None)
}
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
fn from_env_with_prefix<P: AsRef<str>>(prefix: P) -> Result<Self, FromEnvError> {
Self::from_env_with_provider_and_optional_prefix(&StdEnv, Some(prefix.as_ref()))
}
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
fn from_env() -> Result<Self, FromEnvError> {
Self::from_env_with_provider_and_optional_prefix(&StdEnv, None)
}
}
impl<T: Config> Config for Option<T> {
type Partial = Option<T::Partial>;
fn from_partial(partial: Self::Partial) -> Result<Self, FromPartialError> {
match partial {
None => Ok(None),
Some(inner) => {
if inner.is_empty() {
Ok(None)
} else {
let v = T::from_partial(inner)?;
Ok(Some(v))
}
}
}
}
}
impl<T: PartialConfig> PartialConfig for Option<T> {
fn defaults() -> Self {
let inner = T::defaults();
if inner.is_empty() {
None
} else {
Some(inner)
}
}
fn merge(&mut self, other: Self) -> Result<(), MergeError> {
match (self.as_mut(), other) {
(None, Some(other)) => *self = Some(other),
(Some(me), Some(other)) => me.merge(other)?,
(Some(_), None) => {}
(None, None) => {}
};
Ok(())
}
fn list_missing_properties(&self) -> Vec<String> {
match self {
None => vec![],
Some(me) => {
if !me.is_empty() {
me.list_missing_properties()
} else {
vec![]
}
}
}
}
fn is_empty(&self) -> bool {
match self {
None => true,
Some(me) => me.is_empty(),
}
}
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
fn from_env_with_provider_and_optional_prefix<E: EnvProvider>(
env: &E,
prefix: Option<&str>,
) -> Result<Self, FromEnvError> {
let v = T::from_env_with_provider_and_optional_prefix(env, prefix)?;
if v.is_empty() {
Ok(None)
} else {
Ok(Some(v))
}
}
}
pub trait EnvProvider {
type Error: Display;
fn get(&self, key: &str) -> Result<Option<String>, Self::Error>;
}
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
macro_rules! impl_env_provider_for_map {
($ty:ty) => {
impl EnvProvider for $ty {
type Error = Infallible;
fn get(&self, key: &str) -> Result<Option<String>, Self::Error> {
Ok(self.get(key).map(ToString::to_string))
}
}
};
}
#[cfg(feature = "env")]
impl_env_provider_for_map!(HashMap<String, String>);
#[cfg(feature = "env")]
impl_env_provider_for_map!(HashMap<&str, String>);
#[cfg(feature = "env")]
impl_env_provider_for_map!(HashMap<String, &str>);
#[cfg(feature = "env")]
impl_env_provider_for_map!(HashMap<&str, &str>);
#[cfg(feature = "env")]
impl_env_provider_for_map!(BTreeMap<String, String>);
#[cfg(feature = "env")]
impl_env_provider_for_map!(BTreeMap<&str, String>);
#[cfg(feature = "env")]
impl_env_provider_for_map!(BTreeMap<String, &str>);
#[cfg(feature = "env")]
impl_env_provider_for_map!(BTreeMap<&str, &str>);
#[derive(Debug, Clone, Copy)]
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
pub struct StdEnv;
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
impl EnvProvider for StdEnv {
type Error = VarError;
fn get(&self, key: &str) -> Result<Option<String>, Self::Error> {
match std::env::var(key) {
Err(e) => match &e {
VarError::NotPresent => Ok(None),
VarError::NotUnicode(_) => Err(e),
},
Ok(v) => Ok(Some(v)),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum LoadLocation {
Memory,
File(String),
#[cfg(any(feature = "url-blocking", feature = "url-async"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "url-blocking", feature = "url-async"))))]
Url(String),
}
impl Display for LoadLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use LoadLocation::*;
match self {
Memory => write!(f, "{}", "memory".yellow()),
File(location) => write!(f, "file: {}", location.yellow()),
#[cfg(any(feature = "url-blocking", feature = "url-async"))]
Url(location) => write!(f, "url: {}", location.yellow()),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Format {
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
Json,
#[cfg(feature = "jsonc")]
#[cfg_attr(docsrs, doc(cfg(feature = "jsonc")))]
Jsonc,
#[cfg(feature = "toml")]
#[cfg_attr(docsrs, doc(cfg(feature = "toml")))]
Toml,
#[cfg(feature = "yaml")]
#[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
Yaml,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct ConfigLoader<T: Config> {
partial: T::Partial,
}
impl<T: Config> ConfigLoader<T> {
pub fn new() -> Self {
Self {
partial: T::Partial::default(),
}
}
#[allow(clippy::result_large_err)]
pub fn file(&mut self, path: &str, format: Format) -> Result<&mut Self, Error> {
let code = std::fs::read_to_string(path).map_err(|e| Error::Io {
path: path.into(),
source: Arc::new(e),
})?;
self.code_with_location(&code, format, LoadLocation::File(path.to_string()))
}
#[allow(clippy::result_large_err)]
pub fn file_optional(&mut self, path: &str, format: Format) -> Result<&mut Self, Error> {
let exists = Path::new(path).try_exists().map_err(|e| Error::Io {
path: path.into(),
source: Arc::new(e),
})?;
if exists {
self.file(path, format)
} else {
Ok(self)
}
}
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
#[inline(always)]
#[allow(clippy::result_large_err)]
pub fn env(&mut self) -> Result<&mut Self, Error> {
self._env(&StdEnv, None)
}
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
#[inline(always)]
#[allow(clippy::result_large_err)]
pub fn env_with_prefix(&mut self, prefix: &str) -> Result<&mut Self, Error> {
self._env(&StdEnv, Some(prefix))
}
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
#[inline(always)]
#[allow(clippy::result_large_err)]
pub fn env_with_provider<E: EnvProvider>(&mut self, env: &E) -> Result<&mut Self, Error> {
self._env(env, None)
}
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
#[inline(always)]
#[allow(clippy::result_large_err)]
pub fn env_with_provider_and_prefix<E: EnvProvider>(
&mut self,
env: &E,
prefix: &str,
) -> Result<&mut Self, Error> {
self._env(env, Some(prefix))
}
#[inline(always)]
#[allow(clippy::result_large_err)]
pub fn code<S: AsRef<str>>(&mut self, code: S, format: Format) -> Result<&mut Self, Error> {
self._code(code.as_ref(), format, LoadLocation::Memory)
}
#[inline(always)]
#[allow(clippy::result_large_err)]
pub fn code_with_location<S: AsRef<str>>(
&mut self,
code: S,
format: Format,
location: LoadLocation,
) -> Result<&mut Self, Error> {
self._code(code.as_ref(), format, location)
}
#[cfg(feature = "url-blocking")]
#[cfg_attr(docsrs, doc(cfg(feature = "url-blocking")))]
#[allow(clippy::result_large_err)]
pub fn url(&mut self, url: &str, format: Format) -> Result<&mut Self, Error> {
let map_err = |e| Error::Network {
url: url.to_string(),
source: Arc::new(e),
};
let code = reqwest::blocking::get(url)
.map_err(map_err)?
.text()
.map_err(map_err)?;
self._code(&code, format, LoadLocation::Url(url.to_string()))
}
#[cfg(feature = "url-async")]
#[cfg_attr(docsrs, doc(cfg(feature = "url-async")))]
pub async fn url_async(&mut self, url: &str, format: Format) -> Result<&mut Self, Error> {
let map_err = |e| Error::Network {
url: url.to_string(),
source: Arc::new(e),
};
let code = reqwest::get(url)
.await
.map_err(map_err)?
.text()
.await
.map_err(map_err)?;
self._code(&code, format, LoadLocation::Url(url.to_string()))
}
#[cfg(feature = "env")]
#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
#[inline(always)]
#[allow(clippy::result_large_err)]
fn _env<E: EnvProvider>(&mut self, env: &E, prefix: Option<&str>) -> Result<&mut Self, Error> {
let partial = T::Partial::from_env_with_provider_and_optional_prefix(env, prefix)?;
self._add(partial)
}
#[allow(unused)]
#[allow(clippy::result_large_err)]
fn _code(
&mut self,
code: &str,
format: Format,
location: LoadLocation,
) -> Result<&mut Self, Error> {
let partial = match format {
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
Format::Json => serde_json::from_str(code).map_err(|e| Error::Json {
location,
source: Arc::new(e),
})?,
#[cfg(feature = "jsonc")]
#[cfg_attr(docsrs, doc(cfg(feature = "jsonc")))]
Format::Jsonc => {
let reader = json_comments::StripComments::new(code.as_bytes());
serde_json::from_reader(reader).map_err(|e| Error::Json {
location,
source: Arc::new(e),
})?
}
#[cfg(feature = "toml")]
#[cfg_attr(docsrs, doc(cfg(feature = "toml")))]
Format::Toml => toml::from_str(code).map_err(|e| Error::Toml {
location,
source: e,
})?,
#[cfg(feature = "yaml")]
#[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
Format::Yaml => serde_yaml::from_str(code).map_err(|e| Error::Yaml {
location,
source: Arc::new(e),
})?,
};
self._add(partial)
}
#[inline(always)]
#[allow(clippy::result_large_err)]
pub fn defaults(&mut self) -> Result<&mut Self, Error> {
self._add(T::Partial::defaults())
}
#[inline(always)]
#[allow(clippy::result_large_err)]
pub fn partial(&mut self, partial: T::Partial) -> Result<&mut Self, Error> {
self._add(partial)
}
#[inline(always)]
#[allow(clippy::result_large_err)]
fn _add(&mut self, partial: T::Partial) -> Result<&mut Self, Error> {
self.partial.merge(partial)?;
Ok(self)
}
#[inline(always)]
#[allow(clippy::result_large_err)]
pub fn partial_state(&self) -> &T::Partial {
&self.partial
}
#[inline(always)]
#[allow(clippy::result_large_err)]
pub fn partial_state_mut(&mut self) -> &mut T::Partial {
&mut self.partial
}
#[inline(always)]
#[allow(clippy::result_large_err)]
pub fn finish(self) -> Result<T, Error> {
let v = T::from_partial(self.partial)?;
Ok(v)
}
}
impl<T: Config> Default for ConfigLoader<T> {
fn default() -> Self {
Self::new()
}
}