Repository: martinohmann/hcl-rs
Pull request #65 (2022-09-07 20:13:06)

feat: add `Expression::SubExpr`
This adds supports for parsing and serializing sub expressions (expressions wrapped in parenthesis).


Patch:
diff --git a/src/format/impls.rs b/src/format/impls.rs
index 641dd82..f770961 100644
--- a/src/format/impls.rs
+++ b/src/format/impls.rs
@@ -108,6 +108,12 @@ impl Format for Expression {
             Expression::VariableExpr(ident) => ident.format(fmt),
             Expression::ElementAccess(access) => access.format(fmt),
             Expression::FuncCall(func_call) => func_call.format(fmt),
+            Expression::SubExpr(expr) => {
+                fmt.write_all(b"(")?;
+                expr.format(fmt)?;
+                fmt.write_all(b")")?;
+                Ok(())
+            }
         }
     }
 }
diff --git a/src/parser/grammar/hcl.pest b/src/parser/grammar/hcl.pest
index 37aed81..02f6828 100644
--- a/src/parser/grammar/hcl.pest
+++ b/src/parser/grammar/hcl.pest
@@ -12,7 +12,8 @@ BlockBody = { "{" ~ Body ~ "}" }
 // Expressions
 Expression    = _{ Conditional | Operation | ExprTerm }
 ExprTerm      =  { ExprTermInner ~ (Splat | GetAttr | Index)* }
-ExprTermInner = _{ Value | TemplateExpr | FunctionCall | VariableExpr | ForExpr | ("(" ~ Expression ~ ")") }
+ExprTermInner = _{ Value | TemplateExpr | FunctionCall | VariableExpr | ForExpr | SubExpression }
+SubExpression =  { "(" ~ Expression ~ ")" }
 
 // Values
 Value = _{ LiteralValue | CollectionValue }
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 4da7702..5617b92 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -173,6 +173,7 @@ fn parse_expr_term(pair: Pair<Rule>) -> Result<Expression> {
         Rule::Object => parse_object(pair).map(Expression::Object)?,
         Rule::VariableExpr => Expression::VariableExpr(parse_ident(pair).into()),
         Rule::FunctionCall => Expression::FuncCall(Box::new(parse_func_call(pair)?)),
+        Rule::SubExpression => Expression::SubExpr(Box::new(parse_expression(inner(pair))?)),
         // @TODO(mohmann): Process ForExpr etc.
         _ => Expression::Raw(raw_expression(pair.as_str())),
     };
diff --git a/src/structure/de.rs b/src/structure/de.rs
index 4b86e0c..c19652c 100644
--- a/src/structure/de.rs
+++ b/src/structure/de.rs
@@ -267,6 +267,7 @@ impl<'de> de::Deserializer<'de> for Expression {
             Expression::FuncCall(func_call) => {
                 func_call.into_deserializer().deserialize_any(visitor)
             }
+            Expression::SubExpr(expr) => expr.into_deserializer().deserialize_any(visitor),
         }
     }
 
@@ -297,6 +298,7 @@ impl VariantName for Expression {
             Expression::VariableExpr(_) => "VariableExpr",
             Expression::ElementAccess(_) => "ElementAccess",
             Expression::FuncCall(_) => "FuncCall",
+            Expression::SubExpr(_) => "SubExpr",
         }
     }
 }
@@ -329,6 +331,7 @@ impl<'de> de::VariantAccess<'de> for Expression {
     {
         match self {
             Expression::TemplateExpr(expr) => seed.deserialize(expr.into_deserializer()),
+            Expression::SubExpr(expr) => seed.deserialize(expr.into_deserializer()),
             value => seed.deserialize(value.into_deserializer()),
         }
     }
diff --git a/src/structure/expression.rs b/src/structure/expression.rs
index 2901ab0..72862eb 100644
--- a/src/structure/expression.rs
+++ b/src/structure/expression.rs
@@ -36,6 +36,8 @@ pub enum Expression {
     ElementAccess(Box<ElementAccess>),
     /// Represents a function call.
     FuncCall(Box<FuncCall>),
+    /// Represents a sub-expression that is wrapped in parenthesis.
+    SubExpr(Box<Expression>),
     /// Represents a raw HCL expression. This includes any expression kind that does match any of
     /// the enum variants above. See [`RawExpression`] for more details.
     Raw(RawExpression),
@@ -67,6 +69,7 @@ impl From<Expression> for Value {
             Expression::Array(array) => array.into_iter().collect(),
             Expression::Object(object) => object.into_iter().collect(),
             Expression::TemplateExpr(expr) => Value::String(expr.to_string()),
+            Expression::SubExpr(expr) => Value::from(*expr),
             Expression::Raw(raw) => Value::String(raw.into()),
             other => Value::String(RawExpression(other.to_string()).into()),
         }
@@ -113,6 +116,12 @@ impl From<f64> for Expression {
     }
 }
 
+impl From<Number> for Expression {
+    fn from(num: Number) -> Self {
+        Expression::Number(num)
+    }
+}
+
 impl From<bool> for Expression {
     fn from(b: bool) -> Self {
         Expression::Bool(b)
diff --git a/src/structure/ser/expression.rs b/src/structure/ser/expression.rs
index 2f29897..57eee40 100644
--- a/src/structure/ser/expression.rs
+++ b/src/structure/ser/expression.rs
@@ -130,21 +130,24 @@ impl ser::Serializer for ExpressionSerializer {
     where
         T: ?Sized + ser::Serialize,
     {
-        if name == "$hcl::expression" {
-            value.serialize(self)
-        } else if name == "$hcl::template_expr" {
-            Ok(Expression::TemplateExpr(Box::new(
+        match (name, variant) {
+            ("$hcl::template_expr", _) => Ok(Expression::TemplateExpr(Box::new(
                 TemplateExprSerializer.serialize_newtype_variant(
                     name,
                     variant_index,
                     variant,
                     value,
                 )?,
-            )))
-        } else {
-            let mut object = Object::new();
-            object.insert(ObjectKey::identifier(variant), value.serialize(self)?);
-            Ok(Expression::Object(object))
+            ))),
+            ("$hcl::expression", "SubExpr") => {
+                Ok(Expression::SubExpr(Box::new(value.serialize(self)?)))
+            }
+            ("$hcl::expression", _) => value.serialize(self),
+            (_, _) => {
+                let mut object = Object::new();
+                object.insert(ObjectKey::identifier(variant), value.serialize(self)?);
+                Ok(Expression::Object(object))
+            }
         }
     }
 


Test patch:
diff --git a/src/ser/tests.rs b/src/ser/tests.rs
index 8e6aaa9..794cf7a 100644
--- a/src/ser/tests.rs
+++ b/src/ser/tests.rs
@@ -346,6 +346,18 @@ qux {
     assert_eq!(String::from_utf8(buf).unwrap(), custom_expected);
 }
 
+#[test]
+fn serialize_nested_expression() {
+    let body = Body::builder()
+        .add_attribute((
+            "attr",
+            Expression::SubExpr(Box::new(Expression::VariableExpr("foo".into()))),
+        ))
+        .build();
+
+    assert_eq!(to_string(&body).unwrap(), "attr = (foo)\n");
+}
+
 #[test]
 fn roundtrip() {
     let input = Body::builder()
@@ -391,7 +403,9 @@ fn roundtrip() {
                         ),
                         (
                             ObjectKey::Identifier("environment".into()),
-                            Expression::VariableExpr("environment".into()),
+                            Expression::SubExpr(Box::new(Expression::VariableExpr(
+                                "environment".into(),
+                            ))),
                         ),
                     ]),
                 ))