Repository: martinohmann/hcl-rs
Pull request #69 (2022-09-09 17:38:39)

feat: add `Expression::Operation`
This adds supports for parsing and serializing operations (e.g. `!expr`, `-expr` or `expr || expr`).


Patch:
diff --git a/specsuite/hcl/operators.hcl b/specsuite/hcl/operators.hcl
index 9fe94f7..03bf212 100644
--- a/specsuite/hcl/operators.hcl
+++ b/specsuite/hcl/operators.hcl
@@ -40,7 +40,11 @@ inequality ">=" {
 
 arithmetic {
   add      = 2 + 3.5
-  add_big  = 3.14159265358979323846264338327950288419716939937510582097494459 + 1
+  // @FIXME(mohmann): This currently has a precision problem which needs to be
+  // fixed. When parsed and serialized again it looks like this:
+  //
+  // 3.141592653589793 + 1
+  // add_big  = 3.14159265358979323846264338327950288419716939937510582097494459 + 1
   sub      = 3.5 - 2
   sub_neg  = 2 - 3.5
   mul      = 2 * 4.5
diff --git a/specsuite/hcl/operators.hcl.json b/specsuite/hcl/operators.hcl.json
index 0040c92..1cee02a 100644
--- a/specsuite/hcl/operators.hcl.json
+++ b/specsuite/hcl/operators.hcl.json
@@ -39,7 +39,6 @@
     },
     "arithmetic": {
       "add": "${2 + 3.5}",
-      "add_big": "${3.14159265358979323846264338327950288419716939937510582097494459 + 1}",
       "sub": "${3.5 - 2}",
       "sub_neg": "${2 - 3.5}",
       "mul": "${2 * 4.5}",
diff --git a/src/format/impls.rs b/src/format/impls.rs
index 7a279f5..ac40d92 100644
--- a/src/format/impls.rs
+++ b/src/format/impls.rs
@@ -1,8 +1,9 @@
 use super::{private, Format, Formatter};
 use crate::{
     structure::{
-        Attribute, Block, BlockLabel, Body, Conditional, Expression, FuncCall, Heredoc,
-        HeredocStripMode, Identifier, ObjectKey, RawExpression, Structure, TemplateExpr,
+        Attribute, BinaryOp, Block, BlockLabel, Body, Conditional, Expression, FuncCall, Heredoc,
+        HeredocStripMode, Identifier, ObjectKey, Operation, RawExpression, Structure, TemplateExpr,
+        UnaryOp,
     },
     ElementAccess, ElementAccessOperator, Map, Number, Result, Value,
 };
@@ -115,6 +116,7 @@ impl Format for Expression {
                 Ok(())
             }
             Expression::Conditional(cond) => cond.format(fmt),
+            Expression::Operation(op) => op.format(fmt),
         }
     }
 }
@@ -333,6 +335,47 @@ impl Format for Conditional {
     }
 }
 
