mod value;
pub use value::FromEnvValue;
mod macros;
pub mod drift;
pub mod presets;
#[cfg(test)]
mod tests;
use std::fmt;
pub const PROFILE_VAR: &str = "APP_ENV";
pub const DEFAULT_PROFILE: &str = "development";
pub const EX_CONFIG: i32 = 78;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SettingsError {
pub errors: Vec<FieldError>,
}
impl fmt::Display for SettingsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let noun = if self.errors.len() == 1 { "problem" } else { "problems" };
write!(f, "invalid settings ({} {noun})", self.errors.len())?;
for error in &self.errors {
write!(f, "\n - {error}")?;
}
Ok(())
}
}
impl std::error::Error for SettingsError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FieldError {
pub var: String,
pub kind: FieldErrorKind,
}
impl fmt::Display for FieldError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
FieldErrorKind::Missing => write!(f, "{}: missing", self.var),
FieldErrorKind::MissingInProfile { profile } => write!(f, "{}: missing (required when {PROFILE_VAR}={profile})", self.var),
FieldErrorKind::Invalid { value: Some(value), message } => write!(f, "{}: invalid value {value:?}: {message}", self.var),
FieldErrorKind::Invalid { value: None, message } => write!(f, "{}: invalid value: {message}", self.var),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum FieldErrorKind {
Missing,
MissingInProfile { profile: String },
Invalid { value: Option<String>, message: String },
}
pub fn profile(source: &mut impl FnMut(&str) -> Option<String>) -> String {
lookup(source, PROFILE_VAR).unwrap_or_else(|| DEFAULT_PROFILE.to_string())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn or_exit<T>(loaded: Result<T, SettingsError>) -> T {
match loaded {
Ok(settings) => settings,
Err(error) => {
eprintln!("{error}");
std::process::exit(EX_CONFIG);
}
}
}
pub fn env_name(prefix: Option<&str>, explicit: Option<&str>, field: &str) -> String {
match explicit {
Some(name) => name.to_string(),
None => {
let shouty = field.strip_prefix("r#").unwrap_or(field).to_ascii_uppercase();
match prefix {
Some(prefix) => format!("{prefix}_{shouty}"),
None => shouty,
}
}
}
}
pub fn lookup(source: &mut impl FnMut(&str) -> Option<String>, var: &str) -> Option<String> {
source(var).filter(|value| !value.is_empty())
}
pub fn require<T: FromEnvValue>(raw: Option<String>, var: &str, secret: bool, errors: &mut Vec<FieldError>) -> Option<T> {
match raw {
None => {
errors.push(FieldError {
var: var.to_string(),
kind: FieldErrorKind::Missing,
});
None
}
Some(raw) => parse_value(&raw, var, secret, errors),
}
}
pub fn optional<T: FromEnvValue>(raw: Option<String>, var: &str, secret: bool, errors: &mut Vec<FieldError>) -> Option<Option<T>> {
match raw {
None => Some(None),
Some(raw) => parse_value(&raw, var, secret, errors).map(Some),
}
}
pub fn optional_required_in<T: FromEnvValue>(raw: Option<String>, var: &str, secret: bool, errors: &mut Vec<FieldError>, profile: &str, required_in: &[&str]) -> Option<Option<T>> {
match raw {
None if required_in.contains(&profile) => {
errors.push(FieldError {
var: var.to_string(),
kind: FieldErrorKind::MissingInProfile { profile: profile.to_string() },
});
None
}
None => Some(None),
Some(raw) => parse_value(&raw, var, secret, errors).map(Some),
}
}
pub fn with_default_required_in<T: FromEnvValue>(
raw: Option<String>,
default: &str,
var: &str,
secret: bool,
errors: &mut Vec<FieldError>,
profile: &str,
required_in: &[&str],
) -> Option<T> {
match raw {
None if required_in.contains(&profile) => {
errors.push(FieldError {
var: var.to_string(),
kind: FieldErrorKind::MissingInProfile { profile: profile.to_string() },
});
None
}
raw => with_default(raw, default, var, secret, errors),
}
}
pub fn with_default<T: FromEnvValue>(raw: Option<String>, default: &str, var: &str, secret: bool, errors: &mut Vec<FieldError>) -> Option<T> {
match raw {
Some(raw) => parse_value(&raw, var, secret, errors),
None => match T::from_env_value(default) {
Ok(value) => Some(value),
Err(message) => {
errors.push(FieldError {
var: var.to_string(),
kind: FieldErrorKind::Invalid {
value: Some(default.to_string()),
message: format!("invalid default: {message}"),
},
});
None
}
},
}
}
fn parse_value<T: FromEnvValue>(raw: &str, var: &str, secret: bool, errors: &mut Vec<FieldError>) -> Option<T> {
match T::from_env_value(raw) {
Ok(value) => Some(value),
Err(message) => {
errors.push(FieldError {
var: var.to_string(),
kind: FieldErrorKind::Invalid {
value: (!secret).then(|| raw.to_string()),
message,
},
});
None
}
}
}