Repository: martinohmann/hcl-rs
Pull request #68 (2022-09-09 05:37:45)

feat: add `Expression::Conditional`
This adds supports for parsing and serializing conditionals (e.g. `attr = var.some_cond ? true_expr : false_expr`).


Patch:
diff --git a/specsuite/hcl/invalid-heredoc.hcl.json b/specsuite/hcl/invalid-heredoc.hcl.json
index e9eb3b9..e968c49 100644
--- a/specsuite/hcl/invalid-heredoc.hcl.json
+++ b/specsuite/hcl/invalid-heredoc.hcl.json
@@ -1,6 +1,6 @@
 {
   "message": "EOF must be immediately followed by a newline",
   "body": {
-    "message": " --> 2:12\n  |\n2 |   source = <<-EOF # This is invalid\n  |            ^---\n  |\n  = expected ExprTerm, Operation, or CondExpr"
+    "message": " --> 2:12\n  |\n2 |   source = <<-EOF # This is invalid\n  |            ^---\n  |\n  = expected ExprTerm, Operation, or Conditional"
   }
 }
diff --git a/src/format/impls.rs b/src/format/impls.rs
index f770961..7a279f5 100644
--- a/src/format/impls.rs
+++ b/src/format/impls.rs
@@ -1,8 +1,8 @@
 use super::{private, Format, Formatter};
 use crate::{
     structure::{
-        Attribute, Block, BlockLabel, Body, Expression, FuncCall, Heredoc, HeredocStripMode,
-        Identifier, ObjectKey, RawExpression, Structure, TemplateExpr,
+        Attribute, Block, BlockLabel, Body, Conditional, Expression, FuncCall, Heredoc,
+        HeredocStripMode, Identifier, ObjectKey, RawExpression, Structure, TemplateExpr,
     },
     ElementAccess, ElementAccessOperator, Map, Number, Result, Value,
 };
@@ -114,6 +114,7 @@ impl Format for Expression {
                 fmt.write_all(b")")?;
                 Ok(())
             }
+            Expression::Conditional(cond) => cond.format(fmt),
         }
     }
 }
@@ -316,6 +317,22 @@ impl Format for FuncCall {
     }
 }
 