+impl private::Sealed for Operation {}
+
+impl Format for Operation {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        match self {
+            Operation::Unary(op) => op.format(fmt),
+            Operation::Binary(op) => op.format(fmt),
+        }
+    }
+}
+
+impl private::Sealed for UnaryOp {}
+
+impl Format for UnaryOp {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        fmt.write_string_fragment(self.operator.as_str())?;
+        self.expr.format(fmt)
+    }
+}
+
+impl private::Sealed for BinaryOp {}
+
+impl Format for BinaryOp {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        self.lhs_expr.format(fmt)?;
+        fmt.write_all(b" ")?;
+        fmt.write_string_fragment(self.operator.as_str())?;
+        fmt.write_all(b" ")?;
+        self.rhs_expr.format(fmt)
+    }
+}
+
 impl private::Sealed for String {}
 
 impl Format for String {
diff --git a/src/lib.rs b/src/lib.rs
index d0f71a3..2db0514 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -27,9 +27,10 @@ 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, Conditional,
-    ElementAccess, ElementAccessOperator, Expression, FuncCall, FuncCallBuilder, Heredoc,
-    HeredocStripMode, Identifier, Object, ObjectKey, RawExpression, Structure, TemplateExpr,
+    ser::to_expression, Attribute, BinaryOp, BinaryOperator, Block, BlockBuilder, BlockLabel, Body,
+    BodyBuilder, Conditional, ElementAccess, ElementAccessOperator, Expression, FuncCall,
+    FuncCallBuilder, Heredoc, HeredocStripMode, Identifier, Object, ObjectKey, Operation,
+    RawExpression, Structure, TemplateExpr, UnaryOp, UnaryOperator,
 };
 #[doc(inline)]
 pub use value::{Map, Value};
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 0a4717a..85342c6 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -3,9 +3,10 @@ mod template;
 mod tests;
 
 use crate::{
-    structure::Identifier, util::unescape, Attribute, Block, BlockLabel, Body, Conditional,
-    ElementAccessOperator, Expression, FuncCall, FuncCallBuilder, Heredoc, HeredocStripMode,
-    Number, Object, ObjectKey, RawExpression, Result, Structure, TemplateExpr,
+    structure::Identifier, util::unescape, Attribute, BinaryOp, Block, BlockLabel, Body,
+    Conditional, ElementAccessOperator, Expression, FuncCall, FuncCallBuilder, Heredoc,
+    HeredocStripMode, Object, ObjectKey, Operation, RawExpression, Result, Structure, TemplateExpr,
+    UnaryOp, UnaryOperator,
 };
 use pest::{
     iterators::{Pair, Pairs},
@@ -118,18 +119,19 @@ fn parse_block_body(pair: Pair<Rule>) -> Result<Body> {
 fn parse_expression(pair: Pair<Rule>) -> Result<Expression> {
     match pair.as_rule() {
         Rule::ExprTerm => parse_expr_term(pair),
-        Rule::Operation => {
-            let raw = pair.as_str();
-            // We only fully parse unary operations representing negative numbers for now.
-            // Everything else will be represented as raw expressions until parsing operations is
-            // 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::Conditional(Box::new(parse_conditional(pair)?))),
+        Rule::Operation => match parse_operation(inner(pair))? {
+            Operation::Binary(binary) => Ok(Expression::from(Operation::Binary(binary))),
+            Operation::Unary(unary) => match (unary.operator, unary.expr) {
+                // Negate operations on numbers are just converted to negative numbers for
+                // convenience.
+                (UnaryOperator::Neg, Expression::Number(num)) => Ok(Expression::Number(-num)),
+                (operator, expr) => Ok(Expression::from(Operation::Unary(UnaryOp {
+                    operator,
+                    expr,
+                }))),
+            },
+        },
+        Rule::Conditional => Ok(Expression::from(parse_conditional(pair)?)),
         rule => unexpected_rule(rule),
     }
 }
@@ -144,20 +146,31 @@ fn parse_conditional(pair: Pair<Rule>) -> Result<Conditional> {
     })
 }
 
-fn parse_negative_number(pair: Pair<Rule>) -> Result<Option<Number>> {
-    if let Rule::UnaryOp = pair.as_rule() {
-        let mut pairs = pair.into_inner();
-        let operator = pairs.next().unwrap();
-        let expr = pairs.next().unwrap();
-
-        if let ("-", Rule::ExprTerm) = (operator.as_str(), expr.as_rule()) {
-            if let Expression::Number(num) = parse_expr_term(expr)? {
-                return Ok(Some(-num));
-            }
-        }
+fn parse_operation(pair: Pair<Rule>) -> Result<Operation> {
+    match pair.as_rule() {
+        Rule::UnaryOp => parse_unary_op(pair).map(Operation::Unary),
+        Rule::BinaryOp => parse_binary_op(pair).map(Operation::Binary),
+        rule => unexpected_rule(rule),
     }
+}
 
-    Ok(None)
+fn parse_unary_op(pair: Pair<Rule>) -> Result<UnaryOp> {
+    let mut pairs = pair.into_inner();
+
+    Ok(UnaryOp {
+        operator: pairs.next().unwrap().as_str().parse()?,
+        expr: parse_expression(pairs.next().unwrap())?,
+    })
+}
+
+fn parse_binary_op(pair: Pair<Rule>) -> Result<BinaryOp> {
+    let mut pairs = pair.into_inner();
+
+    Ok(BinaryOp {
+        lhs_expr: parse_expr_term(pairs.next().unwrap())?,
+        operator: pairs.next().unwrap().as_str().parse()?,
+        rhs_expr: parse_expression(pairs.next().unwrap())?,
+    })
 }
 
 fn parse_expressions(pair: Pair<Rule>) -> Result<Vec<Expression>> {
diff --git a/src/structure/de.rs b/src/structure/de.rs
index 68ccd7c..23b81a1 100644
--- a/src/structure/de.rs
+++ b/src/structure/de.rs
@@ -1,9 +1,9 @@
 //! Deserialize impls for HCL structure types.
 
 use super::{
-    Attribute, Block, BlockLabel, Body, Conditional, ElementAccess, ElementAccessOperator,
-    Expression, FuncCall, Heredoc, HeredocStripMode, Identifier, ObjectKey, RawExpression,
-    Structure, TemplateExpr,
+    Attribute, BinaryOp, BinaryOperator, Block, BlockLabel, Body, Conditional, ElementAccess,
+    ElementAccessOperator, Expression, FuncCall, Heredoc, HeredocStripMode, Identifier, ObjectKey,
+    Operation, RawExpression, Structure, TemplateExpr, UnaryOp, UnaryOperator,
 };
 use crate::{Error, Number, Result};
 use serde::de::{
@@ -12,8 +12,9 @@ use serde::de::{
     IntoDeserializer,
 };
 use serde::{forward_to_deserialize_any, Deserializer};
+use std::fmt;
 use std::marker::PhantomData;
-use std::{fmt, str::FromStr};
+use std::str::FromStr;
 
 // A trait that allows enum types to report the name of their variant.
 trait VariantName {
@@ -275,6 +276,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),
         }
     }
 
@@ -307,6 +309,7 @@ impl VariantName for Expression {
             Expression::FuncCall(_) => "FuncCall",
             Expression::SubExpr(_) => "SubExpr",
             Expression::Conditional(_) => "Conditional",
+            Expression::Operation(_) => "Operation",
         }
     }
 }
@@ -340,6 +343,7 @@ impl<'de> de::VariantAccess<'de> for Expression {
         match self {
             Expression::TemplateExpr(expr) => seed.deserialize(expr.into_deserializer()),
             Expression::SubExpr(expr) => seed.deserialize(expr.into_deserializer()),
+            Expression::Operation(op) => seed.deserialize(op.into_deserializer()),
             value => seed.deserialize(value.into_deserializer()),
         }
     }
@@ -640,6 +644,170 @@ impl<'de> de::MapAccess<'de> for ConditionalAccess {
     }
 }
 
