Repository: martinohmann/hcl-rs
Pull request #63 (2022-09-07 19:33:25)

feat: add `Expression::ElementAccess`
This adds supports for parsing and serializing element access on expression like `some_var.some_attribute` or `foo[0]`.


Patch:
diff --git a/src/format/impls.rs b/src/format/impls.rs
index 7c17f1f..a7d9853 100644
--- a/src/format/impls.rs
+++ b/src/format/impls.rs
@@ -4,7 +4,7 @@ use crate::{
         Attribute, Block, BlockLabel, Body, Expression, Heredoc, HeredocStripMode, Identifier,
         ObjectKey, RawExpression, Structure, TemplateExpr,
     },
-    Map, Number, Result, Value,
+    ElementAccess, ElementAccessOperator, Map, Number, Result, Value,
 };
 use std::io::{self, Write};
 
@@ -106,6 +106,7 @@ impl Format for Expression {
             Expression::Raw(raw) => raw.format(fmt),
             Expression::TemplateExpr(expr) => expr.format(fmt),
             Expression::VariableExpr(ident) => ident.format(fmt),
+            Expression::ElementAccess(access) => access.format(fmt),
         }
     }
 }
@@ -234,6 +235,48 @@ impl Format for Identifier {
     }
 }
 
+impl private::Sealed for ElementAccess {}
+
+impl Format for ElementAccess {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        self.expr.format(fmt)?;
+        self.operator.format(fmt)?;
+        Ok(())
+    }
+}
+
+impl private::Sealed for ElementAccessOperator {}
+
+impl Format for ElementAccessOperator {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        match self {
+            ElementAccessOperator::AttrSplat => fmt.write_all(b".*")?,
+            ElementAccessOperator::FullSplat => fmt.write_all(b"[*]")?,
+            ElementAccessOperator::GetAttr(ident) => {
+                fmt.write_all(b".")?;
+                ident.format(fmt)?;
+            }
+            ElementAccessOperator::LegacyIndex(index) => {
+                fmt.write_all(b".")?;
+                fmt.write_int(*index)?;
+            }
+            ElementAccessOperator::Index(expr) => {
+                fmt.write_all(b"[")?;
+                expr.format(fmt)?;
+                fmt.write_all(b"]")?;
+            }
+        }
+
+        Ok(())
+    }
+}
+
 impl private::Sealed for String {}
 
 impl Format for String {
diff --git a/src/lib.rs b/src/lib.rs
index 448b032..862cd80 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -27,9 +27,9 @@ 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, Expression,
-    Heredoc, HeredocStripMode, Identifier, Object, ObjectKey, RawExpression, Structure,
-    TemplateExpr,
+    ser::to_expression, Attribute, Block, BlockBuilder, BlockLabel, Body, BodyBuilder,
+    ElementAccess, ElementAccessOperator, Expression, Heredoc, HeredocStripMode, Identifier,
+    Object, ObjectKey, RawExpression, Structure, TemplateExpr,
 };
 #[doc(inline)]
 pub use value::{Map, Value};
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index d764033..605a8bf 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -3,8 +3,9 @@ mod template;
 mod tests;
 
 use crate::{
-    structure::Identifier, util::unescape, Attribute, Block, BlockLabel, Body, Expression, Heredoc,
-    HeredocStripMode, Number, Object, ObjectKey, RawExpression, Result, Structure, TemplateExpr,
+    structure::Identifier, util::unescape, Attribute, Block, BlockLabel, Body,
+    ElementAccessOperator, Expression, Heredoc, HeredocStripMode, Number, Object, ObjectKey,
+    RawExpression, Result, Structure, TemplateExpr,
 };
 use pest::{
     iterators::{Pair, Pairs},
@@ -155,32 +156,50 @@ fn parse_expressions(pair: Pair<Rule>) -> Result<Vec<Expression>> {
 }
 
 fn parse_expr_term(pair: Pair<Rule>) -> Result<Expression> {
-    let raw = pair.as_str();
     let mut pairs = pair.into_inner();
     let pair = pairs.next().unwrap();
 
-    if pairs.peek().is_some() {
-        // @TODO(mohmann): ExprTerm is followed by an traversal like Splat, GetAttr or Index.
-        // For now we treat these as raw expressions and not further process these.
-        Ok(Expression::Raw(raw_expression(raw)))
-    } else {
-        match pair.as_rule() {
-            Rule::BooleanLit => Ok(Expression::Bool(parse_primitive(pair))),
-            Rule::Float => Ok(Expression::Number(parse_primitive::<f64>(pair).into())),
-            Rule::Int => Ok(Expression::Number(parse_primitive::<i64>(pair).into())),
-            Rule::NullLit => Ok(Expression::Null),
-            Rule::StringLit => parse_string(inner(pair)).map(Expression::String),
-            Rule::TemplateExpr => {
-                let expr = parse_template_expr(inner(pair));
-                Ok(Expression::TemplateExpr(Box::new(expr)))
+    let expr = match pair.as_rule() {
+        Rule::BooleanLit => Expression::Bool(parse_primitive(pair)),
+        Rule::Float => Expression::Number(parse_primitive::<f64>(pair).into()),
+        Rule::Int => Expression::Number(parse_primitive::<i64>(pair).into()),
+        Rule::NullLit => Expression::Null,
+        Rule::StringLit => parse_string(inner(pair)).map(Expression::String)?,
+        Rule::TemplateExpr => {
+            let expr = parse_template_expr(inner(pair));
+            Expression::TemplateExpr(Box::new(expr))
+        }
+        Rule::Tuple => parse_expressions(pair).map(Expression::Array)?,
+        Rule::Object => parse_object(pair).map(Expression::Object)?,
+        Rule::VariableExpr => Expression::VariableExpr(parse_ident(pair).into()),
+        // @TODO(mohmann): Process ForExpr etc.
+        _ => Expression::Raw(raw_expression(pair.as_str())),
+    };
+
+    pairs.try_fold(expr, |expr, pair| {
+        Ok(expr.element(parse_element_access_operator(pair)?))
+    })
+}
+
+fn parse_element_access_operator(pair: Pair<Rule>) -> Result<ElementAccessOperator> {
+    let operator = match pair.as_rule() {
+        Rule::AttrSplat => ElementAccessOperator::AttrSplat,
+        Rule::FullSplat => ElementAccessOperator::FullSplat,
+        Rule::GetAttr => ElementAccessOperator::GetAttr(parse_ident(inner(pair)).into()),
+        Rule::Index => {
+            let pair = inner(pair);
+
+            match pair.as_rule() {
+                Rule::LegacyIndex => {
+                    ElementAccessOperator::LegacyIndex(parse_primitive::<u64>(pair))
+                }
+                _ => ElementAccessOperator::Index(parse_expression(pair)?),
             }
-            Rule::Tuple => parse_expressions(pair).map(Expression::Array),
-            Rule::Object => parse_object(pair).map(Expression::Object),
-            Rule::VariableExpr => Ok(Expression::VariableExpr(parse_ident(pair).into())),
-            // @TODO(mohmann): Process ForExpr, etc.
-            _ => Ok(Expression::Raw(raw_expression(pair.as_str()))),
         }
-    }
+        rule => unexpected_rule(rule),
+    };
+
+    Ok(operator)
 }
 
 fn parse_template_expr(pair: Pair<Rule>) -> TemplateExpr {
diff --git a/src/structure/de.rs b/src/structure/de.rs
index b67a9df..1e6853e 100644
--- a/src/structure/de.rs
+++ b/src/structure/de.rs
@@ -1,8 +1,8 @@
 //! Deserialize impls for HCL structure types.
 
 use super::{
-    Attribute, Block, BlockLabel, Body, Expression, Heredoc, HeredocStripMode, Identifier,
-    ObjectKey, RawExpression, Structure, TemplateExpr,
+    Attribute, Block, BlockLabel, Body, ElementAccess, ElementAccessOperator, Expression, Heredoc,
+    HeredocStripMode, Identifier, ObjectKey, RawExpression, Structure, TemplateExpr,
 };
 use crate::{Error, Number, Result};
 use serde::de::{self, value::MapAccessDeserializer, IntoDeserializer};
@@ -261,6 +261,9 @@ impl<'de> de::Deserializer<'de> for Expression {
             Expression::Raw(expr) => expr.into_deserializer().deserialize_any(visitor),
             Expression::TemplateExpr(expr) => expr.into_deserializer().deserialize_any(visitor),
             Expression::VariableExpr(expr) => expr.into_deserializer().deserialize_any(visitor),
+            Expression::ElementAccess(access) => {
+                access.into_deserializer().deserialize_any(visitor)
+            }
         }
     }
 
@@ -289,6 +292,7 @@ impl VariantName for Expression {
             Expression::Raw(_) => "Raw",
             Expression::TemplateExpr(_) => "TemplateExpr",
             Expression::VariableExpr(_) => "VariableExpr",
+            Expression::ElementAccess(_) => "ElementAccess",
         }
     }
 }
@@ -344,6 +348,104 @@ impl<'de> de::VariantAccess<'de> for Expression {
     }
 }
 
