use std::fmt;
use serde::Serialize;
pub const TARGET: &str = "DRE_TARGET";
pub const TARGET_PATH: &str = "DRE_TARGET_PATH";
pub const PROFILES_DIR: &str = "DRE_PROFILES_DIR";
pub const PLUGINS_DIR: &str = "DRE_PLUGINS_DIR";
pub const REGISTRY_URL: &str = "DRE_REGISTRY_URL";
pub const GITHUB_API_URL: &str = "DRE_GITHUB_API_URL";
pub const TIMEZONE: &str = "DRE_TIMEZONE";
pub const RUN_DATE: &str = "DRE_RUN_DATE";
pub const RUN_AT: &str = "DRE_RUN_AT";
pub const HTTP_TIMEOUT: &str = "DRE_HTTP_TIMEOUT";
pub const RUN_TIMEOUT: &str = "DRE_RUN_TIMEOUT";
pub const KEEP_RUNS: &str = "DRE_KEEP_RUNS";
pub const THREADS: &str = "DRE_THREADS";
pub const ROW_CONCURRENCY: &str = "DRE_ROW_CONCURRENCY";
pub fn env(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.is_empty())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Source {
Flag(&'static str),
Env(&'static str),
Project(&'static str),
Found(&'static str),
Default,
}
impl fmt::Display for Source {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Source::Flag(name) | Source::Env(name) | Source::Found(name) => f.write_str(name),
Source::Project(key) => write!(f, "`{key}` in dre_project.yml"),
Source::Default => f.write_str("default"),
}
}
}
impl Serialize for Source {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Setting {
pub name: &'static str,
pub value: Option<String>,
pub source: Source,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[serde(transparent)]
pub struct Settings(Vec<Setting>);
impl Settings {
pub fn set(&mut self, name: &'static str, value: Option<String>, source: Source) {
self.0.retain(|s| s.name != name);
self.0.push(Setting { name, value, source });
}
pub fn get(&self, name: &str) -> Option<&Setting> {
self.0.iter().find(|s| s.name == name)
}
pub fn iter(&self) -> impl Iterator<Item = &Setting> {
self.0.iter()
}
pub fn lines(&self) -> Vec<String> {
self.0
.iter()
.map(|s| {
format!(
"{} = {} ({})",
s.name,
s.value.as_deref().unwrap_or("unset"),
s.source
)
})
.collect()
}
}
pub fn run_settings(timezone_flag: Option<&str>) -> Settings {
let mut s = Settings::default();
let (tz, src) = match flag_or_env(timezone_flag, "--timezone", TIMEZONE) {
Some((v, src)) => (Some(v), src),
None => (None, Source::Default),
};
s.set("timezone", tz, src);
for (name, var) in [("run_date", RUN_DATE), ("scheduled_at", RUN_AT)] {
match env(var) {
Some(v) => s.set(name, Some(v), Source::Env(var)),
None => s.set(name, None, Source::Default),
}
}
s
}
pub fn flag_or_env(
flag: Option<&str>,
flag_name: &'static str,
var: &'static str,
) -> Option<(String, Source)> {
match flag.filter(|f| !f.is_empty()) {
Some(f) => Some((f.to_string(), Source::Flag(flag_name))),
None => env(var).map(|v| (v, Source::Env(var))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_flag_beats_the_environment() {
assert_eq!(
flag_or_env(Some("prod"), "--target", "DRE_TEST_SETTINGS_UNSET"),
Some(("prod".into(), Source::Flag("--target")))
);
assert_eq!(flag_or_env(None, "--target", "DRE_TEST_SETTINGS_UNSET"), None);
assert_eq!(flag_or_env(Some(""), "--target", "DRE_TEST_SETTINGS_UNSET"), None);
}
#[test]
fn settings_record_their_source_and_replace_earlier_values() {
let mut s = Settings::default();
s.set("target", Some("dev".into()), Source::Default);
s.set("target_path", None, Source::Default);
s.set("target", Some("prod".into()), Source::Env(TARGET));
assert_eq!(
s.lines(),
["target_path = unset (default)", "target = prod (DRE_TARGET)"]
);
assert_eq!(
serde_json::to_value(&s).unwrap()[1],
serde_json::json!({"name": "target", "value": "prod", "source": "DRE_TARGET"})
);
}
}