Skip to main content

txtx_addon_kit/helpers/
hcl.rs

1use std::collections::VecDeque;
2
3use hcl_edit::{expr::Object, structure::Body};
4
5use crate::{
6    hcl::{
7        expr::{Expression, ObjectKey},
8        structure::{Block, BlockLabel},
9        template::{Element, StringTemplate},
10    },
11    types::EvaluatableInput,
12};
13
14use crate::{helpers::fs::FileLocation, types::diagnostics::Diagnostic};
15
16#[derive(Debug, Clone)]
17pub enum StringExpression {
18    Literal(String),
19    Template(StringTemplate),
20}
21
22#[derive(Debug)]
23pub enum VisitorError {
24    MissingField(String),
25    MissingAttribute(String),
26    TypeMismatch(String, String),
27    TypeExpected(String),
28}
29
30pub fn visit_label(index: usize, name: &str, block: &Block) -> Result<String, VisitorError> {
31    let label = block.labels.get(index).ok_or(VisitorError::MissingField(name.to_string()))?;
32    match label {
33        BlockLabel::String(literal) => Ok(literal.to_string()),
34        BlockLabel::Ident(_e) => Err(VisitorError::TypeMismatch("string".into(), name.to_string())),
35    }
36}
37
38pub fn visit_optional_string_attribute(
39    field_name: &str,
40    block: &Block,
41) -> Result<Option<StringExpression>, VisitorError> {
42    let Some(attribute) = block.body.get_attribute(field_name) else {
43        return Ok(None);
44    };
45
46    match attribute.value.clone() {
47        Expression::String(value) => Ok(Some(StringExpression::Literal(value.to_string()))),
48        Expression::StringTemplate(template) => Ok(Some(StringExpression::Template(template))),
49        _ => Err(VisitorError::TypeExpected("string".into())),
50    }
51}
52
53pub fn visit_required_string_literal_attribute(
54    field_name: &str,
55    block: &Block,
56) -> Result<String, VisitorError> {
57    let Some(attribute) = block.body.get_attribute(field_name) else {
58        return Err(VisitorError::MissingAttribute(field_name.to_string()));
59    };
60
61    match attribute.value.clone() {
62        Expression::String(value) => Ok(value.to_string()),
63        _ => Err(VisitorError::TypeExpected("string".into())),
64    }
65}
66
67pub fn visit_optional_untyped_attribute(field_name: &str, block: &Block) -> Option<Expression> {
68    let Some(attribute) = block.body.get_attribute(field_name) else {
69        return None;
70    };
71    Some(attribute.value.clone())
72}
73
74pub fn get_object_expression_key(obj: &Object, key: &str) -> Option<hcl_edit::expr::ObjectValue> {
75    obj.into_iter()
76        .find(|(k, _)| k.as_ident().and_then(|i| Some(i.as_str().eq(key))).unwrap_or(false))
77        .map(|(_, v)| v)
78        .cloned()
79}
80
81pub fn build_diagnostics_for_unused_fields(
82    fields_names: Vec<&str>,
83    block: &Block,
84    location: &FileLocation,
85) -> Vec<Diagnostic> {
86    let mut diagnostics = vec![];
87    for attr in block.body.attributes().into_iter() {
88        if fields_names.contains(&attr.key.as_str()) {
89            continue;
90        }
91        diagnostics.push(
92            Diagnostic::error_from_string(format!("'{}' field is unused", attr.key.as_str()))
93                .location(&location),
94        )
95    }
96    diagnostics
97}
98
99/// Takes an HCL block and traverses all inner expressions and blocks,
100/// recursively collecting all the references to constructs (variables and traversals).
101pub fn collect_constructs_references_from_block<'a>(
102    block: &Block,
103    input: Option<Box<dyn EvaluatableInput>>,
104    dependencies: &mut Vec<(Option<Box<dyn EvaluatableInput>>, Expression)>,
105) {
106    for attribute in block.body.attributes() {
107        let expr = attribute.value.clone();
108        let mut references = vec![];
109        collect_constructs_references_from_expression(&expr, input.clone(), &mut references);
110        dependencies.append(&mut references);
111    }
112    for block in block.body.blocks() {
113        collect_constructs_references_from_block(block, input.clone(), dependencies);
114    }
115}
116
117/// Takes an HCL expression and boils it down to a Variable or Traversal expression,
118/// pushing those low level expressions to the dependencies vector. For example:
119/// ```hcl
120/// val = [variable.a, variable.b]
121/// ```
122/// will push `variable.a` and `variable.b` to the dependencies vector.
123pub fn collect_constructs_references_from_expression<'a>(
124    expr: &Expression,
125    input: Option<Box<dyn EvaluatableInput>>,
126    dependencies: &mut Vec<(Option<Box<dyn EvaluatableInput>>, Expression)>,
127) {
128    match expr {
129        Expression::Variable(_) => {
130            dependencies.push((input.clone(), expr.clone()));
131        }
132        Expression::Array(elements) => {
133            for element in elements.iter() {
134                collect_constructs_references_from_expression(element, input.clone(), dependencies);
135            }
136        }
137        Expression::BinaryOp(op) => {
138            collect_constructs_references_from_expression(
139                &op.lhs_expr,
140                input.clone(),
141                dependencies,
142            );
143            collect_constructs_references_from_expression(
144                &op.rhs_expr,
145                input.clone(),
146                dependencies,
147            );
148        }
149        Expression::Bool(_)
150        | Expression::Null(_)
151        | Expression::Number(_)
152        | Expression::String(_) => return,
153        Expression::Conditional(cond) => {
154            collect_constructs_references_from_expression(
155                &cond.cond_expr,
156                input.clone(),
157                dependencies,
158            );
159            collect_constructs_references_from_expression(
160                &cond.false_expr,
161                input.clone(),
162                dependencies,
163            );
164            collect_constructs_references_from_expression(
165                &cond.true_expr,
166                input.clone(),
167                dependencies,
168            );
169        }
170        Expression::ForExpr(for_expr) => {
171            collect_constructs_references_from_expression(
172                &for_expr.value_expr,
173                input.clone(),
174                dependencies,
175            );
176            if let Some(ref key_expr) = for_expr.key_expr {
177                collect_constructs_references_from_expression(
178                    &key_expr,
179                    input.clone(),
180                    dependencies,
181                );
182            }
183            if let Some(ref cond) = for_expr.cond {
184                collect_constructs_references_from_expression(
185                    &cond.expr,
186                    input.clone(),
187                    dependencies,
188                );
189            }
190        }
191        Expression::FuncCall(expr) => {
192            for arg in expr.args.iter() {
193                collect_constructs_references_from_expression(arg, input.clone(), dependencies);
194            }
195        }
196        Expression::HeredocTemplate(expr) => {
197            for element in expr.template.iter() {
198                match element {
199                    Element::Directive(_) | Element::Literal(_) => {}
200                    Element::Interpolation(interpolation) => {
201                        collect_constructs_references_from_expression(
202                            &interpolation.expr,
203                            input.clone(),
204                            dependencies,
205                        );
206                    }
207                }
208            }
209        }
210        Expression::Object(obj) => {
211            for (k, v) in obj.iter() {
212                match k {
213                    ObjectKey::Expression(expr) => {
214                        collect_constructs_references_from_expression(
215                            &expr,
216                            input.clone(),
217                            dependencies,
218                        );
219                    }
220                    ObjectKey::Ident(_) => {}
221                }
222                collect_constructs_references_from_expression(
223                    &v.expr(),
224                    input.clone(),
225                    dependencies,
226                );
227            }
228        }
229        Expression::Parenthesis(expr) => {
230            collect_constructs_references_from_expression(
231                &expr.inner(),
232                input.clone(),
233                dependencies,
234            );
235        }
236        Expression::StringTemplate(template) => {
237            for element in template.iter() {
238                match element {
239                    Element::Directive(_) | Element::Literal(_) => {}
240                    Element::Interpolation(interpolation) => {
241                        collect_constructs_references_from_expression(
242                            &interpolation.expr,
243                            input.clone(),
244                            dependencies,
245                        );
246                    }
247                }
248            }
249        }
250        Expression::Traversal(traversal) => {
251            let Expression::Variable(_) = traversal.expr else {
252                return;
253            };
254            dependencies.push((input.clone(), expr.clone()));
255        }
256        Expression::UnaryOp(op) => {
257            collect_constructs_references_from_expression(&op.expr, input, dependencies);
258        }
259    }
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct RawHclContent(String);
264impl RawHclContent {
265    pub fn from_string(s: String) -> Self {
266        RawHclContent(s)
267    }
268    pub fn from_file_location(file_location: &FileLocation) -> Result<Self, Diagnostic> {
269        file_location
270            .read_content_as_utf8()
271            .map_err(|e| {
272                Diagnostic::error_from_string(format!("{}", e.to_string())).location(&file_location)
273            })
274            .map(|s| RawHclContent(s))
275    }
276
277    pub fn into_blocks(&self) -> Result<VecDeque<Block>, Diagnostic> {
278        let content = crate::hcl::parser::parse_body(&self.0).map_err(|e| {
279            Diagnostic::error_from_string(format!("parsing error: {}", e.to_string()))
280        })?;
281        Ok(content.into_blocks().into_iter().collect::<VecDeque<Block>>())
282    }
283
284    /// Parse the HCL content into OwnedTypedBlocks with construct types resolved at parse time.
285    ///
286    /// This is the preferred method for parsing blocks as it provides type-safe access
287    /// to construct types (Action, Variable, etc.) instead of string matching.
288    ///
289    /// Returns owned typed blocks that can be consumed via iteration.
290    pub fn into_typed_blocks(&self) -> Result<VecDeque<crate::types::typed_block::OwnedTypedBlock>, Diagnostic> {
291        Ok(self.into_blocks()?
292            .into_iter()
293            .map(crate::types::typed_block::OwnedTypedBlock::new)
294            .collect())
295    }
296
297    pub fn into_block_instance(&self) -> Result<Block, Diagnostic> {
298        let mut blocks = self.into_blocks()?;
299        if blocks.len() != 1 {
300            return Err(Diagnostic::error_from_string(
301                "expected exactly one block instance".into(),
302            ));
303        }
304        Ok(blocks.pop_front().unwrap())
305    }
306
307    pub fn to_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
308        let mut bytes = vec![0u8; 2 * self.0.len()];
309        crate::hex::encode_to_slice(self.0.clone(), &mut bytes).map_err(|e| {
310            Diagnostic::error_from_string(format!("failed to encode raw content: {e}"))
311        })?;
312        Ok(bytes)
313    }
314    pub fn to_string(&self) -> String {
315        self.0.clone()
316    }
317    pub fn from_block(block: &Block) -> Self {
318        RawHclContent::from_string(
319            Body::builder().block(block.clone()).build().to_string().trim().to_string(),
320        )
321    }
322}
323
324#[cfg(test)]
325mod tests {
326
327    use super::*;
328
329    #[test]
330    fn test_block_to_raw_hcl() {
331        let addon_block_str = r#"
332            addon "evm" {
333                test = "hi"
334                chain_id = input.chain_id
335                rpc_api_url = input.rpc_api_url
336            }
337        "#
338        .trim();
339
340        let signer_block_str = r#"
341        signer "deployer" "evm::web_wallet" {
342            expected_address = "0xCe246168E59dd8e28e367BB49b38Dc621768F425"
343        }
344        "#
345        .trim();
346
347        let runbook_block_str = r#"
348            runbook "test" {
349                location = "./embedded-runbook.json"
350                chain_id = input.chain_id
351                rpc_api_url = input.rpc_api_url
352                deployer = signer.deployer
353            }
354        "#
355        .trim();
356
357        let output_block_str = r#"
358            output "contract_address1" {
359                value = runbook.test.action.deploy1.contract_address
360            }
361        "#
362        .trim();
363
364        let input = format!(
365            r#"
366        {addon_block_str}
367
368        {signer_block_str}
369
370        {runbook_block_str}
371
372        {output_block_str}
373        "#
374        );
375
376        let raw_hcl = RawHclContent::from_string(input.trim().to_string());
377        let blocks = raw_hcl.into_blocks().unwrap();
378        assert_eq!(blocks.len(), 4);
379        let addon_block = RawHclContent::from_block(&blocks[0]).to_string();
380        assert_eq!(addon_block, addon_block_str);
381        let signer_block = RawHclContent::from_block(&blocks[1]).to_string();
382        assert_eq!(signer_block, signer_block_str);
383        let runbook_block = RawHclContent::from_block(&blocks[2]).to_string();
384        assert_eq!(runbook_block, runbook_block_str);
385        let output_block = RawHclContent::from_block(&blocks[3]).to_string();
386        assert_eq!(output_block, output_block_str);
387    }
388
389    #[test]
390    fn test_collect_constructs_references_from_block() {
391        let input = r#"
392            runbook "test" {
393                location = "./embedded-runbook.json"
394                chain_id = input.chain_id
395                rpc_api_url = input.rpc_api_url
396                deployer = signer.deployer
397                arr = [variable.a, variable.b]
398                my_map {
399                    key1 = variable.a
400                    my_inner_map {
401                        key2 = variable.b
402                    }
403                }
404            }
405        "#;
406
407        let raw_hcl = RawHclContent::from_string(input.trim().to_string());
408        let block = raw_hcl.into_block_instance().unwrap();
409        let mut dependencies = vec![];
410        collect_constructs_references_from_block(
411            &block,
412            None::<Box<dyn EvaluatableInput>>,
413            &mut dependencies,
414        );
415
416        assert_eq!(dependencies.len(), 7);
417    }
418
419    #[test]
420    fn test_collect_constructs_references_expression() {
421        let input = r#"
422            runbook "test" {
423                location = "./embedded-runbook.json"
424                chain_id = input.chain_id
425                rpc_api_url = input.rpc_api_url
426                deployer = signer.deployer
427                arr = [variable.a, variable.b]
428                my_map {
429                    key1 = variable.a
430                    my_inner_map {
431                        key2 = variable.b
432                    }
433                }
434            }
435        "#;
436
437        let raw_hcl = RawHclContent::from_string(input.trim().to_string());
438        let block = raw_hcl.into_block_instance().unwrap();
439        let attribute = block.body.get_attribute("chain_id").unwrap();
440
441        let mut dependencies = vec![];
442        collect_constructs_references_from_expression(
443            &attribute.value,
444            None::<Box<dyn EvaluatableInput>>,
445            &mut dependencies,
446        );
447
448        assert_eq!(dependencies.len(), 1);
449    }
450}