tfparser_core/eval/
adapter.rs1use std::sync::Arc;
23
24use hcl::{Map as HclMap, Number, Value as HclValue};
25
26use crate::ir::Value;
27
28#[must_use]
40pub fn value_to_hcl(v: &Value) -> HclValue {
41 match v {
42 Value::Null => HclValue::Null,
43 Value::Bool(b) => HclValue::Bool(*b),
44 Value::Int(n) => HclValue::Number(Number::from(*n)),
45 Value::Number(f) => Number::from_f64(*f).map_or(HclValue::Null, HclValue::Number),
46 Value::Str(s) => HclValue::String(s.to_string()),
47 Value::List(items) => HclValue::Array(items.iter().map(value_to_hcl).collect()),
48 Value::Map(entries) => {
49 let mut out: HclMap<String, HclValue> = HclMap::with_capacity(entries.len());
50 for (key, val) in entries {
51 out.insert(key.to_string(), value_to_hcl(val));
52 }
53 HclValue::Object(out)
54 }
55 }
56}
57
58#[must_use]
65pub fn hcl_to_value(v: &HclValue) -> Value {
66 match v {
67 HclValue::Null => Value::Null,
68 HclValue::Bool(b) => Value::Bool(*b),
69 HclValue::Number(n) => n
70 .as_i64()
71 .map_or_else(|| Value::Number(n.as_f64().unwrap_or(0.0)), Value::Int),
72 HclValue::String(s) => Value::Str(Arc::from(s.as_str())),
73 HclValue::Array(items) => Value::List(items.iter().map(hcl_to_value).collect()),
74 HclValue::Object(entries) => Value::Map(
75 entries
76 .iter()
77 .map(|(k, val)| (Arc::from(k.as_str()), hcl_to_value(val)))
78 .collect(),
79 ),
80 }
81}
82
83#[cfg(test)]
84#[allow(
85 clippy::unwrap_used,
86 clippy::expect_used,
87 clippy::panic,
88 clippy::indexing_slicing
89)]
90mod tests {
91 use super::*;
92 use crate::ir::Map;
93
94 #[test]
95 fn test_should_round_trip_scalars() {
96 for v in [
97 Value::Null,
98 Value::Bool(true),
99 Value::Bool(false),
100 Value::Int(0),
101 Value::Int(42),
102 Value::Int(-1),
103 Value::Int(i64::MAX),
104 Value::Int(i64::MIN),
105 Value::Number(1.5),
106 Value::Str(Arc::from("hello")),
107 ] {
108 let h = value_to_hcl(&v);
109 let back = hcl_to_value(&h);
110 assert_eq!(v, back, "round trip mismatch: {v:?} → {h:?} → {back:?}");
111 }
112 }
113
114 #[test]
115 fn test_should_round_trip_nested_collections() {
116 let v = Value::Map(vec![
117 (Arc::from("a"), Value::Int(1)),
118 (
119 Arc::from("list"),
120 Value::List(vec![Value::Bool(true), Value::Str(Arc::from("x"))]),
121 ),
122 (
123 Arc::from("nested"),
124 Value::Map(vec![(Arc::from("z"), Value::Number(2.5))]),
125 ),
126 ]);
127 let back = hcl_to_value(&value_to_hcl(&v));
128 assert_eq!(v, back);
129 }
130
131 #[test]
132 fn test_should_preserve_map_insertion_order() {
133 let m: Map = vec![
134 (Arc::from("z"), Value::Int(1)),
135 (Arc::from("a"), Value::Int(2)),
136 (Arc::from("m"), Value::Int(3)),
137 ];
138 let v = Value::Map(m.clone());
139 let back = hcl_to_value(&value_to_hcl(&v));
140 let Value::Map(out) = back else {
141 panic!("expected Map");
142 };
143 let keys: Vec<&str> = out.iter().map(|(k, _)| k.as_ref()).collect();
144 assert_eq!(keys, vec!["z", "a", "m"]);
145 }
146
147 #[test]
148 fn test_should_collapse_nan_and_infinity_to_null() {
149 for f in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
150 let v = Value::Number(f);
151 assert_eq!(value_to_hcl(&v), HclValue::Null);
152 }
153 }
154
155 #[test]
156 fn test_should_prefer_int_fast_path_on_back_conversion() {
157 let h = HclValue::Number(Number::from(42_i64));
158 match hcl_to_value(&h) {
159 Value::Int(42) => {}
160 other => panic!("expected Value::Int(42), got {other:?}"),
161 }
162 }
163}