Repository: martinohmann/hcl-rs
Pull request #70 (2022-09-10 10:44:53)

feat: add `Expression::ForExpr`
This adds supports for parsing and serializing `for` expressions, e.g. `attr = [for item in items : func(item)]` and `attr = {for key, value in objects : tolower(key) => value if value != null}`.


Patch:
diff --git a/src/format/impls.rs b/src/format/impls.rs
index 5658f6c..f8e3c08 100644
--- a/src/format/impls.rs
+++ b/src/format/impls.rs
@@ -110,6 +110,7 @@ impl Format for Expression {
             }
             Expression::Conditional(cond) => cond.format(fmt),
             Expression::Operation(op) => op.format(fmt),
+            Expression::ForExpr(expr) => expr.format(fmt),
         }
     }
 }
@@ -369,6 +370,83 @@ impl Format for BinaryOp {
     }
 }
 
+impl private::Sealed for ForExpr {}
+
+impl Format for ForExpr {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        match self {
+            ForExpr::List(expr) => expr.format(fmt),
+            ForExpr::Object(expr) => expr.format(fmt),
+        }
+    }
+}
+
+impl private::Sealed for ForListExpr {}
+
+impl Format for ForListExpr {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        fmt.write_all(b"[")?;
+        self.intro.format(fmt)?;
+        self.expr.format(fmt)?;
+        if let Some(cond) = &self.cond {
+            fmt.write_all(b" if ")?;
+            cond.format(fmt)?;
+        }
+        fmt.write_all(b"]")?;
+        Ok(())
+    }
+}
+
+impl private::Sealed for ForObjectExpr {}
+
+impl Format for ForObjectExpr {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        fmt.write_all(b"{")?;
+        self.intro.format(fmt)?;
+        self.key_expr.format(fmt)?;
+        fmt.write_all(b" => ")?;
+        self.value_expr.format(fmt)?;
+        if self.value_grouping {
+            fmt.write_all(b"...")?;
+        }
+        if let Some(cond) = &self.cond {
+            fmt.write_all(b" if ")?;
+            cond.format(fmt)?;
+        }
+        fmt.write_all(b"}")?;
+        Ok(())
+    }
+}
+
+impl private::Sealed for ForIntro {}
+
+impl Format for ForIntro {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        fmt.write_all(b"for ")?;
+        if let Some(key) = &self.key {
+            key.format(fmt)?;
+            fmt.write_all(b", ")?;
+        }
+        self.value.format(fmt)?;
+        fmt.write_all(b" in ")?;
+        self.expr.format(fmt)?;
+        fmt.write_all(b" : ")?;
+        Ok(())
+    }
+}
+
 impl private::Sealed for String {}
 
 impl Format for String {
diff --git a/src/macros.rs b/src/macros.rs
index 97d736d..e2e1681 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -16,9 +16,8 @@
 /// - Attribute expression values can be any valid primitive, collection, expression or raw HCL
 ///   expression (`#{raw_expr}`).
 ///
-/// Please note that HCL actually uses `${}` for interpolating raw expressions, but since rust
-/// macros do not support matching on `$` it was chosen to use `#{}` instead to support raw
-/// expressions.
+/// Please note that HCL actually uses `${}` for interpolating expression, but since rust macros do
+/// not support matching on `$` it was chosen to use `#{}` instead to support raw expressions.
 ///
 /// ## Unsupported syntax:
 ///
diff --git a/src/parser/grammar/hcl.pest b/src/parser/grammar/hcl.pest
index 09a6fd5..22c180b 100644
--- a/src/parser/grammar/hcl.pest
+++ b/src/parser/grammar/hcl.pest
@@ -98,9 +98,10 @@ Variadic  =  { "..." }
 // For expressions
 ForExpr       = { ForTupleExpr | ForObjectExpr }
 ForTupleExpr  = { "[" ~ ForIntro ~ Expression ~ ForCond? ~ "]" }
-ForObjectExpr = { "{" ~ ForIntro ~ Expression ~ "=>" ~ Expression ~ "..."? ~ ForCond? ~ "}" }
+ForObjectExpr = { "{" ~ ForIntro ~ Expression ~ "=>" ~ Expression ~ ValueGrouping? ~ ForCond? ~ "}" }
 ForIntro      = { "for" ~ Identifier ~ ("," ~ Identifier)? ~ "in" ~ Expression ~ ":" }
 ForCond       = { "if" ~ Expression }
+ValueGrouping = { "..." }
 
 // Variables and variable expressions
 VariableExpr = @{ Identifier }
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index e678adf..b173044 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -191,8 +191,8 @@ 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.
-        _ => Expression::Raw(raw_expression(pair.as_str())),
+        Rule::ForExpr => Expression::from(parse_for_expr(inner(pair))?),
+        rule => unexpected_rule(rule),
     };
 
     pairs.try_fold(expr, |expr, pair| {
@@ -200,6 +200,73 @@ fn parse_expr_term(pair: Pair<Rule>) -> Result<Expression> {
     })
 }
 
