Repository: martinohmann/hcl-rs
Pull request #122 (2022-11-06 16:58:55)

feat(format): add support for compact collection formatting
The `Formatter` can be configured to emit more compact arrays and objects via the newly added builder methods `compact`, `compact_arrays` and `compact_objects`.


Patch:
diff --git a/src/format/mod.rs b/src/format/mod.rs
index f6c2eea..f88263d 100644
--- a/src/format/mod.rs
+++ b/src/format/mod.rs
@@ -6,7 +6,7 @@
 //! For serialization of other Rust data structures implementing [`serde::Serialize`] refer to the
 //! documentation of the [`ser`](crate::ser) module.
 //!
-//! ## Examples
+//! # Examples
 //!
 //! Format an HCL block as string:
 //!
@@ -54,7 +54,7 @@ mod private {
 pub trait Format: private::Sealed {
     /// Formats a HCL structure using a formatter and writes the result to the provided writer.
     ///
-    /// ## Errors
+    /// # Errors
     ///
     /// Formatting the data structure or writing to the writer may fail with an `Error`.
     fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
@@ -75,6 +75,8 @@ enum FormatState {
 struct FormatConfig<'a> {
     indent: &'a [u8],
     dense: bool,
+    compact_arrays: bool,
+    compact_objects: bool,
 }
 
 impl<'a> Default for FormatConfig<'a> {
@@ -82,13 +84,15 @@ impl<'a> Default for FormatConfig<'a> {
         FormatConfig {
             indent: b"  ",
             dense: false,
+            compact_arrays: false,
+            compact_objects: false,
         }
     }
 }
 
 /// A pretty printing HCL formatter.
 ///
-/// ## Examples
+/// # Examples
 ///
 /// Format an HCL block as string:
 ///
@@ -159,6 +163,8 @@ pub struct FormatterBuilder<'a, W> {
 
 impl<'a, W> FormatterBuilder<'a, W> {
     /// Set the indent for indenting nested HCL structures.
+    ///
+    /// The default indentation is two spaces.
     pub fn indent(mut self, indent: &'a [u8]) -> Self {
         self.config.indent = indent;
         self
@@ -166,11 +172,83 @@ impl<'a, W> FormatterBuilder<'a, W> {
 
     /// If set, blocks are not visually separated by empty lines from attributes and adjacent
     /// blocks.
+    ///
+    /// Default formatting:
+    ///
+    /// ```hcl
+    /// attr1 = "value1"
+    /// attr2 = "value2"
+    ///
+    /// block1 {}
+    ///
+    /// block2 {}
+    /// ```
+    ///
+    /// Dense formatting:
+    ///
+    /// ```hcl
+    /// attr1 = "value1"
+    /// attr2 = "value2"
+    /// block1 {}
+    /// block2 {}
+    /// ```
     pub fn dense(mut self, yes: bool) -> Self {
         self.config.dense = yes;
         self
     }
 
+    /// If set, arrays and objects are formatted in a more compact way.
+    ///
+    /// See the method documation of [`compact_arrays`][FormatterBuilder::compact_arrays] and
+    /// [`compact_objects`][FormatterBuilder::compact_objects].
+    pub fn compact(self, yes: bool) -> Self {
+        self.compact_arrays(yes).compact_objects(yes)
+    }
+
+    /// Controls the array formatting.
+    ///
+    /// By default, array elements are separated by newlines:
+    ///
+    /// ```hcl
+    /// array = [
+    ///   1,
+    ///   2,
+    ///   3,
+    /// ]
+    /// ```
+    ///
+    /// When compact array formatting is enabled no newlines are inserted between elements:
+    ///
+    /// ```hcl
+    /// array = [1, 2, 3]
+    /// ```
+    pub fn compact_arrays(mut self, yes: bool) -> Self {
+        self.config.compact_arrays = yes;
+        self
+    }
+
+    /// Controls the object formatting.
+    ///
+    /// By default, object items are separated by newlines:
+    ///
+    /// ```hcl
+    /// object = {
+    ///   one = "foo"
+    ///   two = "bar"
+    ///   three = "baz"
+    /// }
+    /// ```
+    ///
+    /// When compact object formatting is enabled no newlines are inserted between items:
+    ///
+    /// ```hcl
+    /// object = { one = "foo", two = "bar", three = "baz" }
+    /// ```
+    pub fn compact_objects(mut self, yes: bool) -> Self {
+        self.config.compact_objects = yes;
+        self
+    }
+
     /// Consumes the `FormatterBuilder` and turns it into a `Formatter` which writes HCL to the
     /// provided writer.
     pub fn build(self, writer: W) -> Formatter<'a, W> {
@@ -281,7 +359,7 @@ where
 
     /// Signals the start of an array to the formatter.
     fn begin_array(&mut self) -> io::Result<()> {
-        if !self.in_compact_mode() {
+        if !self.compact_arrays() {
             self.current_indent += 1;
         }
         self.has_value = false;
@@ -293,11 +371,11 @@ where
     fn begin_array_value(&mut self) -> io::Result<()> {
         if self.first_element {
             self.first_element = false;
-            if !self.in_compact_mode() {
+            if !self.compact_arrays() {
                 self.write_all(b"\n")?;
                 self.write_indent(self.current_indent)?;
             }
-        } else if self.in_compact_mode() {
+        } else if self.compact_arrays() {
             self.write_all(b", ")?;
         } else {
             self.write_all(b",\n")?;
@@ -315,7 +393,7 @@ where
 
     /// Signals the end of an array to the formatter.
     fn end_array(&mut self) -> io::Result<()> {
-        if !self.in_compact_mode() {
+        if !self.compact_arrays() {
             self.current_indent -= 1;
 
             if self.has_value {
@@ -329,7 +407,7 @@ where
 
     /// Signals the start of an object to the formatter.
     fn begin_object(&mut self) -> io::Result<()> {
-        if !self.in_compact_mode() {
+        if !self.compact_objects() {
             self.current_indent += 1;
         }
         self.has_value = false;
@@ -341,13 +419,13 @@ where
     fn begin_object_key(&mut self) -> io::Result<()> {
         if self.first_element {
             self.first_element = false;
-            if self.in_compact_mode() {
+            if self.compact_objects() {
                 self.write_all(b" ")?;
             } else {
                 self.write_all(b"\n")?;
                 self.write_indent(self.current_indent)?;
             }
-        } else if self.in_compact_mode() {
+        } else if self.compact_objects() {
             self.write_all(b", ")?;
         } else {
             self.write_all(b"\n")?;
@@ -369,7 +447,7 @@ where
 
     /// Signals the end of an object to the formatter.
     fn end_object(&mut self) -> io::Result<()> {
-        if self.in_compact_mode() {
+        if self.compact_objects() {
             if self.has_value {
                 self.write_all(b" ")?;
             }
@@ -486,6 +564,14 @@ where
         result
     }
 
+    fn compact_arrays(&self) -> bool {
+        self.config.compact_arrays || self.in_compact_mode()
+    }
+
+    fn compact_objects(&self) -> bool {
+        self.config.compact_objects || self.in_compact_mode()
+    }
+
     fn in_compact_mode(&self) -> bool {
         self.compact_mode_level > 0
     }


Test patch:
diff --git a/src/format/tests.rs b/src/format/tests.rs
index 6adb5dc..aabac06 100644
--- a/src/format/tests.rs
+++ b/src/format/tests.rs
@@ -7,6 +7,18 @@ fn expect_format<T: Format>(value: T, expected: &str) {
     assert_eq!(to_string(&value).unwrap(), expected);
 }
 
+fn expect_formatb<'a, F, T>(f: F, value: T, expected: &str)
+where
+    T: Format,
+    F: FnOnce(FormatterBuilder<'a, &'a mut Vec<u8>>) -> FormatterBuilder<'a, &'a mut Vec<u8>>,
+{
+    let mut buf = Vec::with_capacity(128);
+    let mut fmt = f(Formatter::builder()).build(&mut buf);
+    value.format(&mut fmt).unwrap();
+    let formatted = std::str::from_utf8(&buf).unwrap();
+    assert_eq!(formatted, expected);
+}
+
 #[test]
 fn issue_87() {
     let expr = Expression::from(
@@ -35,3 +47,33 @@ fn compact_func_args() {
         "func([1, 2, 3], { foo = \"bar\", baz = \"qux\" })",
     );
 }
+
+#[test]
+fn compact_arrays() {
+    let attr = Attribute::new("array", expression!([1, 2, 3, [4, 5]]));
+
+    expect_formatb(
+        |b| b.compact_arrays(true),
+        attr,
+        "array = [1, 2, 3, [4, 5]]\n",
+    );
+}
+
+#[test]
+fn compact_objects() {
+    let attr = Attribute::new(
+        "object",
+        expression!({
+            foo = {
+                bar = "baz"
+            }
+            qux = "bam"
+        }),
+    );
+
+    expect_formatb(
+        |b| b.compact_objects(true),
+        attr,
+        "object = { foo = { bar = \"baz\" }, qux = \"bam\" }\n",
+    );
+}