Repository: smithy-lang/smithy
Pull request #1944 (2023-08-22 21:36:43)

Clarify how trait values are provided in IDL
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/.github/workflows/update-brew.yml b/.github/workflows/update-brew.yml
index 23e1e3839..aecda5096 100644
--- a/.github/workflows/update-brew.yml
+++ b/.github/workflows/update-brew.yml
@@ -5,7 +5,7 @@ on:
   workflow_dispatch: # on button click
 
 env:
-  release_prefix: https://github.com/awslabs/smithy/releases/download
+  release_prefix: https://github.com/smithy-lang/smithy/releases/download
 
 jobs:
   download-and-save:
@@ -98,12 +98,13 @@ jobs:
           tmp=$(mktemp)
           jq --arg sha "$sha" '.bottle.sha256.linux_arm = "'$sha'"' homebrew-tap/bottle-configs/smithy-cli.json > "$tmp" && mv "$tmp" homebrew-tap/bottle-configs/smithy-cli.json
       - name: Set up new git branch for version bump
+        working-directory: homebrew-tap
         run: |
-          cd homebrew-tap
           git config user.name 'smithy-automation'
           git config user.email 'github-smithy-automation@amazon.com'
           git checkout -b "automation/bump-smithy-cli-version/${GITHUB_REF##*/}"
       - name: Create PR
+        working-directory: homebrew-tap
         run: |
           git add bottle-configs/smithy-cli.json
           git commit -m "chore: upgrade smithy-cli to ${GITHUB_REF##*/}"
diff --git a/docs/source-2.0/spec/idl.rst b/docs/source-2.0/spec/idl.rst
index 1016dd6e6..2a9bf1be5 100644
--- a/docs/source-2.0/spec/idl.rst
+++ b/docs/source-2.0/spec/idl.rst
@@ -1745,8 +1745,10 @@ Trait values
 ------------
 
 The value that can be provided for a trait depends on its type. A value for a
-trait is defined by enclosing the value in parenthesis. Trait values can only
-appear immediately before a shape.
+trait is defined by enclosing the value in parenthesis, provided as a
+:ref:`node value <node-values>`. Trait values MUST adhere to the JSON type
+mappings defined in :ref:`trait-node-values`. Trait values can only appear
+immediately before a shape.
 
 The following example applies various traits to a structure shape and its
 members.
@@ -1759,36 +1761,39 @@ members.
         name: smithy.api#String,
 
         @length(min: 0)
-        @tags(["private-beta"])
+        @tags(["private-beta", "metered"])
         age: smithy.api#Integer,
     }
 
 
-Structure, map, and union trait values
---------------------------------------
+.. _structured-trait-values:
 
-Traits that are a ``structure``, ``union``, or ``map`` are defined using
-a special syntax that places key-value pairs inside of the trait
-parenthesis. Wrapping braces, "{" and "}", are not permitted.
+Structure, map, and union trait value syntax
+--------------------------------------------
+
+A special syntax is provided for structure, map, and union traits that
+allows placing key-value pairs directly inside of the trait parenthesis.
 
 .. code-block:: smithy
 
-    @structuredTrait(foo: "bar", baz: "bam")
+    @structuredTrait(bar: "baz", qux: "true")
 
-Nested structure, map, and union values are defined using
-:ref:`node value <node-values>` productions:
+Is equivalent to:
 
 .. code-block:: smithy
 
-    @structuredTrait(
-        foo: {
-            bar: "baz",
-            qux: "true",
-        }
-    )
+    @structuredTrait({bar: "baz", qux: "true"})
+
+
+Omitted trait values
+--------------------
+
+An applied trait with no value, with or without empty parenthesis, assumes
+a default value based on the shape of the trait.
+
+If a value is omitted for a ``structure`` or ``map``, the value defaults to an empty
+object (``{}``).
 
-Omitting a value is allowed on ``list``, ``set``, ``map``, and ``structure``
-traits if the shapes have no ``length`` constraints or ``required`` members.
 The following applications of the ``foo`` trait are equivalent:
 
 .. tab:: Smithy
@@ -1796,8 +1801,10 @@ The following applications of the ``foo`` trait are equivalent:
     .. code-block:: smithy
 
         $version: "2"
+
         namespace smithy.example
 
+        // Define an example structure trait.
         @trait
         structure foo {}
 
@@ -1807,6 +1814,9 @@ The following applications of the ``foo`` trait are equivalent:
         @foo()
         string MyString2
 
+        @foo({})
+        string MyString3
+
 .. tab:: JSON
 
     .. code-block:: json
@@ -1831,33 +1841,68 @@ The following applications of the ``foo`` trait are equivalent:
                     "traits": {
                         "smithy.api#foo": {}
                     }
+                },
+                "smithy.example#MyString3": {
+                    "type": "string",
+                    "traits": {
+                        "smithy.api#foo": {}
+                    }
                 }
             }
         }
 
