use std::collections::VecDeque;
use hcl_edit::{expr::Object, structure::Body};
use crate::{
hcl::{
expr::{Expression, ObjectKey},
structure::{Block, BlockLabel},
template::{Element, StringTemplate},
},
types::EvaluatableInput,
};
use crate::{helpers::fs::FileLocation, types::diagnostics::Diagnostic};
#[derive(Debug, Clone)]
pub enum StringExpression {
Literal(String),
Template(StringTemplate),
}
#[derive(Debug)]
pub enum VisitorError {
MissingField(String),
MissingAttribute(String),
TypeMismatch(String, String),
TypeExpected(String),
}
pub fn visit_label(index: usize, name: &str, block: &Block) -> Result<String, VisitorError> {
let label = block.labels.get(index).ok_or(VisitorError::MissingField(name.to_string()))?;
match label {
BlockLabel::String(literal) => Ok(literal.to_string()),
BlockLabel::Ident(_e) => Err(VisitorError::TypeMismatch("string".into(), name.to_string())),
}
}
pub fn visit_optional_string_attribute(
field_name: &str,
block: &Block,
) -> Result<Option<StringExpression>, VisitorError> {
let Some(attribute) = block.body.get_attribute(field_name) else {
return Ok(None);
};
match attribute.value.clone() {
Expression::String(value) => Ok(Some(StringExpression::Literal(value.to_string()))),
Expression::StringTemplate(template) => Ok(Some(StringExpression::Template(template))),
_ => Err(VisitorError::TypeExpected("string".into())),
}
}
pub fn visit_required_string_literal_attribute(
field_name: &str,
block: &Block,
) -> Result<String, VisitorError> {
let Some(attribute) = block.body.get_attribute(field_name) else {
return Err(VisitorError::MissingAttribute(field_name.to_string()));
};
match attribute.value.clone() {
Expression::String(value) => Ok(value.to_string()),
_ => Err(VisitorError::TypeExpected("string".into())),
}
}
pub fn visit_optional_untyped_attribute(field_name: &str, block: &Block) -> Option<Expression> {
let Some(attribute) = block.body.get_attribute(field_name) else {
return None;
};
Some(attribute.value.clone())
}
pub fn get_object_expression_key(obj: &Object, key: &str) -> Option<hcl_edit::expr::ObjectValue> {
obj.into_iter()
.find(|(k, _)| k.as_ident().and_then(|i| Some(i.as_str().eq(key))).unwrap_or(false))
.map(|(_, v)| v)
.cloned()
}
pub fn build_diagnostics_for_unused_fields(
fields_names: Vec<&str>,
block: &Block,
location: &FileLocation,
) -> Vec<Diagnostic> {
let mut diagnostics = vec![];
for attr in block.body.attributes().into_iter() {
if fields_names.contains(&attr.key.as_str()) {
continue;
}
diagnostics.push(
Diagnostic::error_from_string(format!("'{}' field is unused", attr.key.as_str()))
.location(&location),
)
}
diagnostics
}
pub fn collect_constructs_references_from_expression<'a, T: EvaluatableInput>(
expr: &Expression,
input: Option<&'a T>,
dependencies: &mut Vec<(Option<&'a T>, Expression)>,
) {
match expr {
Expression::Variable(_) => {
dependencies.push((input, expr.clone()));
}
Expression::Array(elements) => {
for element in elements.iter() {
collect_constructs_references_from_expression(element, input, dependencies);
}
}
Expression::BinaryOp(op) => {
collect_constructs_references_from_expression(&op.lhs_expr, input, dependencies);
collect_constructs_references_from_expression(&op.rhs_expr, input, dependencies);
}
Expression::Bool(_)
| Expression::Null(_)
| Expression::Number(_)
| Expression::String(_) => return,
Expression::Conditional(cond) => {
collect_constructs_references_from_expression(&cond.cond_expr, input, dependencies);
collect_constructs_references_from_expression(&cond.false_expr, input, dependencies);
collect_constructs_references_from_expression(&cond.true_expr, input, dependencies);
}
Expression::ForExpr(for_expr) => {
collect_constructs_references_from_expression(
&for_expr.value_expr,
input,
dependencies,
);
if let Some(ref key_expr) = for_expr.key_expr {
collect_constructs_references_from_expression(&key_expr, input, dependencies);
}
if let Some(ref cond) = for_expr.cond {
collect_constructs_references_from_expression(&cond.expr, input, dependencies);
}
}
Expression::FuncCall(expr) => {
for arg in expr.args.iter() {
collect_constructs_references_from_expression(arg, input, dependencies);
}
}
Expression::HeredocTemplate(expr) => {
for element in expr.template.iter() {
match element {
Element::Directive(_) | Element::Literal(_) => {}
Element::Interpolation(interpolation) => {
collect_constructs_references_from_expression(
&interpolation.expr,
input,
dependencies,
);
}
}
}
}
Expression::Object(obj) => {
for (k, v) in obj.iter() {
match k {
ObjectKey::Expression(expr) => {
collect_constructs_references_from_expression(&expr, input, dependencies);
}
ObjectKey::Ident(_) => {}
}
collect_constructs_references_from_expression(&v.expr(), input, dependencies);
}
}
Expression::Parenthesis(expr) => {
collect_constructs_references_from_expression(&expr.inner(), input, dependencies);
}
Expression::StringTemplate(template) => {
for element in template.iter() {
match element {
Element::Directive(_) | Element::Literal(_) => {}
Element::Interpolation(interpolation) => {
collect_constructs_references_from_expression(
&interpolation.expr,
input,
dependencies,
);
}
}
}
}
Expression::Traversal(traversal) => {
let Expression::Variable(_) = traversal.expr else {
return;
};
dependencies.push((input.clone(), expr.clone()));
}
Expression::UnaryOp(op) => {
collect_constructs_references_from_expression(&op.expr, input, dependencies);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawHclContent(String);
impl RawHclContent {
pub fn from_string(s: String) -> Self {
RawHclContent(s)
}
pub fn from_file_location(file_location: &FileLocation) -> Result<Self, Diagnostic> {
file_location
.read_content_as_utf8()
.map_err(|e| {
Diagnostic::error_from_string(format!("{}", e.to_string())).location(&file_location)
})
.map(|s| RawHclContent(s))
}
pub fn into_blocks(&self) -> Result<VecDeque<Block>, Diagnostic> {
let content = crate::hcl::parser::parse_body(&self.0).map_err(|e| {
Diagnostic::error_from_string(format!("parsing error: {}", e.to_string()))
})?;
Ok(content.into_blocks().into_iter().collect::<VecDeque<Block>>())
}
pub fn into_block_instance(&self) -> Result<Block, Diagnostic> {
let mut blocks = self.into_blocks()?;
if blocks.len() != 1 {
return Err(Diagnostic::error_from_string(
"expected exactly one block instance".into(),
));
}
Ok(blocks.pop_front().unwrap())
}
pub fn to_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
let mut bytes = vec![0u8; 2 * self.0.len()];
crate::hex::encode_to_slice(self.0.clone(), &mut bytes).map_err(|e| {
Diagnostic::error_from_string(format!("failed to encode raw content: {e}"))
})?;
Ok(bytes)
}
pub fn to_string(&self) -> String {
self.0.clone()
}
pub fn from_block(block: &Block) -> Self {
RawHclContent::from_string(
Body::builder().block(block.clone()).build().to_string().trim().to_string(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_block_to_raw_hcl() {
let addon_block_str = r#"
addon "evm" {
test = "hi"
chain_id = input.chain_id
rpc_api_url = input.rpc_api_url
}
"#
.trim();
let signer_block_str = r#"
signer "deployer" "evm::web_wallet" {
expected_address = "0xCe246168E59dd8e28e367BB49b38Dc621768F425"
}
"#
.trim();
let runbook_block_str = r#"
runbook "test" {
location = "./embedded-runbook.json"
chain_id = input.chain_id
rpc_api_url = input.rpc_api_url
deployer = signer.deployer
}
"#
.trim();
let output_block_str = r#"
output "contract_address1" {
value = runbook.test.action.deploy1.contract_address
}
"#
.trim();
let input = format!(
r#"
{addon_block_str}
{signer_block_str}
{runbook_block_str}
{output_block_str}
"#
);
let raw_hcl = RawHclContent::from_string(input.trim().to_string());
let blocks = raw_hcl.into_blocks().unwrap();
assert_eq!(blocks.len(), 4);
let addon_block = RawHclContent::from_block(&blocks[0]).to_string();
assert_eq!(addon_block, addon_block_str);
let signer_block = RawHclContent::from_block(&blocks[1]).to_string();
assert_eq!(signer_block, signer_block_str);
let runbook_block = RawHclContent::from_block(&blocks[2]).to_string();
assert_eq!(runbook_block, runbook_block_str);
let output_block = RawHclContent::from_block(&blocks[3]).to_string();
assert_eq!(output_block, output_block_str);
}
}