use crate::config::SettingsSections;
include!(concat!(env!("OUT_DIR"), "/settings_generated.rs"));
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Overrides {
pub env_deny: Option<Vec<String>>,
pub env_allow: Option<Vec<String>>,
pub gate_pct: Option<f64>,
pub credit: Option<bool>,
}
pub fn from_cli(v: Vec<String>) -> Option<Vec<String>> {
(!v.is_empty()).then_some(v)
}
pub type EnvLookup<'a> = &'a dyn Fn(&str) -> Option<String>;
fn bool_from_env(env: EnvLookup, key: &str) -> Option<bool> {
let raw = env(key)?;
match raw.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
_ => {
eprintln!("warning: {key} is not a boolean: {raw:?}");
None
}
}
}
fn list_from_env(env: EnvLookup, key: &str) -> Option<Vec<String>> {
env(key).map(|raw| {
raw.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
})
}
impl Settings {
pub fn resolve(cli: &Overrides, config: &SettingsSections, env: EnvLookup) -> Self {
let defaults = Self::default();
let envs = config.env.as_ref();
Self {
credit: cli
.credit
.or_else(|| bool_from_env(env, "TAK_CREDIT"))
.or_else(|| config.report.as_ref().and_then(|r| r.credit))
.unwrap_or(defaults.credit),
gate_pct: cli
.gate_pct
.or_else(|| {
env("TAK_GATE_PCT").and_then(|raw| match raw.trim().parse::<f64>() {
Ok(v) => Some(v),
Err(_) => {
eprintln!("warning: TAK_GATE_PCT is not a number: {raw:?}");
None
}
})
})
.or_else(|| config.gate.as_ref().and_then(|g| g.pct))
.unwrap_or(defaults.gate_pct),
env_allow: cli
.env_allow
.clone()
.or_else(|| list_from_env(env, "TAK_ENV_ALLOW"))
.or_else(|| envs.and_then(|c| c.allow.clone()))
.unwrap_or(defaults.env_allow),
env_deny: cli
.env_deny
.clone()
.or_else(|| list_from_env(env, "TAK_ENV_DENY"))
.or_else(|| envs.and_then(|c| c.deny.clone()))
.unwrap_or(defaults.env_deny),
}
}
pub fn from_process(cli: &Overrides, config: &SettingsSections) -> Self {
Self::resolve(cli, config, &|key| std::env::var(key).ok())
}
pub fn display_value(&self, name: &str) -> Option<String> {
match name {
"env_allow" => Some(format!("{:?}", self.env_allow)),
"env_deny" => Some(format!("{:?}", self.env_deny)),
"credit" => Some(format!("{}", self.credit)),
"gate_pct" => Some(format!("{}", self.gate_pct)),
_ => None,
}
}
pub fn scrubbed_env(&self) -> impl Iterator<Item = &str> {
self.env_deny
.iter()
.filter(|name| !self.env_allow.contains(name))
.map(String::as_str)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn no_env(_: &str) -> Option<String> {
None
}
fn env_config(deny: Option<&[&str]>, allow: Option<&[&str]>) -> SettingsSections {
SettingsSections {
env: Some(crate::config::EnvSection {
deny: deny.map(|v| v.iter().map(|s| s.to_string()).collect()),
allow: allow.map(|v| v.iter().map(|s| s.to_string()).collect()),
}),
gate: None,
report: None,
}
}
#[test]
fn the_default_protects_forge_tokens() {
let s = Settings::default();
let scrubbed: Vec<_> = s.scrubbed_env().collect();
assert!(scrubbed.contains(&"GITHUB_TOKEN"));
assert!(scrubbed.contains(&"GH_TOKEN"));
}
#[test]
fn allow_subtracts_from_deny() {
let s = Settings {
env_deny: vec!["A".into(), "B".into()],
env_allow: vec!["B".into()],
gate_pct: Settings::default().gate_pct,
credit: Settings::default().credit,
};
assert_eq!(s.scrubbed_env().collect::<Vec<_>>(), ["A"]);
}
#[test]
fn allowing_an_undenied_variable_does_nothing() {
let s = Settings {
env_deny: vec!["A".into()],
env_allow: vec!["ZZZ".into()],
gate_pct: Settings::default().gate_pct,
credit: Settings::default().credit,
};
assert_eq!(s.scrubbed_env().collect::<Vec<_>>(), ["A"]);
}
#[test]
fn cli_beats_env_beats_config() {
let cfg = env_config(Some(&["FROM_CONFIG"]), None);
let env = |k: &str| (k == "TAK_ENV_DENY").then(|| "FROM_ENV".to_string());
let from_config = Settings::resolve(&Overrides::default(), &cfg, &no_env);
assert_eq!(from_config.env_deny, ["FROM_CONFIG"]);
let from_env = Settings::resolve(&Overrides::default(), &cfg, &env);
assert_eq!(from_env.env_deny, ["FROM_ENV"]);
let cli = Overrides {
env_deny: Some(vec!["FROM_CLI".into()]),
..Default::default()
};
let from_cli = Settings::resolve(&cli, &cfg, &env);
assert_eq!(from_cli.env_deny, ["FROM_CLI"]);
}
#[test]
fn an_absent_source_defers_rather_than_clearing() {
let cfg = env_config(Some(&["FROM_CONFIG"]), None);
let s = Settings::resolve(&Overrides::default(), &cfg, &no_env);
assert_eq!(s.env_deny, ["FROM_CONFIG"]);
}
#[test]
fn an_empty_variable_means_an_empty_list() {
let cfg = env_config(Some(&["FROM_CONFIG"]), None);
let env = |k: &str| (k == "TAK_ENV_DENY").then(String::new);
let s = Settings::resolve(&Overrides::default(), &cfg, &env);
assert!(s.env_deny.is_empty());
}
#[test]
fn a_variable_is_split_on_commas_and_trimmed() {
let env = |k: &str| (k == "TAK_ENV_DENY").then(|| " A , B ,, C ".to_string());
let s = Settings::resolve(&Overrides::default(), &SettingsSections::default(), &env);
assert_eq!(s.env_deny, ["A", "B", "C"]);
}
#[test]
fn an_unused_cli_flag_is_not_an_empty_list() {
assert_eq!(from_cli(vec![]), None);
assert_eq!(from_cli(vec!["A".into()]), Some(vec!["A".to_string()]));
}
const ENV_SENTINEL: &str = "12345";
const BOOL_ENV_SENTINEL: &str = "false";
fn config_sentinel(type_: &str) -> String {
match type_ {
"list<string>" => "[\"SENTINEL\"]".to_string(),
"float" => "12345.0".to_string(),
"bool" => "false".to_string(),
other => panic!("the drift check has no sentinel for type `{other}`"),
}
}
#[test]
fn every_declared_env_var_is_honoured() {
for setting in SETTINGS {
for var in setting.env_vars {
let sentinel = if setting.type_ == "bool" {
BOOL_ENV_SENTINEL
} else {
ENV_SENTINEL
};
let env = |k: &str| (k == *var).then(|| sentinel.to_string());
let got =
Settings::resolve(&Overrides::default(), &SettingsSections::default(), &env);
assert_ne!(
got,
Settings::default(),
"`{}` declares {var} but setting it changes nothing — \
is it wired into Settings::resolve?",
setting.name
);
}
}
}
#[test]
fn every_declared_config_key_is_honoured() {
for setting in SETTINGS {
for key in setting.config_keys {
let text = format!("{key} = {}\n", config_sentinel(setting.type_));
let cfg: SettingsSections = toml::from_str(&text).unwrap_or_else(|e| {
panic!(
"`{}` declares config key `{key}`, which does not parse: {e}",
setting.name
)
});
let got = Settings::resolve(&Overrides::default(), &cfg, &no_env);
assert_ne!(
got,
Settings::default(),
"`{}` declares config key `{key}` but setting it changes nothing",
setting.name
);
}
}
}
#[test]
fn every_setting_has_an_accessor() {
let s = Settings::default();
for setting in SETTINGS {
assert!(
s.display_value(setting.name).is_some(),
"`{}` has no accessor in Settings::get",
setting.name
);
}
}
#[test]
fn every_setting_declares_a_source() {
for s in SETTINGS {
assert!(
!s.cli_flags.is_empty() || !s.env_vars.is_empty() || !s.config_keys.is_empty(),
"`{}` has no sources",
s.name
);
}
}
}