isoloom_render/hcl_builder/
expr.rs1use hcl::Identifier;
4use hcl::expr::{
5 BinaryOp, BinaryOperator, Conditional, Expression, ForExpr, FuncCall, Object, ObjectKey, Traversal, TraversalOperator, UnaryOp, UnaryOperator, Variable,
6};
7
8#[must_use]
10pub fn string(s: impl Into<String>) -> Expression {
11 Expression::String(s.into())
12}
13
14#[must_use]
16pub fn number(n: u64) -> Expression {
17 Expression::from(n)
18}
19
20#[must_use]
22pub fn boolean(b: bool) -> Expression {
23 Expression::Bool(b)
24}
25
26#[must_use]
28pub fn null() -> Expression {
29 Expression::Null
30}
31
32#[must_use]
35pub fn reference(path: &str) -> Expression {
36 let mut parts = path.split('.');
37 let root = Variable::sanitized(parts.next().unwrap_or_default());
38 let ops: Vec<TraversalOperator> = parts.map(|p| TraversalOperator::GetAttr(Identifier::sanitized(p))).collect();
39 if ops.is_empty() {
40 Expression::Variable(root)
41 } else {
42 Traversal::new(root, ops).into()
43 }
44}
45
46#[must_use]
48pub fn index(expr: Expression, i: u64) -> Expression {
49 Traversal::new(expr, [TraversalOperator::Index(number(i))]).into()
50}
51
52#[must_use]
54pub fn entry(expr: Expression, key: &str) -> Expression {
55 Traversal::new(expr, [TraversalOperator::Index(string(key))]).into()
56}
57
58#[must_use]
60pub fn attr(expr: Expression, name: &str) -> Expression {
61 Traversal::new(expr, [TraversalOperator::GetAttr(Identifier::sanitized(name))]).into()
62}
63
64#[must_use]
66pub fn call(name: &str, args: impl IntoIterator<Item = Expression>) -> Expression {
67 let mut c = FuncCall::new(Identifier::sanitized(name));
68 c.args = args.into_iter().collect();
69 c.into()
70}
71
72#[must_use]
74pub fn list(items: impl IntoIterator<Item = Expression>) -> Expression {
75 Expression::Array(items.into_iter().collect())
76}
77
78const KEYWORDS: [&str; 6] = ["for", "if", "in", "null", "true", "false"];
80
81#[must_use]
83pub fn object<K: AsRef<str>>(items: impl IntoIterator<Item = (K, Expression)>) -> Expression {
84 let object: Object<ObjectKey, Expression> = items
85 .into_iter()
86 .map(|(k, v)| {
87 let k = k.as_ref();
88 let key = match Identifier::new(k) {
89 Ok(i) if !KEYWORDS.contains(&k) => ObjectKey::Identifier(i),
90 _ => ObjectKey::Expression(string(k)),
91 };
92 (key, v)
93 })
94 .collect();
95 Expression::Object(object)
96}
97
98#[must_use]
100pub fn conditional(cond: Expression, yes: Expression, no: Expression) -> Expression {
101 Conditional::new(cond, yes, no).into()
102}
103
104#[must_use]
106pub fn binary(lhs: Expression, op: BinaryOperator, rhs: Expression) -> Expression {
107 BinaryOp::new(lhs, op, rhs).into()
108}
109
110#[must_use]
112pub fn not(expr: Expression) -> Expression {
113 UnaryOp::new(UnaryOperator::Not, expr).into()
114}
115
116#[must_use]
118pub fn for_list(var: &str, collection: Expression, value: Expression, cond: Option<Expression>) -> Expression {
119 let f = ForExpr::new(Identifier::sanitized(var), collection, value);
120 match cond {
121 Some(c) => f.with_cond_expr(c).into(),
122 None => f.into(),
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::hcl_builder::write::compact;
130
131 fn written(e: &Expression) -> String {
133 compact(e).expect("written")
134 }
135
136 #[test]
137 fn each_value_writes_its_hcl() {
138 assert_eq!(written(&string("a")), "\"a\"");
139 assert_eq!(written(&number(7)), "7");
140 assert_eq!((written(&boolean(true)), written(&boolean(false))), ("true".to_owned(), "false".to_owned()));
141 assert_eq!(written(&null()), "null");
142 assert_eq!(written(&list([number(1), string("b")])), "[1, \"b\"]");
143 }
144
145 #[test]
146 fn each_reference_writes_its_path() {
147 assert_eq!(written(&reference("var")), "var");
148 assert_eq!(written(&reference("aws_vpc.env.id")), "aws_vpc.env.id");
149 assert_eq!(written(&index(reference("x.list"), 2)), "x.list[2]");
150 assert_eq!(written(&attr(index(reference("x.list"), 0), "id")), "x.list[0].id");
151 assert_eq!(written(&entry(reference("var.images"), "debian-12")), r#"var.images["debian-12"]"#);
152 }
153
154 #[test]
155 fn each_operation_writes_its_hcl() {
156 assert_eq!(written(&call("max", [number(1), number(2)])), "max(1, 2)");
157 assert_eq!(written(&conditional(reference("c"), number(1), number(2))), "c ? 1 : 2");
158 assert_eq!(written(&binary(reference("a"), BinaryOperator::Eq, number(1))), "a == 1");
159 assert_eq!(written(¬(reference("a"))), "!a");
160 assert_eq!(written(&for_list("x", reference("xs"), reference("x"), None)), "[for x in xs : x]");
161 assert_eq!(
162 written(&for_list("x", reference("xs"), reference("x"), Some(reference("x.ok")))),
163 "[for x in xs : x if x.ok]"
164 );
165 }
166
167 #[test]
168 fn an_object_key_that_isnt_a_plain_identifier_is_quoted() {
169 let o = object([("web", number(1)), ("in", number(2)), ("a-b", number(3)), ("1x", number(4)), ("a.b", number(5))]);
170 assert_eq!(written(&o), "{ web = 1, \"in\" = 2, a-b = 3, \"1x\" = 4, \"a.b\" = 5 }");
171 }
172}