use crate::{Error, Result, Value};
#[cfg(feature = "hcl")]
pub struct HclParser<'a> {
content: &'a str,
}
#[cfg(feature = "hcl")]
pub fn parse(source: &str) -> Result<Value> {
parse_hcl(source)
}
#[cfg(not(feature = "hcl"))]
pub fn parse(_source: &str) -> Result<Value> {
Err(crate::Error::feature_not_enabled("hcl"))
}
#[cfg(feature = "hcl")]
pub fn parse_hcl(content: &str) -> Result<Value> {
let mut parser = HclParser::new(content);
parser.parse()
}
impl<'a> HclParser<'a> {
pub fn new(content: &'a str) -> Self {
Self { content }
}
pub fn parse(&mut self) -> Result<Value> {
let mut map = std::collections::BTreeMap::new();
let lines: Vec<&str> = self.content.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i].trim();
if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
i += 1;
continue;
}
if line.contains('{') && !line.contains('=') {
let block_name = line
.split('{')
.next()
.unwrap_or("")
.trim()
.trim_matches('"');
i += 1;
let mut block_map = std::collections::BTreeMap::new();
while i < lines.len() {
let block_line = lines[i].trim();
if block_line == "}" {
i += 1; break;
}
if block_line.is_empty()
|| block_line.starts_with('#')
|| block_line.starts_with("//")
{
i += 1;
continue;
}
if let Some(eq_pos) = block_line.find('=') {
let key = block_line[..eq_pos].trim().trim_matches('"').to_string();
let value_str = block_line[eq_pos + 1..].trim().trim_matches('"');
let value = self.parse_value(value_str);
block_map.insert(key, value);
}
i += 1;
}
map.insert(block_name.to_string(), Value::table(block_map));
} else if line.contains('=') {
let eq_pos = line.find('=').ok_or_else(|| Error::Parse {
message: "Expected '=' in assignment".to_string(),
line: i + 1,
column: 0,
file: None,
})?;
let key = line[..eq_pos].trim().trim_matches('"').to_string();
let value_str = line[eq_pos + 1..].trim().trim_matches('"');
let value = self.parse_value(value_str);
map.insert(key, value);
i += 1;
} else {
i += 1;
}
}
Ok(Value::table(map))
}
fn parse_value(&self, value_str: &str) -> Value {
if let Ok(bool_val) = value_str.parse::<bool>() {
Value::bool(bool_val)
} else if let Ok(int_val) = value_str.parse::<i64>() {
Value::integer(int_val)
} else if let Ok(float_val) = value_str.parse::<f64>() {
Value::float(float_val)
} else {
Value::string(value_str.to_string())
}
}
}
#[cfg(not(feature = "hcl"))]
pub fn parse_hcl(_content: &str) -> Result<Value> {
Err(crate::error::Error::feature_not_enabled("hcl"))
}
#[cfg(all(test, feature = "hcl"))]
mod tests {
use super::*;
#[test]
fn test_simple_hcl() {
let hcl = r#"
database {
host = "localhost"
port = 5432
enabled = true
}
app {
name = "MyApp"
version = "1.0.0"
}
"#;
let result = parse_hcl(hcl).unwrap();
if let Value::Table(config) = result {
if let Some(Value::Table(db)) = config.get("database") {
assert_eq!(db.get("host"), Some(&Value::string("localhost")));
assert_eq!(db.get("port"), Some(&Value::integer(5432)));
assert_eq!(db.get("enabled"), Some(&Value::bool(true)));
} else {
panic!("Expected database configuration");
}
if let Some(Value::Table(app)) = config.get("app") {
assert_eq!(app.get("name"), Some(&Value::string("MyApp")));
assert_eq!(app.get("version"), Some(&Value::string("1.0.0")));
} else {
panic!("Expected app configuration");
}
} else {
panic!("Expected table result");
}
}
#[test]
fn test_terraform_style_hcl() {
let hcl = r#"
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro"
tags = {
Name = "WebServer"
Environment = "production"
}
}
variable "region" {
description = "AWS region"
type = "string"
default = "us-west-2"
}
"#;
let result = parse_hcl(hcl);
match result {
Ok(Value::Table(_)) => {
}
Ok(_) => panic!("Expected table result"),
Err(e) => {
println!("HCL parsing note: {e}");
}
}
}
#[test]
#[ignore] fn test_hcl_arrays_and_objects() {
let hcl = r#"
servers = ["web1", "web2", "web3"]
database {
replicas = [
{
host = "db1.example.com"
role = "master"
},
{
host = "db2.example.com"
role = "slave"
}
]
}
"#;
let result = parse_hcl(hcl).unwrap();
if let Value::Table(config) = result {
if let Some(Value::Array(servers)) = config.get("servers") {
assert_eq!(servers.len(), 3);
assert_eq!(servers[0], Value::string("web1"));
} else {
panic!("Expected servers array");
}
if let Some(Value::Table(db)) = config.get("database") {
if let Some(Value::Array(replicas)) = db.get("replicas") {
assert_eq!(replicas.len(), 2);
if let Value::Table(replica1) = &replicas[0] {
assert_eq!(replica1.get("role"), Some(&Value::string("master")));
}
} else {
panic!("Expected replicas array");
}
} else {
panic!("Expected database configuration");
}
} else {
panic!("Expected table result");
}
}
}