Skip to main content

isoloom_render/hcl_builder/
expr.rs

1//! Expression constructors: short names for the `hcl-rs` expressions a module is made of.
2
3use hcl::Identifier;
4use hcl::expr::{
5    BinaryOp, BinaryOperator, Conditional, Expression, ForExpr, FuncCall, Object, ObjectKey, Traversal, TraversalOperator, UnaryOp, UnaryOperator, Variable,
6};
7
8/// A string value (escaped when written).
9#[must_use]
10pub fn string(s: impl Into<String>) -> Expression {
11    Expression::String(s.into())
12}
13
14/// A whole number.
15#[must_use]
16pub fn number(n: u64) -> Expression {
17    Expression::from(n)
18}
19
20/// `true` or `false`.
21#[must_use]
22pub fn boolean(b: bool) -> Expression {
23    Expression::Bool(b)
24}
25
26/// `null`.
27#[must_use]
28pub fn null() -> Expression {
29    Expression::Null
30}
31
32/// A reference by its dotted path: `var.node`, `aws_vpc.env.id`. Each part is an identifier
33/// (resource names come from `res`, which keeps them so).
34#[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/// `expr[i]`.
47#[must_use]
48pub fn index(expr: Expression, i: u64) -> Expression {
49    Traversal::new(expr, [TraversalOperator::Index(number(i))]).into()
50}
51
52/// `expr["key"]`: a map's entry by its key.
53#[must_use]
54pub fn entry(expr: Expression, key: &str) -> Expression {
55    Traversal::new(expr, [TraversalOperator::Index(string(key))]).into()
56}
57
58/// `expr.name`: an attribute of an expression that isn't a plain path (`list[0].id`).
59#[must_use]
60pub fn attr(expr: Expression, name: &str) -> Expression {
61    Traversal::new(expr, [TraversalOperator::GetAttr(Identifier::sanitized(name))]).into()
62}
63
64/// A function call.
65#[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/// A list.
73#[must_use]
74pub fn list(items: impl IntoIterator<Item = Expression>) -> Expression {
75    Expression::Array(items.into_iter().collect())
76}
77
78/// Words a bare object key can't be: HCL reads them as a `for` expression or a literal.
79const KEYWORDS: [&str; 6] = ["for", "if", "in", "null", "true", "false"];
80
81/// An object, in the order given. A key that is an identifier is written bare, any other quoted.
82#[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/// `cond ? yes : no`.
99#[must_use]
100pub fn conditional(cond: Expression, yes: Expression, no: Expression) -> Expression {
101    Conditional::new(cond, yes, no).into()
102}
103
104/// `lhs <op> rhs`.
105#[must_use]
106pub fn binary(lhs: Expression, op: BinaryOperator, rhs: Expression) -> Expression {
107    BinaryOp::new(lhs, op, rhs).into()
108}
109
110/// `!expr`.
111#[must_use]
112pub fn not(expr: Expression) -> Expression {
113    UnaryOp::new(UnaryOperator::Not, expr).into()
114}
115
116/// `[for <var> in <collection> : <value> if <cond>]`.
117#[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    /// An expression on one line, as a module writes a short one.
132    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(&not(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}