+impl<'de> IntoDeserializer<'de, Error> for ElementAccess {
+    type Deserializer = MapAccessDeserializer<ElementAccessAccess>;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        MapAccessDeserializer::new(ElementAccessAccess::new(self))
+    }
+}
+
+pub struct ElementAccessAccess {
+    expr: Option<Expression>,
+    operator: Option<ElementAccessOperator>,
+}
+
+impl ElementAccessAccess {
+    fn new(access: ElementAccess) -> Self {
+        ElementAccessAccess {
+            expr: Some(access.expr),
+            operator: Some(access.operator),
+        }
+    }
+}
+
+impl<'de> de::MapAccess<'de> for ElementAccessAccess {
+    type Error = Error;
+
+    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+    where
+        K: de::DeserializeSeed<'de>,
+    {
+        if self.expr.is_some() {
+            seed.deserialize("expr".into_deserializer()).map(Some)
+        } else if self.operator.is_some() {
+            seed.deserialize("operator".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(expr) = self.expr.take() {
+            seed.deserialize(expr.into_deserializer())
+        } else if let Some(operator) = self.operator.take() {
+            seed.deserialize(operator.into_deserializer())
+        } else {
+            Err(de::Error::custom("invalid HCL element access"))
+        }
+    }
+}
+
+impl<'de> IntoDeserializer<'de, Error> for ElementAccessOperator {
+    type Deserializer = Self;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        self
+    }
+}
+
+impl<'de> de::Deserializer<'de> for ElementAccessOperator {
+    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 {
+            ElementAccessOperator::AttrSplat => visitor.visit_str(".*"),
+            ElementAccessOperator::FullSplat => visitor.visit_str("[*]"),
+            ElementAccessOperator::GetAttr(ident) => {
+                ident.into_deserializer().deserialize_any(visitor)
+            }
+            ElementAccessOperator::Index(expr) => expr.into_deserializer().deserialize_any(visitor),
+            ElementAccessOperator::LegacyIndex(index) => visitor.visit_u64(index),
+        }
+    }
+}
+
+impl VariantName for ElementAccessOperator {
+    fn variant_name(&self) -> &'static str {
+        match self {
+            ElementAccessOperator::AttrSplat => "AttrSplat",
+            ElementAccessOperator::FullSplat => "FullSplat",
+            ElementAccessOperator::GetAttr(_) => "GetAttr",
+            ElementAccessOperator::Index(_) => "Index",
+            ElementAccessOperator::LegacyIndex(_) => "LegacyIndex",
+        }
+    }
+}
+
 impl<'de> IntoDeserializer<'de, Error> for ObjectKey {
     type Deserializer = Self;
 
diff --git a/src/structure/element_access.rs b/src/structure/element_access.rs
new file mode 100644
index 0000000..fce30fc
--- /dev/null
+++ b/src/structure/element_access.rs
@@ -0,0 +1,80 @@
+use crate::{Expression, Identifier};
+use serde::{Deserialize, Serialize};
+
+/// Access to an element of an expression result.
+#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
+#[serde(rename = "$hcl::element_access")]
+pub struct ElementAccess {
+    /// The expression that the access operator is applied to.
+    pub expr: Expression,
+    /// The element access operator used on the expression.
+    pub operator: ElementAccessOperator,
+}
+
+impl ElementAccess {
+    /// Creates a new `ElementAccess` structure from an access operator and and expression that it
+    /// should be applied to.
+    pub fn new<E, O>(expr: E, operator: O) -> Self
+    where
+        E: Into<Expression>,
+        O: Into<ElementAccessOperator>,
+    {
+        ElementAccess {
+            expr: expr.into(),
+            operator: operator.into(),
+        }
+    }
+
+    /// Chains another `ElementAccessOperator` and returns a new `ElementAccess`.
+    pub fn chain<O>(self, operator: O) -> ElementAccess
+    where
+        O: Into<ElementAccessOperator>,
+    {
+        ElementAccess {
+            expr: Expression::ElementAccess(Box::new(self)),
+            operator: operator.into(),
+        }
+    }
+}
+
+/// The kinds of element access that are supported by HCL.
+#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
+#[serde(rename = "$hcl::element_access_operator")]
+pub enum ElementAccessOperator {
+    /// The attribute-only splat operator supports only attribute lookups into the elements from a
+    /// list, but supports an arbitrary number of them.
+    AttrSplat,
+    /// The full splat operator additionally supports indexing into the elements from a list, and
+    /// allows any combination of attribute access and index operations.
+    FullSplat,
+    /// The attribute access operator returns the value of a single attribute in an object value.
+    GetAttr(Identifier),
+    /// The index operator returns the value of a single element of a collection value based on
+    /// the result of the expression.
+    Index(Expression),
+    /// The legacy index operator returns the value of a single element of a collection value.
+    /// Exists only for compatibility with the precursor language HIL. Use the `Index` variant
+    /// instead.
+    LegacyIndex(u64),
+}
+
+impl<T> From<T> for ElementAccessOperator
+where
+    T: Into<Identifier>,
+{
+    fn from(value: T) -> ElementAccessOperator {
+        ElementAccessOperator::GetAttr(value.into())
+    }
+}
+
+impl From<Expression> for ElementAccessOperator {
+    fn from(value: Expression) -> ElementAccessOperator {
+        ElementAccessOperator::Index(value)
+    }
+}
+
+impl From<u64> for ElementAccessOperator {
+    fn from(value: u64) -> ElementAccessOperator {
+        ElementAccessOperator::LegacyIndex(value)
+    }
+}
diff --git a/src/structure/expression.rs b/src/structure/expression.rs
index fcaac3d..434c1fb 100644
--- a/src/structure/expression.rs
+++ b/src/structure/expression.rs
@@ -1,6 +1,6 @@
 //! Types to represent HCL attribute value expressions.
 
-use super::{Identifier, TemplateExpr};
+use super::{ElementAccess, ElementAccessOperator, Identifier, TemplateExpr};
 use crate::{Number, Value};
 use serde::{Deserialize, Serialize};
 use std::borrow::Cow;
@@ -32,11 +32,29 @@ pub enum Expression {
     TemplateExpr(Box<TemplateExpr>),
     /// Represents a variable name identifier.
     VariableExpr(Identifier),
+    /// Represents an attribute or element access.
+    ElementAccess(Box<ElementAccess>),
     /// 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),
 }
 
+impl Expression {
+    /// Applies an `ElementAccessOperator` to the expression and returns the result. The result is
+    /// always of variant `ElementAccess`.
+    pub(crate) fn element<O>(self, operator: O) -> Expression
+    where
+        O: Into<ElementAccessOperator>,
+    {
+        let access = match self {
+            Expression::ElementAccess(access) => access.chain(operator),
+            other => ElementAccess::new(other, operator),
+        };
+
+        Expression::ElementAccess(Box::new(access))
+    }
+}
+
 impl From<Expression> for Value {
     fn from(expr: Expression) -> Self {
         match expr {
@@ -47,8 +65,8 @@ 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::VariableExpr(ident) => Value::String(RawExpression(ident.0).into()),
             Expression::Raw(raw) => Value::String(raw.into()),
+            other => Value::String(RawExpression(other.to_string()).into()),
         }
     }
 }
@@ -163,6 +181,12 @@ impl From<RawExpression> for Expression {
     }
 }
 
