use std::io;
use std::path::{Path, PathBuf};
use config::builder::DefaultState;
use config::{Config, ConfigBuilder};
use path_absolutize::Absolutize;
use crate::provenance::{SettingSource, SourceMap, SourceMetadata};
use crate::scope::ConfigScope;
use crate::{Environment, SettingsError};
use config::Source;
#[derive(Debug)]
struct LayerResolution {
metadata: SourceMetadata,
config_source: Box<dyn Source + Send + Sync>,
provenance_source: Box<dyn Source + Send + Sync>,
}
type LayerResult = Result<LayerResolution, SettingsError>;
type ConfigFile = config::File<config::FileSourceFile, config::FileFormat>;
#[derive(Debug, Clone)]
pub enum ConfigLayer {
Path(PathBuf),
EnvVar(String),
EnvSearch { env: Environment, dirs: Vec<PathBuf> },
Secrets(PathBuf),
EnvVars { prefix: String, separator: String },
ScopedPath {
path: PathBuf,
scope: crate::scope::ConfigScope,
},
}
impl ConfigLayer {
pub fn is_path(&self) -> bool {
matches!(self, ConfigLayer::Path(_))
}
pub fn is_env_var(&self) -> bool {
matches!(self, ConfigLayer::EnvVar(_))
}
pub fn is_env_search(&self) -> bool {
matches!(self, ConfigLayer::EnvSearch { .. })
}
pub fn is_secrets(&self) -> bool {
matches!(self, ConfigLayer::Secrets(_))
}
pub fn is_env_vars(&self) -> bool {
matches!(self, ConfigLayer::EnvVars { .. })
}
pub fn is_scoped_path(&self) -> bool {
matches!(self, ConfigLayer::ScopedPath { .. })
}
}
#[derive(Debug, Clone)]
pub struct LayerBuilder {
layers: Vec<ConfigLayer>,
}
impl LayerBuilder {
pub fn new() -> Self {
LayerBuilder { layers: Vec::new() }
}
pub fn with_path(mut self, path: impl AsRef<std::path::Path>) -> Self {
self.layers.push(ConfigLayer::Path(path.as_ref().to_path_buf()));
self
}
pub fn with_path_in_dir(mut self, dir: impl AsRef<std::path::Path>, basename: impl AsRef<str>) -> Self {
let dir = dir.as_ref();
let basename = basename.as_ref();
let extensions = ["yaml", "yml", "toml", "json", "ron", "hjson", "json5"];
for ext in &extensions {
let path = dir.join(format!("{}.{}", basename, ext));
if path.exists() {
self.layers.push(ConfigLayer::Path(path));
return self;
}
}
let attempted_path = dir.join(format!("{}.yaml", basename));
self.layers.push(ConfigLayer::Path(attempted_path));
self
}
pub fn with_env_var(mut self, var_name: &str) -> Self {
self.layers.push(ConfigLayer::EnvVar(var_name.to_string()));
self
}
pub fn with_env_search(mut self, env: Environment, dirs: impl IntoIterator<Item = PathBuf>) -> Self {
self.layers
.push(ConfigLayer::EnvSearch { env, dirs: dirs.into_iter().collect() });
self
}
pub fn with_secrets(mut self, path: impl AsRef<std::path::Path>) -> Self {
self.layers.push(ConfigLayer::Secrets(path.as_ref().to_path_buf()));
self
}
pub fn with_env_vars(mut self, prefix: &str, separator: &str) -> Self {
self.layers.push(ConfigLayer::EnvVars {
prefix: prefix.to_string(),
separator: separator.to_string(),
});
self
}
pub fn with_scopes<T: crate::MultiScopeConfig>(
mut self, scopes: impl IntoIterator<Item = crate::ConfigScope>,
) -> Self {
for scope in scopes {
if let Some(path) = T::resolve_path(scope) {
self.layers.push(ConfigLayer::ScopedPath { path, scope });
}
}
self
}
pub fn layers(&self) -> &[ConfigLayer] {
&self.layers
}
pub fn layer_count(&self) -> usize {
self.layers.len()
}
pub fn is_empty(&self) -> bool {
self.layers.is_empty()
}
pub fn has_layers(&self) -> bool {
!self.is_empty()
}
pub fn has_path_layer(&self) -> bool {
self.layers.iter().any(|l| matches!(l, ConfigLayer::Path(_)))
}
pub fn has_env_var_layer(&self, name: &str) -> bool {
self.layers
.iter()
.any(|l| matches!(l, ConfigLayer::EnvVar(ref var_name) if var_name == name))
}
pub fn has_secrets_layer(&self) -> bool {
self.layers.iter().any(|l| matches!(l, ConfigLayer::Secrets(_)))
}
pub fn has_env_vars_layer(&self, prefix: &str, sep: &str) -> bool {
self.layers.iter().any(|l| {
matches!(l, ConfigLayer::EnvVars { prefix: ref p, separator: ref s }
if p == prefix && s == sep)
})
}
pub fn build(mut self) -> Result<ConfigBuilder<DefaultState>, SettingsError> {
let mut config = Config::builder();
let layers = std::mem::take(&mut self.layers);
for (idx, layer) in layers.into_iter().enumerate() {
let resolution = self.resolve_layer_sources(&layer, idx)?;
config = config.add_source(vec![resolution.config_source]);
}
Ok(config)
}
pub fn build_with_provenance(mut self) -> Result<(ConfigBuilder<DefaultState>, SourceMap), SettingsError> {
let mut config = Config::builder();
let mut source_map = SourceMap::new();
let layers = std::mem::take(&mut self.layers);
for (idx, layer) in layers.into_iter().enumerate() {
let resolution = self.resolve_layer_sources(&layer, idx)?;
config = config.add_source(vec![resolution.config_source]);
source_map.insert_layer(resolution.metadata, resolution.provenance_source.collect()?);
}
Ok((config, source_map))
}
fn resolve_layer_sources(&self, layer: &ConfigLayer, idx: usize) -> LayerResult {
match layer {
ConfigLayer::Path(path) | ConfigLayer::ScopedPath { path, .. } => {
let scope = if let ConfigLayer::ScopedPath { scope, .. } = layer {
Some(*scope)
} else {
None
};
self.resolve_file_layer(path, scope, idx)
},
ConfigLayer::EnvVar(var_name) => self.resolve_env_var_layer(var_name, idx),
ConfigLayer::EnvSearch { env, dirs } => self.resolve_env_search_layer(env, dirs, idx),
ConfigLayer::Secrets(path) => self.resolve_secrets_layer(path, idx),
ConfigLayer::EnvVars { prefix, separator } => self.resolve_env_vars_layer(prefix, separator, idx),
}
}
fn resolve_file_layer(&self, path: &Path, scope: Option<ConfigScope>, idx: usize) -> LayerResult {
let abs_path = path.absolutize()?;
if !abs_path.exists() {
return Err(SettingsError::from(io::Error::new(
io::ErrorKind::NotFound,
format!("config file not found: {}", abs_path.display()),
)));
}
let meta = SourceMetadata::file(abs_path.to_path_buf(), scope, idx);
let s1 = ConfigFile::from(abs_path.as_ref()).required(true);
let s2 = ConfigFile::from(abs_path.as_ref()).required(true);
Ok(LayerResolution {
metadata: meta,
config_source: Box::new(s1),
provenance_source: Box::new(s2),
})
}
fn resolve_env_var_layer(&self, var_name: &str, idx: usize) -> LayerResult {
let meta = SourceMetadata::env(var_name.to_string(), idx);
match std::env::var(var_name) {
Ok(env_path) => {
let abs_path = Path::new(&env_path).absolutize()?;
if !abs_path.exists() {
return Err(SettingsError::from(io::Error::new(
io::ErrorKind::NotFound,
format!(
"config file from env var {} not found: {}",
var_name,
abs_path.display()
),
)));
}
let s1 = ConfigFile::from(abs_path.as_ref()).required(true);
let s2 = ConfigFile::from(abs_path.as_ref()).required(true);
Ok(LayerResolution {
metadata: meta,
config_source: Box::new(s1),
provenance_source: Box::new(s2),
})
},
Err(_) => {
let s1 = config::File::from_str("{}", config::FileFormat::Json);
let s2 = config::File::from_str("{}", config::FileFormat::Json);
Ok(LayerResolution {
metadata: meta,
config_source: Box::new(s1),
provenance_source: Box::new(s2),
})
},
}
}
fn resolve_env_search_layer(&self, env: &Environment, dirs: &[PathBuf], idx: usize) -> LayerResult {
let env_name = env.as_ref();
let extensions = ["toml", "yaml", "yml", "json", "ron", "hjson", "json5"];
for dir in dirs {
for ext in &extensions {
let path = dir.join(format!("{}.{}", env_name, ext));
if path.exists() {
let abs_path = path.absolutize()?;
let meta = SourceMetadata::file(abs_path.to_path_buf(), None, idx);
let s1 = ConfigFile::from(abs_path.as_ref()).required(true);
let s2 = ConfigFile::from(abs_path.as_ref()).required(true);
return Ok(LayerResolution {
metadata: meta,
config_source: Box::new(s1),
provenance_source: Box::new(s2),
});
}
}
}
let meta = SourceMetadata {
source: SettingSource::Override { name: format!("env_search:{}", env) },
layer_index: idx,
};
let s1 = config::File::from_str("{}", config::FileFormat::Json);
let s2 = config::File::from_str("{}", config::FileFormat::Json);
Ok(LayerResolution {
metadata: meta,
config_source: Box::new(s1),
provenance_source: Box::new(s2),
})
}
fn resolve_secrets_layer(&self, path: &Path, idx: usize) -> LayerResult {
let abs_path = path.absolutize()?;
if !abs_path.exists() {
return Err(SettingsError::from(io::Error::new(
io::ErrorKind::NotFound,
format!("secrets file not found: {}", abs_path.display()),
)));
}
let meta = SourceMetadata {
source: SettingSource::Secrets { path: abs_path.to_path_buf() },
layer_index: idx,
};
let s1 = ConfigFile::from(abs_path.as_ref()).required(true);
let s2 = ConfigFile::from(abs_path.as_ref()).required(true);
Ok(LayerResolution {
metadata: meta,
config_source: Box::new(s1),
provenance_source: Box::new(s2),
})
}
fn resolve_env_vars_layer(&self, prefix: &str, separator: &str, idx: usize) -> LayerResult {
let meta = SourceMetadata {
source: SettingSource::EnvVars { prefix: prefix.to_string() },
layer_index: idx,
};
let s1 = config::Environment::default()
.prefix(prefix)
.separator(separator)
.try_parsing(true);
let s2 = config::Environment::default()
.prefix(prefix)
.separator(separator)
.try_parsing(true);
Ok(LayerResolution {
metadata: meta,
config_source: Box::new(s1),
provenance_source: Box::new(s2),
})
}
}
impl Default for LayerBuilder {
fn default() -> Self {
Self::new()
}
}