use figment::Metadata;
use crate::error::{Error, ErrorKind, Origin};
use crate::layer::{DEFAULTS_NAME, FLAGS_NAME, OVERRIDES_NAME};
use super::{CACHED_NAME, REMOTE_PREFIX};
const INLINE_SUFFIX: &str = "source string";
const ENV_SUFFIX: &str = "environment variable(s)";
fn message(error: &figment::Error) -> String {
use figment::error::Kind;
match &error.kind {
Kind::InvalidType(actual, expected) => {
format!(
"invalid type: found {}, expected {expected}",
kind_of(actual)
)
}
Kind::InvalidValue(actual, expected) => {
format!(
"invalid value: found {}, expected {expected}",
kind_of(actual)
)
}
_ => error.to_string(),
}
}
fn kind_of(actual: &figment::error::Actual) -> &'static str {
use figment::error::Actual;
match actual {
Actual::Bool(_) => "a boolean",
Actual::Unsigned(_) => "an unsigned integer",
Actual::Signed(_) => "a signed integer",
Actual::Float(_) => "a float",
Actual::Char(_) => "a character",
Actual::Str(_) => "a string",
Actual::Bytes(_) => "a byte string",
Actual::Unit => "a unit",
Actual::Option => "an option",
Actual::NewtypeStruct => "a newtype struct",
Actual::Seq => "a list",
Actual::Map => "a table",
Actual::Enum => "an enum",
Actual::UnitVariant => "a unit variant",
Actual::NewtypeVariant => "a newtype variant",
Actual::TupleVariant => "a tuple variant",
Actual::StructVariant => "a struct variant",
Actual::Other(_) => "something else",
}
}
pub(super) fn convert(error: figment::Error, spec: &crate::source::LoadSpec<'_>) -> Error {
use figment::error::Kind;
let kind = match &error.kind {
Kind::MissingField(_) => ErrorKind::Missing,
Kind::InvalidType(..)
| Kind::InvalidValue(..)
| Kind::InvalidLength(..)
| Kind::ISizeOutOfRange(_)
| Kind::USizeOutOfRange(_) => ErrorKind::Type,
Kind::Message(_) if error.path.is_empty() => ErrorKind::Parse,
Kind::Message(_) => ErrorKind::Type,
_ => ErrorKind::Backend,
};
let mut path = error.path.clone();
if path.is_empty() {
if let Kind::MissingField(field) = &error.kind {
path.push(field.to_string());
}
}
let origin = refine_env(
error.metadata.as_ref().map_or(Origin::Unknown, origin_of),
path.iter().map(String::as_str),
spec.nest,
);
let mut translated = Error::new(kind, message(&error)).with_origin(origin);
for segment in path.into_iter().rev() {
translated = translated.prepend_key(segment);
}
translated
}
pub(super) fn refine_env<'a>(
origin: Origin,
path: impl Iterator<Item = &'a str>,
nest: &str,
) -> Origin {
let Origin::Env(prefix) = &origin else {
return origin;
};
let Some(stem) = prefix.strip_suffix('*') else {
return origin;
};
let segments: Vec<String> = path.map(str::to_ascii_uppercase).collect();
if segments.is_empty() {
return origin;
}
let variable = format!("{stem}{}", segments.join(&nest.to_ascii_uppercase()));
if std::env::var_os(&variable).is_none() {
return origin;
}
Origin::Env(variable)
}
fn env_prefix(name: &str) -> String {
let prefix = name.trim_end_matches(ENV_SUFFIX).trim().trim_matches('`');
if prefix.is_empty() {
return "the environment".to_owned();
}
format!("{prefix}*")
}
pub(super) fn origin_of(metadata: &Metadata) -> Origin {
if metadata.name == DEFAULTS_NAME {
return Origin::Runtime("default");
}
if metadata.name == OVERRIDES_NAME {
return Origin::Runtime("override");
}
if metadata.name == FLAGS_NAME {
return Origin::Runtime("command-line flag");
}
if metadata.name == CACHED_NAME {
return Origin::Runtime("cached configuration");
}
if let Some(variable) = metadata.name.strip_prefix(crate::bindings::BINDING_PREFIX) {
return Origin::Env(variable.to_owned());
}
#[cfg(feature = "dotenv")]
if let Some(file) = metadata.name.strip_prefix(crate::dotenv::PREFIX) {
return Origin::File(std::path::PathBuf::from(file));
}
if let Some(store) = metadata.name.strip_prefix(REMOTE_PREFIX) {
return Origin::Remote(store.to_owned());
}
match metadata.source.as_ref() {
Some(figment::Source::File(path)) => Origin::File(path.clone()),
Some(figment::Source::Custom(name)) => Origin::Env(name.clone()),
Some(_) => Origin::Inline,
None if metadata.name.ends_with(ENV_SUFFIX) => Origin::Env(env_prefix(&metadata.name)),
None if metadata.name.ends_with(INLINE_SUFFIX) => Origin::Inline,
None => Origin::Unknown,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_environment_prefix_is_recovered_from_figments_name() {
assert_eq!(env_prefix("`APP_DB_` environment variable(s)"), "APP_DB_*");
assert_eq!(env_prefix("environment variable(s)"), "the environment");
}
}