Repository: martinohmann/hcl-rs
Pull request #123 (2022-11-07 19:04:45)

feat(template): implement `Format` for template types


Patch:
diff --git a/src/format/impls.rs b/src/format/impls.rs
index a123fa0..c317e2b 100644
--- a/src/format/impls.rs
+++ b/src/format/impls.rs
@@ -4,6 +4,9 @@ use crate::expr::{
     Operation, RawExpression, TemplateExpr, Traversal, TraversalOperator, UnaryOp,
 };
 use crate::structure::{Attribute, Block, BlockLabel, Body, Structure};
+use crate::template::{
+    Directive, Element, ForDirective, IfDirective, Interpolation, StripMode, Template,
+};
 use crate::{Identifier, Number, Result, Value};
 use std::io;
 
@@ -407,6 +410,108 @@ impl Format for ForExpr {
     }
 }
 
+impl private::Sealed for Template {}
+
+impl Format for Template {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        for element in self.elements() {
+            element.format(fmt)?;
+        }
+
+        Ok(())
+    }
+}
+
+impl private::Sealed for Element {}
+
+impl Format for Element {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        match self {
+            Element::Literal(lit) => fmt.write_string_fragment(lit),
+            Element::Interpolation(interp) => interp.format(fmt),
+            Element::Directive(dir) => dir.format(fmt),
+        }
+    }
+}
+
+impl private::Sealed for Interpolation {}
+
+impl Format for Interpolation {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        format_interpolation(fmt, self.strip, |fmt| self.expr.format(fmt))
+    }
+}
+
+impl private::Sealed for Directive {}
+
+impl Format for Directive {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        match self {
+            Directive::If(if_dir) => if_dir.format(fmt),
+            Directive::For(for_dir) => for_dir.format(fmt),
+        }
+    }
+}
+
+impl private::Sealed for IfDirective {}
+
+impl Format for IfDirective {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        format_directive(fmt, self.if_strip, |fmt| {
+            fmt.write_bytes(b"if ")?;
+            self.cond_expr.format(fmt)
+        })?;
+        self.true_template.format(fmt)?;
+
+        if let Some(false_template) = &self.false_template {
+            format_directive(fmt, self.else_strip, |fmt| fmt.write_bytes(b"else"))?;
+            false_template.format(fmt)?;
+        }
+
+        format_directive(fmt, self.endif_strip, |fmt| fmt.write_bytes(b"endif"))
+    }
+}
+
+impl private::Sealed for ForDirective {}
+
+impl Format for ForDirective {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        format_directive(fmt, self.for_strip, |fmt| {
+            fmt.write_bytes(b"for ")?;
+
+            if let Some(key_var) = &self.key_var {
+                key_var.format(fmt)?;
+                fmt.write_bytes(b", ")?;
+            }
+
+            self.value_var.format(fmt)?;
+            fmt.write_bytes(b" in ")?;
+            self.collection_expr.format(fmt)
+        })?;
+
+        self.template.format(fmt)?;
+        format_directive(fmt, self.endfor_strip, |fmt| fmt.write_bytes(b"endfor"))
+    }
+}
+
 impl private::Sealed for String {}
 
 impl Format for String {
@@ -455,3 +560,45 @@ where
 
     fmt.end_object()
 }
+
+fn format_strip<W, F>(fmt: &mut Formatter<W>, strip: StripMode, f: F) -> Result<()>
+where
+    W: io::Write,
+    F: FnOnce(&mut Formatter<W>) -> Result<()>,
+{
+    if strip.strip_start() {
+        fmt.write_bytes(b"~")?;
+    }
+
+    f(fmt)?;
+
+    if strip.strip_end() {
+        fmt.write_bytes(b"~")?;
+    }
+
+    Ok(())
+}
+
+fn format_interpolation<W, F>(fmt: &mut Formatter<W>, strip: StripMode, f: F) -> Result<()>
+where
+    W: io::Write,
+    F: FnOnce(&mut Formatter<W>) -> Result<()>,
+{
+    fmt.write_bytes(b"${")?;
+    format_strip(fmt, strip, f)?;
+    fmt.write_bytes(b"}")
+}
+
+fn format_directive<W, F>(fmt: &mut Formatter<W>, strip: StripMode, f: F) -> Result<()>
+where
+    W: io::Write,
+    F: FnOnce(&mut Formatter<W>) -> Result<()>,
+{
+    fmt.write_bytes(b"%{")?;
+    format_strip(fmt, strip, |fmt| {
+        fmt.write_bytes(b" ")?;
+        f(fmt)?;
+        fmt.write_bytes(b" ")
+    })?;
+    fmt.write_bytes(b"}")
+}
diff --git a/src/template.rs b/src/template.rs
index 247371e..b7505e7 100644
--- a/src/template.rs
+++ b/src/template.rs
@@ -84,7 +84,8 @@
 //! expressions. See the [module-level documentation][crate::eval] for examples.
 
 use crate::expr::{Expression, TemplateExpr};
-use crate::{parser, Error, Identifier, Result};
+use crate::{format, parser, Error, Identifier, Result};
+use std::fmt::{self, Display};
 use std::str::FromStr;
 
 /// The main type to represent the HCL template sub-languange.
@@ -185,6 +186,14 @@ where
     }
 }
 
+impl Display for Template {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        // Formatting a `Template` as string cannot fail.
+        let formatted = format::to_string(self).expect("a Template failed to format unexpectedly");
+        f.write_str(&formatted)
+    }
+}
+
 /// An element of an HCL template.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum Element {


Test patch:
diff --git a/src/format/tests.rs b/src/format/tests.rs
index aabac06..7e31ed8 100644
--- a/src/format/tests.rs
+++ b/src/format/tests.rs
@@ -1,6 +1,8 @@
 use super::*;
-use crate::expr::{Expression, FuncCall};
+use crate::expr::{BinaryOp, BinaryOperator, Expression, FuncCall, Operation, Variable};
 use crate::structure::Attribute;
+use crate::template::{ForDirective, IfDirective, StripMode, Template};
+use crate::Identifier;
 
 #[track_caller]
 fn expect_format<T: Format>(value: T, expected: &str) {
@@ -77,3 +79,43 @@ fn compact_objects() {
         "object = { foo = { bar = \"baz\" }, qux = \"bam\" }\n",
     );
 }
+
+#[test]
+fn template() {
+    let template = Template::new().add_directive(
+        ForDirective::new(
+            Identifier::unchecked("item"),
+            Variable::unchecked("items"),
+            Template::new()
+                .add_literal("\nHello ")
+                .add_directive(
+                    IfDirective::new(
+                        Operation::from(BinaryOp::new(
+                            Variable::unchecked("item"),
+                            BinaryOperator::Eq,
+                            "world",
+                        )),
+                        Template::new().add_literal(" World! "),
+                    )
+                    .with_false_template(
+                        Template::new()
+                            .add_literal(" ")
+                            .add_interpolation(Variable::unchecked("item"))
+                            .add_literal("."),
+                    )
+                    .with_if_strip(StripMode::Start)
+                    .with_else_strip(StripMode::Both)
+                    .with_endif_strip(StripMode::End),
+                )
+                .add_literal("\n"),
+        )
+        .with_for_strip(StripMode::End)
+        .with_endfor_strip(StripMode::End),
+    );
+
+    let expected = r#"%{ for item in items ~}
+Hello %{~ if item == "world" } World! %{~ else ~} ${item}.%{ endif ~}
+%{ endfor ~}"#;
+
+    expect_format(template, expected);
+}