use std::env;
#[inline]
pub fn get_flexible_key<T>(object: &serde_json::Value, key: &str) -> Option<T>
where
T: serde::de::DeserializeOwned + Clone,
{
if let Some(value) = object.get(key) {
match serde_json::from_value::<T>(value.clone()) {
Ok(parsed) => return Some(parsed),
Err(e) => {
tracing::trace!("Failed to parse {} (underscore): {}", key, e);
}
}
}
let dash_key = key.replace('_', "-");
if dash_key != key {
if let Some(value) = object.get(&dash_key) {
match serde_json::from_value::<T>(value.clone()) {
Ok(parsed) => return Some(parsed),
Err(e) => {
tracing::trace!("Failed to parse {} (dash '{}'): {}", key, dash_key, e);
}
}
}
}
None
}
#[inline]
pub fn get_env_string(env_var: &str) -> Option<String> {
env::var(env_var).ok()
}
#[inline]
pub fn parse_env_vec(env_var: &str) -> Option<Vec<String>> {
env::var(env_var).ok().map(|value| {
if value.is_empty() {
Vec::new()
} else {
value.split(',').map(|s| s.trim().to_string()).collect()
}
})
}
#[inline]
pub fn parse_env<T: 'static + std::str::FromStr>(env_var: &str) -> Option<T> {
let value = env::var(env_var).ok()?;
let type_name = std::any::type_name::<T>();
tracing::debug!("Found env var {} (type: {}): {}", env_var, type_name, value);
if std::any::TypeId::of::<T>() == std::any::TypeId::of::<bool>() {
let bool_result = match value.as_str() {
"1" | "true" | "TRUE" | "yes" | "YES" | "on" | "ON" => Some(true),
"0" | "false" | "FALSE" | "no" | "NO" | "off" | "OFF" => Some(false),
_ => None,
};
if let Some(bool_val) = bool_result {
tracing::debug!("Parsed {} as boolean: {}", env_var, bool_val);
return unsafe { std::mem::transmute_copy(&bool_val) };
}
}
value.parse().ok()
}
#[macro_export]
macro_rules! config_build {
(
$struct_name:ident<$cli_type:ty> {
$(
$field:ident: $field_type:ty => {
$(cli: $cli_expr:expr,)?
$(env: $env_var:expr,)?
default: $default:expr,
$(file: $file_path:expr,)?
}
),* $(,)?
}
) => {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct $struct_name {
$(pub $field: $field_type,)*
}
#[derive(Debug, Default)]
struct EnvVars {
$(
$field: Option<$field_type>,
)*
}
impl $struct_name {
#[tracing::instrument(level = "debug", skip_all)]
fn load_env_vars() -> EnvVars {
let start = std::time::Instant::now();
let mut env = EnvVars::default();
let mut loaded_count = 0;
$(
env.$field = None;
)*
$(
$(
env.$field = config_build!(@parse_env_value $field, $field_type, $env_var);
if env.$field.is_some() {
loaded_count += 1;
}
)?
)*
let duration = start.elapsed();
tracing::debug!("⚡ Loaded {} env vars in {:?}", loaded_count, duration);
env
}
pub fn build(
cli_args: Option<&$cli_type>,
) -> Self {
let env = Self::load_env_vars();
let section_name = stringify!($struct_name)
.to_lowercase()
.replace("config", "");
let file_section = &$crate::config::files::MERGED_FILE_CONFIG
.get(§ion_name)
.unwrap_or(&serde_json::Value::Null);
if tracing::enabled!(tracing::Level::TRACE) {
if let Ok(pretty) = serde_json::to_string_pretty(file_section) {
tracing::trace!("{} section from file: {}", section_name, pretty);
}
}
let config = Self {
$(
$field: config_build!(@resolve_value
$field,
$field_type,
cli_args,
env.$field,
$crate::config::macros::get_flexible_key::<$field_type>(
file_section,
stringify!($field)
),
$default
$(, $cli_expr)?
),
)*
};
tracing::debug!("{} config check: {:?}", section_name, config);
config
}
}
};
(@parse_env_value repo, $type:ty, $env_var:expr) => {
$crate::config::macros::get_env_string($env_var).map(Some)
};
(@parse_env_value version, $type:ty, $env_var:expr) => {
$crate::config::macros::get_env_string($env_var).map(Some)
};
(@parse_env_value source_path, $type:ty, $env_var:expr) => {
$crate::config::macros::get_env_string($env_var).map(Some)
};
(@parse_env_value dest_path, $type:ty, $env_var:expr) => {
$crate::config::macros::get_env_string($env_var).map(Some)
};
(@parse_env_value include, $type:ty, $env_var:expr) => {
$crate::config::macros::parse_env_vec($env_var)
};
(@parse_env_value exclude, $type:ty, $env_var:expr) => {
$crate::config::macros::parse_env_vec($env_var)
};
(@parse_env_value $field:ident, $type:ty, $env_var:expr) => {
$crate::config::macros::parse_env::<$type>($env_var)
};
(@resolve_value $field:ident, $field_type:ty, $cli_args:expr, $env_value:expr, $file_value:expr, $default:expr, $cli_expr:expr) => {{
if let Some(cli_args) = $cli_args {
let cli_result = $cli_expr(cli_args);
match serde_json::to_value(&cli_result) {
Ok(serde_json::Value::Null) => {
$env_value.or($file_value).unwrap_or($default)
},
Ok(cli_value) => {
match serde_json::from_value::<$field_type>(cli_value) {
Ok(deserialized) => deserialized,
Err(_) => {
tracing::warn!("Failed to deserialize CLI value for field {}, using env/file/default", stringify!($field));
$env_value.or($file_value).unwrap_or($default)
}
}
},
Err(_) => {
tracing::warn!("Failed to serialize CLI value for field {}, using env/file/default", stringify!($field));
$env_value.or($file_value).unwrap_or($default)
}
}
} else {
$env_value.or($file_value).unwrap_or($default)
}
}};
(@resolve_value $field:ident, $field_type:ty, $cli_args:expr, $env_value:expr, $file_value:expr, $default:expr) => {
$env_value.or($file_value).unwrap_or($default)
};
}