hq_rs/
query.rs

1//! use the [`hcl-rs`][hcl] crate to query HCL documents
2
3use std::error::Error;
4
5use hcl::{Block, Body, Expression, Identifier, ObjectKey};
6
7use crate::parser::Field;
8
9/// a portion of an HCL document that matched the provided filter
10pub enum QueryResult {
11    /// an HCL [`Expression`] matched the filter
12    Expr(Expression),
13    /// an HCL [`Block`] matched the filter
14    Block(Block),
15}
16
17impl QueryResult {
18    pub fn to_string(&self) -> Result<String, Box<dyn Error>> {
19        let s = match self {
20            // beware `hcl::to_string`!
21            // https://github.com/martinohmann/hcl-rs/issues/344
22            Self::Expr(expr) => hcl::format::to_string(expr)?,
23            Self::Block(block) => hcl::format::to_string(block)?,
24        };
25        Ok(s)
26    }
27}
28
29/// given a vector of [`Field`]s return a vector of [`QueryResult`]s
30///
31/// a result vector with multiple results indicates that multiple entities
32/// matched the provided filter
33pub fn query(fields: &mut Vec<Field>, body: &Body) -> Vec<QueryResult> {
34    if fields.is_empty() {
35        // our grammar/parser for filters won't allow an empty filter
36        unreachable!();
37    }
38
39    // take the first field and do a `Body` query
40    // e.g. `.foo.bar` will start with 'foo'
41    let field = fields.remove(0);
42    let mut query_result = body_query(&field, body);
43
44    // iteratively evaluate each subsequent field
45    // e.g. having handled 'foo' we move on to 'bar'
46    while !fields.is_empty() {
47        let field = fields.remove(0);
48        query_result = result_query(&field, query_result);
49    }
50
51    query_result
52}
53
54fn body_query(field: &Field, body: &Body) -> Vec<QueryResult> {
55    let mut matches = Vec::new();
56    let mut attr_matches = attr_query(&field.name, body);
57    matches.append(&mut attr_matches);
58    let mut block_matches = block_query(field, body);
59    matches.append(&mut block_matches);
60    matches
61}
62
63fn attr_query(field: &str, body: &Body) -> Vec<QueryResult> {
64    let mut matches = Vec::new();
65    for attr in body.attributes() {
66        if attr.key() == field {
67            matches.push(QueryResult::Expr(attr.expr().clone()));
68        }
69    }
70    matches
71}
72
73fn block_query(field: &Field, body: &Body) -> Vec<QueryResult> {
74    let mut matches = Vec::new();
75    for block in body.blocks() {
76        if block.identifier() != field.name {
77            continue;
78        }
79        if field.labels.is_empty() {
80            matches.push(QueryResult::Block(block.clone()));
81        }
82        for filter_label in &field.labels {
83            for block_label in block.labels() {
84                if block_label.as_str() == filter_label {
85                    matches.push(QueryResult::Block(block.clone()));
86                }
87            }
88        }
89    }
90    matches
91}
92
93fn result_query(field: &Field, query_results: Vec<QueryResult>) -> Vec<QueryResult> {
94    let mut matches = Vec::new();
95    for query_result in query_results {
96        match query_result {
97            QueryResult::Expr(expr) => {
98                if let Expression::Object(object) = expr {
99                    // some objects are keyed with an Identifier
100                    if let Ok(id) = Identifier::new(&field.name) {
101                        let key = ObjectKey::Identifier(id);
102                        if let Some(expr) = object.get(&key) {
103                            matches.push(QueryResult::Expr(expr.clone()));
104                        }
105                    }
106                    // some objects are keyed with a String Expression
107                    let key = ObjectKey::Expression(Expression::String(field.name.clone()));
108                    if let Some(expr) = object.get(&key) {
109                        matches.push(QueryResult::Expr(expr.clone()));
110                    }
111                }
112            }
113            QueryResult::Block(block) => {
114                let mut body_matches = body_query(field, block.body());
115                matches.append(&mut body_matches);
116            }
117        }
118    }
119    matches
120}