+impl private::Sealed for Conditional {}
+
+impl Format for Conditional {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        self.predicate.format(fmt)?;
+        fmt.write_all(b" ? ")?;
+        self.true_expr.format(fmt)?;
+        fmt.write_all(b" : ")?;
+        self.false_expr.format(fmt)?;
+        Ok(())
+    }
+}
+
 impl private::Sealed for String {}
 
 impl Format for String {
diff --git a/src/lib.rs b/src/lib.rs
index 4b9790a..d0f71a3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -27,7 +27,7 @@ pub use parser::parse;
 pub use ser::{to_string, to_vec, to_writer};
 #[doc(inline)]
 pub use structure::{
-    ser::to_expression, Attribute, Block, BlockBuilder, BlockLabel, Body, BodyBuilder,
+    ser::to_expression, Attribute, Block, BlockBuilder, BlockLabel, Body, BodyBuilder, Conditional,
     ElementAccess, ElementAccessOperator, Expression, FuncCall, FuncCallBuilder, Heredoc,
     HeredocStripMode, Identifier, Object, ObjectKey, RawExpression, Structure, TemplateExpr,
 };
diff --git a/src/parser/grammar/hcl.pest b/src/parser/grammar/hcl.pest
index 02f6828..09a6fd5 100644
--- a/src/parser/grammar/hcl.pest
+++ b/src/parser/grammar/hcl.pest
@@ -129,8 +129,8 @@ LogicOperator      = { "&&" | "||" }
 
 
 // Conditional operator
-CondExpr    = { ExprTerm | Operation }
-Conditional = { CondExpr ~ "?" ~ Expression ~ ":" ~ Expression }
+CondExpr    = _{ ExprTerm | Operation }
+Conditional =  { CondExpr ~ "?" ~ Expression ~ ":" ~ Expression }
 
 // Comments
 COMMENT         = _{ InlineComment | BlockComment }
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 5617b92..0a4717a 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -3,7 +3,7 @@ mod template;
 mod tests;
 
 use crate::{
-    structure::Identifier, util::unescape, Attribute, Block, BlockLabel, Body,
+    structure::Identifier, util::unescape, Attribute, Block, BlockLabel, Body, Conditional,
     ElementAccessOperator, Expression, FuncCall, FuncCallBuilder, Heredoc, HeredocStripMode,
     Number, Object, ObjectKey, RawExpression, Result, Structure, TemplateExpr,
 };
@@ -116,8 +116,6 @@ fn parse_block_body(pair: Pair<Rule>) -> Result<Body> {
 }
 
 fn parse_expression(pair: Pair<Rule>) -> Result<Expression> {
-    // @TODO(mohmann): Process Conditional and Operation at one point. This will require a
-    // PrecClimber to get precedence right though.
     match pair.as_rule() {
         Rule::ExprTerm => parse_expr_term(pair),
         Rule::Operation => {
@@ -127,14 +125,25 @@ fn parse_expression(pair: Pair<Rule>) -> Result<Expression> {
             // fully implemented.
             match parse_negative_number(inner(pair))? {
                 Some(num) => Ok(Expression::Number(num)),
+                // @TODO(mohmann): Process Operation at one point.
                 None => Ok(Expression::Raw(raw_expression(raw))),
             }
         }
-        Rule::Conditional => Ok(Expression::Raw(raw_expression(pair.as_str()))),
+        Rule::Conditional => Ok(Expression::Conditional(Box::new(parse_conditional(pair)?))),
         rule => unexpected_rule(rule),
     }
 }
 
+fn parse_conditional(pair: Pair<Rule>) -> Result<Conditional> {
+    let mut pairs = pair.into_inner();
+
+    Ok(Conditional {
+        predicate: parse_expression(pairs.next().unwrap())?,
+        true_expr: parse_expression(pairs.next().unwrap())?,
+        false_expr: parse_expression(pairs.next().unwrap())?,
+    })
+}
+
 fn parse_negative_number(pair: Pair<Rule>) -> Result<Option<Number>> {
     if let Rule::UnaryOp = pair.as_rule() {
         let mut pairs = pair.into_inner();
@@ -174,7 +183,7 @@ fn parse_expr_term(pair: Pair<Rule>) -> Result<Expression> {
         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.
+        // @TODO(mohmann): Process ForExpr.
         _ => Expression::Raw(raw_expression(pair.as_str())),
     };
 
diff --git a/src/structure/conditional.rs b/src/structure/conditional.rs
new file mode 100644
index 0000000..bf2647b
--- /dev/null
+++ b/src/structure/conditional.rs
@@ -0,0 +1,32 @@
+use super::Expression;
+use serde::{Deserialize, Serialize};
+
+/// The conditional operator allows selecting from one of two expressions based on the outcome of a
+/// boolean expression.
+#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+#[serde(rename = "$hcl::conditional")]
+pub struct Conditional {
+    /// A predicate expression that evaluates to a boolean value.
+    pub predicate: Expression,
+    /// The expression returned by the conditional if the predicate evaluates to `true`.
+    pub true_expr: Expression,
+    /// The expression returned by the conditional if the predicate evaluates to `false`.
+    pub false_expr: Expression,
+}
+
+impl Conditional {
+    /// Creates a new `Conditional` from a predicate and two expressions for the branches of the
+    /// conditional.
+    pub fn new<P, T, F>(predicate: P, true_expr: T, false_expr: F) -> Conditional
+    where
+        P: Into<Expression>,
+        T: Into<Expression>,
+        F: Into<Expression>,
+    {
+        Conditional {
+            predicate: predicate.into(),
+            true_expr: true_expr.into(),
+            false_expr: false_expr.into(),
+        }
+    }
+}
diff --git a/src/structure/de.rs b/src/structure/de.rs
index cc8bdc6..86cdbcc 100644
--- a/src/structure/de.rs
+++ b/src/structure/de.rs
@@ -1,8 +1,9 @@
 //! Deserialize impls for HCL structure types.
 
 use super::{
-    Attribute, Block, BlockLabel, Body, ElementAccess, ElementAccessOperator, Expression, FuncCall,
-    Heredoc, HeredocStripMode, Identifier, ObjectKey, RawExpression, Structure, TemplateExpr,
+    Attribute, Block, BlockLabel, Body, Conditional, ElementAccess, ElementAccessOperator,
+    Expression, FuncCall, Heredoc, HeredocStripMode, Identifier, ObjectKey, RawExpression,
+    Structure, TemplateExpr,
 };
 use crate::{Error, Number, Result};
 use serde::de::{self, value::MapAccessDeserializer, IntoDeserializer};
@@ -268,6 +269,7 @@ impl<'de> de::Deserializer<'de> for Expression {
                 func_call.into_deserializer().deserialize_any(visitor)
             }
             Expression::SubExpr(expr) => expr.into_deserializer().deserialize_any(visitor),
+            Expression::Conditional(cond) => cond.into_deserializer().deserialize_any(visitor),
         }
     }
 
@@ -299,6 +301,7 @@ impl VariantName for Expression {
             Expression::ElementAccess(_) => "ElementAccess",
             Expression::FuncCall(_) => "FuncCall",
             Expression::SubExpr(_) => "SubExpr",
+            Expression::Conditional(_) => "Conditional",
         }
     }
 }
