use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fmt::{self, Display, Formatter};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use crate::ConfigError;
use crate::report::{canonicalize_path_with_aliases, normalize_path, path_matches_pattern};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ConfigMetadata {
fields: Vec<FieldMetadata>,
checks: Vec<ValidationCheck>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct MetadataMatchScore {
segment_count: usize,
specificity: usize,
positional_specificity: Vec<bool>,
}
impl ConfigMetadata {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_fields<I>(fields: I) -> Self
where
I: IntoIterator<Item = FieldMetadata>,
{
let mut metadata = Self::default();
metadata.extend_fields(fields);
metadata
}
#[must_use]
pub fn fields(&self) -> &[FieldMetadata] {
&self.fields
}
#[must_use]
pub fn checks(&self) -> &[ValidationCheck] {
&self.checks
}
#[must_use]
pub fn field(&self, path: &str) -> Option<&FieldMetadata> {
let normalized = normalize_path(path);
let mut best = None::<(MetadataMatchScore, &FieldMetadata)>;
for field in &self.fields {
for candidate in
std::iter::once(field.path.as_str()).chain(field.aliases.iter().map(String::as_str))
{
let Some(score) = metadata_match_score(&normalized, candidate) else {
continue;
};
match &mut best {
Some((best_score, best_field)) if score > *best_score => {
*best_score = score;
*best_field = field;
}
None => best = Some((score, field)),
_ => {}
}
}
}
best.map(|(_, field)| field)
}
#[must_use]
pub fn fields_by_path(&self) -> BTreeMap<String, FieldMetadata> {
self.fields
.iter()
.cloned()
.map(|field| (field.path.clone(), field))
.collect()
}
pub fn push(&mut self, field: FieldMetadata) {
self.fields.push(field);
self.normalize();
}
pub fn extend_fields<I>(&mut self, fields: I)
where
I: IntoIterator<Item = FieldMetadata>,
{
self.fields.extend(fields);
self.normalize();
}
pub fn extend(&mut self, other: Self) {
self.fields.extend(other.fields);
self.checks.extend(other.checks);
self.normalize();
}
pub fn push_check(&mut self, check: ValidationCheck) {
self.checks.push(check);
self.normalize();
}
pub fn extend_checks<I>(&mut self, checks: I)
where
I: IntoIterator<Item = ValidationCheck>,
{
self.checks.extend(checks);
self.normalize();
}
#[must_use]
pub fn check(mut self, check: ValidationCheck) -> Self {
self.push_check(check);
self
}
#[must_use]
pub fn at_least_one_of<I, S>(self, paths: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.check(ValidationCheck::AtLeastOneOf {
paths: paths.into_iter().map(Into::into).collect(),
})
}
#[must_use]
pub fn exactly_one_of<I, S>(self, paths: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.check(ValidationCheck::ExactlyOneOf {
paths: paths.into_iter().map(Into::into).collect(),
})
}
#[must_use]
pub fn mutually_exclusive<I, S>(self, paths: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.check(ValidationCheck::MutuallyExclusive {
paths: paths.into_iter().map(Into::into).collect(),
})
}
#[must_use]
pub fn required_with<I, S>(self, path: impl Into<String>, requires: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.check(ValidationCheck::RequiredWith {
path: path.into(),
requires: requires.into_iter().map(Into::into).collect(),
})
}
#[must_use]
pub fn required_if<I, S, V>(self, path: impl Into<String>, equals: V, requires: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
V: Into<ValidationValue>,
{
self.check(ValidationCheck::RequiredIf {
path: path.into(),
equals: equals.into(),
requires: requires.into_iter().map(Into::into).collect(),
})
}
#[must_use]
pub fn secret_paths(&self) -> Vec<String> {
self.fields
.iter()
.filter(|field| field.secret)
.map(|field| field.path.clone())
.collect()
}
pub fn env_overrides(&self) -> Result<BTreeMap<String, String>, ConfigError> {
let mut envs = BTreeMap::new();
for field in &self.fields {
let Some(env) = &field.env else {
continue;
};
if field.path.split('.').any(|segment| segment == "*") {
return Err(ConfigError::MetadataInvalid {
path: field.path.clone(),
message: "explicit environment variable names cannot target wildcard paths"
.to_owned(),
});
}
if let Some(first_path) = envs.insert(env.clone(), field.path.clone())
&& first_path != field.path
{
return Err(ConfigError::MetadataConflict {
kind: "environment variable",
name: env.clone(),
first_path,
second_path: field.path.clone(),
});
}
}
Ok(envs)
}
pub fn alias_overrides(&self) -> Result<BTreeMap<String, String>, ConfigError> {
let mut aliases = BTreeMap::<String, String>::new();
let canonical_paths = self
.fields
.iter()
.map(|field| field.path.clone())
.collect::<BTreeSet<_>>();
for field in &self.fields {
for alias in &field.aliases {
if !alias_mapping_is_lossless(alias, &field.path) {
return Err(ConfigError::MetadataInvalid {
path: alias.clone(),
message: format!(
"alias `{alias}` must preserve wildcard positions and cannot be deeper than canonical path `{}`",
field.path
),
});
}
if canonical_paths.contains(alias) && alias != &field.path {
return Err(ConfigError::MetadataConflict {
kind: "alias",
name: alias.clone(),
first_path: alias.clone(),
second_path: field.path.clone(),
});
}
if let Some(first_path) = aliases.get(alias)
&& first_path != &field.path
{
return Err(ConfigError::MetadataConflict {
kind: "alias",
name: alias.clone(),
first_path: first_path.clone(),
second_path: field.path.clone(),
});
}
if let Some((other_alias, sample_path)) =
aliases.iter().find_map(|(other_alias, other_canonical)| {
alias_patterns_are_ambiguous(
alias,
&field.path,
other_alias,
other_canonical,
)
.then(|| {
(
other_alias.clone(),
alias_overlap_sample_path(alias, other_alias),
)
})
})
{
return Err(ConfigError::MetadataInvalid {
path: alias.clone(),
message: format!(
"alias `{alias}` overlaps ambiguously with `{other_alias}` for concrete path `{sample_path}`"
),
});
}
aliases.insert(alias.clone(), field.path.clone());
}
}
Ok(aliases)
}
#[must_use]
pub fn merge_strategies(&self) -> BTreeMap<String, MergeStrategy> {
self.fields
.iter()
.map(|field| (field.path.clone(), field.merge))
.collect()
}
#[must_use]
pub fn merge_strategy_for(&self, path: &str) -> Option<MergeStrategy> {
let normalized = normalize_path(path);
if normalized.is_empty() {
return None;
}
let mut best = None::<(MetadataMatchScore, MergeStrategy)>;
for field in &self.fields {
for candidate in
std::iter::once(field.path.as_str()).chain(field.aliases.iter().map(String::as_str))
{
let Some(score) = metadata_match_score(&normalized, candidate) else {
continue;
};
match &mut best {
Some((best_score, best_merge)) if score > *best_score => {
*best_score = score;
*best_merge = field.merge;
}
None => best = Some((score, field.merge)),
_ => {}
}
}
}
best.map(|(_, merge)| merge)
}
pub(crate) fn canonicalize_env_decoder_paths(&mut self) -> Result<(), ConfigError> {
let alias_source_fields = self
.fields
.iter()
.filter(|field| !field.is_env_decoder_only())
.cloned()
.collect::<Vec<_>>();
let aliases = ConfigMetadata {
fields: alias_source_fields,
checks: Vec::new(),
}
.alias_overrides()?;
let mut seen = BTreeMap::<String, (String, EnvDecoder)>::new();
for field in &mut self.fields {
if !field.is_env_decoder_only() {
continue;
}
let original_path = field.path.clone();
let canonical = canonicalize_path_with_aliases(&original_path, &aliases);
let decoder = field
.env_decode
.expect("decoder-only fields must have a decoder");
if let Some((first_path, first_decoder)) = seen.get(&canonical)
&& (first_path != &original_path || *first_decoder != decoder)
{
return Err(ConfigError::MetadataConflict {
kind: "environment decoder",
name: canonical,
first_path: first_path.clone(),
second_path: original_path,
});
}
seen.insert(canonical.clone(), (original_path, decoder));
field.path = canonical;
}
self.normalize();
Ok(())
}
fn normalize(&mut self) {
let mut merged = BTreeMap::<String, FieldMetadata>::new();
for mut field in self.fields.drain(..) {
field.path = normalize_path(&field.path);
field.aliases = field
.aliases
.into_iter()
.map(|alias| normalize_path(&alias))
.filter(|alias| !alias.is_empty() && alias != &field.path)
.collect();
field.aliases.sort();
field.aliases.dedup();
match merged.get_mut(&field.path) {
Some(existing) => existing.merge_from(field),
None => {
merged.insert(field.path.clone(), field);
}
}
}
self.fields = merged.into_values().collect();
self.checks = normalize_checks(self.checks.drain(..));
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FieldMetadata {
pub path: String,
pub aliases: Vec<String>,
pub secret: bool,
pub env: Option<String>,
pub env_decode: Option<EnvDecoder>,
pub doc: Option<String>,
pub example: Option<String>,
pub deprecated: Option<String>,
pub has_default: bool,
pub merge: MergeStrategy,
pub validations: Vec<ValidationRule>,
}
impl FieldMetadata {
#[must_use]
pub fn new(path: impl Into<String>) -> Self {
Self {
path: normalize_path(&path.into()),
aliases: Vec::new(),
secret: false,
env: None,
env_decode: None,
doc: None,
example: None,
deprecated: None,
has_default: false,
merge: MergeStrategy::Merge,
validations: Vec::new(),
}
}
#[must_use]
pub fn alias(mut self, alias: impl Into<String>) -> Self {
self.aliases.push(alias.into());
self
}
#[must_use]
pub fn secret(mut self) -> Self {
self.secret = true;
self
}
#[must_use]
pub fn env(mut self, env: impl Into<String>) -> Self {
self.env = Some(env.into());
self
}
#[must_use]
pub fn env_decoder(mut self, decoder: EnvDecoder) -> Self {
self.env_decode = Some(decoder);
self
}
#[must_use]
pub fn doc(mut self, doc: impl Into<String>) -> Self {
self.doc = Some(doc.into());
self
}
#[must_use]
pub fn example(mut self, example: impl Into<String>) -> Self {
self.example = Some(example.into());
self
}
#[must_use]
pub fn deprecated(mut self, note: impl Into<String>) -> Self {
self.deprecated = Some(note.into());
self
}
#[must_use]
pub fn defaulted(mut self) -> Self {
self.has_default = true;
self
}
#[must_use]
pub fn merge_strategy(mut self, merge: MergeStrategy) -> Self {
self.merge = merge;
self
}
#[must_use]
pub fn validate(mut self, rule: ValidationRule) -> Self {
self.validations.push(rule);
self
}
#[must_use]
pub fn non_empty(self) -> Self {
self.validate(ValidationRule::NonEmpty)
}
#[must_use]
pub fn min(self, min: impl Into<ValidationNumber>) -> Self {
self.validate(ValidationRule::Min(min.into()))
}
#[must_use]
pub fn max(self, max: impl Into<ValidationNumber>) -> Self {
self.validate(ValidationRule::Max(max.into()))
}
#[must_use]
pub fn min_length(self, min: usize) -> Self {
self.validate(ValidationRule::MinLength(min))
}
#[must_use]
pub fn max_length(self, max: usize) -> Self {
self.validate(ValidationRule::MaxLength(max))
}
#[must_use]
pub fn one_of<I, V>(self, values: I) -> Self
where
I: IntoIterator<Item = V>,
V: Into<ValidationValue>,
{
self.validate(ValidationRule::OneOf(
values.into_iter().map(Into::into).collect(),
))
}
#[must_use]
pub fn hostname(self) -> Self {
self.validate(ValidationRule::Hostname)
}
#[must_use]
pub fn ip_addr(self) -> Self {
self.validate(ValidationRule::IpAddr)
}
#[must_use]
pub fn socket_addr(self) -> Self {
self.validate(ValidationRule::SocketAddr)
}
#[must_use]
pub fn absolute_path(self) -> Self {
self.validate(ValidationRule::AbsolutePath)
}
fn merge_from(&mut self, other: Self) {
self.aliases.extend(other.aliases);
self.aliases.sort();
self.aliases.dedup();
self.secret |= other.secret;
if let Some(env) = other.env {
self.env = Some(env);
}
if let Some(env_decode) = other.env_decode {
self.env_decode = Some(env_decode);
}
if let Some(doc) = other.doc {
self.doc = Some(doc);
}
if let Some(example) = other.example {
self.example = Some(example);
}
if let Some(deprecated) = other.deprecated {
self.deprecated = Some(deprecated);
}
self.has_default |= other.has_default;
if other.merge != MergeStrategy::Merge || self.merge == MergeStrategy::Merge {
self.merge = other.merge;
}
for rule in other.validations {
if !self.validations.contains(&rule) {
self.validations.push(rule);
}
}
}
fn is_env_decoder_only(&self) -> bool {
self.env_decode.is_some()
&& self.aliases.is_empty()
&& !self.secret
&& self.env.is_none()
&& self.doc.is_none()
&& self.example.is_none()
&& self.deprecated.is_none()
&& !self.has_default
&& self.merge == MergeStrategy::Merge
&& self.validations.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum EnvDecoder {
Csv,
PathList,
KeyValueMap,
Whitespace,
}
impl Display for EnvDecoder {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Csv => write!(f, "csv"),
Self::PathList => write!(f, "path_list"),
Self::KeyValueMap => write!(f, "key_value_map"),
Self::Whitespace => write!(f, "whitespace"),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum MergeStrategy {
#[default]
Merge,
Replace,
Append,
}
impl Display for MergeStrategy {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Merge => write!(f, "merge"),
Self::Replace => write!(f, "replace"),
Self::Append => write!(f, "append"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum ValidationNumber {
Finite(serde_json::Number),
Invalid(String),
}
impl ValidationNumber {
#[must_use]
pub fn as_f64(&self) -> Option<f64> {
match self {
Self::Finite(number) => number.as_f64(),
Self::Invalid(_) => None,
}
}
#[must_use]
pub fn is_finite(&self) -> bool {
matches!(self, Self::Finite(_))
}
#[must_use]
pub fn as_json_value(&self) -> serde_json::Value {
match self {
Self::Finite(number) => serde_json::Value::Number(number.clone()),
Self::Invalid(value) => serde_json::Value::String(value.clone()),
}
}
}
impl Display for ValidationNumber {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Finite(number) => Display::fmt(number, f),
Self::Invalid(value) => Display::fmt(value, f),
}
}
}
impl From<i8> for ValidationNumber {
fn from(value: i8) -> Self {
Self::Finite(serde_json::Number::from(value))
}
}
impl From<i16> for ValidationNumber {
fn from(value: i16) -> Self {
Self::Finite(serde_json::Number::from(value))
}
}
impl From<i32> for ValidationNumber {
fn from(value: i32) -> Self {
Self::Finite(serde_json::Number::from(value))
}
}
impl From<i64> for ValidationNumber {
fn from(value: i64) -> Self {
Self::Finite(serde_json::Number::from(value))
}
}
impl From<isize> for ValidationNumber {
fn from(value: isize) -> Self {
Self::Finite(serde_json::Number::from(value as i64))
}
}
impl From<u8> for ValidationNumber {
fn from(value: u8) -> Self {
Self::Finite(serde_json::Number::from(value))
}
}
impl From<u16> for ValidationNumber {
fn from(value: u16) -> Self {
Self::Finite(serde_json::Number::from(value))
}
}
impl From<u32> for ValidationNumber {
fn from(value: u32) -> Self {
Self::Finite(serde_json::Number::from(value))
}
}
impl From<u64> for ValidationNumber {
fn from(value: u64) -> Self {
Self::Finite(serde_json::Number::from(value))
}
}
impl From<usize> for ValidationNumber {
fn from(value: usize) -> Self {
Self::Finite(serde_json::Number::from(value as u64))
}
}
impl From<f32> for ValidationNumber {
fn from(value: f32) -> Self {
match serde_json::Number::from_f64(value as f64) {
Some(number) => Self::Finite(number),
None => Self::Invalid(value.to_string()),
}
}
}
impl From<f64> for ValidationNumber {
fn from(value: f64) -> Self {
match serde_json::Number::from_f64(value) {
Some(number) => Self::Finite(number),
None => Self::Invalid(value.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct ValidationValue(pub serde_json::Value);
impl Display for ValidationValue {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match &self.0 {
serde_json::Value::String(value) => write!(f, "{value:?}"),
value => Display::fmt(value, f),
}
}
}
impl From<bool> for ValidationValue {
fn from(value: bool) -> Self {
Self(serde_json::Value::Bool(value))
}
}
impl From<String> for ValidationValue {
fn from(value: String) -> Self {
Self(serde_json::Value::String(value))
}
}
impl From<&str> for ValidationValue {
fn from(value: &str) -> Self {
Self(serde_json::Value::String(value.to_owned()))
}
}
impl From<f32> for ValidationValue {
fn from(value: f32) -> Self {
match serde_json::Number::from_f64(value as f64) {
Some(number) => Self(serde_json::Value::Number(number)),
None => Self(serde_json::Value::String(value.to_string())),
}
}
}
impl From<f64> for ValidationValue {
fn from(value: f64) -> Self {
match serde_json::Number::from_f64(value) {
Some(number) => Self(serde_json::Value::Number(number)),
None => Self(serde_json::Value::String(value.to_string())),
}
}
}
macro_rules! impl_validation_value_from_number {
($($ty:ty),* $(,)?) => {
$(
impl From<$ty> for ValidationValue {
fn from(value: $ty) -> Self {
Self(serde_json::to_value(value).expect("validation values must serialize"))
}
}
)*
};
}
impl_validation_value_from_number!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ValidationRule {
NonEmpty,
Min(ValidationNumber),
Max(ValidationNumber),
MinLength(usize),
MaxLength(usize),
OneOf(Vec<ValidationValue>),
Hostname,
IpAddr,
SocketAddr,
AbsolutePath,
}
impl ValidationRule {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::NonEmpty => "non_empty",
Self::Min(_) => "min",
Self::Max(_) => "max",
Self::MinLength(_) => "min_length",
Self::MaxLength(_) => "max_length",
Self::OneOf(_) => "one_of",
Self::Hostname => "hostname",
Self::IpAddr => "ip_addr",
Self::SocketAddr => "socket_addr",
Self::AbsolutePath => "absolute_path",
}
}
}
impl Display for ValidationRule {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::NonEmpty => write!(f, "non_empty"),
Self::Min(value) => write!(f, "min={value}"),
Self::Max(value) => write!(f, "max={value}"),
Self::MinLength(value) => write!(f, "min_length={value}"),
Self::MaxLength(value) => write!(f, "max_length={value}"),
Self::OneOf(values) => write!(
f,
"one_of=[{}]",
values
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
),
Self::Hostname => write!(f, "hostname"),
Self::IpAddr => write!(f, "ip_addr"),
Self::SocketAddr => write!(f, "socket_addr"),
Self::AbsolutePath => write!(f, "absolute_path"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ValidationCheck {
AtLeastOneOf { paths: Vec<String> },
ExactlyOneOf { paths: Vec<String> },
MutuallyExclusive { paths: Vec<String> },
RequiredWith { path: String, requires: Vec<String> },
RequiredIf {
path: String,
equals: ValidationValue,
requires: Vec<String>,
},
}
impl ValidationCheck {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::AtLeastOneOf { .. } => "at_least_one_of",
Self::ExactlyOneOf { .. } => "exactly_one_of",
Self::MutuallyExclusive { .. } => "mutually_exclusive",
Self::RequiredWith { .. } => "required_with",
Self::RequiredIf { .. } => "required_if",
}
}
fn normalize(self) -> Option<Self> {
match self {
Self::AtLeastOneOf { paths } => {
normalize_path_group(paths).map(|paths| Self::AtLeastOneOf { paths })
}
Self::ExactlyOneOf { paths } => {
normalize_path_group(paths).map(|paths| Self::ExactlyOneOf { paths })
}
Self::MutuallyExclusive { paths } => {
normalize_path_group(paths).map(|paths| Self::MutuallyExclusive { paths })
}
Self::RequiredWith { path, requires } => {
let path = normalize_path(&path);
let requires = normalize_path_group(requires)?;
(!path.is_empty()).then_some(Self::RequiredWith { path, requires })
}
Self::RequiredIf {
path,
equals,
requires,
} => {
let path = normalize_path(&path);
let requires = normalize_path_group(requires)?;
(!path.is_empty()).then_some(Self::RequiredIf {
path,
equals,
requires,
})
}
}
}
fn prefixed(self, prefix: &str) -> Option<Self> {
let prefix = normalize_path(prefix);
if prefix.is_empty() {
return self.normalize();
}
let join = |path: String| {
if path.is_empty() {
prefix.clone()
} else {
format!("{prefix}.{path}")
}
};
match self {
Self::AtLeastOneOf { paths } => Some(Self::AtLeastOneOf {
paths: paths.into_iter().map(join).collect(),
})
.and_then(Self::normalize),
Self::ExactlyOneOf { paths } => Some(Self::ExactlyOneOf {
paths: paths.into_iter().map(join).collect(),
})
.and_then(Self::normalize),
Self::MutuallyExclusive { paths } => Some(Self::MutuallyExclusive {
paths: paths.into_iter().map(join).collect(),
})
.and_then(Self::normalize),
Self::RequiredWith { path, requires } => Some(Self::RequiredWith {
path: join(path),
requires: requires.into_iter().map(join).collect(),
})
.and_then(Self::normalize),
Self::RequiredIf {
path,
equals,
requires,
} => Some(Self::RequiredIf {
path: join(path),
equals,
requires: requires.into_iter().map(join).collect(),
})
.and_then(Self::normalize),
}
}
}
impl Display for ValidationCheck {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::AtLeastOneOf { paths } => {
write!(f, "at_least_one_of({})", paths.join(", "))
}
Self::ExactlyOneOf { paths } => {
write!(f, "exactly_one_of({})", paths.join(", "))
}
Self::MutuallyExclusive { paths } => {
write!(f, "mutually_exclusive({})", paths.join(", "))
}
Self::RequiredWith { path, requires } => {
write!(f, "required_with({path} -> {})", requires.join(", "))
}
Self::RequiredIf {
path,
equals,
requires,
} => write!(
f,
"required_if({path} == {equals} -> {})",
requires.join(", ")
),
}
}
}
pub trait TierMetadata {
#[must_use]
fn metadata() -> ConfigMetadata {
ConfigMetadata::default()
}
#[must_use]
fn secret_paths() -> Vec<String> {
Self::metadata().secret_paths()
}
}
impl<T> TierMetadata for super::Secret<T> {
fn metadata() -> ConfigMetadata {
ConfigMetadata::from_fields([FieldMetadata::new("").secret()])
}
}
impl TierMetadata for String {}
impl TierMetadata for bool {}
impl TierMetadata for char {}
impl TierMetadata for u8 {}
impl TierMetadata for u16 {}
impl TierMetadata for u32 {}
impl TierMetadata for u64 {}
impl TierMetadata for u128 {}
impl TierMetadata for usize {}
impl TierMetadata for i8 {}
impl TierMetadata for i16 {}
impl TierMetadata for i32 {}
impl TierMetadata for i64 {}
impl TierMetadata for i128 {}
impl TierMetadata for isize {}
impl TierMetadata for f32 {}
impl TierMetadata for f64 {}
impl TierMetadata for Duration {}
impl TierMetadata for SystemTime {}
impl TierMetadata for PathBuf {}
impl TierMetadata for IpAddr {}
impl TierMetadata for Ipv4Addr {}
impl TierMetadata for Ipv6Addr {}
impl TierMetadata for SocketAddr {}
impl TierMetadata for SocketAddrV4 {}
impl TierMetadata for SocketAddrV6 {}
impl<T> TierMetadata for Option<T>
where
T: TierMetadata,
{
fn metadata() -> ConfigMetadata {
T::metadata()
}
}
impl<T> TierMetadata for Vec<T>
where
T: TierMetadata,
{
fn metadata() -> ConfigMetadata {
prefixed_metadata("*", Vec::new(), T::metadata())
}
}
impl<T, const N: usize> TierMetadata for [T; N]
where
T: TierMetadata,
{
fn metadata() -> ConfigMetadata {
prefixed_metadata("*", Vec::new(), T::metadata())
}
}
impl<T> TierMetadata for BTreeSet<T>
where
T: TierMetadata,
{
fn metadata() -> ConfigMetadata {
prefixed_metadata("*", Vec::new(), T::metadata())
}
}
impl<T> TierMetadata for HashSet<T>
where
T: TierMetadata,
{
fn metadata() -> ConfigMetadata {
prefixed_metadata("*", Vec::new(), T::metadata())
}
}
impl<K, V> TierMetadata for BTreeMap<K, V>
where
V: TierMetadata,
{
fn metadata() -> ConfigMetadata {
prefixed_metadata("*", Vec::new(), V::metadata())
}
}
impl<K, V, S> TierMetadata for HashMap<K, V, S>
where
V: TierMetadata,
{
fn metadata() -> ConfigMetadata {
prefixed_metadata("*", Vec::new(), V::metadata())
}
}
impl<T> TierMetadata for Box<T>
where
T: TierMetadata,
{
fn metadata() -> ConfigMetadata {
T::metadata()
}
}
impl<T> TierMetadata for Arc<T>
where
T: TierMetadata,
{
fn metadata() -> ConfigMetadata {
T::metadata()
}
}
#[must_use]
pub fn prefixed_metadata(
prefix: &str,
prefix_aliases: Vec<String>,
metadata: ConfigMetadata,
) -> ConfigMetadata {
let prefix = normalize_path(prefix);
if prefix.is_empty() {
return metadata;
}
let mut prefixed = ConfigMetadata::from_fields(metadata.fields.into_iter().map(|field| {
let canonical_suffix = field.path.clone();
let alias_suffixes = if field.aliases.is_empty() {
vec![canonical_suffix.clone()]
} else {
let mut suffixes = vec![canonical_suffix.clone()];
suffixes.extend(field.aliases.iter().cloned());
suffixes
};
let path = if canonical_suffix.is_empty() {
prefix.clone()
} else {
format!("{prefix}.{}", canonical_suffix)
};
let mut aliases = field
.aliases
.into_iter()
.map(|alias| {
if alias.is_empty() {
prefix.clone()
} else {
format!("{prefix}.{}", alias)
}
})
.collect::<Vec<_>>();
for prefix_alias in &prefix_aliases {
if canonical_suffix.is_empty() {
aliases.push(prefix_alias.clone());
continue;
}
for suffix in &alias_suffixes {
aliases.push(format!("{prefix_alias}.{suffix}"));
}
}
FieldMetadata {
path,
aliases,
..field
}
}));
prefixed.extend_checks(
metadata
.checks
.into_iter()
.filter_map(|check| check.prefixed(&prefix)),
);
prefixed
}
impl IntoIterator for ConfigMetadata {
type Item = FieldMetadata;
type IntoIter = std::vec::IntoIter<FieldMetadata>;
fn into_iter(self) -> Self::IntoIter {
self.fields.into_iter()
}
}
fn normalize_checks<I>(checks: I) -> Vec<ValidationCheck>
where
I: IntoIterator<Item = ValidationCheck>,
{
let mut normalized = Vec::new();
for check in checks {
let Some(check) = check.normalize() else {
continue;
};
if !normalized.contains(&check) {
normalized.push(check);
}
}
normalized
}
fn normalize_path_group<I>(paths: I) -> Option<Vec<String>>
where
I: IntoIterator<Item = String>,
{
let mut normalized = Vec::new();
for path in paths {
let path = normalize_path(&path);
if path.is_empty() || normalized.contains(&path) {
continue;
}
normalized.push(path);
}
(!normalized.is_empty()).then_some(normalized)
}
fn metadata_match_score(path: &str, candidate: &str) -> Option<MetadataMatchScore> {
if candidate != path && !path_matches_pattern(path, candidate) {
return None;
}
let segments = candidate
.split('.')
.filter(|segment| !segment.is_empty())
.collect::<Vec<_>>();
let positional_specificity = segments
.iter()
.map(|segment| *segment != "*")
.collect::<Vec<_>>();
let specificity = positional_specificity
.iter()
.filter(|segment| **segment)
.count();
Some(MetadataMatchScore {
segment_count: segments.len(),
specificity,
positional_specificity,
})
}
fn alias_mapping_is_lossless(alias: &str, canonical: &str) -> bool {
let alias_segments = path_segments(alias);
let canonical_segments = path_segments(canonical);
if canonical_segments.len() < alias_segments.len() {
return false;
}
for index in 0..alias_segments.len() {
let alias_wildcard = alias_segments[index] == "*";
let canonical_wildcard = canonical_segments[index] == "*";
if alias_wildcard != canonical_wildcard {
return false;
}
}
!canonical_segments[alias_segments.len()..].contains(&"*")
}
fn alias_patterns_are_ambiguous(
left_alias: &str,
left_canonical: &str,
right_alias: &str,
right_canonical: &str,
) -> bool {
if alias_rank(left_alias) != alias_rank(right_alias) {
return false;
}
let left_segments = path_segments(left_alias);
let right_segments = path_segments(right_alias);
if left_segments.len() != right_segments.len() {
return false;
}
if !left_segments
.iter()
.zip(right_segments.iter())
.all(|(left, right)| *left == "*" || *right == "*" || left == right)
{
return false;
}
let sample_path = alias_overlap_sample_path(left_alias, right_alias);
rewrite_alias_sample(&sample_path, left_alias, left_canonical)
!= rewrite_alias_sample(&sample_path, right_alias, right_canonical)
}
fn alias_rank(alias: &str) -> (usize, usize) {
let segments = path_segments(alias);
let specificity = segments.iter().filter(|segment| **segment != "*").count();
(segments.len(), specificity)
}
fn alias_overlap_sample_path(left: &str, right: &str) -> String {
path_segments(left)
.into_iter()
.zip(path_segments(right))
.map(|(left, right)| {
if left == "*" && right == "*" {
"item".to_owned()
} else if left == "*" {
right.to_owned()
} else {
left.to_owned()
}
})
.collect::<Vec<_>>()
.join(".")
}
fn rewrite_alias_sample(path: &str, alias: &str, canonical: &str) -> String {
let concrete_segments = path_segments(path);
let alias_segments = path_segments(alias);
let canonical_segments = path_segments(canonical);
let mut rewritten = canonical_segments
.iter()
.enumerate()
.map(|(index, segment)| {
if *segment == "*" && alias_segments.get(index) == Some(&"*") {
concrete_segments[index].to_owned()
} else {
(*segment).to_owned()
}
})
.collect::<Vec<_>>();
rewritten.extend(
concrete_segments[alias_segments.len()..]
.iter()
.map(|segment| (*segment).to_owned()),
);
normalize_path(&rewritten.join("."))
}
fn path_segments(path: &str) -> Vec<&str> {
path.split('.')
.filter(|segment| !segment.is_empty())
.collect()
}