+If a value is omitted for a ``list``, the value defaults to an empty list
+(``[]``).
 
-List and set trait values
--------------------------
+The following applications of the :ref:`tags-trait` are equivalent:
 
-Traits that are a ``list`` or ``set`` shape are defined inside
-of brackets (``[``) and (``]``) using a :token:`smithy:NodeArray` production.
+.. tab:: Smithy
 
-.. code-block:: smithy
+    .. code-block:: smithy
 
-    @tags(["a", "b"])
+        $version: "2"
 
+        namespace smithy.example
 
-Other trait values
-------------------
+        @tags
+        string MyString1
 
-All other trait values MUST adhere to the JSON type mappings defined
-in :ref:`trait-node-values`.
+        @tags()
+        string MyString2
 
-The following example defines a string trait value:
+        @tags([])
+        string MyString3
 
-.. code-block:: smithy
+.. tab:: JSON
+
+    .. code-block:: json
+
+        {
+            "smithy": "2",
+            "shapes": {
+                "smithy.example#MyString1": {
+                    "type": "string",
+                    "traits": {
+                        "smithy.api#tags": []
+                    }
+                },
+                "smithy.example#MyString2": {
+                    "type": "string",
+                    "traits": {
+                        "smithy.api#tags": []
+                    }
+                },
+                "smithy.example#MyString3": {
+                    "type": "string",
+                    "traits": {
+                        "smithy.api#tags": []
+                    }
+                }
+            }
+        }
 
-    @documentation("Hello")
+All other shapes default to ``null``, which may or may not be valid for the
+shape of the trait.
 
 
 .. _apply-statement:
diff --git a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java
index 273523b35..59d2995ff 100644
--- a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java
+++ b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java
@@ -328,24 +328,29 @@ final class FormatVisitor {
                             .detectHardLines(cursor)
                             .write();
                 } else {
-                    // Check the inner trait node for hard line breaks rather than the wrapper.
-                    TreeCursor traitNode = cursor
-                            .getFirstChild(TreeType.TRAIT_NODE)
-                            .getFirstChild(TreeType.NODE_VALUE)
-                            .getFirstChild(); // The actual node value.
-                    return new BracketFormatter()
-                            .open(Formatter.LPAREN)
-                            .close(Formatter.RPAREN)
-                            .extractChildren(cursor, BracketFormatter.extractor(this::visit, child -> {
-                                if (child.getTree().getType() == TreeType.TRAIT_NODE) {
-                                    // Split WS and NODE_VALUE so that they appear on different lines.
-                                    return child.getChildrenByType(TreeType.NODE_VALUE, TreeType.WS).stream();
-                                } else {
-                                    return Stream.empty();
-                                }
-                            }))
-                            .detectHardLines(traitNode)
-                            .write();
+                    if (cursor.getFirstChild(TreeType.TRAIT_NODE) != null) {
+                        // Check the inner trait node for hard line breaks rather than the wrapper.
+                        TreeCursor traitNode = cursor
+                                .getFirstChild(TreeType.TRAIT_NODE)
+                                .getFirstChild(TreeType.NODE_VALUE)
+                                .getFirstChild(); // The actual node value.
+                        return new BracketFormatter()
+                                .open(Formatter.LPAREN)
+                                .close(Formatter.RPAREN)
+                                .extractChildren(cursor, BracketFormatter.extractor(this::visit, child -> {
+                                    if (child.getTree().getType() == TreeType.TRAIT_NODE) {
+                                        // Split WS and NODE_VALUE so that they appear on different lines.
+                                        return child.getChildrenByType(TreeType.NODE_VALUE, TreeType.WS).stream();
+                                    } else {
+                                        return Stream.empty();
+                                    }
+                                }))
+                                .detectHardLines(traitNode)
+                                .write();
+                    } else {
+                        // If the trait node is empty, remove the empty parentheses.
+                        return Doc.text("");
+                    }
                 }
             }
 


Test patch:
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/null-trait-body.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/null-trait-body.formatted.smithy
new file mode 100644
index 000000000..96bebad04
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/null-trait-body.formatted.smithy
@@ -0,0 +1,17 @@
+$version: "2.0"
+
+namespace smithy.example
+
+structure Struct {
+    @default
+    a: String
+
+    @default
+    b: String
+
+    @default
+    c: String
+
+    @default(null)
+    d: String
+}
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/null-trait-body.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/null-trait-body.smithy
new file mode 100644
index 000000000..75e6ebf59
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/null-trait-body.smithy
@@ -0,0 +1,17 @@
+$version: "2.0"
+
+namespace smithy.example
+
+structure Struct {
+    @default
+    a: String
+
+    @default()
+    b: String
+
+    @default( )
+    c: String
+
+    @default(null)
+    d: String
+}