use std::{
any,
collections::{BTreeMap, HashSet},
iter,
marker::PhantomData,
sync::Arc,
};
pub use self::{env::Environment, json::Json, yaml::Yaml};
use crate::{
DescribeConfig, DeserializeConfig, DeserializeConfigError, ParseError, ParseErrors,
de::{DeserializeContext, DeserializerOptions},
fallback::Fallbacks,
metadata::{BasicTypes, ConfigTag, ConfigVariant, TypeSuffixes},
schema::{ConfigData, ConfigRef, ConfigSchema},
utils::{EnumVariant, JsonObject, merge_json},
value::{Map, Pointer, Value, ValueOrigin, WithOrigin},
visit::Serializer,
};
#[macro_use]
mod macros;
mod env;
mod json;
#[cfg(test)]
mod tests;
mod yaml;
pub trait ConfigSourceKind: crate::utils::Sealed {
#[doc(hidden)] const IS_FLAT: bool;
}
#[derive(Debug)]
pub struct Hierarchical(());
impl crate::utils::Sealed for Hierarchical {}
impl ConfigSourceKind for Hierarchical {
const IS_FLAT: bool = false;
}
#[derive(Debug)]
pub struct Flat(());
impl crate::utils::Sealed for Flat {}
impl ConfigSourceKind for Flat {
const IS_FLAT: bool = true;
}
pub trait ConfigSource {
type Kind: ConfigSourceKind;
fn into_contents(self) -> WithOrigin<Map>;
}
#[derive(Debug, Clone)]
pub struct Prefixed<T> {
inner: T,
prefix: String,
}
impl<T: ConfigSource<Kind = Hierarchical>> Prefixed<T> {
pub fn new(inner: T, prefix: impl Into<String>) -> Self {
Self {
inner,
prefix: prefix.into(),
}
}
}
impl<T: ConfigSource<Kind = Hierarchical>> ConfigSource for Prefixed<T> {
type Kind = Hierarchical;
fn into_contents(self) -> WithOrigin<Map> {
let contents = self.inner.into_contents();
let origin = Arc::new(ValueOrigin::Synthetic {
source: contents.origin.clone(),
transform: format!("prefixed with `{}`", self.prefix),
});
if let Some((parent, key_in_parent)) = Pointer(&self.prefix).split_last() {
let mut root = WithOrigin::new(Value::Object(Map::new()), origin.clone());
root.ensure_object(parent, |_| origin.clone())
.insert(key_in_parent.to_owned(), contents.map(Value::Object));
root.map(|value| match value {
Value::Object(map) => map,
_ => unreachable!(), })
} else {
contents
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ConfigSources {
inner: Vec<(WithOrigin<Map>, bool)>,
}
impl ConfigSources {
pub fn push<S: ConfigSource>(&mut self, source: S) {
self.inner
.push((source.into_contents(), <S::Kind>::IS_FLAT));
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SourceInfo {
pub origin: Arc<ValueOrigin>,
pub param_count: usize,
}
#[derive(Debug, Clone, Default)]
pub struct SerializerOptions {
pub(crate) diff_with_default: bool,
pub(crate) secret_placeholder: Option<String>,
pub(crate) flat: bool,
}
impl SerializerOptions {
pub fn diff_with_default() -> Self {
Self {
diff_with_default: true,
secret_placeholder: None,
flat: false,
}
}
#[must_use]
pub fn flat(mut self, flat: bool) -> Self {
self.flat = flat;
self
}
#[must_use]
pub fn with_secret_placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.secret_placeholder = Some(placeholder.into());
self
}
pub fn serialize<C: DescribeConfig>(self, config: &C) -> JsonObject {
let mut visitor = Serializer::new(&C::DESCRIPTION, "", self);
config.visit_config(&mut visitor);
visitor.into_inner()
}
}
#[derive(Debug, Clone)]
pub struct ConfigRepository<'a> {
schema: &'a ConfigSchema,
prefixes_for_canonical_configs: HashSet<Pointer<'a>>,
de_options: DeserializerOptions,
sources: Vec<SourceInfo>,
merged: WithOrigin,
}
impl<'a> ConfigRepository<'a> {
pub fn new(schema: &'a ConfigSchema) -> Self {
let prefixes_for_canonical_configs: HashSet<_> = schema
.iter_ll()
.flat_map(|(path, _)| path.with_ancestors())
.chain([Pointer("")])
.collect();
let this = Self {
schema,
prefixes_for_canonical_configs,
de_options: DeserializerOptions::default(),
sources: vec![],
merged: WithOrigin {
inner: Value::Object(Map::default()),
origin: Arc::default(),
},
};
if let Some(fallbacks) = Fallbacks::new(schema) {
this.with(fallbacks)
} else {
this
}
}
pub fn schema(&self) -> &'a ConfigSchema {
self.schema
}
pub fn deserializer_options(&mut self) -> &mut DeserializerOptions {
&mut self.de_options
}
#[must_use]
pub fn with<S: ConfigSource>(mut self, source: S) -> Self {
self.insert_inner(source.into_contents(), <S::Kind>::IS_FLAT);
self
}
#[tracing::instrument(
level = "debug",
name = "ConfigRepository::insert",
skip(self, contents)
)]
fn insert_inner(&mut self, contents: WithOrigin<Map>, is_flat: bool) {
let mut source_value = if is_flat {
WithOrigin::nest_kvs(contents.inner, self.schema, &contents.origin)
} else {
WithOrigin {
inner: Value::Object(contents.inner),
origin: contents.origin.clone(),
}
};
let param_count =
source_value.preprocess_source(self.schema, &self.prefixes_for_canonical_configs);
tracing::debug!(param_count, "Inserted source into config repo");
self.merged
.guided_merge(source_value, self.schema, Pointer(""));
self.sources.push(SourceInfo {
origin: contents.origin,
param_count,
});
}
#[must_use]
pub fn with_all(mut self, sources: ConfigSources) -> Self {
for (contents, is_flat) in sources.inner {
self.insert_inner(contents, is_flat);
}
self
}
pub fn sources(&self) -> &[SourceInfo] {
&self.sources
}
#[doc(hidden)] pub fn merged(&self) -> &WithOrigin {
&self.merged
}
#[doc(hidden)] pub fn canonicalize(&self, options: &SerializerOptions) -> Result<JsonObject, ParseErrors> {
let mut json = serde_json::Map::new();
for config_parser in self.iter() {
if !config_parser.config().is_top_level() {
continue;
}
let parsed = match config_parser.parse_opt() {
Ok(Some(config)) => config,
Ok(None) => continue,
Err(err) => return Err(err),
};
let metadata = config_parser.config().metadata();
let prefix = config_parser.config().prefix();
let mut visitor = Serializer::new(metadata, prefix, options.clone());
(metadata.visitor)(parsed.as_ref(), &mut visitor);
let serialized = visitor.into_inner();
if options.flat {
json.extend(serialized);
} else {
merge_json(&mut json, metadata, prefix, serialized);
}
}
Ok(json)
}
pub fn iter(&self) -> impl Iterator<Item = ConfigParser<'_, ()>> + '_ {
self.schema.iter().map(|config_ref| ConfigParser {
repo: self,
config_ref,
_config: PhantomData,
})
}
pub fn single<C: DeserializeConfig>(&self) -> anyhow::Result<ConfigParser<'_, C>> {
let config_ref = self.schema.single(&C::DESCRIPTION)?;
Ok(ConfigParser {
repo: self,
config_ref,
_config: PhantomData,
})
}
pub fn get<'s, C: DeserializeConfig>(&'s self, prefix: &'s str) -> Option<ConfigParser<'s, C>> {
let config_ref = self.schema.get(&C::DESCRIPTION, prefix)?;
Some(ConfigParser {
repo: self,
config_ref,
_config: PhantomData,
})
}
}
#[derive(Debug)]
pub struct ConfigParser<'a, C> {
repo: &'a ConfigRepository<'a>,
config_ref: ConfigRef<'a>,
_config: PhantomData<C>,
}
impl ConfigParser<'_, ()> {
#[doc(hidden)] #[allow(clippy::redundant_closure_for_method_calls)] pub fn parse(&self) -> Result<Box<dyn any::Any>, ParseErrors> {
self.with_context(|ctx| ctx.deserialize_any_config())
}
#[doc(hidden)] #[allow(clippy::redundant_closure_for_method_calls)] pub fn parse_opt(&self) -> Result<Option<Box<dyn any::Any>>, ParseErrors> {
self.with_context(|ctx| ctx.deserialize_any_config_opt())
}
}
impl<'a, C> ConfigParser<'a, C> {
pub fn config(&self) -> ConfigRef<'a> {
self.config_ref
}
fn with_context<R>(
&self,
action: impl FnOnce(DeserializeContext<'_>) -> Result<R, DeserializeConfigError>,
) -> Result<R, ParseErrors> {
let mut errors = ParseErrors::default();
let prefix = self.config_ref.prefix();
let metadata = self.config_ref.data.metadata;
let ctx = DeserializeContext::new(
&self.repo.de_options,
&self.repo.merged,
prefix.to_owned(),
metadata,
&mut errors,
);
action(ctx).map_err(|_| {
if errors.len() == 0 {
errors.push(ParseError::generic(prefix.to_owned(), metadata));
}
errors
})
}
}
impl<C: DeserializeConfig> ConfigParser<'_, C> {
#[allow(clippy::redundant_closure_for_method_calls)] pub fn parse(self) -> Result<C, ParseErrors> {
self.with_context(|ctx| ctx.deserialize_config::<C>())
}
#[allow(clippy::redundant_closure_for_method_calls)] pub fn parse_opt(self) -> Result<Option<C>, ParseErrors> {
self.with_context(|ctx| ctx.deserialize_config_opt::<C>())
}
}
impl WithOrigin {
fn preprocess_source(
&mut self,
schema: &ConfigSchema,
prefixes_for_canonical_configs: &HashSet<Pointer<'_>>,
) -> usize {
self.copy_aliased_values(schema);
self.mark_secrets(schema);
self.convert_serde_enums(schema);
self.nest_object_params_and_sub_configs(schema);
self.nest_array_params(schema);
self.collect_garbage(schema, prefixes_for_canonical_configs, Pointer(""))
}
#[tracing::instrument(level = "debug", skip_all)]
fn copy_aliased_values(&mut self, schema: &ConfigSchema) {
for (prefix, config_data) in schema.iter_ll() {
let (new_values, new_map_origin) = self.copy_aliases_for_config(config_data);
if new_values.is_empty() {
continue;
}
let new_map_origin = new_map_origin.map(|source| {
Arc::new(ValueOrigin::Synthetic {
source,
transform: format!("copy to '{prefix}' per aliasing rules"),
})
});
self.ensure_object(prefix, |_| new_map_origin.clone().unwrap())
.extend(new_values);
}
}
#[must_use = "returned map should be inserted into the config"]
fn copy_aliases_for_config(&self, config: &ConfigData) -> (Map, Option<Arc<ValueOrigin>>) {
let prefix = config.prefix();
let canonical_map = match self.get(prefix).map(|val| &val.inner) {
Some(Value::Object(map)) => Some(map),
Some(_) => {
tracing::warn!(
prefix = prefix.0,
config = ?config.metadata.ty,
"canonical config location contains a non-object"
);
return (Map::new(), None);
}
None => None,
};
let mut new_values = Map::new();
let mut new_map_origin = None;
for param in config.metadata.params {
let all_paths = config.all_paths_for_param(param);
for (path, alias_options) in all_paths {
let (prefix, name) = Pointer(&path)
.split_last()
.expect("param paths are never empty");
let Some(map) = self.get(prefix) else {
continue;
};
let map_origin = &map.origin;
let Some(map) = map.inner.as_object() else {
continue;
};
let matching_values: Vec<_> =
if let Some(suffixes) = param.type_description().suffixes() {
let matching_values = map.iter().filter_map(|(key, val)| {
let suffix = if key == name {
None } else {
let key_suffix = Self::strip_prefix(key, name)?;
if !suffixes.contains(key_suffix) {
return None;
}
Some(key_suffix)
};
Some((suffix, val))
});
matching_values.collect()
} else if let Some(val) = map.get(name) {
vec![(None, val)]
} else {
vec![]
};
for (suffix, val) in matching_values {
let canonical_key_string;
let canonical_key = if let Some(suffix) = suffix {
canonical_key_string = format!("{}_{suffix}", param.name);
&canonical_key_string
} else {
param.name
};
if canonical_map.is_some_and(|map| map.contains_key(canonical_key)) {
continue;
}
if !new_values.contains_key(canonical_key) {
if alias_options.is_deprecated {
tracing::warn!(
path,
origin = %val.origin,
config = ?config.metadata.ty,
param = param.rust_field_name,
canonical_path = prefix.join(canonical_key),
"using deprecated alias; please use canonical_path instead"
);
}
tracing::trace!(
prefix = prefix.0,
config = ?config.metadata.ty,
param = param.rust_field_name,
name,
origin = ?map_origin,
canonical_key,
"copied aliased param"
);
new_values.insert(canonical_key.to_owned(), val.clone());
if new_map_origin.is_none() {
new_map_origin = Some(map_origin.clone());
}
}
}
}
}
(new_values, new_map_origin)
}
fn strip_prefix<'s>(s: &'s str, prefix: &str) -> Option<&'s str> {
s.strip_prefix(prefix)?
.strip_prefix('_')
.filter(|suffix| !suffix.is_empty())
}
fn mark_secrets(&mut self, schema: &ConfigSchema) {
for (prefix, config_data) in schema.iter_ll() {
let Some(Self {
inner: Value::Object(config_object),
..
}) = self.get_mut(prefix)
else {
continue;
};
for param in config_data.metadata.params {
if !param.type_description().contains_secrets() {
continue;
}
let Some(value) = config_object.get_mut(param.name) else {
continue;
};
if let Value::String(str) = &mut value.inner {
tracing::trace!(
prefix = prefix.0,
config = ?config_data.metadata.ty,
param = param.rust_field_name,
"marked param as secret"
);
str.make_secret();
} else {
tracing::warn!(
prefix = prefix.0,
config = ?config_data.metadata.ty,
param = param.rust_field_name,
"param marked as secret has non-string value"
);
}
}
}
}
#[tracing::instrument(level = "debug", skip_all)]
fn convert_serde_enums(&mut self, schema: &ConfigSchema) {
for config_data in schema.iter() {
let config_meta = config_data.metadata();
let prefix = Pointer(config_data.prefix());
let Some(tag) = &config_meta.tag else {
continue; };
if !config_data.data.coerce_serde_enums {
continue;
}
let canonical_map = self.get(prefix).and_then(|val| val.inner.as_object());
let alias_maps = config_data
.aliases()
.filter_map(|(alias, _)| self.get(Pointer(alias))?.inner.as_object());
if canonical_map.is_some_and(|map| map.contains_key(tag.param.name)) {
continue;
}
let _span_guard = tracing::info_span!(
"convert_serde_enum",
config = ?config_meta.ty,
prefix = prefix.0,
tag = tag.param.name,
)
.entered();
if let Some((variant, variant_content)) =
Self::detect_serde_enum_variant(canonical_map, alias_maps, tag)
{
tracing::debug!(
variant = variant.name,
origin = %variant_content.origin,
"adding detected tag variant"
);
let origin = ValueOrigin::Synthetic {
source: variant_content.origin.clone(),
transform: "coercing serde enum".to_owned(),
};
let canonical_map = self.ensure_object(prefix, |_| {
Arc::new(ValueOrigin::Synthetic {
source: Arc::default(),
transform: "enum coercion".to_string(),
})
});
canonical_map.insert(
tag.param.name.to_owned(),
WithOrigin::new(variant.name.to_owned().into(), Arc::new(origin)),
);
}
}
}
fn detect_serde_enum_variant<'a>(
canonical_map: Option<&'a Map>,
alias_maps: impl Iterator<Item = &'a Map>,
tag: &'static ConfigTag,
) -> Option<(&'static ConfigVariant, &'a Self)> {
let all_variant_names = tag.variants.iter().flat_map(|variant| {
iter::once(variant.name)
.chain(variant.aliases.iter().copied())
.filter_map(move |name| Some((EnumVariant::new(name)?.to_snake_case(), variant)))
});
let mut variant_match = None;
for map in canonical_map.into_iter().chain(alias_maps) {
for (candidate_field_name, variant) in all_variant_names.clone() {
if map.contains_key(&candidate_field_name) {
if let Some((_, prev_field, _)) = &variant_match
&& *prev_field != candidate_field_name
{
tracing::info!(
prev_field,
field = candidate_field_name,
"multiple serde-like variant fields present"
);
return None;
}
variant_match = Some((map, candidate_field_name, variant));
}
}
}
let Some((map, field_name, variant)) = variant_match else {
return None; };
let variant_content = map.get(&field_name).unwrap();
if !matches!(&variant_content.inner, Value::Object(_)) {
tracing::info!(
field = field_name,
"variant contents is not an object, skipping"
);
return None;
}
Some((variant, variant_content))
}
fn collect_garbage(
&mut self,
schema: &ConfigSchema,
prefixes_for_canonical_configs: &HashSet<Pointer<'_>>,
at: Pointer<'_>,
) -> usize {
if schema.contains_canonical_param(at) {
1
} else if prefixes_for_canonical_configs.contains(&at) {
if let Value::Object(map) = &mut self.inner {
let mut count = 0;
map.retain(|key, value| {
let child_path = at.join(key);
let descendant_count = value.collect_garbage(
schema,
prefixes_for_canonical_configs,
Pointer(&child_path),
);
count += descendant_count;
descendant_count > 0
});
count
} else {
1
}
} else {
0
}
}
#[tracing::instrument(level = "debug", skip_all)]
fn nest_object_params_and_sub_configs(&mut self, schema: &ConfigSchema) {
for (prefix, config_data) in schema.iter_ll() {
let Some(config_object) = self.get_mut(prefix) else {
continue;
};
let config_origin = &config_object.origin;
let Value::Object(config_object) = &mut config_object.inner else {
continue;
};
let params_with_suffixes = config_data.metadata.params.iter().filter_map(|param| {
let suffixes = param.type_description().suffixes()?;
Some((param.name, suffixes))
});
let nested_configs = config_data
.metadata
.nested_configs
.iter()
.filter_map(|nested| {
(!nested.name.is_empty()).then_some((nested.name, TypeSuffixes::All))
});
let mut insertions = vec![];
for (child_name, suffixes) in params_with_suffixes.chain(nested_configs) {
let target_object = match config_object.get(child_name) {
None => None,
Some(WithOrigin {
inner: Value::Object(obj),
..
}) => Some(obj),
Some(_) => continue,
};
let matching_fields: Vec<_> = config_object
.iter()
.filter_map(|(name, field)| {
let suffix = Self::strip_prefix(name, child_name)?;
if !suffixes.contains(suffix) {
return None;
}
if let Some(param_object) = target_object
&& param_object.contains_key(suffix)
{
return None; }
Some((suffix.to_owned(), field.clone()))
})
.collect();
if matching_fields.is_empty() {
continue;
}
tracing::trace!(
prefix = prefix.0,
config = ?config_data.metadata.ty,
child_name,
fields = ?matching_fields.iter().map(|(name, _)| name).collect::<Vec<_>>(),
"nesting for object param / config"
);
insertions.push((child_name, matching_fields));
}
for (child_name, matching_fields) in insertions {
if !config_object.contains_key(child_name) {
let origin = Arc::new(ValueOrigin::Synthetic {
source: config_origin.clone(),
transform: format!("nesting for object param '{child_name}'"),
});
let val = Self::new(Value::Object(Map::new()), origin);
config_object.insert(child_name.to_owned(), val);
}
let Value::Object(target_object) =
&mut config_object.get_mut(child_name).unwrap().inner
else {
unreachable!(); };
target_object.extend(matching_fields);
}
}
}
#[tracing::instrument(level = "debug", skip_all)]
fn nest_array_params(&mut self, schema: &ConfigSchema) {
for (prefix, config_data) in schema.iter_ll() {
let Some(config_object) = self.get_mut(prefix) else {
continue;
};
let config_origin = &config_object.origin;
let Value::Object(config_object) = &mut config_object.inner else {
continue;
};
for param in config_data.metadata.params {
if !param.expecting.contains(BasicTypes::ARRAY)
|| param.expecting.contains(BasicTypes::OBJECT)
{
continue;
}
if config_object.contains_key(param.name) {
continue;
}
let matching_fields: BTreeMap<_, _> = config_object
.iter()
.filter_map(|(name, field)| {
let stripped_name = Self::strip_prefix(name, param.name)?;
let idx: usize = stripped_name.parse().ok()?;
Some((idx, field.clone()))
})
.collect();
let Some(&last_idx) = matching_fields.keys().next_back() else {
continue; };
if last_idx != matching_fields.len() - 1 {
tracing::info!(
prefix = prefix.0,
config = ?config_data.metadata.ty,
param = param.rust_field_name,
indexes = ?matching_fields.keys().copied().collect::<Vec<_>>(),
"indexes for array nesting are not sequential"
);
continue;
}
tracing::trace!(
prefix = prefix.0,
config = ?config_data.metadata.ty,
param = param.rust_field_name,
len = matching_fields.len(),
"nesting for array param"
);
let origin = Arc::new(ValueOrigin::Synthetic {
source: config_origin.clone(),
transform: format!("nesting for array param '{}'", param.name),
});
let array_items = matching_fields.into_values().collect();
let val = Self::new(Value::Array(array_items), origin);
config_object.insert(param.name.to_owned(), val);
}
}
}
#[tracing::instrument(level = "debug", skip_all)]
fn nest_kvs(kvs: Map, schema: &ConfigSchema, source_origin: &Arc<ValueOrigin>) -> Self {
let mut dest = Self {
inner: Value::Object(Map::new()),
origin: source_origin.clone(),
};
for (key, value) in kvs {
let mut key_prefix = key.as_str();
while !key_prefix.is_empty() {
for (param_path, expecting) in schema.params_with_kv_path(key_prefix) {
let should_copy = key_prefix == key || expecting.contains(BasicTypes::OBJECT);
if should_copy {
tracing::trace!(
param_path = param_path.0,
?expecting,
key,
key_prefix,
"copied key–value entry"
);
dest.copy_kv_entry(source_origin, param_path, &key, value.clone());
}
}
key_prefix = match key_prefix.rsplit_once('_') {
Some((prefix, _)) => prefix,
None => break,
};
}
let Some((key_prefix, maybe_idx)) = key.rsplit_once('_') else {
continue;
};
if !maybe_idx.bytes().all(|ch| ch.is_ascii_digit()) {
continue;
}
for (param_path, expecting) in schema.params_with_kv_path(key_prefix) {
if expecting.contains(BasicTypes::ARRAY) && !expecting.contains(BasicTypes::OBJECT)
{
dest.copy_kv_entry(source_origin, param_path, &key, value.clone());
}
}
}
dest
}
fn copy_kv_entry(
&mut self,
source_origin: &Arc<ValueOrigin>,
param_path: Pointer<'_>,
key: &str,
value: WithOrigin,
) {
let (parent, _) = param_path.split_last().unwrap();
let field_name_start = if parent.0.is_empty() {
parent.0.len()
} else {
parent.0.len() + 1 };
let field_name = key[field_name_start..].to_owned();
let origin = Arc::new(ValueOrigin::Synthetic {
source: source_origin.clone(),
transform: format!("nesting kv entries for '{param_path}'"),
});
self.ensure_object(parent, |_| origin.clone())
.insert(field_name, value);
}
fn guided_merge(&mut self, overrides: Self, schema: &ConfigSchema, current_path: Pointer<'_>) {
match (&mut self.inner, overrides.inner) {
(Value::Object(this), Value::Object(other))
if !schema.contains_canonical_param(current_path) =>
{
for (key, value) in other {
if let Some(existing_value) = this.get_mut(&key) {
let child_path = current_path.join(&key);
existing_value.guided_merge(value, schema, Pointer(&child_path));
} else {
this.insert(key, value);
}
}
}
(this, value) => {
*this = value;
self.origin = overrides.origin;
}
}
}
}