use anyhow::Result;
use hcl::expr::{Expression, ObjectKey, TraversalOperator};
use hcl::Body;
use serde_json::{Map, Number as JsonNumber, Value};
pub fn hcl_to_json_schema(body: &Body) -> Result<Value> {
let mut map = Map::new();
for block in body.blocks() {
let block_name = block.identifier();
let mut definition_map = Map::new();
for attr in block.body().attributes() {
definition_map.insert(attr.key().to_string(), hcl_expr_to_json(attr.expr())?);
}
let defs_entry = map
.entry(block_name)
.or_insert_with(|| Value::Object(Map::new()));
if let Value::Object(defs_map) = defs_entry {
if let Some(label) = block.labels().first() {
defs_map.insert(label.as_str().to_string(), Value::Object(definition_map));
}
}
}
let mut root_props = Map::new();
for attr in body.attributes() {
let key = attr.key().to_string();
let val = hcl_expr_to_json(attr.expr())?;
if is_property_definition(attr.expr()) {
root_props.insert(key, val);
} else {
map.insert(key, val);
}
}
if !root_props.is_empty() {
map.insert("properties".to_string(), Value::Object(root_props));
if !map.contains_key("type") {
map.insert("type".to_string(), Value::String("object".to_string()));
}
}
Ok(Value::Object(map))
}
fn is_property_definition(expr: &Expression) -> bool {
const TYPE_FUNCS: [&str; 7] = [
"string", "integer", "number", "boolean", "array", "object", "map",
];
match expr {
Expression::FuncCall(func) => TYPE_FUNCS.contains(&func.name.name.as_str()),
Expression::Traversal(_) => true,
_ => false,
}
}
pub fn hcl_expr_to_json(expr: &Expression) -> Result<Value> {
match expr {
Expression::Null => Ok(Value::Null),
Expression::Bool(b) => Ok(Value::Bool(*b)),
Expression::Number(n) => {
if let Some(i) = n.as_u64() {
Ok(Value::Number(JsonNumber::from(i)))
} else if let Some(i) = n.as_i64() {
Ok(Value::Number(JsonNumber::from(i)))
} else if let Some(f) = n.as_f64() {
Ok(Value::Number(JsonNumber::from_f64(f).unwrap()))
} else {
Ok(Value::Null)
}
}
Expression::String(s) => Ok(Value::String(s.clone())),
Expression::Array(arr) => {
let mut elements = Vec::new();
for item in arr {
elements.push(hcl_expr_to_json(item)?);
}
Ok(Value::Array(elements))
}
Expression::Object(obj) => {
let mut map = Map::new();
for (k, v) in obj {
let key_str = match k {
ObjectKey::Identifier(i) => i.to_string(),
ObjectKey::Expression(Expression::String(s)) => s.to_string(),
_ => continue,
};
map.insert(key_str, hcl_expr_to_json(v)?);
}
Ok(Value::Object(map))
}
Expression::FuncCall(func) => {
let name = func.name.name.as_str();
if [
"string", "integer", "number", "boolean", "array", "object", "map",
]
.contains(&name)
{
let mut map = Map::new();
map.insert("type".to_string(), Value::String(name.to_string()));
for arg in &func.args {
if let Expression::FuncCall(inner_func) = arg {
let inner_name = inner_func.name.name.as_str();
let is_type_func = [
"string", "integer", "number", "boolean", "array", "object", "map",
]
.contains(&inner_name);
if name == "array" && is_type_func {
map.insert("items".to_string(), hcl_expr_to_json(arg)?);
} else if let Some(first_arg) = inner_func.args.first() {
map.insert(inner_name.to_string(), hcl_expr_to_json(first_arg)?);
}
} else if name == "array" {
map.insert("items".to_string(), hcl_expr_to_json(arg)?);
}
}
return Ok(Value::Object(map));
}
Ok(Value::Null)
}
Expression::Traversal(traversal) => {
let mut path = String::from("#");
let expr = &traversal.expr;
if let Expression::Variable(var) = expr {
path.push('/');
path.push_str(var.as_str());
}
for op in &traversal.operators {
if let TraversalOperator::GetAttr(attr) = op {
path.push('/');
path.push_str(attr.as_str());
}
}
let mut map = Map::new();
map.insert("$ref".to_string(), Value::String(path));
Ok(Value::Object(map))
}
_ => Ok(Value::Null),
}
}