Skip to main content

opys_core/
interpolate.rs

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
32/// Resolve every `${ref}` in `vars`. Circular references throw.
33pub 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        // Two-phase to satisfy borrow checker: first collect needed lookups,
54        // then run replace. Simpler: just do a recursive replace with closure.
55        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
82/// Substitute resolved vars into `template`. Missing names stay as `${name}`.
83pub 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}