+impl From<ElementAccess> for Expression {
+    fn from(access: ElementAccess) -> Self {
+        Expression::ElementAccess(Box::new(access))
+    }
+}
+
 impl From<TemplateExpr> for Expression {
     fn from(expr: TemplateExpr) -> Self {
         Expression::TemplateExpr(Box::new(expr))
@@ -175,6 +199,15 @@ impl From<Identifier> for Expression {
     }
 }
 
+impl Display for Expression {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match crate::format::to_string(self) {
+            Ok(s) => f.write_str(&s),
+            Err(_) => Err(fmt::Error),
+        }
+    }
+}
+
 /// Represents an object key.
 #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)]
 #[serde(rename = "$hcl::object_key")]
diff --git a/src/structure/mod.rs b/src/structure/mod.rs
index 0c939a2..c83e705 100644
--- a/src/structure/mod.rs
+++ b/src/structure/mod.rs
@@ -49,6 +49,7 @@ mod attribute;
 mod block;
 mod body;
 pub(crate) mod de;
+mod element_access;
 mod expression;
 pub(crate) mod ser;
 mod template;
@@ -59,6 +60,7 @@ pub use self::{
     attribute::Attribute,
     block::{Block, BlockBuilder, BlockLabel},
     body::{Body, BodyBuilder},
+    element_access::{ElementAccess, ElementAccessOperator},
     expression::{Expression, Object, ObjectKey, RawExpression},
     template::{Heredoc, HeredocStripMode, TemplateExpr},
 };