+fn parse_for_expr(pair: Pair<Rule>) -> Result<ForExpr> {
+    match pair.as_rule() {
+        Rule::ForTupleExpr => parse_for_list_expr(pair).map(ForExpr::List),
+        Rule::ForObjectExpr => parse_for_object_expr(pair).map(ForExpr::Object),
+        rule => unexpected_rule(rule),
+    }
+}
+
+fn parse_for_list_expr(pair: Pair<Rule>) -> Result<ForListExpr> {
+    let mut pairs = pair.into_inner();
+    let intro = parse_for_intro(pairs.next().unwrap())?;
+    let expr = parse_expression(pairs.next().unwrap())?;
+    let cond = match pairs.next() {
+        Some(pair) => Some(parse_expression(inner(pair))?),
+        None => None,
+    };
+
+    Ok(ForListExpr { intro, expr, cond })
+}
+
+fn parse_for_object_expr(pair: Pair<Rule>) -> Result<ForObjectExpr> {
+    let mut pairs = pair.into_inner();
+    let intro = parse_for_intro(pairs.next().unwrap())?;
+    let key_expr = parse_expression(pairs.next().unwrap())?;
+    let value_expr = parse_expression(pairs.next().unwrap())?;
+    let (value_grouping, cond) = match (pairs.next(), pairs.next()) {
+        (Some(_), Some(pair)) => (true, Some(parse_expression(inner(pair))?)),
+        (Some(pair), None) => match pair.as_rule() {
+            Rule::ValueGrouping => (true, None),
+            Rule::ForCond => (false, Some(parse_expression(inner(pair))?)),
+            rule => unexpected_rule(rule),
+        },
+        (_, _) => (false, None),
+    };
+
+    Ok(ForObjectExpr {
+        intro,
+        key_expr,
+        value_expr,
+        value_grouping,
+        cond,
+    })
+}
+
+fn parse_for_intro(pair: Pair<Rule>) -> Result<ForIntro> {
+    let mut pairs = pair.into_inner();
+    let value = pairs.next().unwrap();
+    let mut value = Some(Identifier::new(value.as_str()));
+    let mut expr = pairs.next().unwrap();
+
+    // If there are two identifiers, the first one is the key and the second one the value.
+    let key = match expr.as_rule() {
+        Rule::Identifier => {
+            let key = value.replace(Identifier::new(expr.as_str()));
+            expr = pairs.next().unwrap();
+            key
+        }
+        _ => None,
+    };
+
+    Ok(ForIntro {
+        key,
+        value: value.take().unwrap(),
+        expr: parse_expression(expr)?,
+    })
+}
+
 fn parse_func_call(pair: Pair<Rule>) -> Result<FuncCall> {
     let mut pairs = pair.into_inner();
     let name = pairs.next().unwrap();
diff --git a/src/parser/template.rs b/src/parser/template.rs
index 7f84175..9f4679c 100644
--- a/src/parser/template.rs
+++ b/src/parser/template.rs
@@ -1,5 +1,5 @@
 use super::*;
-use crate::template::*;
+use crate::template::{ForExpr, *};
 
 pub fn parse(input: &str) -> Result<Template> {
     let pair = HclParser::parse(Rule::HclTemplate, input)?.next().unwrap();
diff --git a/src/structure/de.rs b/src/structure/de.rs
index d0534d5..ca34297 100644
--- a/src/structure/de.rs
+++ b/src/structure/de.rs
@@ -270,6 +270,7 @@ impl<'de> de::Deserializer<'de> for Expression {
             Expression::SubExpr(expr) => expr.into_deserializer().deserialize_any(visitor),
             Expression::Conditional(cond) => cond.into_deserializer().deserialize_any(visitor),
             Expression::Operation(op) => op.into_deserializer().deserialize_any(visitor),
+            Expression::ForExpr(expr) => expr.into_deserializer().deserialize_any(visitor),
         }
     }
 
@@ -303,6 +304,7 @@ impl VariantName for Expression {
             Expression::SubExpr(_) => "SubExpr",
             Expression::Conditional(_) => "Conditional",
             Expression::Operation(_) => "Operation",
+            Expression::ForExpr(_) => "ForExpr",
         }
     }
 }
@@ -337,6 +339,7 @@ impl<'de> de::VariantAccess<'de> for Expression {
             Expression::TemplateExpr(expr) => seed.deserialize(expr.into_deserializer()),
             Expression::SubExpr(expr) => seed.deserialize(expr.into_deserializer()),
             Expression::Operation(op) => seed.deserialize(op.into_deserializer()),
+            Expression::ForExpr(expr) => seed.deserialize(expr.into_deserializer()),
             value => seed.deserialize(value.into_deserializer()),
         }
     }
@@ -801,6 +804,231 @@ impl<'de> IntoDeserializer<'de, Error> for BinaryOperator {
     }
 }
 