@@ -574,6 +577,64 @@ impl<'de> de::MapAccess<'de> for FuncCallAccess {
     }
 }
 
+impl<'de> IntoDeserializer<'de, Error> for Conditional {
+    type Deserializer = MapAccessDeserializer<ConditionalAccess>;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        MapAccessDeserializer::new(ConditionalAccess::new(self))
+    }
+}
+
+pub struct ConditionalAccess {
+    predicate: Option<Expression>,
+    true_expr: Option<Expression>,
+    false_expr: Option<Expression>,
+}
+
+impl ConditionalAccess {
+    fn new(cond: Conditional) -> Self {
+        ConditionalAccess {
+            predicate: Some(cond.predicate),
+            true_expr: Some(cond.true_expr),
+            false_expr: Some(cond.false_expr),
+        }
+    }
+}
+
+impl<'de> de::MapAccess<'de> for ConditionalAccess {
+    type Error = Error;
+
+    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+    where
+        K: de::DeserializeSeed<'de>,
+    {
+        if self.predicate.is_some() {
+            seed.deserialize("predicate".into_deserializer()).map(Some)
+        } else if self.true_expr.is_some() {
+            seed.deserialize("true_expr".into_deserializer()).map(Some)
+        } else if self.false_expr.is_some() {
+            seed.deserialize("false_expr".into_deserializer()).map(Some)
+        } else {
+            Ok(None)
+        }
+    }
+
+    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Error>
+    where
+        V: de::DeserializeSeed<'de>,
+    {
+        if let Some(predicate) = self.predicate.take() {
+            seed.deserialize(predicate.into_deserializer())
+        } else if let Some(true_expr) = self.true_expr.take() {
+            seed.deserialize(true_expr.into_deserializer())
+        } else if let Some(false_expr) = self.false_expr.take() {
+            seed.deserialize(false_expr.into_deserializer())
+        } else {
+            Err(de::Error::custom("invalid HCL condition"))
+        }
+    }
+}
+
 impl<'de> IntoDeserializer<'de, Error> for ObjectKey {
     type Deserializer = Self;
 
diff --git a/src/structure/expression.rs b/src/structure/expression.rs
index 72862eb..694fce0 100644
--- a/src/structure/expression.rs
+++ b/src/structure/expression.rs
@@ -1,7 +1,7 @@
 //! Types to represent HCL attribute value expressions.
 
 use super::{ElementAccess, ElementAccessOperator, FuncCall, Identifier, TemplateExpr};
-use crate::{Number, Value};
+use crate::{Conditional, Number, Value};
 use serde::{Deserialize, Serialize};
 use std::borrow::Cow;
 use std::fmt::{self, Display, Write};
@@ -38,6 +38,9 @@ pub enum Expression {
     FuncCall(Box<FuncCall>),
     /// Represents a sub-expression that is wrapped in parenthesis.
     SubExpr(Box<Expression>),
+    /// A conditional operator which selects one of two rexpressions based on the outcome of a
+    /// boolean expression.
+    Conditional(Box<Conditional>),
     /// 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),
@@ -204,6 +207,12 @@ impl From<FuncCall> for Expression {
     }
 }
 
+impl From<Conditional> for Expression {
+    fn from(cond: Conditional) -> Self {
+        Expression::Conditional(Box::new(cond))
+    }
+}
+
 impl From<TemplateExpr> for Expression {
     fn from(expr: TemplateExpr) -> Self {
         Expression::TemplateExpr(Box::new(expr))
diff --git a/src/structure/mod.rs b/src/structure/mod.rs
index 9e28765..2d37dfb 100644
--- a/src/structure/mod.rs
+++ b/src/structure/mod.rs
@@ -48,6 +48,7 @@
 mod attribute;
 mod block;
 mod body;
+mod conditional;
 pub(crate) mod de;
 mod element_access;
 mod expression;
@@ -61,6 +62,7 @@ pub use self::{
     attribute::Attribute,
     block::{Block, BlockBuilder, BlockLabel},
     body::{Body, BodyBuilder},
+    conditional::Conditional,
     element_access::{ElementAccess, ElementAccessOperator},
     expression::{Expression, Object, ObjectKey, RawExpression},
     func::{FuncCall, FuncCallBuilder},
diff --git a/src/structure/ser/conditional.rs b/src/structure/ser/conditional.rs
new file mode 100644
index 0000000..adbe312
--- /dev/null
+++ b/src/structure/ser/conditional.rs
@@ -0,0 +1,143 @@
+use super::expression::ExpressionSerializer;
+use crate::{serialize_unsupported, Conditional, Error, Expression, Result};
+use serde::ser::{self, Impossible};
+
+pub struct ConditionalSerializer;
+
+impl ser::Serializer for ConditionalSerializer {
+    type Ok = Conditional;
+    type Error = Error;
+
+    type SerializeSeq = SerializeConditionalSeq;
+    type SerializeTuple = SerializeConditionalSeq;
+    type SerializeTupleStruct = SerializeConditionalSeq;
+    type SerializeTupleVariant = Impossible<Conditional, Error>;
+    type SerializeMap = Impossible<Conditional, Error>;
+    type SerializeStruct = SerializeConditionalStruct;
+    type SerializeStructVariant = Impossible<Conditional, Error>;
+
+    serialize_unsupported! {
+        bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64
+        char str bytes none unit unit_struct unit_variant
+        newtype_variant tuple_variant map struct_variant
+    }
+    serialize_self! { some newtype_struct }
+    forward_to_serialize_seq! { tuple tuple_struct }
+
+    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
+        Ok(SerializeConditionalSeq::new())
+    }
+
+    fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+        Ok(SerializeConditionalStruct::new())
+    }
+}
+
+pub struct SerializeConditionalSeq {
+    predicate: Option<Expression>,
+    true_expr: Option<Expression>,
+    false_expr: Option<Expression>,
+}
+
+impl SerializeConditionalSeq {
+    pub fn new() -> Self {
+        SerializeConditionalSeq {
+            predicate: None,
+            true_expr: None,
+            false_expr: None,
+        }
+    }
+}
+
+impl ser::SerializeSeq for SerializeConditionalSeq {
+    type Ok = Conditional;
+    type Error = Error;
+
+    fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        if self.predicate.is_none() {
+            self.predicate = Some(value.serialize(ExpressionSerializer)?);
+        } else if self.true_expr.is_none() {
+            self.true_expr = Some(value.serialize(ExpressionSerializer)?);
+        } else if self.false_expr.is_none() {
+            self.false_expr = Some(value.serialize(ExpressionSerializer)?);
+        } else {
+            return Err(ser::Error::custom("expected sequence with 3 elements"));
+        }
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match (self.predicate, self.true_expr, self.false_expr) {
+            (Some(predicate), Some(true_expr), Some(false_expr)) => Ok(Conditional {
+                predicate,
+                true_expr,
+                false_expr,
+            }),
+            (_, _, _) => Err(ser::Error::custom("expected sequence with 3 elements")),
+        }
+    }
+}
+
+impl ser::SerializeTuple for SerializeConditionalSeq {
+    impl_forward_to_serialize_seq!(serialize_element, Conditional);
+}
+
+impl ser::SerializeTupleStruct for SerializeConditionalSeq {
+    impl_forward_to_serialize_seq!(serialize_field, Conditional);
+}
+
+pub struct SerializeConditionalStruct {
+    predicate: Option<Expression>,
+    true_expr: Option<Expression>,
+    false_expr: Option<Expression>,
+}
+
+impl SerializeConditionalStruct {
+    pub fn new() -> Self {
+        SerializeConditionalStruct {
+            predicate: None,
+            true_expr: None,
+            false_expr: None,
+        }
+    }
+}
+
+impl ser::SerializeStruct for SerializeConditionalStruct {
+    type Ok = Conditional;
+    type Error = Error;
+
+    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        match key {
+            "predicate" => self.predicate = Some(value.serialize(ExpressionSerializer)?),
+            "true_expr" => self.true_expr = Some(value.serialize(ExpressionSerializer)?),
+            "false_expr" => self.false_expr = Some(value.serialize(ExpressionSerializer)?),
+            _ => {
+                return Err(ser::Error::custom(
+                    "expected struct with fields `predicate`, `true_expr` and `false_expr`",
+                ))
+            }
+        };
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match (self.predicate, self.true_expr, self.false_expr) {
+            (Some(predicate), Some(true_expr), Some(false_expr)) => Ok(Conditional {
+                predicate,
+                true_expr,
+                false_expr,
+            }),
+            (_, _, _) => Err(ser::Error::custom(
+                "expected struct with fields `predicate`, `true_expr` and `false_expr`",
+            )),
+        }
+    }
+}
diff --git a/src/structure/ser/expression.rs b/src/structure/ser/expression.rs
index 57eee40..29da80b 100644
--- a/src/structure/ser/expression.rs
+++ b/src/structure/ser/expression.rs
@@ -1,6 +1,6 @@
 use super::{
-    element_access::SerializeElementAccessStruct, func::SerializeFuncCallStruct,
-    template::TemplateExprSerializer, StringSerializer,
+    conditional::ConditionalSerializer, element_access::SerializeElementAccessStruct,
+    func::SerializeFuncCallStruct, template::TemplateExprSerializer, StringSerializer,
 };
 use crate::{
     serialize_unsupported, Error, Expression, Identifier, Object, ObjectKey, RawExpression, Result,
@@ -123,31 +123,28 @@ impl ser::Serializer for ExpressionSerializer {
     fn serialize_newtype_variant<T>(
         self,
         name: &'static str,
-        variant_index: u32,
+        _variant_index: u32,
         variant: &'static str,
         value: &T,
     ) -> Result<Self::Ok>
     where
         T: ?Sized + ser::Serialize,
     {
-        match (name, variant) {
-            ("$hcl::template_expr", _) => Ok(Expression::TemplateExpr(Box::new(
-                TemplateExprSerializer.serialize_newtype_variant(
-                    name,
-                    variant_index,
-                    variant,
-                    value,
-                )?,
-            ))),
-            ("$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))
+        if name == "$hcl::expression" {
+            match variant {
+                "Conditional" => Ok(Expression::Conditional(Box::new(
+                    value.serialize(ConditionalSerializer)?,
+                ))),
+                "SubExpr" => Ok(Expression::SubExpr(Box::new(value.serialize(self)?))),
+                "TemplateExpr" => Ok(Expression::TemplateExpr(Box::new(
+                    value.serialize(TemplateExprSerializer)?,
+                ))),
+                _ => value.serialize(self),
             }
+        } else {
+            let mut object = Object::new();
+            object.insert(ObjectKey::identifier(variant), value.serialize(self)?);
+            Ok(Expression::Object(object))
         }
     }
 
diff --git a/src/structure/ser/mod.rs b/src/structure/ser/mod.rs
index 168109b..9404b45 100644
--- a/src/structure/ser/mod.rs
+++ b/src/structure/ser/mod.rs
@@ -3,6 +3,7 @@
 mod attribute;
 mod block;
 pub(crate) mod body;
+mod conditional;
 mod element_access;
 mod expression;
 mod func;


Test patch:
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index 14f974b..328aa26 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -77,12 +77,10 @@ fn conditional() {
         rule: Rule::Conditional,
         tokens: [
             Conditional(0, 19, [
-                CondExpr(0, 11, [
-                    ExprTerm(0, 11, [
-                        VariableExpr(0, 3),
-                        GetAttr(3, 11, [
-                            Identifier(4, 11)
-                        ])
+                ExprTerm(0, 11, [
+                    VariableExpr(0, 3),
+                    GetAttr(3, 11, [
+                        Identifier(4, 11)
                     ])
                 ]),
                 ExprTerm(14, 16, [
@@ -335,12 +333,10 @@ fn parse_cond_in_interpolation() {
                                 TemplateInterpolation(8, 36, [
                                     TemplateIExprStartNormal(8, 10),
                                     Conditional(10, 35, [
-                                        CondExpr(10, 15, [
-                                            ExprTerm(10, 15, [
-                                                VariableExpr(10, 13),
-                                                GetAttr(13, 15, [
-                                                    Identifier(14, 15)
-                                                ])
+                                        ExprTerm(10, 15, [
+                                            VariableExpr(10, 13),
+                                            GetAttr(13, 15, [
+                                                Identifier(14, 15)
                                             ])
                                         ]),
                                         ExprTerm(18, 31, [
diff --git a/src/ser/tests.rs b/src/ser/tests.rs
index 794cf7a..5a5e3e8 100644
--- a/src/ser/tests.rs
+++ b/src/ser/tests.rs
@@ -1,7 +1,8 @@
 use super::*;
 use crate::{
-    Block, BlockLabel, Body, ElementAccess, ElementAccessOperator, Expression, FuncCall, Heredoc,
-    HeredocStripMode, Identifier, Object, ObjectKey, RawExpression, TemplateExpr,
+    Block, BlockLabel, Body, Conditional, ElementAccess, ElementAccessOperator, Expression,
+    FuncCall, Heredoc, HeredocStripMode, Identifier, Object, ObjectKey, RawExpression,
+    TemplateExpr,
 };
 use pretty_assertions::assert_eq;
 use serde_json::json;
@@ -216,6 +217,21 @@ fn serialize_element_access() {
     );
 }
 
+#[test]
+fn serialize_conditional() {
+    let body = Body::builder()
+        .add_attribute((
+            "cond",
+            Conditional::new(Identifier::new("cond_var"), "yes", "no"),
+        ))
+        .build();
+
+    assert_eq!(
+        to_string(&body).unwrap(),
+        "cond = cond_var ? \"yes\" : \"no\"\n"
+    );
+}
+
 #[test]
 fn serialize_func_call() {
     let body = Body::builder()
@@ -365,6 +381,10 @@ fn roundtrip() {
             Block::builder("resource")
                 .add_label("aws_s3_bucket")
                 .add_label("mybucket")
+                .add_attribute((
+                    "count",
+                    Conditional::new(ElementAccess::new(Identifier::new("var"), "enabled"), 1, 0),
+                ))
                 .add_attribute(("bucket", "mybucket"))
                 .add_attribute(("force_destroy", true))
                 .add_block(
diff --git a/src/structure/ser/tests.rs b/src/structure/ser/tests.rs
index 06fe69c..74abfb7 100644
--- a/src/structure/ser/tests.rs
+++ b/src/structure/ser/tests.rs
@@ -2,12 +2,13 @@ use super::{
     attribute::AttributeSerializer,
     block::{BlockLabelSerializer, BlockSerializer},
     body::BodySerializer,
+    conditional::ConditionalSerializer,
     expression::ExpressionSerializer,
     template::TemplateExprSerializer,
     *,
 };
 use crate::{
-    structure::Identifier, Attribute, Block, BlockLabel, Body, Expression, Heredoc,
+    structure::Identifier, Attribute, Block, BlockLabel, Body, Conditional, Expression, Heredoc,
     HeredocStripMode, Map, TemplateExpr,
 };
 use serde::Serialize;
@@ -76,6 +77,10 @@ fn identity() {
         TemplateExprSerializer,
         TemplateExpr::Heredoc(Heredoc::new(Identifier::new("EOS"), "${foo}")),
     );
+    test_identity(
+        ConditionalSerializer,
+        Conditional::new(Identifier::new("some_cond_var"), "yes", "no"),
+    );
 }
 
 #[test]
@@ -167,4 +172,14 @@ fn custom() {
         Some(1u8),
         Expression::Number(1u8.into()),
     );
+
+    test_serialize(
+        ConditionalSerializer,
+        (
+            Expression::VariableExpr(Identifier::new("some_cond_var")),
+            Expression::String("yes".into()),
+            Expression::String("no".into()),
+        ),
+        Conditional::new(Identifier::new("some_cond_var"), "yes", "no"),
+    );
 }