1use indexmap::IndexMap;
2use regex::Regex;
3use std::collections::HashSet;
4use std::sync::OnceLock;
5
6pub type VarMap = IndexMap<String, String>;
7
8fn placeholder_re() -> &'static Regex {
9 static RE: OnceLock<Regex> = OnceLock::new();
10 RE.get_or_init(|| Regex::new(r"\\\$\{|\$\{([^}\s]+)\}").unwrap())
11}
12
13fn replace_placeholders(template: &str, mut lookup: impl FnMut(&str) -> String) -> String {
14 let mut out = String::with_capacity(template.len());
15 let mut last = 0;
16 for caps in placeholder_re().captures_iter(template) {
17 let m = caps.get(0).unwrap();
18 out.push_str(&template[last..m.start()]);
19 if m.as_str() == "\\${" {
20 out.push_str("${");
21 } else if let Some(name) = caps.get(1) {
22 out.push_str(&lookup(name.as_str()));
23 } else {
24 out.push_str(m.as_str());
25 }
26 last = m.end();
27 }
28 out.push_str(&template[last..]);
29 out
30}
31
32pub fn resolve_vars(vars: &VarMap) -> Result<VarMap, String> {
34 let mut resolved: VarMap = IndexMap::new();
35 let mut resolving: HashSet<String> = HashSet::new();
36
37 fn go(
38 key: &str,
39 vars: &VarMap,
40 resolved: &mut VarMap,
41 resolving: &mut HashSet<String>,
42 ) -> Result<String, String> {
43 if let Some(v) = resolved.get(key) {
44 return Ok(v.clone());
45 }
46 if resolving.contains(key) {
47 return Err(format!("Circular variable reference: {key}"));
48 }
49 let Some(template) = vars.get(key) else {
50 return Ok(format!("${{{key}}}"));
51 };
52 resolving.insert(key.to_owned());
53 let result = {
56 let mut err: Option<String> = None;
57 let s = replace_placeholders(template, |name| match go(name, vars, resolved, resolving)
58 {
59 Ok(v) => v,
60 Err(e) => {
61 err = Some(e);
62 String::new()
63 }
64 });
65 if let Some(e) = err {
66 return Err(e);
67 }
68 s
69 };
70 resolving.remove(key);
71 resolved.insert(key.to_owned(), result.clone());
72 Ok(result)
73 }
74
75 let keys: Vec<String> = vars.keys().cloned().collect();
76 for k in keys {
77 go(&k, vars, &mut resolved, &mut resolving)?;
78 }
79 Ok(resolved)
80}
81
82pub fn interpolate(template: &str, vars: &VarMap) -> String {
84 replace_placeholders(template, |name| {
85 vars.get(name)
86 .cloned()
87 .unwrap_or_else(|| format!("${{{name}}}"))
88 })
89}