stormchaser_model/
hcl_eval.rs1use anyhow::Result;
4use hcl::eval::{Context as HclContext, Evaluate};
5use hcl::Value as HclValue;
6use regex::Regex;
7use serde_json::Value;
8
9pub 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
32pub 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
63pub 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
98pub 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
128pub 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}