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,
}
}
}