hcl/structure/
json_spec.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use super::{Block, Body, Structure};
use crate::{Expression, Identifier, Map, Value};
use indexmap::map::Entry;

/// A trait to convert an HCL structure into its [JSON representation][json-spec].
///
/// This is used internally by the `Body` and `Block` types to convert into an `Expression`.
///
/// [json-spec]: https://github.com/hashicorp/hcl/blob/main/json/spec.md#blocks
pub(crate) trait IntoJsonSpec: Sized {
    /// Converts a value to an expression that conforms to the HCL JSON specification.
    ///
    /// Provides a default implementation which converts the result of `into_nodes` into an
    /// `Expression` and unsually does not need to be overridden.
    fn into_json_spec(self) -> Expression {
        Expression::from_iter(self.into_json_nodes())
    }

    /// Converts the value into a map of nodes.
    ///
    /// The detour over a map of nodes is necessary as HCL blocks with the same identifier and
    /// labels need to be merged so that the `Expression` resulting from `into_json_spec` conforms
    /// to the HCL JSON specification.
    fn into_json_nodes(self) -> Map<String, JsonNode>;
}

impl IntoJsonSpec for Body {
    fn into_json_nodes(self) -> Map<String, JsonNode> {
        self.into_iter().fold(Map::new(), |mut map, structure| {
            match structure {
                Structure::Attribute(attr) => {
                    map.insert(attr.key.into_inner(), JsonNode::Expr(attr.expr));
                }
                Structure::Block(block) => {
                    for (key, node) in block.into_json_nodes() {
                        node.deep_merge_into(&mut map, key);
                    }
                }
            };

            map
        })
    }
}

impl IntoJsonSpec for Block {
    fn into_json_nodes(self) -> Map<String, JsonNode> {
        let mut labels = self.labels.into_iter();

        let node = match labels.next() {
            Some(label) => {
                let block = Block {
                    identifier: Identifier::unchecked(label.into_inner()),
                    labels: labels.collect(),
                    body: self.body,
                };

                JsonNode::Map(block.into_json_nodes())
            }
            None => JsonNode::Body(vec![self.body]),
        };

        std::iter::once((self.identifier.into_inner(), node)).collect()
    }
}

pub(crate) enum JsonNode {
    Map(Map<String, JsonNode>),
    Body(Vec<Body>),
    Expr(Expression),
}

impl From<JsonNode> for Expression {
    fn from(node: JsonNode) -> Self {
        match node {
            JsonNode::Map(map) => Expression::from_iter(map),
            JsonNode::Body(mut vec) => {
                // Flatten as per the [HCL JSON spec][json-spec].
                //
                // > After any labelling levels, the next nested value is either a JSON
                // > object representing a single block body, or a JSON array of JSON
                // > objects that each represent a single block body.
                //
                // [json-spec]: https://github.com/hashicorp/hcl/blob/main/json/spec.md#blocks
                if vec.len() == 1 {
                    vec.remove(0).into()
                } else {
                    vec.into()
                }
            }
            JsonNode::Expr(expr) => expr,
        }
    }
}

impl<T> From<T> for Expression
where
    T: IntoJsonSpec,
{
    fn from(value: T) -> Expression {
        value.into_json_spec()
    }
}

impl<T> From<T> for Value
where
    T: IntoJsonSpec,
{
    fn from(value: T) -> Value {
        Value::from(value.into_json_spec())
    }
}

impl JsonNode {
    fn deep_merge_into(self, map: &mut Map<String, JsonNode>, key: String) {
        match map.entry(key) {
            Entry::Occupied(o) => o.into_mut().deep_merge(self),
            Entry::Vacant(v) => {
                v.insert(self);
            }
        }
    }

    fn deep_merge(&mut self, other: JsonNode) {
        match (self, other) {
            (JsonNode::Map(lhs), JsonNode::Map(rhs)) => {
                for (key, node) in rhs {
                    node.deep_merge_into(lhs, key);
                }
            }
            (JsonNode::Body(lhs), JsonNode::Body(mut rhs)) => {
                lhs.append(&mut rhs);
            }
            (lhs, rhs) => *lhs = rhs,
        }
    }
}