use crate::config::cacher::{BoxedCacher, Cacher, MemoryCache};
use crate::config::errors::ConfigError;
use crate::config::format::Format;
use crate::config::layer::Layer;
use crate::config::source::Source;
use crate::config::{Config, ExtendsFrom, PartialConfig};
use serde::Serialize;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tracing::trace;
#[derive(Serialize)]
pub struct ConfigLoadResult<T: Config> {
pub config: T,
pub layers: Vec<Layer<T>>,
}
pub struct ConfigLoader<T: Config> {
_config: PhantomData<T>,
cacher: Mutex<BoxedCacher>,
help: Option<String>,
sources: Vec<Source>,
root: Option<PathBuf>,
}
impl<T: Config> ConfigLoader<T> {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
ConfigLoader {
_config: PhantomData,
cacher: Mutex::new(Box::<MemoryCache>::default()),
help: None,
sources: vec![],
root: None,
}
}
pub fn code<S: TryInto<String>>(
&mut self,
code: S,
format: Format,
) -> Result<&mut Self, ConfigError> {
self.sources.push(Source::code(code, format)?);
Ok(self)
}
pub fn file<S: TryInto<PathBuf>>(&mut self, path: S) -> Result<&mut Self, ConfigError> {
self.sources.push(Source::file(path, true)?);
Ok(self)
}
pub fn file_optional<S: TryInto<PathBuf>>(
&mut self,
path: S,
) -> Result<&mut Self, ConfigError> {
self.sources.push(Source::file(path, false)?);
Ok(self)
}
#[cfg(feature = "url")]
pub fn url<S: TryInto<String>>(&mut self, url: S) -> Result<&mut Self, ConfigError> {
self.sources.push(Source::url(url)?);
Ok(self)
}
pub fn load(&self) -> Result<ConfigLoadResult<T>, ConfigError> {
let context = <T::Partial as PartialConfig>::Context::default();
self.load_with_context(&context)
}
pub fn load_with_context(
&self,
context: &<T::Partial as PartialConfig>::Context,
) -> Result<ConfigLoadResult<T>, ConfigError> {
trace!(config = T::META.name, "Loading configuration");
let layers = self.parse_into_layers(&self.sources, context)?;
let partial = self.merge_layers(&layers, context)?.finalize(context)?;
partial
.validate(context, true)
.map_err(|error| ConfigError::Validator {
config: match layers.last() {
Some(last) => self.get_location(&last.source).to_owned(),
None => T::META.name.to_owned(),
},
error,
help: self.help.clone(),
})?;
Ok(ConfigLoadResult {
config: T::from_partial(partial),
layers,
})
}
pub fn load_partial(
&self,
context: &<T::Partial as PartialConfig>::Context,
) -> Result<T::Partial, ConfigError> {
trace!(config = T::META.name, "Loading partial configuration");
let layers = self.parse_into_layers(&self.sources, context)?;
let partial = self.merge_layers(&layers, context)?;
Ok(partial)
}
pub fn set_cacher(&mut self, cacher: impl Cacher + 'static) -> &mut Self {
self.cacher = Mutex::new(Box::new(cacher));
self
}
pub fn set_help<H: AsRef<str>>(&mut self, help: H) -> &mut Self {
self.help = Some(help.as_ref().to_owned());
self
}
pub fn set_root<P: AsRef<Path>>(&mut self, root: P) -> &mut Self {
self.root = Some(root.as_ref().to_path_buf());
self
}
fn extend_additional_layers(
&self,
context: &<T::Partial as PartialConfig>::Context,
parent_source: &Source,
extends_from: &ExtendsFrom,
) -> Result<Vec<Layer<T>>, ConfigError> {
let mut sources = vec![];
let mut extend_source = |value: &str| {
let source = Source::new(value, Some(parent_source))?;
if matches!(source, Source::Code { .. }) {
return Err(ConfigError::ExtendsFromNoCode);
}
trace!(
config = T::META.name,
source = source.as_str(),
"Extending additional source"
);
sources.push(source);
Ok(())
};
match extends_from {
ExtendsFrom::String(value) => {
extend_source(value)?;
}
ExtendsFrom::List(values) => {
for value in values.iter() {
extend_source(value)?;
}
}
};
self.parse_into_layers(&sources, context)
}
fn get_location<'l>(&self, source: &'l Source) -> &'l str {
match source {
Source::Code { .. } => T::META.name,
Source::File { path, .. } => {
let rel_path = if let Some(root) = &self.root {
if let Ok(other_path) = path.strip_prefix(root) {
other_path
} else {
path
}
} else {
path
};
rel_path.to_str().unwrap_or(T::META.name)
}
Source::Url { url, .. } => url,
}
}
fn merge_layers(
&self,
layers: &[Layer<T>],
context: &<T::Partial as PartialConfig>::Context,
) -> Result<T::Partial, ConfigError> {
trace!(
config = T::META.name,
"Merging partial layers into a final result"
);
let mut merged = T::Partial::default();
for layer in layers {
merged.merge(context, layer.partial.clone())?;
}
Ok(merged)
}
fn parse_into_layers(
&self,
sources_to_parse: &[Source],
context: &<T::Partial as PartialConfig>::Context,
) -> Result<Vec<Layer<T>>, ConfigError> {
let mut layers: Vec<Layer<T>> = vec![];
for source in sources_to_parse {
trace!(
config = T::META.name,
source = source.as_str(),
"Creating layer from source"
);
let location = self.get_location(source);
let partial: T::Partial = {
let mut cacher = self.cacher.lock().unwrap();
source.parse(location, &mut cacher, self.help.as_deref())?
};
partial
.validate(context, false)
.map_err(|error| ConfigError::Validator {
config: location.to_owned(),
error,
help: self.help.clone(),
})?;
if let Some(extends_from) = partial.extends_from() {
layers.extend(self.extend_additional_layers(context, source, &extends_from)?);
}
layers.push(Layer {
partial,
source: source.clone(),
});
}
Ok(layers)
}
}