+impl<'de> IntoDeserializer<'de, Error> for ForExpr {
+    type Deserializer = Self;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        self
+    }
+}
+
+impl<'de> de::Deserializer<'de> for ForExpr {
+    type Error = Error;
+
+    forward_to_deserialize_any! {
+        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str
+        string bytes byte_buf option unit unit_struct newtype_struct seq
+        tuple tuple_struct map struct identifier ignored_any
+    }
+    impl_deserialize_enum!();
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self {
+            ForExpr::List(expr) => expr.into_deserializer().deserialize_any(visitor),
+            ForExpr::Object(expr) => expr.into_deserializer().deserialize_any(visitor),
+        }
+    }
+}
+
+impl VariantName for ForExpr {
+    fn variant_name(&self) -> &'static str {
+        match self {
+            ForExpr::List(_) => "List",
+            ForExpr::Object(_) => "Object",
+        }
+    }
+}
+
+impl<'de> IntoDeserializer<'de, Error> for ForListExpr {
+    type Deserializer = MapAccessDeserializer<ForListExprAccess>;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        MapAccessDeserializer::new(ForListExprAccess::new(self))
+    }
+}
+
+pub struct ForListExprAccess {
+    intro: Option<ForIntro>,
+    expr: Option<Expression>,
+    cond: Option<Option<Expression>>,
+}
+
+impl ForListExprAccess {
+    fn new(expr: ForListExpr) -> Self {
+        ForListExprAccess {
+            intro: Some(expr.intro),
+            expr: Some(expr.expr),
+            cond: Some(expr.cond),
+        }
+    }
+}
+
+impl<'de> de::MapAccess<'de> for ForListExprAccess {
+    type Error = Error;
+
+    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+    where
+        K: de::DeserializeSeed<'de>,
+    {
+        if self.intro.is_some() {
+            seed.deserialize("intro".into_deserializer()).map(Some)
+        } else if self.expr.is_some() {
+            seed.deserialize("expr".into_deserializer()).map(Some)
+        } else if self.cond.is_some() {
+            seed.deserialize("cond".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(intro) = self.intro.take() {
+            seed.deserialize(intro.into_deserializer())
+        } else if let Some(expr) = self.expr.take() {
+            seed.deserialize(expr.into_deserializer())
+        } else if let Some(cond) = self.cond.take() {
+            seed.deserialize(OptionDeserializer::new(cond))
+        } else {
+            Err(de::Error::custom("invalid HCL for list expression"))
+        }
+    }
+}
+
+impl<'de> IntoDeserializer<'de, Error> for ForObjectExpr {
+    type Deserializer = MapAccessDeserializer<ForObjectExprAccess>;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        MapAccessDeserializer::new(ForObjectExprAccess::new(self))
+    }
+}
+
+pub struct ForObjectExprAccess {
+    intro: Option<ForIntro>,
+    key_expr: Option<Expression>,
+    value_expr: Option<Expression>,
+    value_grouping: Option<bool>,
+    cond: Option<Option<Expression>>,
+}
+
+impl ForObjectExprAccess {
+    fn new(expr: ForObjectExpr) -> Self {
+        ForObjectExprAccess {
+            intro: Some(expr.intro),
+            key_expr: Some(expr.key_expr),
+            value_expr: Some(expr.value_expr),
+            value_grouping: Some(expr.value_grouping),
+            cond: Some(expr.cond),
+        }
+    }
+}
+
+impl<'de> de::MapAccess<'de> for ForObjectExprAccess {
+    type Error = Error;
+
+    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+    where
+        K: de::DeserializeSeed<'de>,
+    {
+        if self.intro.is_some() {
+            seed.deserialize("intro".into_deserializer()).map(Some)
+        } else if self.key_expr.is_some() {
+            seed.deserialize("key_expr".into_deserializer()).map(Some)
+        } else if self.value_expr.is_some() {
+            seed.deserialize("value_expr".into_deserializer()).map(Some)
+        } else if self.value_grouping.is_some() {
+            seed.deserialize("value_grouping".into_deserializer())
+                .map(Some)
+        } else if self.cond.is_some() {
+            seed.deserialize("cond".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(intro) = self.intro.take() {
+            seed.deserialize(intro.into_deserializer())
+        } else if let Some(key_expr) = self.key_expr.take() {
+            seed.deserialize(key_expr.into_deserializer())
+        } else if let Some(value_expr) = self.value_expr.take() {
+            seed.deserialize(value_expr.into_deserializer())
+        } else if let Some(value_grouping) = self.value_grouping.take() {
+            seed.deserialize(value_grouping.into_deserializer())
+        } else if let Some(cond) = self.cond.take() {
+            seed.deserialize(OptionDeserializer::new(cond))
+        } else {
+            Err(de::Error::custom("invalid HCL for object expression"))
+        }
+    }
+}
+
+impl<'de> IntoDeserializer<'de, Error> for ForIntro {
+    type Deserializer = MapAccessDeserializer<ForIntroAccess>;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        MapAccessDeserializer::new(ForIntroAccess::new(self))
+    }
+}
+
+pub struct ForIntroAccess {
+    key: Option<Option<Identifier>>,
+    value: Option<Identifier>,
+    expr: Option<Expression>,
+}
+
+impl ForIntroAccess {
+    fn new(intro: ForIntro) -> Self {
+        ForIntroAccess {
+            key: Some(intro.key),
+            value: Some(intro.value),
+            expr: Some(intro.expr),
+        }
+    }
+}
+
+impl<'de> de::MapAccess<'de> for ForIntroAccess {
+    type Error = Error;
+
+    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+    where
+        K: de::DeserializeSeed<'de>,
+    {
+        if self.key.is_some() {
+            seed.deserialize("key".into_deserializer()).map(Some)
+        } else if self.value.is_some() {
+            seed.deserialize("value".into_deserializer()).map(Some)
+        } else if self.expr.is_some() {
+            seed.deserialize("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(key) = self.key.take() {
+            seed.deserialize(OptionDeserializer::new(key))
+        } else if let Some(value) = self.value.take() {
+            seed.deserialize(value.into_deserializer())
+        } else if let Some(expr) = self.expr.take() {
+            seed.deserialize(expr.into_deserializer())
+        } else {
+            Err(de::Error::custom("invalid HCL for intro"))
+        }
+    }
+}
+
 impl<'de> IntoDeserializer<'de, Error> for ObjectKey {
     type Deserializer = Self;
 
@@ -996,6 +1224,44 @@ where
     }
 }
 
+pub struct OptionDeserializer<T, E = Error> {
+    value: Option<T>,
+    marker: PhantomData<E>,
+}
+
+impl<T, E> OptionDeserializer<T, E> {
+    fn new(value: Option<T>) -> Self {
+        OptionDeserializer {
+            value,
+            marker: PhantomData,
+        }
+    }
+}
+
+impl<'de, T, E> de::Deserializer<'de> for OptionDeserializer<T, E>
+where
+    T: IntoDeserializer<'de, E>,
+    E: de::Error,
+{
+    type Error = E;
+
+    forward_to_deserialize_any! {
+        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str
+        string bytes byte_buf option unit unit_struct newtype_struct seq
+        tuple tuple_struct map struct enum identifier ignored_any
+    }
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.value {
+            Some(value) => visitor.visit_some(value.into_deserializer()),
+            None => visitor.visit_none(),
+        }
+    }
+}
+
 pub struct EnumAccess<T, E = Error> {
     value: T,
     marker: PhantomData<E>,
diff --git a/src/structure/expression.rs b/src/structure/expression.rs
index 0183f24..0780aac 100644
--- a/src/structure/expression.rs
+++ b/src/structure/expression.rs
@@ -43,7 +43,10 @@ pub enum Expression {
     Conditional(Box<Conditional>),
     /// An operation which applies a particular operator to either one or two expression terms.
     Operation(Box<Operation>),
-    /// Represents a raw HCL expression. See [`RawExpression`] for more details.
+    /// A construct for constructing a collection by projecting the items from another collection.
+    ForExpr(Box<ForExpr>),
+    /// Represents a raw HCL expression. This variant will never be emitted by the parser. See
+    /// [`RawExpression`] for more details.
     Raw(RawExpression),
 }
 
@@ -220,6 +223,12 @@ impl From<Operation> for Expression {
     }
 }
 
+impl From<ForExpr> for Expression {
+    fn from(expr: ForExpr) -> Self {
+        Expression::ForExpr(Box::new(expr))
+    }
+}
+
 impl From<TemplateExpr> for Expression {
     fn from(expr: TemplateExpr) -> Self {
         Expression::TemplateExpr(Box::new(expr))
@@ -250,8 +259,7 @@ pub enum ObjectKey {
     Identifier(Identifier),
     /// Represents a quoted string used as object key.
     String(String),
-    /// Represents a raw HCL expression. This includes any expression kind that does match any of
-    /// the enum variants above. See [`RawExpression`] for more details.
+    /// Represents a raw HCL expression. See [`RawExpression`] for more details.
     RawExpression(RawExpression),
 }
 
@@ -320,8 +328,6 @@ impl Display for ObjectKey {
 ///
 /// *Please note*: raw expressions are not validated during serialization, so it is your
 /// responsiblity to ensure that they are valid HCL.
-///
-/// As of now, only `for` expressions are treated as raw expression and are not further parsed.
 #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)]
 #[serde(rename = "$hcl::raw_expression")]
 pub struct RawExpression(String);
diff --git a/src/structure/for_expr.rs b/src/structure/for_expr.rs
new file mode 100644
index 0000000..89c9aa9
--- /dev/null
+++ b/src/structure/for_expr.rs
@@ -0,0 +1,155 @@
+use super::{Expression, Identifier};
+use serde::{Deserialize, Serialize};
+
+/// A for expression is a construct for constructing a collection by projecting the items from
+/// another collection.
+#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+#[serde(rename = "$hcl::for_expr")]
+pub enum ForExpr {
+    /// Represents a `for` expression that produces a list.
+    List(ForListExpr),
+    /// Represents a `for` expression that produces an object.
+    Object(ForObjectExpr),
+}
+
+impl From<ForListExpr> for ForExpr {
+    fn from(expr: ForListExpr) -> Self {
+        ForExpr::List(expr)
+    }
+}
+
+impl From<ForObjectExpr> for ForExpr {
+    fn from(expr: ForObjectExpr) -> Self {
+        ForExpr::Object(expr)
+    }
+}
+
+/// A `for` expression that produces a list.
+#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+#[serde(rename = "$hcl::for_list_expr")]
+pub struct ForListExpr {
+    /// The "introduction" of the `for` expression.
+    pub intro: ForIntro,
+    /// An expression that is evaluated once for each element in the source collection.
+    pub expr: Expression,
+    /// An optional filter predicate. Elements for which the predicate evaluates to `true` will be
+    /// evaluated as normal, while if `false` the element will be skipped.
+    pub cond: Option<Expression>,
+}
+
+impl ForListExpr {
+    /// Create a new `ForListExpr` with given intro and an expression that is evaluated once for
+    /// each element in the source collection.
+    pub fn new<T>(intro: ForIntro, expr: T) -> ForListExpr
+    where
+        T: Into<Expression>,
+    {
+        ForListExpr {
+            intro,
+            expr: expr.into(),
+            cond: None,
+        }
+    }
+
+    /// Sets filter predicate. Elements for which the predicate evaluates to `true` will be
+    /// evaluated as normal, while if `false` the element will be skipped.
+    pub fn with_cond<T>(mut self, expr: T) -> ForListExpr
+    where
+        T: Into<Expression>,
+    {
+        self.cond = Some(expr.into());
+        self
+    }
+}
+
+/// A `for` expression that produces an object.
+#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+#[serde(rename = "$hcl::for_object_expr")]
+pub struct ForObjectExpr {
+    /// The "introduction" of the `for` expression.
+    pub intro: ForIntro,
+    /// An expression that is evaluated once for each element key in the source collection.
+    pub key_expr: Expression,
+    /// An expression that is evaluated once for each element value in the source collection.
+    pub value_expr: Expression,
+    /// Indicates whether value grouping mode is enabled. In grouping mode, each value in the
+    /// resulting object is a list of all of the values that were produced against each distinct
+    /// key.
+    pub value_grouping: bool,
+    /// An optional filter predicate. Elements for which the predicate evaluates to `true` will be
+    /// evaluated as normal, while if `false` the element will be skipped.
+    pub cond: Option<Expression>,
+}
+
+impl ForObjectExpr {
+    /// Create a new `ForObjectExpr` with given intro and an two expressions that is evaluated
+    /// once for each element's key and value in the source collection.
+    pub fn new<K, V>(intro: ForIntro, key_expr: K, value_expr: V) -> ForObjectExpr
+    where
+        K: Into<Expression>,
+        V: Into<Expression>,
+    {
+        ForObjectExpr {
+            intro,
+            key_expr: key_expr.into(),
+            value_expr: value_expr.into(),
+            value_grouping: false,
+            cond: None,
+        }
+    }
+
+    /// Sets filter predicate. Elements for which the predicate evaluates to `true` will be
+    /// evaluated as normal, while if `false` the element will be skipped.
+    pub fn with_cond<T>(mut self, expr: T) -> ForObjectExpr
+    where
+        T: Into<Expression>,
+    {
+        self.cond = Some(expr.into());
+        self
+    }
+
+    /// Enables or disabled value grouping mode.
+    pub fn with_value_grouping(mut self, yes: bool) -> ForObjectExpr {
+        self.value_grouping = yes;
+        self
+    }
+}
+
+/// The `for` keyword followed by either one or two identifiers, the `in` keyword and an
+/// expression that must evaluate to a value that can be iterated.
+#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+#[serde(rename = "$hcl::for_intro")]
+pub struct ForIntro {
+    /// Optional name of the variable that will be temporarily assigned the key of each element
+    /// during iteration. For lists, this represents the zero-based element index, for objects this
+    /// represents the object key.
+    pub key: Option<Identifier>,
+    /// The name of the variable that will be temporarily assigned the value of each element during
+    /// iteration.
+    pub value: Identifier,
+    /// An expression that must evaluate to a value that can be iterated.
+    pub expr: Expression,
+}
+
+impl ForIntro {
+    /// Creates a new `ForIntro` with the name of the variable that will be temporarily assigned
+    /// the value of each element during iteration and an expression that must evaluate to a value
+    /// that can be iterated.
+    pub fn new<T>(value: Identifier, expr: T) -> ForIntro
+    where
+        T: Into<Expression>,
+    {
+        ForIntro {
+            key: None,
+            value,
+            expr: expr.into(),
+        }
+    }
+
+    /// Adds the iterator key variable identifier to the `for` expression and returns the modified
+    /// `ForIntro`.
+    pub fn with_key(mut self, key: Identifier) -> ForIntro {
+        self.key = Some(key);
+        self
+    }
+}
diff --git a/src/structure/mod.rs b/src/structure/mod.rs
index 110a551..08bc5dc 100644
--- a/src/structure/mod.rs
+++ b/src/structure/mod.rs
@@ -7,6 +7,7 @@
 //! - [`BlockBuilder`]: provides functionality for building `Block`s
 //! - [`Body`]: represent the body of an HCL configuration or block
 //! - [`BodyBuilder`]: provides functionality for building `Body`s
+//! - [`Expression`]: represent the value of an HCL attribute
 //!
 //! ## Examples
 //!
@@ -52,6 +53,7 @@ mod conditional;
 pub(crate) mod de;
 mod element_access;
 mod expression;
+mod for_expr;
 mod func;
 mod operation;
 pub(crate) mod ser;
@@ -66,6 +68,7 @@ pub use self::{
     conditional::Conditional,
     element_access::{ElementAccess, ElementAccessOperator},
     expression::{Expression, Object, ObjectKey, RawExpression},
+    for_expr::{ForExpr, ForIntro, ForListExpr, ForObjectExpr},
     func::{FuncCall, FuncCallBuilder},
     operation::{BinaryOp, BinaryOperator, Operation, UnaryOp, UnaryOperator},
     template::{Heredoc, HeredocStripMode, TemplateExpr},
diff --git a/src/structure/ser/expression.rs b/src/structure/ser/expression.rs
index 38db524..3cd7326 100644
--- a/src/structure/ser/expression.rs
+++ b/src/structure/ser/expression.rs
@@ -1,6 +1,6 @@
 use super::{
     conditional::ConditionalSerializer, element_access::SerializeElementAccessStruct,
-    func::SerializeFuncCallStruct, operation::OperationSerializer,
+    for_expr::ForExprSerializer, func::SerializeFuncCallStruct, operation::OperationSerializer,
     template::TemplateExprSerializer, StringSerializer,
 };
 use crate::{Error, Expression, Identifier, Object, ObjectKey, RawExpression, Result};
@@ -141,6 +141,9 @@ impl ser::Serializer for ExpressionSerializer {
                 "Operation" => Ok(Expression::Operation(Box::new(
                     value.serialize(OperationSerializer)?,
                 ))),
+                "ForExpr" => Ok(Expression::ForExpr(Box::new(
+                    value.serialize(ForExprSerializer)?,
+                ))),
                 "SubExpr" => Ok(Expression::SubExpr(Box::new(value.serialize(self)?))),
                 "TemplateExpr" => Ok(Expression::TemplateExpr(Box::new(
                     value.serialize(TemplateExprSerializer)?,
diff --git a/src/structure/ser/for_expr.rs b/src/structure/ser/for_expr.rs
new file mode 100644
index 0000000..d99564d
--- /dev/null
+++ b/src/structure/ser/for_expr.rs
@@ -0,0 +1,328 @@
+use super::{expression::ExpressionSerializer, BoolSerializer, OptionSerializer, StringSerializer};
+use crate::{Error, Expression, ForExpr, ForIntro, ForListExpr, ForObjectExpr, Identifier, Result};
+use serde::ser::{self, Impossible, Serialize};
+
+pub struct ForExprSerializer;
+
+impl ser::Serializer for ForExprSerializer {
+    type Ok = ForExpr;
+    type Error = Error;
+
+    type SerializeSeq = Impossible<ForExpr, Error>;
+    type SerializeTuple = Impossible<ForExpr, Error>;
+    type SerializeTupleStruct = Impossible<ForExpr, Error>;
+    type SerializeTupleVariant = Impossible<ForExpr, Error>;
+    type SerializeMap = Impossible<ForExpr, Error>;
+    type SerializeStruct = SerializeForExprStruct;
+    type SerializeStructVariant = Impossible<ForExpr, Error>;
+
+    serialize_unsupported! {
+        bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64
+        char str bytes none unit unit_struct unit_variant
+        seq tuple tuple_struct tuple_variant map struct_variant
+    }
+    serialize_self! { some newtype_struct }
+
+    fn serialize_newtype_variant<T>(
+        self,
+        name: &'static str,
+        _variant_index: u32,
+        variant: &'static str,
+        value: &T,
+    ) -> Result<Self::Ok>
+    where
+        T: ?Sized + Serialize,
+    {
+        // Specialization for the `ForExpr` type itself.
+        match (name, variant) {
+            ("$hcl::for_expr", "Object") => {
+                Ok(ForExpr::Object(value.serialize(ForObjectExprSerializer)?))
+            }
+            (_, _) => Ok(ForExpr::List(value.serialize(ForListExprSerializer)?)),
+        }
+    }
+
+    fn serialize_struct(self, name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+        Ok(SerializeForExprStruct::new(name))
+    }
+}
+
+pub enum SerializeForExprStruct {
+    List(SerializeForListExprStruct),
+    Object(SerializeForObjectExprStruct),
+}
+
+impl SerializeForExprStruct {
+    fn new(name: &'static str) -> Self {
+        // Specialization for the `ForListExpr` and `ForObjectExpr` types.
+        match name {
+            "$hcl::for_object_expr" => {
+                SerializeForExprStruct::Object(SerializeForObjectExprStruct::new())
+            }
+            _ => SerializeForExprStruct::List(SerializeForListExprStruct::new()),
+        }
+    }
+}
+
+impl ser::SerializeStruct for SerializeForExprStruct {
+    type Ok = ForExpr;
+    type Error = Error;
+
+    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        match self {
+            SerializeForExprStruct::List(ser) => ser.serialize_field(key, value),
+            SerializeForExprStruct::Object(ser) => ser.serialize_field(key, value),
+        }
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match self {
+            SerializeForExprStruct::List(ser) => ser.end().map(Into::into),
+            SerializeForExprStruct::Object(ser) => ser.end().map(Into::into),
+        }
+    }
+}
+
+pub struct ForListExprSerializer;
+
+impl ser::Serializer for ForListExprSerializer {
+    type Ok = ForListExpr;
+    type Error = Error;
+
+    type SerializeSeq = Impossible<ForListExpr, Error>;
+    type SerializeTuple = Impossible<ForListExpr, Error>;
+    type SerializeTupleStruct = Impossible<ForListExpr, Error>;
+    type SerializeTupleVariant = Impossible<ForListExpr, Error>;
+    type SerializeMap = Impossible<ForListExpr, Error>;
+    type SerializeStruct = SerializeForListExprStruct;
+    type SerializeStructVariant = Impossible<ForListExpr, 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
+        seq tuple tuple_struct tuple_variant map struct_variant
+    }
+    serialize_self! { some newtype_struct }
+
+    fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+        Ok(SerializeForListExprStruct::new())
+    }
+}
+
+pub struct SerializeForListExprStruct {
+    intro: Option<ForIntro>,
+    expr: Option<Expression>,
+    cond: Option<Option<Expression>>,
+}
+
+impl SerializeForListExprStruct {
+    pub fn new() -> Self {
+        SerializeForListExprStruct {
+            intro: None,
+            expr: None,
+            cond: None,
+        }
+    }
+}
+
+impl ser::SerializeStruct for SerializeForListExprStruct {
+    type Ok = ForListExpr;
+    type Error = Error;
+
+    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        match key {
+            "intro" => self.intro = Some(value.serialize(ForIntroSerializer)?),
+            "expr" => self.expr = Some(value.serialize(ExpressionSerializer)?),
+            "cond" => {
+                self.cond = Some(value.serialize(OptionSerializer::new(ExpressionSerializer))?)
+            }
+            _ => {
+                return Err(ser::Error::custom(
+                    "expected struct with fields `intro`, `expr` and `cond`",
+                ))
+            }
+        };
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match (self.intro, self.expr, self.cond) {
+            (Some(intro), Some(expr), Some(cond)) => Ok(ForListExpr { intro, expr, cond }),
+            (_, _, _) => Err(ser::Error::custom(
+                "expected struct with fields `intro`, `expr` and `cond`",
+            )),
+        }
+    }
+}
+
+pub struct ForObjectExprSerializer;
+
+impl ser::Serializer for ForObjectExprSerializer {
+    type Ok = ForObjectExpr;
+    type Error = Error;
+
+    type SerializeSeq = Impossible<ForObjectExpr, Error>;
+    type SerializeTuple = Impossible<ForObjectExpr, Error>;
+    type SerializeTupleStruct = Impossible<ForObjectExpr, Error>;
+    type SerializeTupleVariant = Impossible<ForObjectExpr, Error>;
+    type SerializeMap = Impossible<ForObjectExpr, Error>;
+    type SerializeStruct = SerializeForObjectExprStruct;
+    type SerializeStructVariant = Impossible<ForObjectExpr, 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
+        seq tuple tuple_struct tuple_variant map struct_variant
+    }
+    serialize_self! { some newtype_struct }
+
+    fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+        Ok(SerializeForObjectExprStruct::new())
+    }
+}
+
+pub struct SerializeForObjectExprStruct {
+    intro: Option<ForIntro>,
+    key_expr: Option<Expression>,
+    value_expr: Option<Expression>,
+    value_grouping: Option<bool>,
+    cond: Option<Option<Expression>>,
+}
+
+impl SerializeForObjectExprStruct {
+    pub fn new() -> Self {
+        SerializeForObjectExprStruct {
+            intro: None,
+            key_expr: None,
+            value_expr: None,
+            value_grouping: None,
+            cond: None,
+        }
+    }
+}
+
+impl ser::SerializeStruct for SerializeForObjectExprStruct {
+    type Ok = ForObjectExpr;
+    type Error = Error;
+
+    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        match key {
+            "intro" => self.intro = Some(value.serialize(ForIntroSerializer)?),
+            "key_expr" => self.key_expr = Some(value.serialize(ExpressionSerializer)?),
+            "value_expr" => self.value_expr = Some(value.serialize(ExpressionSerializer)?),
+            "value_grouping" => self.value_grouping = Some(value.serialize(BoolSerializer)?),
+            "cond" => self.cond = Some(value.serialize(OptionSerializer::new(ExpressionSerializer))?),
+            _ => {
+                return Err(ser::Error::custom(
+                    "expected struct with fields `intro`, `key_expr`, `value_expr`, `value_grouping` and `cond`",
+                ))
+            }
+        };
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match (self.intro, self.key_expr, self.value_expr, self.value_grouping, self.cond) {
+            (Some(intro), Some(key_expr), Some(value_expr), Some(value_grouping), Some(cond)) => {
+                Ok(ForObjectExpr {
+                    intro,
+                    key_expr,
+                    value_expr,
+                    value_grouping,
+                    cond,
+                })
+            },
+            (_, _, _, _, _) => Err(ser::Error::custom(
+                "expected struct with fields `intro`, `key_expr`, `value_expr`, `value_grouping` and `cond`",
+            )),
+        }
+    }
+}
+
+pub struct ForIntroSerializer;
+
+impl ser::Serializer for ForIntroSerializer {
+    type Ok = ForIntro;
+    type Error = Error;
+
+    type SerializeSeq = Impossible<ForIntro, Error>;
+    type SerializeTuple = Impossible<ForIntro, Error>;
+    type SerializeTupleStruct = Impossible<ForIntro, Error>;
+    type SerializeTupleVariant = Impossible<ForIntro, Error>;
+    type SerializeMap = Impossible<ForIntro, Error>;
+    type SerializeStruct = SerializeForIntroStruct;
+    type SerializeStructVariant = Impossible<ForIntro, 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
+        seq tuple tuple_struct tuple_variant map struct_variant
+    }
+    serialize_self! { some newtype_struct }
+
+    fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+        Ok(SerializeForIntroStruct::new())
+    }
+}
+
+pub struct SerializeForIntroStruct {
+    key: Option<Option<Identifier>>,
+    value: Option<Identifier>,
+    expr: Option<Expression>,
+}
+
+impl SerializeForIntroStruct {
+    pub fn new() -> Self {
+        SerializeForIntroStruct {
+            key: None,
+            value: None,
+            expr: None,
+        }
+    }
+}
+
+impl ser::SerializeStruct for SerializeForIntroStruct {
+    type Ok = ForIntro;
+    type Error = Error;
+
+    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        match key {
+            "key" => {
+                let key = value.serialize(OptionSerializer::new(StringSerializer))?;
+                self.key = Some(key.map(Identifier::from))
+            }
+            "value" => self.value = Some(Identifier::from(value.serialize(StringSerializer)?)),
+            "expr" => self.expr = Some(value.serialize(ExpressionSerializer)?),
+            _ => {
+                return Err(ser::Error::custom(
+                    "expected struct with fields `key`, `value` and `expr`",
+                ))
+            }
+        };
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match (self.key, self.value, self.expr) {
+            (Some(key), Some(value), Some(expr)) => Ok(ForIntro { key, value, expr }),
+            (_, _, _) => Err(ser::Error::custom(
+                "expected struct with fields `key`, `value` and `expr`",
+            )),
+        }
+    }
+}
diff --git a/src/structure/ser/mod.rs b/src/structure/ser/mod.rs
index 909798a..830a315 100644
--- a/src/structure/ser/mod.rs
+++ b/src/structure/ser/mod.rs
@@ -6,6 +6,7 @@ pub(crate) mod body;
 mod conditional;
 mod element_access;
 mod expression;
