use std::{ffi::OsString, fmt, marker::PhantomData, path::PathBuf};
#[cfg(feature = "toml")]
use std::{fs, path::Path};
#[cfg(feature = "toml")]
use toml_edit::de as toml;
#[cfg(feature = "toml")]
use crate::config::{__private, error::Source, toml::expand_toml};
use crate::config::{
dotenv::load_dotenv,
environment::{Environment as EnvironmentValues, EnvironmentContract, EnvironmentError},
error::{EnvironmentScope, Error as SourceError},
};
pub trait Config: Sized {
type Overrides: Default;
#[doc(hidden)]
type __Toml: serde::de::DeserializeOwned;
#[doc(hidden)]
type __CliArgs: crate::__private::CommandArgs;
#[doc(hidden)]
fn __parse_cli<I, T>(argv: I) -> Result<Self::Overrides, crate::Error>
where
I: IntoIterator<Item = T>,
T: Into<OsString>;
#[doc(hidden)]
fn __cli_overrides(args: Self::__CliArgs) -> Self::Overrides;
#[doc(hidden)]
fn __defaults() -> Self::Overrides;
#[doc(hidden)]
fn __merge(lower: &mut Self::Overrides, higher: Self::Overrides);
#[doc(hidden)]
fn __toml_overrides(input: Self::__Toml) -> Self::Overrides;
#[doc(hidden)]
#[cfg(feature = "toml")]
fn __toml(input: &str) -> Result<Self::Overrides, __private::TomlError> {
let input = toml::from_str::<Self::__Toml>(input)?;
Ok(Self::__toml_overrides(input))
}
#[doc(hidden)]
fn __environment_contract(inherited_prefix: Option<&str>) -> EnvironmentContract;
#[doc(hidden)]
fn __environment_with_prefix(
environment: &EnvironmentValues,
inherited_prefix: Option<&str>,
) -> Result<Self::Overrides, EnvironmentError>;
#[doc(hidden)]
fn __finalize(resolved: Self::Overrides) -> Result<Self, SourceError>;
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Layer {
Defaults,
#[cfg(feature = "toml")]
Toml(Toml),
Dotenv(Dotenv),
Environment,
Argv(Argv),
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Defaults;
#[cfg(feature = "toml")]
#[cfg_attr(docsrs, doc(cfg(feature = "toml")))]
#[derive(Clone, Debug)]
pub struct Toml {
path: PathBuf,
}
#[cfg(feature = "toml")]
impl Toml {
#[must_use]
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
}
#[derive(Clone, Debug)]
pub struct Dotenv {
path: PathBuf,
}
impl Dotenv {
#[must_use]
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Environment;
#[derive(Clone, Debug)]
pub struct Argv {
values: Vec<OsString>,
}
impl Argv {
#[must_use]
pub fn current() -> Self {
Self { values: std::env::args_os().collect() }
}
#[must_use]
pub fn new<I, T>(argv: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<OsString>,
{
Self { values: argv.into_iter().map(Into::into).collect() }
}
}
impl From<Defaults> for Layer {
fn from(_: Defaults) -> Self {
Self::Defaults
}
}
#[cfg(feature = "toml")]
impl From<Toml> for Layer {
fn from(layer: Toml) -> Self {
Self::Toml(layer)
}
}
impl From<Dotenv> for Layer {
fn from(layer: Dotenv) -> Self {
Self::Dotenv(layer)
}
}
impl From<Environment> for Layer {
fn from(_: Environment) -> Self {
Self::Environment
}
}
impl From<Argv> for Layer {
fn from(layer: Argv) -> Self {
Self::Argv(layer)
}
}
#[must_use = "a configuration loader has no effect until it is resolved"]
pub struct Loader<C: Config> {
layers: Vec<Layer>,
marker: PhantomData<fn() -> C>,
}
impl<C: Config> Default for Loader<C> {
fn default() -> Self {
Self { layers: Vec::new(), marker: PhantomData }
}
}
impl<C: Config> fmt::Debug for Loader<C> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("Loader").field("layers", &self.layers.len()).finish()
}
}
impl<C: Config> Loader<C> {
pub fn layer<L: Into<Layer>>(mut self, layer: L) -> Self {
self.layers.push(layer.into());
self
}
pub fn resolve(self) -> Result<C, super::Error> {
C::__environment_contract(None).validate().map_err(SourceError::environment_contract)?;
let mut state = C::Overrides::default();
let mut environment = EnvironmentValues::default();
for layer in self.layers {
match layer {
Layer::Defaults => {
C::__merge(&mut state, C::__defaults());
}
#[cfg(feature = "toml")]
Layer::Toml(layer) => {
merge_toml::<C>(&mut state, &layer.path, &environment)?;
}
Layer::Dotenv(layer) => {
let higher =
load_dotenv(&layer.path, &environment).map_err(SourceError::dotenv)?;
merge_environment::<C>(
&mut state,
&higher,
EnvironmentScope::File(layer.path),
)?;
environment.overlay(higher);
}
Layer::Environment => {
let higher = EnvironmentValues::process();
merge_environment::<C>(&mut state, &higher, EnvironmentScope::Process)?;
environment.overlay(higher);
}
Layer::Argv(layer) => {
let higher = C::__parse_cli(layer.values)?;
C::__merge(&mut state, higher);
}
}
}
C::__finalize(state).map_err(super::Error::from)
}
}
#[cfg(feature = "toml")]
fn merge_toml<C: Config>(
resolved: &mut C::Overrides,
path: &Path,
environment: &EnvironmentValues,
) -> Result<(), SourceError> {
let contents = fs::read_to_string(path)
.map_err(|source| SourceError::read_toml(Source::Toml, path, source))?;
let expansion = expand_toml(&contents, environment)
.map_err(|error| SourceError::interpolate_toml(Source::Toml, path, error))?;
let higher = C::__toml(&expansion.text).map_err(|error| {
SourceError::parse_toml(
Source::Toml,
path,
&contents,
&expansion.text,
error,
expansion.substituted,
)
})?;
C::__merge(resolved, higher);
Ok(())
}
fn merge_environment<C: Config>(
resolved: &mut C::Overrides,
environment: &EnvironmentValues,
scope: EnvironmentScope,
) -> Result<(), SourceError> {
let higher = C::__environment_with_prefix(environment, None)
.map_err(|source| SourceError::environment(scope, source))?;
C::__merge(resolved, higher);
Ok(())
}