#![allow(
clippy::missing_errors_doc,
clippy::missing_docs_in_private_items,
clippy::option_if_let_else
)]
use crate::error::{ConfigError, ConfigFields, Result};
pub trait ConfigLoader: ConfigFields + Default {
fn from_env() -> Result<Self> {
let mut config = Self::default();
config.load_from_env(None)?;
Ok(config)
}
fn from_env_with_prefix(prefix: &str) -> Result<Self> {
let mut config = Self::default();
config.load_from_env(Some(prefix))?;
Ok(config)
}
fn from_env_var(var_name: &str) -> Result<Self>
where
Self: serde::de::DeserializeOwned,
{
let value = std::env::var(var_name)
.map_err(|_| ConfigError::NotFound(format!("environment variable {var_name}")))?;
if let Ok(config) = serde_json::from_str::<Self>(&value) {
return Ok(config);
}
if let Ok(config) = toml::from_str::<Self>(&value) {
return Ok(config);
}
Err(ConfigError::Parse(format!(
"Failed to parse {var_name} as JSON or TOML"
)))
}
}
impl<T: ConfigFields + Default> ConfigLoader for T {}
pub trait EnvConfig {
fn env_mappings() -> Vec<(&'static str, &'static str)>;
fn env_prefix() -> &'static str;
}
#[derive(Debug, Clone)]
pub struct EnvLoader {
prefix: String,
separator: String,
}
impl Default for EnvLoader {
fn default() -> Self {
Self {
prefix: String::new(),
separator: "__".to_string(),
}
}
}
impl EnvLoader {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
#[must_use]
pub fn separator(mut self, separator: impl Into<String>) -> Self {
self.separator = separator.into();
self
}
#[must_use]
pub fn build_name(&self, path: &[&str]) -> String {
let mut parts = Vec::with_capacity(path.len() + 1);
if !self.prefix.is_empty() {
parts.push(self.prefix.to_uppercase());
}
parts.extend(path.iter().map(|s| s.to_uppercase()));
parts.join(&self.separator)
}
pub fn load<T: std::str::FromStr>(&self, path: &[&str]) -> Result<T>
where
T::Err: std::fmt::Display,
{
let name = self.build_name(path);
let value = std::env::var(&name)
.map_err(|_| ConfigError::NotFound(format!("environment variable {name}")))?;
value
.parse()
.map_err(|e| ConfigError::Parse(format!("Failed to parse {name}: {e}")))
}
pub fn try_load<T: std::str::FromStr>(&self, path: &[&str]) -> Result<Option<T>>
where
T::Err: std::fmt::Display,
{
let name = self.build_name(path);
match std::env::var(&name) {
Ok(value) => value
.parse()
.map(Some)
.map_err(|e| ConfigError::Parse(format!("Failed to parse {name}: {e}"))),
Err(_) => Ok(None),
}
}
}