+mod for_expr;
 mod func;
 mod operation;
 mod structure;
@@ -181,3 +182,46 @@ where
 {
     impl_forward_to_serialize_seq!(serialize_field, Vec<S::Ok>, S::Error);
 }
+
+pub struct OptionSerializer<S> {
+    inner: S,
+}
+
+impl<S> OptionSerializer<S> {
+    pub fn new(inner: S) -> Self {
+        OptionSerializer { inner }
+    }
+}
+
+impl<S> ser::Serializer for OptionSerializer<S>
+where
+    S: ser::Serializer,
+{
+    type Ok = Option<S::Ok>;
+    type Error = S::Error;
+
+    type SerializeSeq = Impossible<Self::Ok, Self::Error>;
+    type SerializeTuple = Impossible<Self::Ok, Self::Error>;
+    type SerializeTupleStruct = Impossible<Self::Ok, Self::Error>;
+    type SerializeTupleVariant = Impossible<Self::Ok, Self::Error>;
+    type SerializeMap = Impossible<Self::Ok, Self::Error>;
+    type SerializeStruct = Impossible<Self::Ok, Self::Error>;
+    type SerializeStructVariant = Impossible<Self::Ok, Self::Error>;
+
+    serialize_unsupported! {
+        bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str
+        bytes unit newtype_struct newtype_variant unit_struct unit_variant
+        seq tuple tuple_struct tuple_variant map struct struct_variant
+    }
+
+    fn serialize_some<T: ?Sized>(self, value: &T) -> Result<Self::Ok, Self::Error>
+    where
+        T: serde::Serialize,
+    {
+        Ok(Some(value.serialize(self.inner)?))
+    }
+
+    fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
+        Ok(None)
+    }
+}


