Skip to main content

dre_core/
settings.rs

1//! DRE's settings and where each value came from.
2//!
3//! A setting is chosen, highest first, by its command-line flag, its `DRE_*` environment
4//! variable, its key in `dre_project.yml`, then its default. [`env`] is the one place DRE reads
5//! its own environment variables; [`Settings`] records every resolved value with its source, for
6//! `dre validate --verbose` (and its JSON), `run_results.json` and the run's log.
7
8use std::fmt;
9
10use serde::Serialize;
11
12/// The run's target: `--target`, `DRE_TARGET`.
13pub const TARGET: &str = "DRE_TARGET";
14/// The target path: `--target-path`, `DRE_TARGET_PATH`, `target_path:`.
15pub const TARGET_PATH: &str = "DRE_TARGET_PATH";
16/// Where profiles.yml is: `--profiles-dir`, `DRE_PROFILES_DIR`.
17pub const PROFILES_DIR: &str = "DRE_PROFILES_DIR";
18/// Where plugin packages are installed: `DRE_PLUGINS_DIR`.
19pub const PLUGINS_DIR: &str = "DRE_PLUGINS_DIR";
20/// The plugin registry index: `DRE_REGISTRY_URL`.
21pub const REGISTRY_URL: &str = "DRE_REGISTRY_URL";
22/// The GitHub API DRE asks for releases (tests point it elsewhere): `DRE_GITHUB_API_URL`.
23pub const GITHUB_API_URL: &str = "DRE_GITHUB_API_URL";
24/// The run's timezone: `--timezone`, `DRE_TIMEZONE`.
25pub const TIMEZONE: &str = "DRE_TIMEZONE";
26/// `run.date`: `DRE_RUN_DATE`.
27pub const RUN_DATE: &str = "DRE_RUN_DATE";
28/// The instant a scheduled run was scheduled for: `DRE_RUN_AT`.
29pub const RUN_AT: &str = "DRE_RUN_AT";
30/// Seconds a download may receive nothing: `DRE_HTTP_TIMEOUT`, `flags: http_timeout`.
31pub const HTTP_TIMEOUT: &str = "DRE_HTTP_TIMEOUT";
32/// How long a run may take: `--timeout`, `DRE_RUN_TIMEOUT`, `flags: run_timeout`.
33pub const RUN_TIMEOUT: &str = "DRE_RUN_TIMEOUT";
34/// How many runs of each Binding to keep: `--keep-runs`, `DRE_KEEP_RUNS`, `flags: keep_runs`.
35pub const KEEP_RUNS: &str = "DRE_KEEP_RUNS";
36/// How many Bindings may run at once in the whole run: `--threads`, `DRE_THREADS`, over each
37/// connection entry's `threads:`.
38pub const THREADS: &str = "DRE_THREADS";
39
40/// A `DRE_*` environment variable's value; set but empty counts as unset.
41pub fn env(name: &str) -> Option<String> {
42    std::env::var(name).ok().filter(|v| !v.is_empty())
43}
44
45/// Where a setting's value came from.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum Source {
48    /// A command-line flag, e.g. `--target`.
49    Flag(&'static str),
50    /// An environment variable, e.g. `DRE_TARGET`.
51    Env(&'static str),
52    /// A key of `dre_project.yml`.
53    Project(&'static str),
54    /// Found by looking, e.g. profiles.yml in the project directory.
55    Found(&'static str),
56    /// Its default.
57    Default,
58}
59
60impl fmt::Display for Source {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            Source::Flag(name) | Source::Env(name) | Source::Found(name) => f.write_str(name),
64            Source::Project(key) => write!(f, "`{key}` in dre_project.yml"),
65            Source::Default => f.write_str("default"),
66        }
67    }
68}
69
70impl Serialize for Source {
71    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
72        s.collect_str(self)
73    }
74}
75
76/// One resolved setting.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
78pub struct Setting {
79    pub name: &'static str,
80    /// The value as text; `None` when it's unset and has no default.
81    pub value: Option<String>,
82    pub source: Source,
83}
84
85/// Every resolved setting, in the order they were resolved.
86#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
87#[serde(transparent)]
88pub struct Settings(Vec<Setting>);
89
90impl Settings {
91    /// Record a setting, replacing an earlier value of the same name.
92    pub fn set(&mut self, name: &'static str, value: Option<String>, source: Source) {
93        self.0.retain(|s| s.name != name);
94        self.0.push(Setting { name, value, source });
95    }
96
97    pub fn get(&self, name: &str) -> Option<&Setting> {
98        self.0.iter().find(|s| s.name == name)
99    }
100
101    pub fn iter(&self) -> impl Iterator<Item = &Setting> {
102        self.0.iter()
103    }
104
105    /// One line per setting, `name = value (source)`, for the log and `validate --verbose`.
106    pub fn lines(&self) -> Vec<String> {
107        self.0
108            .iter()
109            .map(|s| {
110                format!(
111                    "{} = {} ({})",
112                    s.name,
113                    s.value.as_deref().unwrap_or("unset"),
114                    s.source
115                )
116            })
117            .collect()
118    }
119}
120
121/// The settings a command line resolves before loading a project: the run's timezone
122/// (`--timezone`, `DRE_TIMEZONE`), `run.date` (`DRE_RUN_DATE`) and the scheduled instant
123/// (`DRE_RUN_AT`).
124pub fn run_settings(timezone_flag: Option<&str>) -> Settings {
125    let mut s = Settings::default();
126    let (tz, src) = match flag_or_env(timezone_flag, "--timezone", TIMEZONE) {
127        Some((v, src)) => (Some(v), src),
128        None => (None, Source::Default),
129    };
130    s.set("timezone", tz, src);
131    for (name, var) in [("run_date", RUN_DATE), ("scheduled_at", RUN_AT)] {
132        match env(var) {
133            Some(v) => s.set(name, Some(v), Source::Env(var)),
134            None => s.set(name, None, Source::Default),
135        }
136    }
137    s
138}
139
140/// The value of a setting given by `flag`, else the environment variable `var`, with its source.
141pub fn flag_or_env(
142    flag: Option<&str>,
143    flag_name: &'static str,
144    var: &'static str,
145) -> Option<(String, Source)> {
146    match flag.filter(|f| !f.is_empty()) {
147        Some(f) => Some((f.to_string(), Source::Flag(flag_name))),
148        None => env(var).map(|v| (v, Source::Env(var))),
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn a_flag_beats_the_environment() {
158        assert_eq!(
159            flag_or_env(Some("prod"), "--target", "DRE_TEST_SETTINGS_UNSET"),
160            Some(("prod".into(), Source::Flag("--target")))
161        );
162        assert_eq!(flag_or_env(None, "--target", "DRE_TEST_SETTINGS_UNSET"), None);
163        assert_eq!(flag_or_env(Some(""), "--target", "DRE_TEST_SETTINGS_UNSET"), None);
164    }
165
166    #[test]
167    fn settings_record_their_source_and_replace_earlier_values() {
168        let mut s = Settings::default();
169        s.set("target", Some("dev".into()), Source::Default);
170        s.set("target_path", None, Source::Default);
171        s.set("target", Some("prod".into()), Source::Env(TARGET));
172        assert_eq!(
173            s.lines(),
174            ["target_path = unset (default)", "target = prod (DRE_TARGET)"]
175        );
176        assert_eq!(
177            serde_json::to_value(&s).unwrap()[1],
178            serde_json::json!({"name": "target", "value": "prod", "source": "DRE_TARGET"})
179        );
180    }
181}