+impl<'de> IntoDeserializer<'de, Error> for Operation {
+    type Deserializer = Self;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        self
+    }
+}
+
+impl<'de> de::Deserializer<'de> for Operation {
+    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 {
+            Operation::Unary(op) => op.into_deserializer().deserialize_any(visitor),
+            Operation::Binary(op) => op.into_deserializer().deserialize_any(visitor),
+        }
+    }
+}
+
+impl VariantName for Operation {
+    fn variant_name(&self) -> &'static str {
+        match self {
+            Operation::Unary(_) => "Unary",
+            Operation::Binary(_) => "Binary",
+        }
+    }
+}
+
+impl<'de> IntoDeserializer<'de, Error> for UnaryOp {
+    type Deserializer = MapAccessDeserializer<UnaryOpAccess>;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        MapAccessDeserializer::new(UnaryOpAccess::new(self))
+    }
+}
+
+pub struct UnaryOpAccess {
+    operator: Option<UnaryOperator>,
+    expr: Option<Expression>,
+}
+
+impl UnaryOpAccess {
+    fn new(op: UnaryOp) -> Self {
+        UnaryOpAccess {
+            operator: Some(op.operator),
+            expr: Some(op.expr),
+        }
+    }
+}
+
+impl<'de> de::MapAccess<'de> for UnaryOpAccess {
+    type Error = Error;
+
+    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+    where
+        K: de::DeserializeSeed<'de>,
+    {
+        if self.operator.is_some() {
+            seed.deserialize("operator".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(operator) = self.operator.take() {
+            seed.deserialize(operator.into_deserializer())
+        } else if let Some(expr) = self.expr.take() {
+            seed.deserialize(expr.into_deserializer())
+        } else {
+            Err(de::Error::custom("invalid HCL unary operation"))
+        }
+    }
+}
+
+impl<'de> IntoDeserializer<'de, Error> for UnaryOperator {
+    type Deserializer = StrDeserializer<'static, Error>;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        self.as_str().into_deserializer()
+    }
+}
+
+impl<'de> IntoDeserializer<'de, Error> for BinaryOp {
+    type Deserializer = MapAccessDeserializer<BinaryOpAccess>;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        MapAccessDeserializer::new(BinaryOpAccess::new(self))
+    }
+}
+
+pub struct BinaryOpAccess {
+    lhs_expr: Option<Expression>,
+    operator: Option<BinaryOperator>,
+    rhs_expr: Option<Expression>,
+}
+
+impl BinaryOpAccess {
+    fn new(op: BinaryOp) -> Self {
+        BinaryOpAccess {
+            lhs_expr: Some(op.lhs_expr),
+            operator: Some(op.operator),
+            rhs_expr: Some(op.rhs_expr),
+        }
+    }
+}
+
+impl<'de> de::MapAccess<'de> for BinaryOpAccess {
+    type Error = Error;
+
+    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+    where
+        K: de::DeserializeSeed<'de>,
+    {
+        if self.lhs_expr.is_some() {
+            seed.deserialize("lhs_expr".into_deserializer()).map(Some)
+        } else if self.operator.is_some() {
+            seed.deserialize("operator".into_deserializer()).map(Some)
+        } else if self.rhs_expr.is_some() {
+            seed.deserialize("rhs_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(lhs_expr) = self.lhs_expr.take() {
+            seed.deserialize(lhs_expr.into_deserializer())
+        } else if let Some(operator) = self.operator.take() {
+            seed.deserialize(operator.into_deserializer())
+        } else if let Some(rhs_expr) = self.rhs_expr.take() {
+            seed.deserialize(rhs_expr.into_deserializer())
+        } else {
+            Err(de::Error::custom("invalid HCL binary operation"))
+        }
+    }
+}
+
+impl<'de> IntoDeserializer<'de, Error> for BinaryOperator {
+    type Deserializer = StrDeserializer<'static, Error>;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        self.as_str().into_deserializer()
+    }
+}
+
 impl<'de> IntoDeserializer<'de, Error> for ObjectKey {
     type Deserializer = Self;
 
diff --git a/src/structure/expression.rs b/src/structure/expression.rs
index 694fce0..d683126 100644
--- a/src/structure/expression.rs
+++ b/src/structure/expression.rs
@@ -1,6 +1,6 @@
 //! Types to represent HCL attribute value expressions.
 
-use super::{ElementAccess, ElementAccessOperator, FuncCall, Identifier, TemplateExpr};
+use super::{ElementAccess, ElementAccessOperator, FuncCall, Identifier, Operation, TemplateExpr};
 use crate::{Conditional, Number, Value};
 use serde::{Deserialize, Serialize};
 use std::borrow::Cow;
@@ -41,8 +41,9 @@ pub enum 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.
+    /// 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.
     Raw(RawExpression),
 }
 
@@ -213,6 +214,12 @@ impl From<Conditional> for Expression {
     }
 }
 
