1use std::fmt;
9
10use serde::Serialize;
11
12pub const TARGET: &str = "DRE_TARGET";
14pub const TARGET_PATH: &str = "DRE_TARGET_PATH";
16pub const PROFILES_DIR: &str = "DRE_PROFILES_DIR";
18pub const PLUGINS_DIR: &str = "DRE_PLUGINS_DIR";
20pub const REGISTRY_URL: &str = "DRE_REGISTRY_URL";
22pub const GITHUB_API_URL: &str = "DRE_GITHUB_API_URL";
24pub const TIMEZONE: &str = "DRE_TIMEZONE";
26pub const RUN_DATE: &str = "DRE_RUN_DATE";
28pub const RUN_AT: &str = "DRE_RUN_AT";
30pub const HTTP_TIMEOUT: &str = "DRE_HTTP_TIMEOUT";
32pub const RUN_TIMEOUT: &str = "DRE_RUN_TIMEOUT";
34pub const KEEP_RUNS: &str = "DRE_KEEP_RUNS";
36pub const THREADS: &str = "DRE_THREADS";
39
40pub const ROW_CONCURRENCY: &str = "DRE_ROW_CONCURRENCY";
43
44pub fn env(name: &str) -> Option<String> {
46 std::env::var(name).ok().filter(|v| !v.is_empty())
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum Source {
52 Flag(&'static str),
54 Env(&'static str),
56 Project(&'static str),
58 Found(&'static str),
60 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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
82pub struct Setting {
83 pub name: &'static str,
84 pub value: Option<String>,
86 pub source: Source,
87}
88
89#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
91#[serde(transparent)]
92pub struct Settings(Vec<Setting>);
93
94impl Settings {
95 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 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
125pub 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
144pub 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}