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/// How many rows of a per-row output render and deliver at once: `--row-concurrency`,
41/// `DRE_ROW_CONCURRENCY`, over `for_each.concurrency`.
42pub const ROW_CONCURRENCY: &str = "DRE_ROW_CONCURRENCY";
43
44/// A `DRE_*` environment variable's value; set but empty counts as unset.
45pub fn env(name: &str) -> Option<String> {
46    std::env::var(name).ok().filter(|v| !v.is_empty())
47}
48
49/// Where a setting's value came from.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum Source {
52    /// A command-line flag, e.g. `--target`.
53    Flag(&'static str),
54    /// An environment variable, e.g. `DRE_TARGET`.
55    Env(&'static str),
56    /// A key of `dre_project.yml`.
57    Project(&'static str),
58    /// Found by looking, e.g. profiles.yml in the project directory.
59    Found(&'static str),
60    /// Its default.
61    Default,
62}
63
64impl fmt::Display for Source {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Source::Flag(name) | Source::Env(name) | Source::Found(name) => f.write_str(name),
68            Source::Project(key) => write!(f, "`{key}` in dre_project.yml"),
69            Source::Default => f.write_str("default"),
70        }
71    }
72}
73
74impl Serialize for Source {
75    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
76        s.collect_str(self)
77    }
78}
79
80/// One resolved setting.
81#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
82pub struct Setting {
83    pub name: &'static str,
84    /// The value as text; `None` when it's unset and has no default.
85    pub value: Option<String>,
86    pub source: Source,
87}
88
89/// Every resolved setting, in the order they were resolved.
90#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
91#[serde(transparent)]
92pub struct Settings(Vec<Setting>);
93
94impl Settings {
95    /// Record a setting, replacing an earlier value of the same name.
96    pub fn set(&mut self, name: &'static str, value: Option<String>, source: Source) {
97        self.0.retain(|s| s.name != name);
98        self.0.push(Setting { name, value, source });
99    }
100
101    pub fn get(&self, name: &str) -> Option<&Setting> {
102        self.0.iter().find(|s| s.name == name)
103    }
104
105    pub fn iter(&self) -> impl Iterator<Item = &Setting> {
106        self.0.iter()
107    }
108
109    /// One line per setting, `name = value (source)`, for the log and `validate --verbose`.
110    pub fn lines(&self) -> Vec<String> {
111        self.0
112            .iter()
113            .map(|s| {
114                format!(
115                    "{} = {} ({})",
116                    s.name,
117                    s.value.as_deref().unwrap_or("unset"),
118                    s.source
119                )
120            })
121            .collect()
122    }
123}
124
125/// The settings a command line resolves before loading a project: the run's timezone
126/// (`--timezone`, `DRE_TIMEZONE`), `run.date` (`DRE_RUN_DATE`) and the scheduled instant
127/// (`DRE_RUN_AT`).
128pub fn run_settings(timezone_flag: Option<&str>) -> Settings {
129    let mut s = Settings::default();
130    let (tz, src) = match flag_or_env(timezone_flag, "--timezone", TIMEZONE) {
131        Some((v, src)) => (Some(v), src),
132        None => (None, Source::Default),
133    };
134    s.set("timezone", tz, src);
135    for (name, var) in [("run_date", RUN_DATE), ("scheduled_at", RUN_AT)] {
136        match env(var) {
137            Some(v) => s.set(name, Some(v), Source::Env(var)),
138            None => s.set(name, None, Source::Default),
139        }
140    }
141    s
142}
143
144/// The value of a setting given by `flag`, else the environment variable `var`, with its source.
145pub fn flag_or_env(
146    flag: Option<&str>,
147    flag_name: &'static str,
148    var: &'static str,
149) -> Option<(String, Source)> {
150    match flag.filter(|f| !f.is_empty()) {
151        Some(f) => Some((f.to_string(), Source::Flag(flag_name))),
152        None => env(var).map(|v| (v, Source::Env(var))),
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn a_flag_beats_the_environment() {
162        assert_eq!(
163            flag_or_env(Some("prod"), "--target", "DRE_TEST_SETTINGS_UNSET"),
164            Some(("prod".into(), Source::Flag("--target")))
165        );
166        assert_eq!(flag_or_env(None, "--target", "DRE_TEST_SETTINGS_UNSET"), None);
167        assert_eq!(flag_or_env(Some(""), "--target", "DRE_TEST_SETTINGS_UNSET"), None);
168    }
169
170    #[test]
171    fn settings_record_their_source_and_replace_earlier_values() {
172        let mut s = Settings::default();
173        s.set("target", Some("dev".into()), Source::Default);
174        s.set("target_path", None, Source::Default);
175        s.set("target", Some("prod".into()), Source::Env(TARGET));
176        assert_eq!(
177            s.lines(),
178            ["target_path = unset (default)", "target = prod (DRE_TARGET)"]
179        );
180        assert_eq!(
181            serde_json::to_value(&s).unwrap()[1],
182            serde_json::json!({"name": "target", "value": "prod", "source": "DRE_TARGET"})
183        );
184    }
185}