Test patch:
diff --git a/src/de/tests.rs b/src/de/tests.rs
index 9c66061..2c0bebd 100644
--- a/src/de/tests.rs
+++ b/src/de/tests.rs
@@ -1,7 +1,7 @@
 use super::*;
 use crate::{
-    BinaryOp, BinaryOperator, Block, Body, ElementAccess, Expression, FuncCall, Identifier,
-    ObjectKey, Operation, UnaryOp, UnaryOperator,
+    BinaryOp, BinaryOperator, Block, Body, ElementAccess, Expression, ForExpr, ForIntro,
+    ForListExpr, ForObjectExpr, FuncCall, Identifier, ObjectKey, Operation, UnaryOp, UnaryOperator,
 };
 use pretty_assertions::assert_eq;
 use serde::Deserialize;
@@ -234,6 +234,52 @@ fn deserialize_operation() {
     assert_eq!(expected, from_str::<Body>(input).unwrap());
 }
 
+#[test]
+fn deserialize_for_expr() {
+    let input = r#"
+        list = [for item in items : func(item) if item]
+        object = {for key, value in items : toupper(key) => tolower(value)...}
+    "#;
+
+    let expected = Body::builder()
+        .add_attribute((
+            "list",
+            ForExpr::List(
+                ForListExpr::new(
+                    ForIntro::new(
+                        Identifier::new("item"),
+                        Expression::VariableExpr(Identifier::new("items")),
+                    ),
+                    FuncCall::builder("func")
+                        .arg(Identifier::new("item"))
+                        .build(),
+                )
+                .with_cond(Identifier::new("item")),
+            ),
+        ))
+        .add_attribute((
+            "object",
+            ForExpr::Object(
+                ForObjectExpr::new(
+                    ForIntro::new(
+                        Identifier::new("value"),
+                        Expression::VariableExpr(Identifier::new("items")),
+                    )
+                    .with_key(Identifier::new("key")),
+                    FuncCall::builder("toupper")
+                        .arg(Identifier::new("key"))
+                        .build(),
+                    FuncCall::builder("tolower")
+                        .arg(Identifier::new("value"))
+                        .build(),
+                )
+                .with_value_grouping(true),
+            ),
+        ))
+        .build();
+    assert_eq!(expected, from_str::<Body>(input).unwrap());
+}
+
 #[test]
 fn deserialize_invalid_hcl() {
     let h = r#"invalid["#;
diff --git a/src/ser/tests.rs b/src/ser/tests.rs
index c4c93a2..e7f0486 100644
--- a/src/ser/tests.rs
+++ b/src/ser/tests.rs
@@ -1,8 +1,9 @@
 use super::*;
 use crate::{
     BinaryOp, BinaryOperator, Block, BlockLabel, Body, Conditional, ElementAccess,
-    ElementAccessOperator, Expression, FuncCall, Heredoc, HeredocStripMode, Identifier, Object,
-    ObjectKey, Operation, RawExpression, TemplateExpr,
+    ElementAccessOperator, Expression, ForExpr, ForIntro, ForListExpr, ForObjectExpr, FuncCall,
+    Heredoc, HeredocStripMode, Identifier, Object, ObjectKey, Operation, RawExpression,
+    TemplateExpr,
 };
 use pretty_assertions::assert_eq;
 use serde_json::json;
@@ -233,7 +234,7 @@ fn serialize_conditional() {
 }
 
 #[test]
-fn serialize_oparation() {
+fn serialize_operation() {
     let body = Body::builder()
         .add_attribute((
             "op",
@@ -244,6 +245,59 @@ fn serialize_oparation() {
     assert_eq!(to_string(&body).unwrap(), "op = 1 + 1\n");
 }
 
+#[test]
+fn serialize_for_expr() {
+    let body = Body::builder()
+        .add_attribute((
+            "list",
+            ForExpr::List(
+                ForListExpr::new(
+                    ForIntro::new(
+                        Identifier::new("item"),
+                        Expression::VariableExpr(Identifier::new("items")),
+                    ),
+                    FuncCall::builder("func")
+                        .arg(Identifier::new("item"))
+                        .build(),
+                )
+                .with_cond(Identifier::new("item")),
+            ),
+        ))
+        .add_attribute((
+            "object",
+            ForExpr::Object(
+                ForObjectExpr::new(
+                    ForIntro::new(
+                        Identifier::new("value"),
+                        Expression::VariableExpr(Identifier::new("items")),
+                    )
+                    .with_key(Identifier::new("key")),
+                    FuncCall::builder("toupper")
+                        .arg(Identifier::new("key"))
+                        .build(),
+                    FuncCall::builder("tolower")
+                        .arg(Identifier::new("value"))
+                        .build(),
+                )
+                .with_cond(Operation::Binary(BinaryOp::new(
+                    Identifier::new("value"),
+                    BinaryOperator::NotEq,
+                    Expression::Null,
+                )))
+                .with_value_grouping(true),
+            ),
+        ))
+        .build();
+
+    let expected = r#"
+list = [for item in items : func(item) if item]
+object = {for key, value in items : toupper(key) => tolower(value)... if value != null}
+"#
+    .trim_start();
+
+    assert_eq!(to_string(&body).unwrap(), expected);
+}
+
 #[test]
 fn serialize_func_call() {
     let body = Body::builder()
diff --git a/src/structure/ser/tests.rs b/src/structure/ser/tests.rs
index 7018107..5f54a86 100644
--- a/src/structure/ser/tests.rs
+++ b/src/structure/ser/tests.rs
@@ -4,6 +4,7 @@ use super::{
     body::BodySerializer,
     conditional::ConditionalSerializer,
     expression::ExpressionSerializer,
+    for_expr::ForExprSerializer,
     operation::OperationSerializer,
     template::TemplateExprSerializer,
 };
@@ -88,6 +89,39 @@ fn identity() {
         OperationSerializer,
         Operation::Binary(BinaryOp::new(1, BinaryOperator::Plus, 1)),
     );
+    test_identity(
+        ForExprSerializer,
+        ForExpr::List(
+            ForListExpr::new(
+                ForIntro::new(
+                    Identifier::new("value"),
+                    vec![Expression::String(String::from("foo"))],
+                )
+                .with_key(Identifier::new("index")),
+                Identifier::new("other_value"),
+            )
+            .with_cond(Expression::Bool(true)),
+        ),
+    );
+    test_identity(
+        ForExprSerializer,
+        ForExpr::Object(
+            ForObjectExpr::new(
+                ForIntro::new(
+                    Identifier::new("value"),
+                    Expression::Object(Object::from_iter(vec![(
+                        ObjectKey::from("k"),
+                        Expression::String(String::from("v")),
+                    )])),
+                )
+                .with_key(Identifier::new("index")),
+                Identifier::new("other_key"),
+                Identifier::new("other_value"),
+            )
+            .with_cond(Expression::Bool(true))
+            .with_value_grouping(true),
+        ),
+    );
 }
 
 #[test]