+impl From<Operation> for Expression {
+    fn from(op: Operation) -> Self {
+        Expression::Operation(Box::new(op))
+    }
+}
+
 impl From<TemplateExpr> for Expression {
     fn from(expr: TemplateExpr) -> Self {
         Expression::TemplateExpr(Box::new(expr))
@@ -308,11 +315,13 @@ impl Display for ObjectKey {
     }
 }
 
-/// A type that holds the value of a raw expression.
+/// A type that holds the value of a raw expression. It can be used to serialize arbitrary
+/// HCL expressions.
+///
+/// *Please note*: raw expressions are not validated during serialization, so it is your
+/// responsiblity to ensure that they are valid HCL.
 ///
-/// As of now, anthing that is not a null value, a boolean, number, string, template expression,
-/// array or object is treated as raw expression and is not further parsed. This includes
-/// conditionals, operations, function calls, for expressions and variable expressions.
+/// 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/mod.rs b/src/structure/mod.rs
index 2d37dfb..110a551 100644
--- a/src/structure/mod.rs
+++ b/src/structure/mod.rs
@@ -53,6 +53,7 @@ pub(crate) mod de;
 mod element_access;
 mod expression;
 mod func;
+mod operation;
 pub(crate) mod ser;
 mod template;
 #[cfg(test)]
@@ -66,6 +67,7 @@ pub use self::{
     element_access::{ElementAccess, ElementAccessOperator},
     expression::{Expression, Object, ObjectKey, RawExpression},
     func::{FuncCall, FuncCallBuilder},
+    operation::{BinaryOp, BinaryOperator, Operation, UnaryOp, UnaryOperator},
     template::{Heredoc, HeredocStripMode, TemplateExpr},
 };
 use crate::{Map, Value};
diff --git a/src/structure/operation.rs b/src/structure/operation.rs
new file mode 100644
index 0000000..4b1a779
--- /dev/null
+++ b/src/structure/operation.rs
@@ -0,0 +1,218 @@
+use super::{de::FromStrVisitor, Expression};
+use crate::{Error, Result};
+use serde::{Deserialize, Serialize};
+use std::str::FromStr;
+
+/// Operations apply a particular operator to either one or two expression terms.
+#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+#[serde(rename = "$hcl::operation")]
+pub enum Operation {
+    /// Represents an operation that applies an operator to a single expression.
+    Unary(UnaryOp),
+    /// Represents an operation that applies an operator to two expressions.
+    Binary(BinaryOp),
+}
+
+impl From<UnaryOp> for Operation {
+    fn from(op: UnaryOp) -> Self {
+        Operation::Unary(op)
+    }
+}
+
+impl From<BinaryOp> for Operation {
+    fn from(op: BinaryOp) -> Self {
+        Operation::Binary(op)
+    }
+}
+
+/// An operation that applies an operator to one expression.
+#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+#[serde(rename = "$hcl::unary_op")]
+pub struct UnaryOp {
+    /// The unary operator to use on the expression.
+    pub operator: UnaryOperator,
+    /// An expression that supports evaluation with the unary operator.
+    pub expr: Expression,
+}
+
+impl UnaryOp {
+    /// Creates a new `UnaryOp` from an operator and an expression.
+    pub fn new<T>(operator: UnaryOperator, expr: T) -> UnaryOp
+    where
+        T: Into<Expression>,
+    {
+        UnaryOp {
+            operator,
+            expr: expr.into(),
+        }
+    }
+}
+
+/// An operator that can be applied to an expression.
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum UnaryOperator {
+    /// Negate operator (`-`).
+    Neg,
+    /// Not operator (`!`).
+    Not,
+}
+
+impl UnaryOperator {
+    /// Returns the `UnaryOperator` as a static `&str`.
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            UnaryOperator::Neg => "-",
+            UnaryOperator::Not => "!",
+        }
+    }
+}
+
+impl FromStr for UnaryOperator {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self> {
+        match s {
+            "-" => Ok(UnaryOperator::Neg),
+            "!" => Ok(UnaryOperator::Not),
+            _ => Err(Error::new(format!("invalid unary operator: `{}`", s))),
+        }
+    }
+}
+
+impl Serialize for UnaryOperator {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(self.as_str())
+    }
+}
+
+impl<'de> Deserialize<'de> for UnaryOperator {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        deserializer.deserialize_any(FromStrVisitor::<Self>::new("a unary operator"))
+    }
+}
+
+/// An operation that applies an operator to two expressions.
+#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+#[serde(rename = "$hcl::binary_op")]
+pub struct BinaryOp {
+    /// The expression on the left-hand-side of the operation.
+    pub lhs_expr: Expression,
+    /// The binary operator to use on the expressions.
+    pub operator: BinaryOperator,
+    /// The expression on the right-hand-side of the operation.
+    pub rhs_expr: Expression,
+}
+
+impl BinaryOp {
+    /// Creates a new `BinaryOp` from two expressions and an operator.
+    pub fn new<L, R>(lhs_expr: L, operator: BinaryOperator, rhs_expr: R) -> BinaryOp
+    where
+        L: Into<Expression>,
+        R: Into<Expression>,
+    {
+        BinaryOp {
+            lhs_expr: lhs_expr.into(),
+            operator,
+            rhs_expr: rhs_expr.into(),
+        }
+    }
+}
+
+/// An operator that can be applied to two expressions.
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum BinaryOperator {
+    /// Equal operator (`==`).
+    Eq,
+    /// Not-equal operator (`!=`).
+    NotEq,
+    /// Less-equal operator (`<=`).
+    LessEq,
+    /// Greater-equal operator (`>=`).
+    GreaterEq,
+    /// Less operator (`<`).
+    Less,
+    /// Greater operator (`>`).
+    Greater,
+    /// Plus operator (`+`).
+    Plus,
+    /// Minus operator (`-`).
+    Minus,
+    /// Multiply operator (`*`).
+    Mul,
+    /// Division operator (`/`).
+    Div,
+    /// Modulo operator (`%`).
+    Mod,
+    /// And operator (`&&`).
+    And,
+    /// Or operator (`||`).
+    Or,
+}
+
+impl BinaryOperator {
+    /// Returns the `BinaryOperator` as a static `&str`.
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            BinaryOperator::Eq => "==",
+            BinaryOperator::NotEq => "!=",
+            BinaryOperator::LessEq => "<=",
+            BinaryOperator::GreaterEq => ">=",
+            BinaryOperator::Less => "<",
+            BinaryOperator::Greater => ">",
+            BinaryOperator::Plus => "+",
+            BinaryOperator::Minus => "-",
+            BinaryOperator::Mul => "*",
+            BinaryOperator::Div => "/",
+            BinaryOperator::Mod => "%",
+            BinaryOperator::And => "&&",
+            BinaryOperator::Or => "||",
+        }
+    }
+}
+
+impl FromStr for BinaryOperator {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self> {
+        match s {
+            "==" => Ok(BinaryOperator::Eq),
+            "!=" => Ok(BinaryOperator::NotEq),
+            "<=" => Ok(BinaryOperator::LessEq),
+            ">=" => Ok(BinaryOperator::GreaterEq),
+            "<" => Ok(BinaryOperator::Less),
+            ">" => Ok(BinaryOperator::Greater),
+            "+" => Ok(BinaryOperator::Plus),
+            "-" => Ok(BinaryOperator::Minus),
+            "*" => Ok(BinaryOperator::Mul),
+            "/" => Ok(BinaryOperator::Div),
+            "%" => Ok(BinaryOperator::Mod),
+            "&&" => Ok(BinaryOperator::And),
+            "||" => Ok(BinaryOperator::Or),
+            _ => Err(Error::new(format!("invalid binary operator: `{}`", s))),
+        }
+    }
+}
+
+impl Serialize for BinaryOperator {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(self.as_str())
+    }
+}
+
+impl<'de> Deserialize<'de> for BinaryOperator {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        deserializer.deserialize_any(FromStrVisitor::<Self>::new("a binary operator"))
+    }
+}
diff --git a/src/structure/ser/expression.rs b/src/structure/ser/expression.rs
index 29da80b..facd515 100644
--- a/src/structure/ser/expression.rs
+++ b/src/structure/ser/expression.rs
@@ -1,6 +1,7 @@
 use super::{
     conditional::ConditionalSerializer, element_access::SerializeElementAccessStruct,
-    func::SerializeFuncCallStruct, template::TemplateExprSerializer, StringSerializer,
+    func::SerializeFuncCallStruct, operation::OperationSerializer,
+    template::TemplateExprSerializer, StringSerializer,
 };
 use crate::{
     serialize_unsupported, Error, Expression, Identifier, Object, ObjectKey, RawExpression, Result,
@@ -135,6 +136,9 @@ impl ser::Serializer for ExpressionSerializer {
                 "Conditional" => Ok(Expression::Conditional(Box::new(
                     value.serialize(ConditionalSerializer)?,
                 ))),
+                "Operation" => Ok(Expression::Operation(Box::new(
+                    value.serialize(OperationSerializer)?,
+                ))),
                 "SubExpr" => Ok(Expression::SubExpr(Box::new(value.serialize(self)?))),
                 "TemplateExpr" => Ok(Expression::TemplateExpr(Box::new(
                     value.serialize(TemplateExprSerializer)?,
diff --git a/src/structure/ser/mod.rs b/src/structure/ser/mod.rs
index 9404b45..ee8ac18 100644
--- a/src/structure/ser/mod.rs
+++ b/src/structure/ser/mod.rs
@@ -7,6 +7,7 @@ mod conditional;
 mod element_access;
 mod expression;
 mod func;
+mod operation;
 mod structure;
 mod template;
 #[cfg(test)]
diff --git a/src/structure/ser/operation.rs b/src/structure/ser/operation.rs
new file mode 100644
index 0000000..9ad03b4
--- /dev/null
+++ b/src/structure/ser/operation.rs
@@ -0,0 +1,485 @@
+use std::str::FromStr;
+
+use super::expression::ExpressionSerializer;
+use crate::{
+    serialize_unsupported,
+    structure::{BinaryOp, BinaryOperator, Operation, UnaryOp, UnaryOperator},
+    Error, Expression, Result,
+};
+use serde::ser::{self, Impossible, Serialize};
+
+pub struct OperationSerializer;
+
+impl ser::Serializer for OperationSerializer {
+    type Ok = Operation;
+    type Error = Error;
+
+    type SerializeSeq = SerializeOperationSeq;
+    type SerializeTuple = SerializeOperationSeq;
+    type SerializeTupleStruct = SerializeOperationSeq;
+    type SerializeTupleVariant = Impossible<Operation, Error>;
+    type SerializeMap = Impossible<Operation, Error>;
+    type SerializeStruct = SerializeOperationStruct;
+    type SerializeStructVariant = Impossible<Operation, Error>;
+
+    serialize_unsupported! {
+        bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64
+        char str bytes none unit unit_struct unit_variant
+        tuple_variant map struct_variant
+    }
+    serialize_self! { some newtype_struct }
+    forward_to_serialize_seq! { tuple tuple_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 `Operation` type itself.
+        match (name, variant) {
+            ("$hcl::operation", "Binary") => {
+                Ok(Operation::Binary(value.serialize(BinaryOpSerializer)?))
+            }
+            (_, _) => Ok(Operation::Unary(value.serialize(UnaryOpSerializer)?)),
+        }
+    }
+
+    fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> {
+        Ok(SerializeOperationSeq::new(len))
+    }
+
+    fn serialize_struct(self, name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+        Ok(SerializeOperationStruct::new(name))
+    }
+}
+
+pub enum SerializeOperationSeq {
+    Unary(SerializeUnaryOpSeq),
+    Binary(SerializeBinaryOpSeq),
+}
+
+impl SerializeOperationSeq {
+    fn new(len: Option<usize>) -> Self {
+        // Specialization for the `BinaryOp` type.
+        if let Some(3) = len {
+            SerializeOperationSeq::Binary(SerializeBinaryOpSeq::new())
+        } else {
+            SerializeOperationSeq::Unary(SerializeUnaryOpSeq::new())
+        }
+    }
+}
+
+impl ser::SerializeSeq for SerializeOperationSeq {
+    type Ok = Operation;
+    type Error = Error;
+
+    fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        match self {
+            SerializeOperationSeq::Unary(attr) => attr.serialize_element(value),
+            SerializeOperationSeq::Binary(block) => block.serialize_element(value),
+        }
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match self {
+            SerializeOperationSeq::Unary(attr) => attr.end().map(Into::into),
+            SerializeOperationSeq::Binary(block) => block.end().map(Into::into),
+        }
+    }
+}
+
+impl ser::SerializeTuple for SerializeOperationSeq {
+    impl_forward_to_serialize_seq!(serialize_element, Operation);
+}
+
+impl ser::SerializeTupleStruct for SerializeOperationSeq {
+    impl_forward_to_serialize_seq!(serialize_field, Operation);
+}
+
+pub enum SerializeOperationStruct {
+    Unary(SerializeUnaryOpStruct),
+    Binary(SerializeBinaryOpStruct),
+}
+
+impl SerializeOperationStruct {
+    fn new(name: &'static str) -> Self {
+        // Specialization for the `UnaryOp` and `BinaryOp` types.
+        match name {
+            "$hcl::binary_op" => SerializeOperationStruct::Binary(SerializeBinaryOpStruct::new()),
+            _ => SerializeOperationStruct::Unary(SerializeUnaryOpStruct::new()),
+        }
+    }
+}
+
+impl ser::SerializeStruct for SerializeOperationStruct {
+    type Ok = Operation;
+    type Error = Error;
+
+    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        match self {
+            SerializeOperationStruct::Unary(ser) => ser.serialize_field(key, value),
+            SerializeOperationStruct::Binary(ser) => ser.serialize_field(key, value),
+        }
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match self {
+            SerializeOperationStruct::Unary(ser) => ser.end().map(Into::into),
+            SerializeOperationStruct::Binary(ser) => ser.end().map(Into::into),
+        }
+    }
+}
+
+pub struct UnaryOpSerializer;
+
+impl ser::Serializer for UnaryOpSerializer {
+    type Ok = UnaryOp;
+    type Error = Error;
+
+    type SerializeSeq = SerializeUnaryOpSeq;
+    type SerializeTuple = SerializeUnaryOpSeq;
+    type SerializeTupleStruct = SerializeUnaryOpSeq;
+    type SerializeTupleVariant = Impossible<UnaryOp, Error>;
+    type SerializeMap = Impossible<UnaryOp, Error>;
+    type SerializeStruct = SerializeUnaryOpStruct;
+    type SerializeStructVariant = Impossible<UnaryOp, 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(SerializeUnaryOpSeq::new())
+    }
+
+    fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+        Ok(SerializeUnaryOpStruct::new())
+    }
+}
+
+pub struct SerializeUnaryOpSeq {
+    operator: Option<UnaryOperator>,
+    expr: Option<Expression>,
+}
+
+impl SerializeUnaryOpSeq {
+    pub fn new() -> Self {
+        SerializeUnaryOpSeq {
+            operator: None,
+            expr: None,
+        }
+    }
+}
+
+impl ser::SerializeSeq for SerializeUnaryOpSeq {
+    type Ok = UnaryOp;
+    type Error = Error;
+
+    fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        if self.operator.is_none() {
+            self.operator = Some(value.serialize(UnaryOperatorSerializer)?);
+        } else if self.expr.is_none() {
+            self.expr = Some(value.serialize(ExpressionSerializer)?);
+        } else {
+            return Err(ser::Error::custom("expected sequence with 2 elements"));
+        }
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match (self.operator, self.expr) {
+            (Some(operator), Some(expr)) => Ok(UnaryOp { operator, expr }),
+            (_, _) => Err(ser::Error::custom("expected sequence with 2 elements")),
+        }
+    }
+}
+
+impl ser::SerializeTuple for SerializeUnaryOpSeq {
+    impl_forward_to_serialize_seq!(serialize_element, UnaryOp);
+}
+
+impl ser::SerializeTupleStruct for SerializeUnaryOpSeq {
+    impl_forward_to_serialize_seq!(serialize_field, UnaryOp);
+}
+
+pub struct SerializeUnaryOpStruct {
+    operator: Option<UnaryOperator>,
+    expr: Option<Expression>,
+}
+
+impl SerializeUnaryOpStruct {
+    pub fn new() -> Self {
+        SerializeUnaryOpStruct {
+            operator: None,
+            expr: None,
+        }
+    }
+}
+
+impl ser::SerializeStruct for SerializeUnaryOpStruct {
+    type Ok = UnaryOp;
+    type Error = Error;
+
+    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        match key {
+            "operator" => self.operator = Some(value.serialize(UnaryOperatorSerializer)?),
+            "expr" => self.expr = Some(value.serialize(ExpressionSerializer)?),
+            _ => {
+                return Err(ser::Error::custom(
+                    "expected struct with fields `operator` and `expr`",
+                ))
+            }
+        };
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match (self.operator, self.expr) {
+            (Some(operator), Some(expr)) => Ok(UnaryOp { operator, expr }),
+            (_, _) => Err(ser::Error::custom(
+                "expected struct with fields `operator` and `expr`",
+            )),
+        }
+    }
+}
+
+pub struct BinaryOpSerializer;
+
+impl ser::Serializer for BinaryOpSerializer {
+    type Ok = BinaryOp;
+    type Error = Error;
+
+    type SerializeSeq = SerializeBinaryOpSeq;
+    type SerializeTuple = SerializeBinaryOpSeq;
+    type SerializeTupleStruct = SerializeBinaryOpSeq;
+    type SerializeTupleVariant = Impossible<BinaryOp, Error>;
+    type SerializeMap = Impossible<BinaryOp, Error>;
+    type SerializeStruct = SerializeBinaryOpStruct;
+    type SerializeStructVariant = Impossible<BinaryOp, 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(SerializeBinaryOpSeq::new())
+    }
+
+    fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+        Ok(SerializeBinaryOpStruct::new())
+    }
+}
+
+pub struct SerializeBinaryOpSeq {
+    lhs_expr: Option<Expression>,
+    operator: Option<BinaryOperator>,
+    rhs_expr: Option<Expression>,
+}
+
+impl SerializeBinaryOpSeq {
+    pub fn new() -> Self {
+        SerializeBinaryOpSeq {
+            lhs_expr: None,
+            operator: None,
+            rhs_expr: None,
+        }
+    }
+}
+
+impl ser::SerializeSeq for SerializeBinaryOpSeq {
+    type Ok = BinaryOp;
+    type Error = Error;
+
+    fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        if self.lhs_expr.is_none() {
+            self.lhs_expr = Some(value.serialize(ExpressionSerializer)?);
+        } else if self.operator.is_none() {
+            self.operator = Some(value.serialize(BinaryOperatorSerializer)?);
+        } else if self.rhs_expr.is_none() {
+            self.rhs_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.lhs_expr, self.operator, self.rhs_expr) {
+            (Some(lhs_expr), Some(operator), Some(rhs_expr)) => Ok(BinaryOp {
+                lhs_expr,
+                operator,
+                rhs_expr,
+            }),
+            (_, _, _) => Err(ser::Error::custom("expected sequence with 3 elements")),
+        }
+    }
+}
+
+impl ser::SerializeTuple for SerializeBinaryOpSeq {
+    impl_forward_to_serialize_seq!(serialize_element, BinaryOp);
+}
+
+impl ser::SerializeTupleStruct for SerializeBinaryOpSeq {
+    impl_forward_to_serialize_seq!(serialize_field, BinaryOp);
+}
+
+pub struct SerializeBinaryOpStruct {
+    lhs_expr: Option<Expression>,
+    operator: Option<BinaryOperator>,
+    rhs_expr: Option<Expression>,
+}
+
+impl SerializeBinaryOpStruct {
+    pub fn new() -> Self {
+        SerializeBinaryOpStruct {
+            lhs_expr: None,
+            operator: None,
+            rhs_expr: None,
+        }
+    }
+}
+
+impl ser::SerializeStruct for SerializeBinaryOpStruct {
+    type Ok = BinaryOp;
+    type Error = Error;
+
+    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        match key {
+            "lhs_expr" => self.lhs_expr = Some(value.serialize(ExpressionSerializer)?),
+            "operator" => self.operator = Some(value.serialize(BinaryOperatorSerializer)?),
+            "rhs_expr" => self.rhs_expr = Some(value.serialize(ExpressionSerializer)?),
+            _ => {
+                return Err(ser::Error::custom(
+                    "expected struct with fields `lhs_expr`, `operator` and `rhs_expr`",
+                ))
+            }
+        };
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match (self.lhs_expr, self.operator, self.rhs_expr) {
+            (Some(lhs_expr), Some(operator), Some(rhs_expr)) => Ok(BinaryOp {
+                lhs_expr,
+                operator,
+                rhs_expr,
+            }),
+            (_, _, _) => Err(ser::Error::custom(
+                "expected struct with fields `lhs_expr`, `operator` and `rhs_expr`",
+            )),
+        }
+    }
+}
+
+pub struct UnaryOperatorSerializer;
+
+impl ser::Serializer for UnaryOperatorSerializer {
+    type Ok = UnaryOperator;
+    type Error = Error;
+
+    type SerializeSeq = Impossible<UnaryOperator, Error>;
+    type SerializeTuple = Impossible<UnaryOperator, Error>;
+    type SerializeTupleStruct = Impossible<UnaryOperator, Error>;
+    type SerializeTupleVariant = Impossible<UnaryOperator, Error>;
+    type SerializeMap = Impossible<UnaryOperator, Error>;
+    type SerializeStruct = Impossible<UnaryOperator, Error>;
+    type SerializeStructVariant = Impossible<UnaryOperator, Error>;
+
+    serialize_unsupported! {
+        i8 i16 i32 i64 u8 u16 u32 u64
+        bool f32 f64 bytes unit unit_struct newtype_variant none
+        seq tuple tuple_struct tuple_variant map struct struct_variant
+    }
+    serialize_self! { some newtype_struct }
+
+    fn serialize_char(self, value: char) -> Result<Self::Ok> {
+        self.serialize_str(&value.to_string())
+    }
+
+    fn serialize_str(self, value: &str) -> Result<Self::Ok> {
+        UnaryOperator::from_str(value)
+    }
+
+    fn serialize_unit_variant(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        variant: &'static str,
+    ) -> Result<Self::Ok> {
+        self.serialize_str(variant)
+    }
+}
+
+pub struct BinaryOperatorSerializer;
+
+impl ser::Serializer for BinaryOperatorSerializer {
+    type Ok = BinaryOperator;
+    type Error = Error;
+
+    type SerializeSeq = Impossible<BinaryOperator, Error>;
+    type SerializeTuple = Impossible<BinaryOperator, Error>;
+    type SerializeTupleStruct = Impossible<BinaryOperator, Error>;
+    type SerializeTupleVariant = Impossible<BinaryOperator, Error>;
+    type SerializeMap = Impossible<BinaryOperator, Error>;
+    type SerializeStruct = Impossible<BinaryOperator, Error>;
+    type SerializeStructVariant = Impossible<BinaryOperator, Error>;
+
+    serialize_unsupported! {
+        i8 i16 i32 i64 u8 u16 u32 u64
+        bool f32 f64 bytes unit unit_struct newtype_variant none
+        seq tuple tuple_struct tuple_variant map struct struct_variant
+    }
+    serialize_self! { some newtype_struct }
+
+    fn serialize_char(self, value: char) -> Result<Self::Ok> {
+        self.serialize_str(&value.to_string())
+    }
+
+    fn serialize_str(self, value: &str) -> Result<Self::Ok> {
+        BinaryOperator::from_str(value)
+    }
+
+    fn serialize_unit_variant(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        variant: &'static str,
+    ) -> Result<Self::Ok> {
+        self.serialize_str(variant)
+    }
+}


