1use std::collections::HashMap;
4
5use super::predefined::Options;
6
7pub trait Lookup {
10 fn lookup(&self, name: &str) -> Option<String>;
11}
12
13pub trait Resolver {
17 fn expand(&self, name: &str) -> String;
20
21 fn all(&self) -> HashMap<String, String>;
24
25 fn lookup(&self, name: &str) -> Option<String>;
27}
28
29pub struct ResolverLookup<'a>(pub &'a dyn Resolver);
38
39impl Lookup for ResolverLookup<'_> {
40 fn lookup(&self, name: &str) -> Option<String> {
41 self.0.lookup(name)
42 }
43}
44
45struct ResolverImpl {
46 resolved: HashMap<String, String>,
47}
48
49impl Resolver for ResolverImpl {
50 fn expand(&self, name: &str) -> String {
51 match self.resolved.get(name) {
52 Some(v) => v.clone(),
53 None => std::env::var(name).unwrap_or_default(),
54 }
55 }
56
57 fn all(&self) -> HashMap<String, String> {
58 self.resolved.clone()
59 }
60
61 fn lookup(&self, name: &str) -> Option<String> {
62 match self.resolved.get(name) {
63 Some(v) => Some(v.clone()),
64 None => std::env::var(name).ok(),
65 }
66 }
67}
68
69pub fn new_resolver(
74 opts: Options,
75 static_vars: Option<HashMap<String, String>>,
76 tool_versions: Option<HashMap<String, String>>,
77) -> Box<dyn Resolver> {
78 use super::predefined::{
79 CAPRONI_CLUSTER_DOMAIN, CAPRONI_CLUSTER_NAME, CAPRONI_PRIMARY_HOSTNAME,
80 XIANYUN_CLUSTER_DOMAIN, XIANYUN_CLUSTER_NAME, XIANYUN_PRIMARY_HOSTNAME, cluster_domain,
81 primary_hostname,
82 };
83
84 let mut resolved = tool_versions.unwrap_or_default();
85
86 resolved.insert(XIANYUN_CLUSTER_NAME.to_string(), opts.profile_name.clone());
90 resolved.insert(CAPRONI_CLUSTER_NAME.to_string(), opts.profile_name.clone());
91 resolved.insert(
92 XIANYUN_PRIMARY_HOSTNAME.to_string(),
93 primary_hostname(&opts),
94 );
95 resolved.insert(
96 CAPRONI_PRIMARY_HOSTNAME.to_string(),
97 primary_hostname(&opts),
98 );
99 if let Ok(domain) = cluster_domain(&opts) {
100 resolved.insert(XIANYUN_CLUSTER_DOMAIN.to_string(), domain.clone());
101 resolved.insert(CAPRONI_CLUSTER_DOMAIN.to_string(), domain);
102 }
103
104 if let Some(static_vars) = static_vars {
105 resolved.extend(static_vars);
106 }
107
108 Box::new(ResolverImpl { resolved })
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
117 fn resolver_expand_cases() {
118 struct Case {
119 name: &'static str,
120 opts: Options,
121 static_vars: Option<HashMap<String, String>>,
122 env_vars: HashMap<&'static str, &'static str>,
123 var_name: &'static str,
124 want: &'static str,
125 }
126
127 let cases = vec![
128 Case {
129 name: "predefined resolves from options",
130 opts: Options {
131 profile_name: "prod".into(),
132 cluster: None,
133 },
134 static_vars: None,
135 env_vars: HashMap::new(),
136 var_name: "CAPRONI_CLUSTER_NAME",
137 want: "prod",
138 },
139 Case {
140 name: "predefined resolves from options via the xianyun primary name",
141 opts: Options {
142 profile_name: "prod".into(),
143 cluster: None,
144 },
145 static_vars: None,
146 env_vars: HashMap::new(),
147 var_name: "XIANYUN_CLUSTER_NAME",
148 want: "prod",
149 },
150 Case {
151 name: "static literal resolves",
152 opts: Options::default(),
153 static_vars: Some(HashMap::from([(
154 "HOSTNAME".to_string(),
155 "foo.example.com".to_string(),
156 )])),
157 env_vars: HashMap::new(),
158 var_name: "HOSTNAME",
159 want: "foo.example.com",
160 },
161 Case {
162 name: "static shadows predefined",
163 opts: Options {
164 profile_name: "prod".into(),
165 cluster: None,
166 },
167 static_vars: Some(HashMap::from([(
168 "CAPRONI_CLUSTER_NAME".to_string(),
169 "override".to_string(),
170 )])),
171 env_vars: HashMap::new(),
172 var_name: "CAPRONI_CLUSTER_NAME",
173 want: "override",
174 },
175 Case {
176 name: "unknown falls back to env",
177 opts: Options::default(),
178 static_vars: None,
179 env_vars: HashMap::from([("CUSTOM", "env-val")]),
180 var_name: "CUSTOM",
181 want: "env-val",
182 },
183 Case {
184 name: "unknown not in env is empty",
185 opts: Options::default(),
186 static_vars: None,
187 env_vars: HashMap::new(),
188 var_name: "MISSING",
189 want: "",
190 },
191 ];
192
193 for case in cases {
194 for (k, v) in &case.env_vars {
195 unsafe {
196 std::env::set_var(k, v);
197 }
198 }
199
200 let r = new_resolver(case.opts, case.static_vars, None);
201 assert_eq!(r.expand(case.var_name), case.want, "{}", case.name);
202 }
203 }
204
205 #[test]
207 fn resolver_expand_reads_env_live() {
208 let r = new_resolver(Options::default(), None, None);
209
210 unsafe {
211 std::env::set_var("LIVE", "first");
212 }
213 assert_eq!(r.expand("LIVE"), "first");
214
215 unsafe {
216 std::env::set_var("LIVE", "second");
217 }
218 assert_eq!(r.expand("LIVE"), "second");
219 }
220
221 #[test]
223 fn resolver_expand_nil_static_vars() {
224 let r = new_resolver(
225 Options {
226 profile_name: "prod".into(),
227 cluster: None,
228 },
229 None,
230 None,
231 );
232 assert_eq!(r.expand("CAPRONI_CLUSTER_NAME"), "prod");
233 }
234
235 #[test]
237 fn resolver_all() {
238 let r = new_resolver(
239 Options {
240 profile_name: "my-cluster".into(),
241 cluster: None,
242 },
243 Some(HashMap::from([(
244 "HOSTNAME".to_string(),
245 "foo.example.com".to_string(),
246 )])),
247 None,
248 );
249
250 let all = r.all();
251 assert_eq!(
252 all.get("CAPRONI_CLUSTER_NAME"),
253 Some(&"my-cluster".to_string())
254 );
255 assert_eq!(all.get("HOSTNAME"), Some(&"foo.example.com".to_string()));
256 }
257
258 #[test]
260 fn resolver_all_static_shadows_predefined() {
261 let r = new_resolver(
262 Options {
263 profile_name: "prod".into(),
264 cluster: None,
265 },
266 Some(HashMap::from([(
267 "CAPRONI_CLUSTER_NAME".to_string(),
268 "override".to_string(),
269 )])),
270 None,
271 );
272
273 assert_eq!(
274 r.all().get("CAPRONI_CLUSTER_NAME"),
275 Some(&"override".to_string())
276 );
277 }
278
279 #[test]
281 fn resolver_lookup_cases() {
282 unsafe {
283 std::env::set_var("CAPRONI_TEST_ENV", "from-env");
284 }
285
286 let static_vars = HashMap::from([("STATIC".to_string(), "from-static".to_string())]);
287 let r = new_resolver(
288 Options {
289 profile_name: "demo".into(),
290 cluster: None,
291 },
292 Some(static_vars),
293 None,
294 );
295
296 let cases = [
297 ("static variable", "STATIC", Some("from-static")),
298 ("predefined variable", "CAPRONI_CLUSTER_NAME", Some("demo")),
299 ("environment variable", "CAPRONI_TEST_ENV", Some("from-env")),
300 ("unknown variable", "NOPE", None),
301 ];
302
303 for (name, var_name, want) in cases {
304 assert_eq!(r.lookup(var_name), want.map(str::to_string), "{name}");
305 }
306 }
307
308 #[test]
310 fn resolver_tool_versions_cases() {
311 {
312 let r = new_resolver(
313 Options::default(),
314 None,
315 Some(HashMap::from([(
316 "GL_CERT_MANAGER_VERSION".to_string(),
317 "v1.2.3".to_string(),
318 )])),
319 );
320 assert_eq!(r.expand("GL_CERT_MANAGER_VERSION"), "v1.2.3");
321 }
322
323 {
324 let static_vars = HashMap::from([(
325 "GL_CERT_MANAGER_VERSION".to_string(),
326 "from-static".to_string(),
327 )]);
328 let tool_versions = HashMap::from([(
329 "GL_CERT_MANAGER_VERSION".to_string(),
330 "from-tool".to_string(),
331 )]);
332 let r = new_resolver(Options::default(), Some(static_vars), Some(tool_versions));
333 assert_eq!(r.expand("GL_CERT_MANAGER_VERSION"), "from-static");
334 }
335
336 {
337 unsafe {
338 std::env::set_var("GL_CERT_MANAGER_VERSION", "from-env");
339 }
340 let tool_versions = HashMap::from([(
341 "GL_CERT_MANAGER_VERSION".to_string(),
342 "from-tool".to_string(),
343 )]);
344 let r = new_resolver(Options::default(), None, Some(tool_versions));
345 assert_eq!(r.expand("GL_CERT_MANAGER_VERSION"), "from-tool");
346 }
347
348 {
349 unsafe {
350 std::env::set_var("SOME_OTHER_VAR", "from-env");
351 }
352 let tool_versions =
353 HashMap::from([("GL_CERT_MANAGER_VERSION".to_string(), "v1.2.3".to_string())]);
354 let r = new_resolver(Options::default(), None, Some(tool_versions));
355 assert_eq!(r.expand("SOME_OTHER_VAR"), "from-env");
356 }
357 }
358}