Skip to main content

stormchaser_model/
hcl_eval.rs

1//! Hcl expression evaluation models and conversion utilities.
2
3use anyhow::Result;
4use hcl::eval::{Context as HclContext, Evaluate};
5use hcl::Value as HclValue;
6use regex::Regex;
7use serde_json::Value;
8
9/// Resolves HCL expressions embedded in strings via `${...}` syntax within a JSON Value.
10pub fn resolve_expressions(value: &mut Value, ctx: &HclContext) -> Result<()> {
11    match value {
12        Value::String(s) => {
13            if let Some(new_val) = evaluate_string(s, ctx)? {
14                *value = new_val;
15            }
16        }
17        Value::Array(arr) => {
18            for v in arr {
19                resolve_expressions(v, ctx)?;
20            }
21        }
22        Value::Object(map) => {
23            for v in map.values_mut() {
24                resolve_expressions(v, ctx)?;
25            }
26        }
27        _ => {}
28    }
29    Ok(())
30}
31
32/// Converts a JSON value to an equivalent HCL value.
33pub fn json_to_hcl(v: Value) -> HclValue {
34    match v {
35        Value::Null => HclValue::Null,
36        Value::Bool(b) => HclValue::Bool(b),
37        Value::Number(n) => {
38            if let Some(i) = n.as_i64() {
39                HclValue::Number(i.into())
40            } else if let Some(u) = n.as_u64() {
41                HclValue::Number(u.into())
42            } else if let Some(f) = n.as_f64() {
43                HclValue::Number(hcl::Number::from_f64(f).unwrap_or_else(|| 0.into()))
44            } else {
45                HclValue::Null
46            }
47        }
48        Value::String(s) => HclValue::String(s),
49        Value::Array(arr) => {
50            let list: Vec<HclValue> = arr.into_iter().map(json_to_hcl).collect();
51            HclValue::Array(list)
52        }
53        Value::Object(map) => {
54            let mut hcl_map = hcl::Map::new();
55            for (k, v) in map {
56                hcl_map.insert(k, json_to_hcl(v));
57            }
58            HclValue::Object(hcl_map)
59        }
60    }
61}
62
63/// Converts an HCL value back to an equivalent JSON value.
64pub fn hcl_to_json(hv: HclValue) -> Value {
65    match hv {
66        HclValue::Null => Value::Null,
67        HclValue::Bool(b) => Value::Bool(b),
68        HclValue::Number(n) => {
69            if let Some(i) = n.as_i64() {
70                Value::Number(serde_json::Number::from(i))
71            } else if let Some(u) = n.as_u64() {
72                Value::Number(serde_json::Number::from(u))
73            } else if let Some(f) = n.as_f64() {
74                if let Some(sn) = serde_json::Number::from_f64(f) {
75                    Value::Number(sn)
76                } else {
77                    Value::Null
78                }
79            } else {
80                Value::Null
81            }
82        }
83        HclValue::String(s) => Value::String(s),
84        HclValue::Array(arr) => {
85            let list: Vec<Value> = arr.into_iter().map(hcl_to_json).collect();
86            Value::Array(list)
87        }
88        HclValue::Object(map) => {
89            let mut json_map = serde_json::Map::new();
90            for (k, v) in map {
91                json_map.insert(k, hcl_to_json(v));
92            }
93            Value::Object(json_map)
94        }
95    }
96}
97
98/// Evaluates a string containing HCL interpolation templates (`${...}`).
99pub fn evaluate_string(s: &str, ctx: &HclContext) -> Result<Option<Value>> {
100    if !s.contains("${") && !s.contains("%{") {
101        return Ok(None);
102    }
103
104    let template: hcl::Template = s
105        .parse()
106        .map_err(|e| anyhow::anyhow!("Failed to parse HCL template '{}': {:?}", s, e))?;
107    let result = template
108        .evaluate(ctx)
109        .map_err(|e| anyhow::anyhow!("Failed to evaluate HCL template '{}': {:?}", s, e))?;
110
111    tracing::info!("evaluate_string template result: {:?} for s: {}", result, s);
112
113    let re = Regex::new(r"^\$\{([^}]+)\}$").unwrap();
114    if let Some(caps) = re.captures(s) {
115        let expr_str = caps.get(1).unwrap().as_str();
116        let eval_res = evaluate_raw_expr(expr_str, ctx)?;
117        tracing::info!(
118            "evaluate_string raw_expr result: {:?} for expr_str: {}",
119            eval_res,
120            expr_str
121        );
122        return Ok(Some(eval_res));
123    }
124
125    Ok(Some(Value::String(result)))
126}
127
128/// Evaluates a raw HCL expression without the template wrapper.
129pub fn evaluate_raw_expr(expr_str: &str, ctx: &HclContext) -> Result<Value> {
130    let expr: hcl::Expression = expr_str
131        .parse()
132        .map_err(|e| anyhow::anyhow!("Failed to parse HCL expression '{}': {:?}", expr_str, e))?;
133    let result = expr.evaluate(ctx).map_err(|e| {
134        anyhow::anyhow!("Failed to evaluate HCL expression '{}': {:?}", expr_str, e)
135    })?;
136    Ok(hcl_to_json(result))
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use hcl::eval::Context;
143    use serde_json::json;
144
145    #[test]
146    fn test_json_to_hcl_and_back() {
147        let original = json!({
148            "string": "hello",
149            "number": 123,
150            "bool": true,
151            "null": null,
152            "array": [1, 2, 3],
153            "object": {"nested": "value"}
154        });
155
156        let hcl_val = json_to_hcl(original.clone());
157        let back = hcl_to_json(hcl_val);
158
159        assert_eq!(original, back);
160    }
161
162    #[test]
163    fn test_evaluate_string_simple() {
164        let mut ctx = Context::new();
165        ctx.declare_var("name", "world");
166
167        let s = "hello ${name}";
168        let result = evaluate_string(s, &ctx).unwrap().unwrap();
169        assert_eq!(result, Value::String("hello world".to_string()));
170    }
171
172    #[test]
173    fn test_evaluate_string_raw_expr() {
174        let mut ctx = Context::new();
175        ctx.declare_var("val", 42);
176
177        let s = "${val}";
178        let result = evaluate_string(s, &ctx).unwrap().unwrap();
179        assert_eq!(result, json!(42));
180    }
181
182    #[test]
183    fn test_resolve_expressions_recursive() {
184        let mut ctx = Context::new();
185        ctx.declare_var("env", "prod");
186
187        let mut val = json!({
188            "service": "api-${env}",
189            "tags": ["cloud", "${env}"],
190            "config": {
191                "db_suffix": "_${env}"
192            }
193        });
194
195        resolve_expressions(&mut val, &ctx).unwrap();
196
197        assert_eq!(val["service"], "api-prod");
198        assert_eq!(val["tags"][1], "prod");
199        assert_eq!(val["config"]["db_suffix"], "_prod");
200    }
201}