#![doc = include_str!("../README.md")]
#![doc(test(no_crate_inject))]
pub use tanzim_load as loader;
pub use tanzim_merge as merge;
pub use tanzim_parse as parser;
pub use tanzim_source as source;
#[doc(inline)]
pub use tanzim_source::Source;
pub mod ext {
pub extern crate tanzim_load;
pub extern crate tanzim_merge;
pub extern crate tanzim_parse;
pub extern crate tanzim_source;
}
mod logging;
use cfg_if::cfg_if;
pub type Parsed = (loader::Payload, parser::LocatedValue);
pub type Merged = merge::Merged;
fn source_display(cs: &Source) -> String {
let mut s = cs.source().to_string();
if cs.skip_errors() {
s.push('?');
}
if cs.resource_colon() || !cs.resource().is_empty() {
s.push(':');
s.push_str(cs.resource());
}
s
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Source(source::ParseError),
#[error(transparent)]
Load(loader::Error),
#[error(transparent)]
Parse(tanzim_value::Error),
#[error(transparent)]
Merge(merge::Error),
#[error("no loader found for `{at}`")]
NoLoader { at: String },
#[error("no parser found for format `{format}` in `{at}`")]
NoParser { format: String, at: String },
}
pub struct ConfigBuilder {
sources: Vec<Source>,
loaders: Vec<Box<dyn loader::Load>>,
parsers: Vec<Box<dyn parser::Deserialize>>,
merger: Box<dyn merge::Merge>,
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self {
sources: Vec::new(),
loaders: Vec::new(),
parsers: Vec::new(),
merger: Box::new(merge::LastWins),
}
}
}
impl ConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_source<S>(mut self, source: S) -> Result<Self, Error>
where
S: TryInto<Source, Error = source::ParseError>,
{
match source.try_into() {
Ok(src) => {
self.sources.push(src);
Ok(self)
}
Err(e) => Err(Error::Source(e)),
}
}
pub fn with_loader(mut self, loader: impl loader::Load + 'static) -> Self {
self.loaders.push(Box::new(loader));
self
}
pub fn with_parser(mut self, parser: impl parser::Deserialize + 'static) -> Self {
self.parsers.push(Box::new(parser));
self
}
pub fn with_merger(mut self, merger: impl merge::Merge + 'static) -> Self {
self.merger = Box::new(merger);
self
}
pub fn build(self) -> Config {
Config {
sources: self.sources,
loaders: self.loaders,
parsers: self.parsers,
merger: self.merger,
}
}
}
pub struct Config {
sources: Vec<Source>,
loaders: Vec<Box<dyn loader::Load>>,
parsers: Vec<Box<dyn parser::Deserialize>>,
merger: Box<dyn merge::Merge>,
}
impl Config {
pub fn sources(&self) -> &[Source] {
&self.sources
}
pub fn sources_mut(&mut self) -> &mut Vec<Source> {
&mut self.sources
}
pub fn loaders(&self) -> &[Box<dyn loader::Load>] {
&self.loaders
}
pub fn loaders_mut(&mut self) -> &mut Vec<Box<dyn loader::Load>> {
&mut self.loaders
}
pub fn parsers(&self) -> &[Box<dyn parser::Deserialize>] {
&self.parsers
}
pub fn parsers_mut(&mut self) -> &mut Vec<Box<dyn parser::Deserialize>> {
&mut self.parsers
}
pub fn merger(&self) -> &dyn merge::Merge {
&*self.merger
}
pub fn merger_mut(&mut self) -> &mut Box<dyn merge::Merge> {
&mut self.merger
}
pub fn with_source<S>(mut self, source: S) -> Result<Self, Error>
where
S: TryInto<Source, Error = source::ParseError>,
{
match source.try_into() {
Ok(src) => {
self.sources.push(src);
Ok(self)
}
Err(e) => Err(Error::Source(e)),
}
}
pub fn with_loader(mut self, loader: impl loader::Load + 'static) -> Self {
self.loaders.push(Box::new(loader));
self
}
pub fn with_parser(mut self, parser: impl parser::Deserialize + 'static) -> Self {
self.parsers.push(Box::new(parser));
self
}
pub fn with_merger(mut self, merger: impl merge::Merge + 'static) -> Self {
self.merger = Box::new(merger);
self
}
pub fn load(&self) -> Result<Vec<loader::Payload>, Error> {
let mut result = Vec::new();
for config_source in &self.sources {
let source_name = config_source.source();
cfg_if! {
if #[cfg(feature = "tracing")] {
tracing::debug!(msg = "Loading configuration source", source = source_name, resource = config_source.resource());
} else if #[cfg(feature = "logging")] {
log::debug!("msg=\"Loading configuration source\" source={source_name} resource={}", config_source.resource());
}
}
let mut found_loader = None;
for loader in &self.loaders {
let supported = loader.supported_source_list();
let mut matches = false;
for s in &supported {
if s.as_str() == source_name {
matches = true;
break;
}
}
if matches {
found_loader = Some(loader);
break;
}
}
let loader = match found_loader {
Some(l) => l,
None => {
return Err(Error::NoLoader {
at: source_display(config_source),
});
}
};
cfg_if! {
if #[cfg(feature = "tracing")] {
tracing::trace!(msg = "Found loader for configuration source", loader = loader.name(), source = source_name);
} else if #[cfg(feature = "logging")] {
log::trace!("msg=\"Found loader for configuration source\" loader={} source={source_name}", loader.name());
}
}
let payloads = match loader.load(config_source.clone()) {
Ok(payloads) => payloads,
Err(e) => {
if config_source.skip_errors() {
cfg_if! {
if #[cfg(feature = "tracing")] {
tracing::warn!(msg = "Skipped load error for source", source = source_display(config_source), error = ?e);
} else if #[cfg(feature = "logging")] {
let display = source_display(config_source);
log::warn!("msg=\"Skipped load error for source\" source={display} error={e:?}");
}
}
continue;
}
return Err(Error::Load(e));
}
};
for payload in payloads {
result.push(payload.normalize());
}
}
cfg_if! {
if #[cfg(feature = "tracing")] {
tracing::info!(msg = "Configuration load stage complete", payload_count = result.len());
} else if #[cfg(feature = "logging")] {
log::info!("msg=\"Configuration load stage complete\" payload_count={}", result.len());
}
}
Ok(result)
}
pub fn parse(&self, loaded: &[loader::Payload]) -> Result<Vec<Parsed>, Error> {
let mut result = Vec::new();
for payload in loaded {
let config_source = &payload.source;
let resource = match (&payload.name, &payload.format) {
(Some(name), Some(format)) => format!("{name}.{format}"),
_ => {
let r = config_source.resource();
if r.is_empty() {
config_source.to_string()
} else {
r.to_string()
}
}
};
let source_name = config_source.source();
cfg_if! {
if #[cfg(feature = "tracing")] {
tracing::debug!(msg = "Parsing configuration payload", source = source_name, resource = resource, format = payload.format.as_deref().unwrap_or("auto"));
} else if #[cfg(feature = "logging")] {
let fmt = payload.format.as_deref().unwrap_or("auto");
log::debug!("msg=\"Parsing configuration payload\" source={source_name} resource={resource} format={fmt}");
}
}
let mut found_parser = None;
if let Some(format) = &payload.format {
for parser in &self.parsers {
let supported = parser.supported_format_list();
let mut matches = false;
for s in &supported {
if s.as_str() == format.as_str() {
matches = true;
break;
}
}
if matches {
found_parser = Some(parser);
break;
}
}
}
if found_parser.is_none() {
for parser in &self.parsers {
if let Some(true) = parser.is_format_supported(&payload.content) {
found_parser = Some(parser);
break;
}
}
}
let parser = match found_parser {
Some(p) => p,
None => {
return Err(Error::NoParser {
format: payload.format.as_deref().unwrap_or("unknown").to_string(),
at: source_display(config_source),
});
}
};
cfg_if! {
if #[cfg(feature = "tracing")] {
tracing::trace!(msg = "Found parser for configuration payload", parser = parser.name(), resource = resource);
} else if #[cfg(feature = "logging")] {
log::trace!("msg=\"Found parser for configuration payload\" parser={} resource={resource}", parser.name());
}
}
let value = match parser.parse(source_name, &resource, &payload.content) {
Ok(v) => v,
Err(e) => {
if config_source.skip_errors() {
cfg_if! {
if #[cfg(feature = "tracing")] {
tracing::warn!(msg = "Skipped parse error for payload", source = source_display(config_source), resource = resource, error = ?e);
} else if #[cfg(feature = "logging")] {
let display = source_display(config_source);
log::warn!("msg=\"Skipped parse error for payload\" source={display} resource={resource} error={e:?}");
}
}
continue;
}
return Err(Error::Parse(e));
}
};
result.push((payload.clone(), value));
}
cfg_if! {
if #[cfg(feature = "tracing")] {
tracing::info!(msg = "Configuration parse stage complete", parsed_count = result.len());
} else if #[cfg(feature = "logging")] {
log::info!("msg=\"Configuration parse stage complete\" parsed_count={}", result.len());
}
}
Ok(result)
}
pub fn merge(&self, parsed: &[Parsed]) -> Result<Merged, Error> {
cfg_if! {
if #[cfg(feature = "tracing")] {
tracing::debug!(msg = "Starting configuration merge stage", entry_count = parsed.len());
} else if #[cfg(feature = "logging")] {
log::debug!("msg=\"Starting configuration merge stage\" entry_count={}", parsed.len());
}
}
match self.merger.merge(parsed) {
Ok(r) => {
cfg_if! {
if #[cfg(feature = "tracing")] {
tracing::info!(msg = "Configuration merge stage complete", group_count = r.len());
} else if #[cfg(feature = "logging")] {
log::info!("msg=\"Configuration merge stage complete\" group_count={}", r.len());
}
}
Ok(r)
}
Err(e) => Err(Error::Merge(e)),
}
}
pub fn run(&self) -> Result<Merged, Error> {
cfg_if! {
if #[cfg(feature = "tracing")] {
tracing::debug!(msg = "Running configuration pipeline", source_count = self.sources.len(), loader_count = self.loaders.len(), parser_count = self.parsers.len());
} else if #[cfg(feature = "logging")] {
log::debug!("msg=\"Running configuration pipeline\" source_count={} loader_count={} parser_count={}", self.sources.len(), self.loaders.len(), self.parsers.len());
}
}
let loaded = self.load()?;
let parsed = self.parse(&loaded)?;
self.merge(&parsed)
}
}