use std::{
collections::VecDeque,
ffi::{OsStr, OsString},
};
use crate::{
Configuration,
internal::{ChangeVerb, CongenChange, Description, ParseError, VerbError},
};
pub struct EnvError<C: CongenChange> {
pub partial_change: C,
pub errors: Vec<PartialVerbError>,
}
impl<C: CongenChange> std::fmt::Debug for EnvError<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EnvError")
.field("errors", &self.errors)
.finish_non_exhaustive()
}
}
impl<C: CongenChange> std::error::Error for EnvError<C> {}
impl<C: CongenChange> std::fmt::Display for EnvError<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{self:#?}"))
}
}
#[derive(Debug)]
pub struct PartialVerbError {
pub var: OsString,
pub value: OsString,
pub error: VerbError,
}
pub fn load_from_env_iter<C: Configuration, E: IntoIterator<Item = (OsString, OsString)>>(
env: E,
prefix: &str,
) -> Result<C::CongenChange, EnvError<C::CongenChange>> {
let mut combined_changes = C::CongenChange::empty();
let mut errors = Vec::new();
let prefix = if prefix.ends_with("_") {
prefix.to_string()
} else {
format!("{prefix}_")
};
for (var, value) in env.into_iter() {
let Some(var_str) = var.to_str() else {
errors.push(PartialVerbError {
var,
value,
error: VerbError::ParseError(ParseError(
"expected env varibale name to be valid utf-8".to_string(),
)),
});
continue;
};
let Some(without_prefix) = var_str.strip_prefix(&prefix) else {
continue;
};
let description = <C>::description("");
let (path, verb) = match get_verb_from_var(without_prefix, &value, &description) {
Ok(verb) => verb,
Err(error) => {
errors.push(PartialVerbError { var, value, error });
continue;
}
};
match C::CongenChange::from_path_and_verb(path.into_iter(), verb) {
Ok(change) => combined_changes.apply_change(change),
Err(error) => errors.push(PartialVerbError { var, value, error }),
}
}
if !errors.is_empty() {
return Err(EnvError {
partial_change: combined_changes,
errors,
});
}
Ok(combined_changes)
}
pub fn load_from_env<C: Configuration>(
prefix: &str,
) -> Result<C::CongenChange, EnvError<C::CongenChange>> {
load_from_env_iter::<C, _>(std::env::vars_os(), prefix)
}
fn get_verb_from_var<'p>(
var: &str,
value: &OsStr,
description: &Description,
) -> Result<(VecDeque<&'p str>, ChangeVerb), VerbError> {
for actionable in description.actionable_fields() {
let mut path: String = actionable
.path
.iter()
.map(|field_name| field_name_to_scream_case(field_name))
.collect::<Vec<_>>()
.join("_");
path.push('_');
let Some(verb) = var.strip_prefix(&path) else {
continue;
};
let verb = match verb {
"UNSET" => ChangeVerb::Unset,
"USE-DEFAULT" | "USE_DEFAULT" | "USEDEFAULT" => ChangeVerb::UseDefault,
"SET" => ChangeVerb::Set(value.to_owned()),
verb => return Err(VerbError::UnknownVerb(verb.to_owned())),
};
return Ok((actionable.path, verb));
}
Err(VerbError::InvalidPath)
}
fn field_name_to_scream_case(name: &str) -> String {
let mut result = String::new();
let chars: Vec<char> = name.chars().collect();
for (i, ch) in chars.iter().copied().enumerate() {
if ch == '_' {
if !result.ends_with('_') {
result.push('_');
}
continue;
}
if ch.is_uppercase() {
let prev = i.checked_sub(1).and_then(|idx| chars.get(idx)).copied();
let next = chars.get(i + 1).copied();
let needs_separator = prev.is_some_and(|p| p != '_')
&& (prev.is_some_and(|p| p.is_lowercase() || p.is_numeric())
|| (prev.is_some_and(char::is_uppercase)
&& next.is_some_and(char::is_lowercase)));
if needs_separator {
result.push('_');
}
result.push(ch.to_ascii_uppercase());
continue;
}
result.push(ch.to_ascii_uppercase());
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_field_name_to_scream_case_simple() {
assert_eq!(field_name_to_scream_case("hello"), "HELLO");
}
#[test]
fn test_field_name_to_scream_case_with_underscore() {
assert_eq!(field_name_to_scream_case("hello_world"), "HELLO_WORLD");
}
#[test]
fn test_field_name_to_scream_case_camel_case() {
assert_eq!(field_name_to_scream_case("helloWorld"), "HELLO_WORLD");
}
#[test]
fn test_field_name_to_scream_case_pascal_case() {
assert_eq!(field_name_to_scream_case("HelloWorld"), "HELLO_WORLD");
}
#[test]
fn test_field_name_to_scream_case_mixed() {
assert_eq!(field_name_to_scream_case("myVarName"), "MY_VAR_NAME");
}
#[test]
fn test_field_name_to_scream_case_already_scream() {
assert_eq!(field_name_to_scream_case("HELLO_WORLD"), "HELLO_WORLD");
}
#[test]
fn test_field_name_to_scream_case_empty() {
assert_eq!(field_name_to_scream_case(""), "");
}
#[test]
fn test_field_name_to_scream_case_single_char() {
assert_eq!(field_name_to_scream_case("a"), "A");
assert_eq!(field_name_to_scream_case("A"), "A");
}
}