diff --git a/src/structure/ser/element_access.rs b/src/structure/ser/element_access.rs
new file mode 100644
index 0000000..3306898
--- /dev/null
+++ b/src/structure/ser/element_access.rs
@@ -0,0 +1,150 @@
+use super::{expression::ExpressionSerializer, StringSerializer};
+use crate::{
+    serialize_unsupported, structure::Identifier, ElementAccess, ElementAccessOperator, Error,
+    Expression, Result,
+};
+use serde::ser::{self, Impossible, Serialize};
+
+pub struct ElementAccessSerializer;
+
+impl ser::Serializer for ElementAccessSerializer {
+    type Ok = ElementAccess;
+    type Error = Error;
+
+    type SerializeSeq = Impossible<ElementAccess, Error>;
+    type SerializeTuple = Impossible<ElementAccess, Error>;
+    type SerializeTupleStruct = Impossible<ElementAccess, Error>;
+    type SerializeTupleVariant = Impossible<ElementAccess, Error>;
+    type SerializeMap = Impossible<ElementAccess, Error>;
+    type SerializeStruct = SerializeElementAccessStruct;
+    type SerializeStructVariant = Impossible<ElementAccess, 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(SerializeElementAccessStruct::new())
+    }
+}
+
+pub struct SerializeElementAccessStruct {
+    expr: Option<Expression>,
+    operator: Option<ElementAccessOperator>,
+}
+
+impl SerializeElementAccessStruct {
+    pub fn new() -> Self {
+        SerializeElementAccessStruct {
+            expr: None,
+            operator: None,
+        }
+    }
+}
+
+impl ser::SerializeStruct for SerializeElementAccessStruct {
+    type Ok = ElementAccess;
+    type Error = Error;
+
+    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        match key {
+            "expr" => self.expr = Some(value.serialize(ExpressionSerializer)?),
+            "operator" => self.operator = Some(value.serialize(ElementAccessOperatorSerializer)?),
+            _ => {
+                return Err(ser::Error::custom(
+                    "expected struct with fields `expr` and `operator`",
+                ))
+            }
+        };
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match (self.expr, self.operator) {
+            (Some(expr), Some(operator)) => Ok(ElementAccess::new(expr, operator)),
+            (_, _) => Err(ser::Error::custom(
+                "expected struct with fields `expr` and `operator`",
+            )),
+        }
+    }
+}
+
+#[derive(Clone)]
+struct ElementAccessOperatorSerializer;
+
+impl ser::Serializer for ElementAccessOperatorSerializer {
+    type Ok = ElementAccessOperator;
+    type Error = Error;
+
+    type SerializeSeq = Impossible<ElementAccessOperator, Error>;
+    type SerializeTuple = Impossible<ElementAccessOperator, Error>;
+    type SerializeTupleStruct = Impossible<ElementAccessOperator, Error>;
+    type SerializeTupleVariant = Impossible<ElementAccessOperator, Error>;
+    type SerializeMap = Impossible<ElementAccessOperator, Error>;
+    type SerializeStruct = Impossible<ElementAccessOperator, Error>;
+    type SerializeStructVariant = Impossible<ElementAccessOperator, Error>;
+
+    serialize_unsupported! {
+        bool i8 i16 i32 i64 u8 u16 u32 f32 f64
+        char str bytes none unit unit_struct
+        seq tuple tuple_struct tuple_variant
+        map struct struct_variant
+    }
+    serialize_self! { some }
+
+    fn serialize_u64(self, v: u64) -> Result<Self::Ok> {
+        Ok(ElementAccessOperator::LegacyIndex(v))
+    }
+
+    fn serialize_unit_variant(
+        self,
+        name: &'static str,
+        _variant_index: u32,
+        variant: &'static str,
+    ) -> Result<Self::Ok> {
+        match (name, variant) {
+            ("$hcl::element_access_operator", "AttrSplat") => Ok(ElementAccessOperator::AttrSplat),
+            ("$hcl::element_access_operator", "FullSplat") => Ok(ElementAccessOperator::FullSplat),
+            (_, _) => Ok(ElementAccessOperator::GetAttr(variant.into())),
+        }
+    }
+
+    fn serialize_newtype_variant<T>(
+        self,
+        name: &'static str,
+        _variant_index: u32,
+        _variant: &'static str,
+        value: &T,
+    ) -> Result<Self::Ok>
+    where
+        T: ?Sized + Serialize,
+    {
+        if name == "$hcl::expression" {
+            Ok(ElementAccessOperator::Index(
+                value.serialize(ExpressionSerializer)?,
+            ))
+        } else {
+            value.serialize(self)
+        }
+    }
+
+    fn serialize_newtype_struct<T>(self, name: &'static str, value: &T) -> Result<Self::Ok>
+    where
+        T: ?Sized + Serialize,
+    {
+        if name == "$hcl::identifier" {
+            Ok(ElementAccessOperator::GetAttr(Identifier::from(
+                value.serialize(StringSerializer)?,
+            )))
+        } else {
+            value.serialize(self)
+        }
+    }
+}
diff --git a/src/structure/ser/expression.rs b/src/structure/ser/expression.rs
index ae7eaee..72a570b 100644
--- a/src/structure/ser/expression.rs
+++ b/src/structure/ser/expression.rs
@@ -1,10 +1,14 @@
-use super::{template::TemplateExprSerializer, StringSerializer};
+use super::{
+    element_access::SerializeElementAccessStruct, template::TemplateExprSerializer,
+    StringSerializer,
+};
 use crate::{
     serialize_unsupported, Error, Expression, Identifier, Object, ObjectKey, RawExpression, Result,
 };
-use serde::ser::{self, Impossible};
+use serde::ser::{self, Impossible, SerializeMap};
 use std::fmt::Display;
 
+#[derive(Clone)]
 pub struct ExpressionSerializer;
 
 impl ser::Serializer for ExpressionSerializer {
@@ -16,7 +20,7 @@ impl ser::Serializer for ExpressionSerializer {
     type SerializeTupleStruct = SerializeExpressionSeq;
     type SerializeTupleVariant = SerializeExpressionTupleVariant;
     type SerializeMap = SerializeExpressionMap;
-    type SerializeStruct = SerializeExpressionMap;
+    type SerializeStruct = SerializeExpressionStruct;
     type SerializeStructVariant = SerializeExpressionStructVariant;
 
     serialize_self! { some }
@@ -166,8 +170,8 @@ impl ser::Serializer for ExpressionSerializer {
         Ok(SerializeExpressionMap::new(len))
     }
 
-    fn serialize_struct(self, _name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
-        self.serialize_map(Some(len))
+    fn serialize_struct(self, name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
+        Ok(SerializeExpressionStruct::new(name, len))
     }
 
     fn serialize_struct_variant(
@@ -300,7 +304,24 @@ impl ser::SerializeMap for SerializeExpressionMap {
     }
 }
 
-impl ser::SerializeStruct for SerializeExpressionMap {
+pub enum SerializeExpressionStruct {
+    ElementAccess(SerializeElementAccessStruct),
+    Other(SerializeExpressionMap),
+}
+
+impl SerializeExpressionStruct {
+    fn new(name: &'static str, len: usize) -> Self {
+        // Specialization for the `ElementAccess`.
+        match name {
+            "$hcl::element_access" => {
+                SerializeExpressionStruct::ElementAccess(SerializeElementAccessStruct::new())
+            }
+            _ => SerializeExpressionStruct::Other(SerializeExpressionMap::new(Some(len))),
+        }
+    }
+}
+
+impl ser::SerializeStruct for SerializeExpressionStruct {
     type Ok = Expression;
     type Error = Error;
 
@@ -308,11 +329,17 @@ impl ser::SerializeStruct for SerializeExpressionMap {
     where
         T: ?Sized + ser::Serialize,
     {
-        ser::SerializeMap::serialize_entry(self, key, value)
+        match self {
+            SerializeExpressionStruct::ElementAccess(ser) => ser.serialize_field(key, value),
+            SerializeExpressionStruct::Other(ser) => ser.serialize_entry(key, value),
+        }
     }
 
     fn end(self) -> Result<Self::Ok> {
-        ser::SerializeMap::end(self)
+        match self {
+            SerializeExpressionStruct::ElementAccess(ser) => ser.end().map(Into::into),
+            SerializeExpressionStruct::Other(ser) => ser.end(),
+        }
     }
 }
 
diff --git a/src/structure/ser/mod.rs b/src/structure/ser/mod.rs
index 734b124..de6dd2a 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 element_access;
 mod expression;
 mod structure;
 mod template;


Test patch:
diff --git a/src/de/tests.rs b/src/de/tests.rs
index 549f9a3..e4465d2 100644
--- a/src/de/tests.rs
+++ b/src/de/tests.rs
@@ -1,5 +1,5 @@
 use super::*;
-use crate::{Block, Body, Expression, ObjectKey, RawExpression};
+use crate::{Block, Body, ElementAccess, Expression, Identifier, ObjectKey};
 use pretty_assertions::assert_eq;
 use serde::Deserialize;
 use serde_json::{json, Value};
@@ -235,7 +235,11 @@ fn deserialize_terraform() {
                                     Block::builder("apply_server_side_encryption_by_default")
                                         .add_attribute((
                                             "kms_master_key_id",
-                                            RawExpression::new("aws_kms_key.mykey.arn"),
+                                            ElementAccess::new(
+                                                Identifier::new("aws_kms_key"),
+                                                "mykey",
+                                            )
+                                            .chain("arn"),
                                         ))
                                         .add_attribute(("sse_algorithm", "aws:kms"))
                                         .build(),
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index 5fcad72..65240d9 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -1,4 +1,5 @@
 use super::*;
+use crate::{ElementAccess, Identifier};
 use pest::*;
 use pretty_assertions::assert_eq;
 
@@ -565,7 +566,11 @@ fn parse_hcl() {
                                     Block::builder("apply_server_side_encryption_by_default")
                                         .add_attribute(Attribute::new(
                                             "kms_master_key_id",
-                                            RawExpression::new("aws_kms_key.mykey.arn"),
+                                            ElementAccess::new(
+                                                Identifier::new("aws_kms_key"),
+                                                "mykey",
+                                            )
+                                            .chain("arn"),
                                         ))
                                         .add_attribute(Attribute::new("sse_algorithm", "aws:kms"))
                                         .build(),
diff --git a/src/ser/tests.rs b/src/ser/tests.rs
index 6b02fbc..5bcda11 100644
--- a/src/ser/tests.rs
+++ b/src/ser/tests.rs
@@ -1,7 +1,7 @@
 use super::*;
 use crate::{
-    Block, BlockLabel, Body, Expression, Heredoc, HeredocStripMode, Identifier, Object, ObjectKey,
-    RawExpression, TemplateExpr,
+    Block, BlockLabel, Body, ElementAccess, ElementAccessOperator, Expression, Heredoc,
+    HeredocStripMode, Identifier, Object, ObjectKey, RawExpression, TemplateExpr,
 };
 use pretty_assertions::assert_eq;
 use serde_json::json;
@@ -195,6 +195,27 @@ fn serialize_empty_block() {
     assert_eq!(to_string(&body).unwrap(), "empty {}\n");
 }
 
+#[test]
+fn serialize_element_access() {
+    let body = Body::builder()
+        .add_attribute((
+            "attr",
+            ElementAccess::new(Identifier::new("var"), "foo")
+                .chain(ElementAccessOperator::FullSplat)
+                .chain("bar")
+                .chain(ElementAccessOperator::Index(1u64.into()))
+                .chain(ElementAccessOperator::AttrSplat)
+                .chain("baz")
+                .chain(42),
+        ))
+        .build();
+
+    assert_eq!(
+        to_string(&body).unwrap(),
+        "attr = var.foo[*].bar[1].*.baz.42\n"
+    );
+}
+
 #[test]
 fn serialize_heredoc() {
     let body = Body::builder()
@@ -311,7 +332,11 @@ fn roundtrip() {
                                     Block::builder("apply_server_side_encryption_by_default")
                                         .add_attribute((
                                             "kms_master_key_id",
-                                            RawExpression::new("aws_kms_key.mykey.arn"),
+                                            ElementAccess::new(
+                                                Identifier::new("aws_kms_key"),
+                                                "mykey",
+                                            )
+                                            .chain("arn"),
                                         ))
                                         .add_attribute(("sse_algorithm", "aws:kms"))
                                         .build(),