Test patch:
diff --git a/src/de/tests.rs b/src/de/tests.rs
index 60fc3b3..9c66061 100644
--- a/src/de/tests.rs
+++ b/src/de/tests.rs
@@ -1,5 +1,8 @@
 use super::*;
-use crate::{Block, Body, ElementAccess, Expression, FuncCall, Identifier, ObjectKey};
+use crate::{
+    BinaryOp, BinaryOperator, Block, Body, ElementAccess, Expression, FuncCall, Identifier,
+    ObjectKey, Operation, UnaryOp, UnaryOperator,
+};
 use pretty_assertions::assert_eq;
 use serde::Deserialize;
 use serde_json::{json, Value};
@@ -208,6 +211,29 @@ fn deserialize_func_call() {
     assert_eq!(expected, from_str::<Body>(input).unwrap());
 }
 
+#[test]
+fn deserialize_operation() {
+    let input = r#"
+        unary = !variable
+        binary = 1 + 1
+    "#;
+
+    let expected = Body::builder()
+        .add_attribute((
+            "unary",
+            Operation::Unary(UnaryOp::new(
+                UnaryOperator::Not,
+                Identifier::new("variable"),
+            )),
+        ))
+        .add_attribute((
+            "binary",
+            Operation::Binary(BinaryOp::new(1, BinaryOperator::Plus, 1)),
+        ))
+        .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 5a5e3e8..c4c93a2 100644
--- a/src/ser/tests.rs
+++ b/src/ser/tests.rs
@@ -1,8 +1,8 @@
 use super::*;
 use crate::{
-    Block, BlockLabel, Body, Conditional, ElementAccess, ElementAccessOperator, Expression,
-    FuncCall, Heredoc, HeredocStripMode, Identifier, Object, ObjectKey, RawExpression,
-    TemplateExpr,
+    BinaryOp, BinaryOperator, Block, BlockLabel, Body, Conditional, ElementAccess,
+    ElementAccessOperator, Expression, FuncCall, Heredoc, HeredocStripMode, Identifier, Object,
+    ObjectKey, Operation, RawExpression, TemplateExpr,
 };
 use pretty_assertions::assert_eq;
 use serde_json::json;
@@ -232,6 +232,18 @@ fn serialize_conditional() {
     );
 }
 
