1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
use anyhow::Result;
use hcl::expr::{Expression, FuncCall, FuncName, Object, ObjectKey, Traversal, Variable};
use hcl::{Block, Body, Identifier, Number as HclNumber};
use serde_json::Value;
/// Serializes a JSON Schema (represented as serde_json::Value) into the HCL data format
/// using functional abstractions described in the specification.
pub fn json_schema_to_hcl(schema: &Value) -> Result<Body> {
let mut body = Body::builder();
if let Value::Object(map) = schema {
for (k, v) in map {
if k == "definitions" || k == "$defs" {
if let Value::Object(defs) = v {
for (def_k, def_v) in defs {
let mut block = Block::builder(k.as_str()).add_label(def_k.as_str());
if let Value::Object(def_map) = def_v {
// Encode the full definition object so schema constraints that
// commonly appear alongside `properties` (for example `required`,
// `additionalProperties`, `description`, `oneOf`, etc.) are preserved.
for (dk, dv) in def_map {
block = block.add_attribute((dk.as_str(), json_to_hcl_expr(dv)?));
}
}
body = body.add_block(block.build());
}
}
} else if k == "properties" {
// Top-level properties as attributes
if let Value::Object(props) = v {
for (pk, pv) in props {
body = body.add_attribute((pk.as_str(), json_to_hcl_expr(pv)?));
}
}
} else {
body = body.add_attribute((k.as_str(), json_to_hcl_expr(v)?));
}
}
}
Ok(body.build())
}
/// Helper to convert a JSON value into an HCL expression, applying functional abbreviations.
pub fn json_to_hcl_expr(v: &Value) -> Result<Expression> {
if let Value::Object(map) = v {
if let Some(Value::String(ref_path)) = map.get("$ref") {
// Convert $ref: "#/definitions/Address" -> definitions.Address
let parts: Vec<&str> = ref_path.trim_start_matches("#/").split('/').collect();
if parts.len() == 2 {
let traversal = Traversal::builder(Variable::new(parts[0])?)
.attr(parts[1])
.build();
return Ok(Expression::Traversal(Box::new(traversal)));
} else {
return Ok(Expression::String(ref_path.to_string())); // Fallback
}
}
if let Some(Value::String(t)) = map.get("type") {
if [
"string", "integer", "number", "boolean", "array", "object", "map",
]
.contains(&t.as_str())
{
let mut args = Vec::new();
// For arrays, the first argument is often the `items` schema
if t == "array" {
if let Some(items) = map.get("items") {
args.push(json_to_hcl_expr(items)?);
}
}
// Add constraints as function calls
for (k, val) in map {
if k == "type" || (t == "array" && k == "items") {
continue; // Skip type and already processed items
}
// For object properties, we might want to pass them as an object argument
if t == "object" && k == "properties" {
// Pass properties as a map expression
let mut props_map = Object::new();
if let Value::Object(p) = val {
for (pk, pv) in p {
props_map.insert(
ObjectKey::Identifier(Identifier::new(pk)?),
json_to_hcl_expr(pv)?,
);
}
}
let prop_func = FuncCall::builder("properties")
.arg(Expression::Object(props_map))
.build();
args.push(Expression::FuncCall(Box::new(prop_func)));
continue;
}
// Standard constraints e.g., format("email"), minimum(16)
let constraint_arg = json_to_hcl_expr(val)?;
let constraint_func = FuncCall::builder(k.as_str()).arg(constraint_arg).build();
args.push(Expression::FuncCall(Box::new(constraint_func)));
}
let mut func = FuncCall::new(FuncName::new(t.as_str()));
func.args = args;
return Ok(Expression::FuncCall(Box::new(func)));
}
}
// Complex logical types (oneOf, anyOf, allOf)
for logical in ["oneOf", "anyOf", "allOf"] {
if let Some(Value::Array(arr)) = map.get(logical) {
let mut elements = Vec::new();
for item in arr {
elements.push(json_to_hcl_expr(item)?);
}
let array_expr = Expression::Array(elements);
// Return as an object `{ oneOf = [...] }`
let mut obj = Object::new();
obj.insert(ObjectKey::Identifier(Identifier::new(logical)?), array_expr);
return Ok(Expression::Object(obj));
}
}
// Fallback to standard HCL object representation
let mut obj = Object::new();
for (k, val) in map {
obj.insert(
ObjectKey::Identifier(Identifier::new(k)?),
json_to_hcl_expr(val)?,
);
}
return Ok(Expression::Object(obj));
}
// Primitive mappings
match v {
Value::Null => Ok(Expression::Null),
Value::Bool(b) => Ok(Expression::Bool(*b)),
Value::Number(n) => {
if let Some(i) = n.as_u64() {
Ok(Expression::Number(HclNumber::from(i)))
} else if let Some(i) = n.as_i64() {
Ok(Expression::Number(HclNumber::from(i)))
} else if let Some(f) = n.as_f64() {
Ok(Expression::Number(HclNumber::from_f64(f).unwrap()))
} else {
Ok(Expression::Null)
}
}
Value::String(s) => Ok(Expression::String(s.clone())),
Value::Array(arr) => {
let mut elements = Vec::new();
for item in arr {
elements.push(json_to_hcl_expr(item)?);
}
Ok(Expression::Array(elements))
}
_ => Ok(Expression::Null),
}
}