Repository: martinohmann/hcl-rs
Pull request #120 (2022-11-03 22:05:20)

feat(macros): add `value!` macro
This can be used in places where `serde_json::json!` was used previously. It has the same syntax as the `expression!` macro with some restrictions around raw expressions.


Patch:
diff --git a/src/de/mod.rs b/src/de/mod.rs
index 463eca8..ceb94ce 100644
--- a/src/de/mod.rs
+++ b/src/de/mod.rs
@@ -48,9 +48,8 @@ impl Deserializer {
 ///
 /// ```
 /// use serde_json::{json, Value};
-/// # use std::error::Error;
-/// #
-/// # fn main() -> Result<(), Box<dyn Error>> {
+///
+/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
 /// let input = r#"
 ///     some_attr = {
 ///       foo = [1, 2]
@@ -99,10 +98,9 @@ where
 /// # Example
 ///
 /// ```
-/// use serde_json::{json, Value};
-/// # use std::error::Error;
-/// #
-/// # fn main() -> Result<(), Box<dyn Error>> {
+/// use hcl::Value;
+///
+/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
 /// let input = r#"
 ///     some_attr = {
 ///       foo = [1, 2]
@@ -114,14 +112,14 @@ where
 ///     }
 /// "#;
 ///
-/// let expected = json!({
-///     "some_attr": {
-///         "foo": [1, 2],
-///         "bar": true
-///     },
-///     "some_block": {
-///         "some_block_label": {
-///             "attr": "value"
+/// let expected = hcl::value!({
+///     some_attr = {
+///         foo = [1, 2]
+///         bar = true
+///     }
+///     some_block = {
+///         some_block_label = {
+///             attr = "value"
 ///         }
 ///     }
 /// });
diff --git a/src/macros.rs b/src/macros.rs
index 4b20911..bfbfa0f 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -3,7 +3,7 @@
 /// The macro supports a subset of the HCL syntax. If you need more flexibility, use the
 /// [`BlockBuilder`][crate::structure::BlockBuilder] instead.
 ///
-/// ## Supported Syntax
+/// # Supported Syntax
 ///
 /// - Attribute keys and block identifiers can be raw identifiers (`identifier`) or parenthesized
 ///   expressions (`(expr)`).
@@ -17,11 +17,11 @@
 /// Please note that HCL actually uses `${}` for interpolating expression, but since rust macros do
 /// not support matching on `$` it was chosen to use `#{}` instead to support raw expressions.
 ///
-/// ## Unsupported syntax:
+/// # Unsupported syntax
 ///
 /// Heredocs are not supported by the `hcl::body` macro.
 ///
-/// ## Related macros
+/// # Related macros
 ///
 /// The `body!` macro is composed out of different other macros that can be used on their own to
 /// construct HCL data structures:
@@ -31,7 +31,10 @@
 /// - [`expression!`][`crate::expression!`]: constructs an [`Expression`][crate::expr::Expression]
 /// - [`structure!`][`crate::structure!`]: constructs a [`Structure`][crate::structure::Structure]
 ///
-/// ## Examples
+/// Additionally the [`value!`][`crate::value!`] macro is provided for constructing a
+/// [`Value`][crate::value::Value].
+///
+/// # Example
 ///
 /// ```
 /// use hcl::{Block, Body};
@@ -173,7 +176,7 @@ macro_rules! body_internal {
 ///
 /// For supported syntax see the [`body!`] macro documentation.
 ///
-/// ## Examples
+/// # Example
 ///
 /// ```
 /// use hcl::{Attribute, Block, Structure};
@@ -228,7 +231,7 @@ macro_rules! structure_internal {
 ///
 /// For supported syntax see the [`body!`] macro documentation.
 ///
-/// ## Examples
+/// # Example
 ///
 /// ```
 /// use hcl::Attribute;
@@ -269,7 +272,7 @@ macro_rules! attribute_internal {
 ///
 /// For supported syntax see the [`body!`] macro documentation.
 ///
-/// ## Examples
+/// # Example
 ///
 /// ```
 /// use hcl::Block;
@@ -379,7 +382,7 @@ macro_rules! object_key {
 ///
 /// For supported syntax see the [`body!`] macro documentation.
 ///
-/// ## Examples
+/// # Example
 ///
 /// ```
 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -646,6 +649,273 @@ macro_rules! expression_internal {
     };
 }
 
+/// Construct an `hcl::Value` from an HCL attribute value value literal.
+///
+/// Supports the same input syntax as the [`expression!`] macro, with the exception of raw value
+/// expressions.
+///
+/// For supported syntax see the [`body!`] macro documentation.
+///
+/// # Example
+///
+/// ```
+/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
+/// use hcl::{Value, Identifier, Map};
+///
+/// let other = "hello";
+///
+/// let value = hcl::value!({
+///     foo       = true
+///     "baz qux" = [1, 2]
+///     (other)   = "world"
+/// });
+///
+/// let expected = Value::Object({
+///     let mut object = Map::new();
+///     object.insert("foo".into(), true.into());
+///     object.insert("baz qux".into(), vec![1u64, 2].into());
+///     object.insert("hello".into(), "world".into());
+///     object
+/// });
+///
+/// assert_eq!(value, expected);
+/// #     Ok(())
+/// # }
+/// ```
+#[macro_export]
+macro_rules! value {
+    // Hide distracting implementation details from the generated rustdoc.
+    ($($expr:tt)+) => {
+        $crate::value_internal!($($expr)+)
+    };
+}
+
+#[macro_export]
+#[doc(hidden)]
+macro_rules! value_internal {
+    //////////////////////////////////////////////////////////////////////////
+    // TT muncher for parsing the inside of an array [...].
+    //
+    // Produces a vec![...] of the elements.
+    //
+    // Must be invoked as: value_internal!(@array [] $($tt)*)
+    //////////////////////////////////////////////////////////////////////////
+
+    // Done with trailing comma.
+    (@array [$($elems:expr,)*]) => {
+        std::vec![$($elems,)*]
+    };
+
+    // Done without trailing comma.
+    (@array [$($elems:expr),*]) => {
+        std::vec![$($elems),*]
+    };
+
+    // Next element is `null`.
+    (@array [$($elems:expr,)*] null $($rest:tt)*) => {
+        $crate::value_internal!(@array [$($elems,)* $crate::value_internal!(null)] $($rest)*)
+    };
+
+    // Next element is `true`.
+    (@array [$($elems:expr,)*] true $($rest:tt)*) => {
+        $crate::value_internal!(@array [$($elems,)* $crate::value_internal!(true)] $($rest)*)
+    };
+
+    // Next element is `false`.
+    (@array [$($elems:expr,)*] false $($rest:tt)*) => {
+        $crate::value_internal!(@array [$($elems,)* $crate::value_internal!(false)] $($rest)*)
+    };
+
+    // Next element is an array.
+    (@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => {
+        $crate::value_internal!(@array [$($elems,)* $crate::value_internal!([$($array)*])] $($rest)*)
+    };
+
+    // Next element is a map.
+    (@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => {
+        $crate::value_internal!(@array [$($elems,)* $crate::value_internal!({$($map)*})] $($rest)*)
+    };
+
+    // Next element is an value followed by comma.
+    (@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => {
+        $crate::value_internal!(@array [$($elems,)* $crate::value_internal!($next),] $($rest)*)
+    };
+
+    // Last element is an value with no trailing comma.
+    (@array [$($elems:expr,)*] $last:expr) => {
+        $crate::value_internal!(@array [$($elems,)* $crate::value_internal!($last)])
+    };
+
+    // Comma after the most recent element.
+    (@array [$($elems:expr),*] , $($rest:tt)*) => {
+        $crate::value_internal!(@array [$($elems,)*] $($rest)*)
+    };
+
+    // Unexpected token after most recent element.
+    (@array [$($elems:expr),*] $unexpected:tt $($rest:tt)*) => {
+        $crate::hcl_unexpected!($unexpected)
+    };
+
+    //////////////////////////////////////////////////////////////////////////
+    // TT muncher for parsing the inside of an object {...}. Each entry is
+    // inserted into the given map variable.
+    //
+    // Must be invoked as: value_internal!(@object $map () ($($tt)*) ($($tt)*))
+    //
+    // We require two copies of the input tokens so that we can match on one
+    // copy and trigger errors on the other copy.
+    //////////////////////////////////////////////////////////////////////////
+
+    // Done.
+    (@object $object:ident () () ()) => {};
+
+    // Insert the current entry followed by trailing comma.
+    (@object $object:ident [$key:expr] ($value:expr) , $($rest:tt)*) => {
+        let _ = $object.insert(($key).into(), $value);
+        $crate::value_internal!(@object $object () ($($rest)*) ($($rest)*));
+    };
+
+    // Insert the current entry not followed by trailing comma.
+    (@object $object:ident [$key:expr] ($value:expr) $($rest:tt)*) => {
+        let _ = $object.insert(($key).into(), $value);
+        $crate::value_internal!(@object $object () ($($rest)*) ($($rest)*));
+    };
+
+    // Insert the last entry without trailing comma.
+    (@object $object:ident [$key:expr] ($value:expr)) => {
+        let _ = $object.insert(($key).into(), $value);
+    };
+
+    // Next value is `null`.
+    (@object $object:ident ($key:expr) (= null $($rest:tt)*) $copy:tt) => {
+        $crate::value_internal!(@object $object [$key] ($crate::value_internal!(null)) $($rest)*);
+    };
+
+    // Next value is `true`.
+    (@object $object:ident ($key:expr) (= true $($rest:tt)*) $copy:tt) => {
+        $crate::value_internal!(@object $object [$key] ($crate::value_internal!(true)) $($rest)*);
+    };
+
+    // Next value is `false`.
+    (@object $object:ident ($key:expr) (= false $($rest:tt)*) $copy:tt) => {
+        $crate::value_internal!(@object $object [$key] ($crate::value_internal!(false)) $($rest)*);
+    };
+
+    // Next value is an array.
+    (@object $object:ident ($key:expr) (= [$($array:tt)*] $($rest:tt)*) $copy:tt) => {
+        $crate::value_internal!(@object $object [$key] ($crate::value_internal!([$($array)*])) $($rest)*);
+    };
+
+    // Next value is a map.
+    (@object $object:ident ($key:expr) (= {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
+        $crate::value_internal!(@object $object [$key] ($crate::value_internal!({$($map)*})) $($rest)*);
+    };
+
+    // Next value is an value followed by comma.
+    (@object $object:ident ($key:expr) (= $value:expr , $($rest:tt)*) $copy:tt) => {
+        $crate::value_internal!(@object $object [$key] ($crate::value_internal!($value)) , $($rest)*);
+    };
+
+    // Next value is an value not followed by comma.
+    (@object $object:ident ($key:expr) (= $value:tt $($rest:tt)*) $copy:tt) => {
+        $crate::value_internal!(@object $object [$key] ($crate::value_internal!($value)) $($rest)*);
+    };
+
+    // Last value is an value with no trailing comma.
+    (@object $object:ident ($key:expr) (= $value:expr) $copy:tt) => {
+        $crate::value_internal!(@object $object [$key] ($crate::value_internal!($value)));
+    };
+
+    // Missing value for last entry. Trigger a reasonable error message.
+    (@object $object:ident ($key:expr) (=) $copy:tt) => {
+        // "unexpected end of macro invocation"
+        $crate::value_internal!();
+    };
+
+    // Missing equals and value for last entry. Trigger a reasonable error
+    // message.
+    (@object $object:ident ($key:expr) () $copy:tt) => {
+        // "unexpected end of macro invocation"
+        $crate::value_internal!();
+    };
+
+    // Misplaced equals. Trigger a reasonable error message.
+    (@object $object:ident () (= $($rest:tt)*) ($equals:tt $($copy:tt)*)) => {
+        // Takes no arguments so "no rules expected the token `:`".
+        $crate::hcl_unexpected!($equals);
+    };
+
+    // Found a comma inside a key. Trigger a reasonable error message.
+    (@object $object:ident ($key:expr) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => {
+        // Takes no arguments so "no rules expected the token `,`".
+        $crate::hcl_unexpected!($comma);
+    };
+
+    // Refuse to absorb equals token into key value.
+    (@object $object:ident ($key:expr) (= $($unexpected:tt)+) $copy:tt) => {
+        $crate::hcl_expect_expr_comma!($($unexpected)+);
+    };
+
+    // Munch an identifier key.
+    (@object $object:ident () ($key:ident $($rest:tt)*) $copy:tt) => {
+        $crate::value_internal!(@object $object ((std::stringify!($key))) ($($rest)*) ($($rest)*));
+    };
+
+    // Munch a literal key.
+    (@object $object:ident () ($key:literal $($rest:tt)*) $copy:tt) => {
+        $crate::value_internal!(@object $object (($key)) ($($rest)*) ($($rest)*));
+    };
+
+    // Munch a parenthesized value key.
+    (@object $object:ident () (($key:expr) $($rest:tt)*) $copy:tt) => {
+        $crate::value_internal!(@object $object (($key)) ($($rest)*) ($($rest)*));
+    };
+
+    //////////////////////////////////////////////////////////////////////////
+    // The main implementation.
+    //
+    // Must be invoked as: value_internal!($($expr)+)
+    //////////////////////////////////////////////////////////////////////////
+
+    (null) => {
+        $crate::value::Value::Null
+    };
+
+    (true) => {
+        $crate::value::Value::Bool(true)
+    };
+
+    (false) => {
+        $crate::value::Value::Bool(false)
+    };
+
+    ([]) => {
+        $crate::value::Value::Array(std::vec![])
+    };
+
+    ([ $($tt:tt)+ ]) => {
+        $crate::value::Value::Array($crate::value_internal!(@array [] $($tt)+))
+    };
+
+    ({}) => {
+        $crate::value::Value::Object($crate::value::Map::new())
+    };
+
+    ({ $($tt:tt)+ }) => {
+        $crate::value::Value::Object({
+            let mut object = $crate::value::Map::new();
+            $crate::value_internal!(@object object () ($($tt)+) ($($tt)+));
+            object
+        })
+    };
+
+    // Any Serialize type: numbers, strings, struct literals, variables etc.
+    // Must be below every other rule.
+    ($other:expr) => {
+        $crate::value::to_value(&$other).unwrap()
+    };
+}
+
 #[macro_export]
 #[doc(hidden)]
 macro_rules! hcl_unexpected {


Test patch:
diff --git a/src/de/tests.rs b/src/de/tests.rs
index 4c593ce..c890ed0 100644
--- a/src/de/tests.rs
+++ b/src/de/tests.rs
@@ -4,10 +4,9 @@ use crate::expr::{
     TraversalOperator, UnaryOp, UnaryOperator, Variable,
 };
 use crate::structure::{Block, Body};
-use crate::Identifier;
+use crate::{value, Identifier, Value};
 use pretty_assertions::assert_eq;
 use serde::Deserialize;
-use serde_json::{json, Value};
 use std::fmt::Debug;
 
 #[track_caller]
@@ -20,14 +19,14 @@ where
 
 #[test]
 fn deserialize_string_attribute() {
-    expect_value(r#"foo = "bar""#, json!({"foo": "bar"}))
+    expect_value(r#"foo = "bar""#, value!({ foo = "bar" }))
 }
 
 #[test]
 fn deserialize_object() {
     expect_value(
         r#"foo = { bar = 42, "baz" = true }"#,
-        json!({"foo": {"bar": 42, "baz": true}}),
+        value!({ foo = { bar = 42, baz = true } }),
     )
 }
 
@@ -35,20 +34,12 @@ fn deserialize_object() {
 fn deserialze_block() {
     expect_value(
         r#"resource "aws_s3_bucket" "mybucket" { name = "mybucket" }"#,
-        json!({
-            "resource": {
-                "aws_s3_bucket": {
-                    "mybucket": {
-                        "name": "mybucket"
-                    }
-                }
-            }
-        }),
+        value!({ resource = { aws_s3_bucket = { mybucket = { name = "mybucket" } } } }),
     );
 
     expect_value(
         r#"block { name = "asdf" }"#,
-        json!({"block": {"name": "asdf"}}),
+        value!({ block = { name = "asdf" } }),
     );
 }
 
@@ -73,22 +64,22 @@ fn deserialize_duplicate_block() {
           bar = "baz"
         }
     "#;
-    let expected = json!({
-        "block": {
-            "foo": [
-                {"bar": "baz"},
-                {"bar": 1}
+    let expected = value!({
+        block = {
+            foo = [
+                { "bar" = "baz" },
+                { "bar" = 1 }
             ]
         },
-        "other": {
-            "one": {
-                "two": {
-                    "foo": "bar"
+        other = {
+            one = {
+                two = {
+                    foo = "bar"
                 }
             },
-            "two": {
-                "three": {
-                    "bar": "baz"
+            two = {
+                three = {
+                    bar = "baz"
                 }
             }
         }
@@ -99,12 +90,7 @@ fn deserialize_duplicate_block() {
         foo { bar = "baz" }
         foo { bar = 1 }
     "#;
-    let expected = json!({
-        "foo": [
-            {"bar": "baz"},
-            {"bar": 1}
-        ]
-    });
+    let expected = value!({ foo = [{ bar = "baz" }, { bar = 1 }] });
     expect_value(input, expected);
 }
 
@@ -114,7 +100,7 @@ fn deserialize_duplicate_attribute() {
         foo = ["bar"]
         foo = ["baz"]
     "#;
-    expect_value(input, json!({"foo": ["baz"]}));
+    expect_value(input, value!({ foo = ["baz"] }));
 }
 
 #[test]
@@ -123,19 +109,19 @@ fn deserialize_duplicate_attribute_and_block() {
         foo = ["bar"]
         foo { bar = "baz" }
     "#;
-    expect_value(input, json!({"foo": {"bar": "baz"}}));
+    expect_value(input, value!({ foo = { bar = "baz" } }));
 
     let input = r#"
         foo { bar = "baz" }
         foo = ["bar"]
     "#;
-    expect_value(input, json!({"foo": ["bar"]}));
+    expect_value(input, value!({ foo = ["bar"] }));
 }
 
 #[test]
 fn deserialize_tuple() {
     let input = r#"foo = [true, 2, "three", var.enabled]"#;
-    let expected = json!({"foo": [true, 2, "three", "${var.enabled}"]});
+    let expected = value!({ foo = [true, 2, "three", "${var.enabled}"] });
     expect_value(input, expected);
 }
 
diff --git a/src/ser/tests.rs b/src/ser/tests.rs
index f06f194..15de4f9 100644
--- a/src/ser/tests.rs
+++ b/src/ser/tests.rs
@@ -5,9 +5,8 @@ use crate::expr::{
     TraversalOperator, Variable,
 };
 use crate::structure::{Attribute, Block, Body};
-use crate::Identifier;
+use crate::{value, Identifier};
 use pretty_assertions::assert_eq;
-use serde_json::json;
 
 #[track_caller]
 fn expect_str<T: Serialize>(value: T, expected: &str) {
@@ -134,12 +133,12 @@ qux {
 
 #[test]
 fn serialize_object() {
-    let value = json!({
-        "foo": [1, 2, 3],
-        "bar": "baz",
-        "qux": {
-            "foo": "bar",
-            "baz": "qux"
+    let value = value!({
+        foo = [1, 2, 3]
+        bar = "baz"
+        qux = {
+            "foo" = "bar"
+            "baz" = "qux"
         }
     });
 
@@ -160,17 +159,17 @@ qux = {
 
 #[test]
 fn serialize_array() {
-    let value = json!([
+    let value = value!([
         {
-            "foo": [1, 2, 3],
+            foo = [1, 2, 3]
         },
         {
-            "bar": "baz",
+            bar = "baz"
         },
         {
-            "qux": {
-                "foo": "bar",
-                "baz": "qux"
+            qux = {
+                "foo" = "bar"
+                "baz" = "qux"
             }
         }
     ]);
@@ -356,7 +355,7 @@ HEREDOC
 fn serialize_errors() {
     assert!(to_string(&true).is_err());
     assert!(to_string("foo").is_err());
-    assert!(to_string(&json!({"\"": "invalid attribute name"})).is_err())
+    assert!(to_string(&value!({ "\"" = "invalid attribute name" })).is_err())
 }
 
 #[test]
diff --git a/src/structure/tests.rs b/src/structure/tests.rs
index 27af4a9..f0311ef 100644
--- a/src/structure/tests.rs
+++ b/src/structure/tests.rs
@@ -1,8 +1,7 @@
 use super::{Block, Body};
 use crate::expr::{Heredoc, HeredocStripMode, RawExpression, TemplateExpr};
-use crate::{Identifier, Value};
+use crate::{value, Identifier, Value};
 use pretty_assertions::assert_eq;
-use serde_json::json;
 
 #[test]
 fn body_into_value() {
@@ -41,26 +40,24 @@ fn body_into_value() {
         ))
         .build();
 
-    let value = json!({
-        "foo": "baz",
-        "bar": {
-            "baz": [
+    let expected = value!({
+        foo = "baz"
+        bar = {
+            baz = [
                 {
-                    "foo": "bar"
+                    foo = "bar"
                 },
                 {
-                    "bar": "baz",
-                    "baz": "${var.foo}"
+                    bar = "baz"
+                    baz = "${var.foo}"
                 }
-            ],
-            "qux": {
-                "foo": 1
-            },
-        },
-        "heredoc": "foo bar ${baz}\\backslash"
+            ]
+            qux = {
+                foo = 1
+            }
+        }
+        heredoc = "foo bar ${baz}\\backslash"
     });
 
-    let expected: Value = serde_json::from_value(value).unwrap();
-
     assert_eq!(Value::from(body), expected);
 }