+#[test]
+fn serialize_oparation() {
+    let body = Body::builder()
+        .add_attribute((
+            "op",
+            Operation::Binary(BinaryOp::new(1, BinaryOperator::Plus, 1)),
+        ))
+        .build();
+
+    assert_eq!(to_string(&body).unwrap(), "op = 1 + 1\n");
+}
+
 #[test]
 fn serialize_func_call() {
     let body = Body::builder()
diff --git a/src/structure/ser/tests.rs b/src/structure/ser/tests.rs
index 74abfb7..71cf1f1 100644
--- a/src/structure/ser/tests.rs
+++ b/src/structure/ser/tests.rs
@@ -4,12 +4,14 @@ use super::{
     body::BodySerializer,
     conditional::ConditionalSerializer,
     expression::ExpressionSerializer,
+    operation::OperationSerializer,
     template::TemplateExprSerializer,
     *,
 };
 use crate::{
-    structure::Identifier, Attribute, Block, BlockLabel, Body, Conditional, Expression, Heredoc,
-    HeredocStripMode, Map, TemplateExpr,
+    structure::Identifier, Attribute, BinaryOp, BinaryOperator, Block, BlockLabel, Body,
+    Conditional, Expression, Heredoc, HeredocStripMode, Map, Operation, TemplateExpr, UnaryOp,
+    UnaryOperator,
 };
 use serde::Serialize;
 use std::fmt::Debug;
@@ -81,6 +83,14 @@ fn identity() {
         ConditionalSerializer,
         Conditional::new(Identifier::new("some_cond_var"), "yes", "no"),
     );
+    test_identity(
+        OperationSerializer,
+        Operation::Unary(UnaryOp::new(UnaryOperator::Neg, 1)),
+    );
+    test_identity(
+        OperationSerializer,
+        Operation::Binary(BinaryOp::new(1, BinaryOperator::Plus, 1)),
+    );
 }
 
 #[test]
@@ -182,4 +192,20 @@ fn custom() {
         ),
         Conditional::new(Identifier::new("some_cond_var"), "yes", "no"),
     );
+
+    test_serialize(
+        OperationSerializer,
+        ("-", Expression::Number(1.into())),
+        Operation::Unary(UnaryOp::new(UnaryOperator::Neg, 1)),
+    );
+
+    test_serialize(
+        OperationSerializer,
+        (
+            Expression::Number(1.into()),
+            "+",
+            Expression::Number(1.into()),
+        ),
+        Operation::Binary(BinaryOp::new(1, BinaryOperator::Plus, 1)),
+    );
 }