Repository: smithy-lang/smithy
Pull request #2113 (2024-01-24 22:56:24)

Add aws smoke test model package
Adds a package that provides a Smithy model with shapes used for https://smithy.io/2.0/additional-specs/smoke-tests.html#smoketestcase `vendorParamsShape` for AWS smoke tests. The package has Java POJOs corresponding to these shapes which can be used by code generators to do aws-specific configuration on generated smoke tests.

The POJOs are meant to be direct translations of the Smithy shapes, so to make it more ergonomic to get the POJOs from a smoke test case I added a util class AwsSmokeTestModel that can be used to check whether a smoke test case has either concrete vendor params shape, and to get the respective POJO.

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/settings.gradle b/settings.gradle
index 89a47440a..bc0abdddc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -33,3 +33,4 @@ include ":smithy-rules-engine"
 include ":smithy-smoke-test-traits"
 include ":smithy-syntax"
 include ":smithy-aws-endpoints"
+include ":smithy-aws-smoke-test-model"
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelLoader.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelLoader.java
index 7b87c6ec3..562c07e8a 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelLoader.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelLoader.java
@@ -668,6 +668,7 @@ final class IdlModelLoader {
         tokenizer.expect(IdlToken.LBRACE);
         tokenizer.next();
         tokenizer.skipWs();
+        Set<String> parsedMemberNames = new HashSet<>();
 
         while (tokenizer.getCurrentToken() != IdlToken.EOF && tokenizer.getCurrentToken() != IdlToken.RBRACE) {
             List<IdlTraitParser.Result> memberTraits = IdlTraitParser
@@ -676,6 +677,10 @@ final class IdlModelLoader {
             tokenizer.expect(IdlToken.IDENTIFIER);
             String memberName = internString(tokenizer.getCurrentTokenLexeme());
 
+            if (!parsedMemberNames.add(memberName)) {
+                throw syntax(id, "Found conflicting member name, `" + memberName + "`");
+            }
+
             MemberShape.Builder memberBuilder = MemberShape.builder()
                     .id(id.withMember(memberName))
                     .source(memberLocation)
diff --git a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/BracketFormatter.java b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/BracketFormatter.java
index dfb992fdd..68c494ff8 100644
--- a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/BracketFormatter.java
+++ b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/BracketFormatter.java
@@ -32,6 +32,7 @@ final class BracketFormatter {
     private Doc close = Formatter.RBRACE;
     private Collection<Doc> children;
     private boolean forceLineBreaks;
+    private boolean forceInline = false;
 
     static Function<TreeCursor, Stream<Doc>> extractor(
             Function<TreeCursor, Doc> visitor,
@@ -91,8 +92,8 @@ final class BracketFormatter {
         return false;
     }
 
-    BracketFormatter forceLineBreaks(boolean forceLineBreaks) {
-        this.forceLineBreaks = forceLineBreaks;
+    BracketFormatter forceLineBreaks() {
+        forceLineBreaks = true;
         return this;
     }
 
@@ -103,15 +104,39 @@ final class BracketFormatter {
         return this;
     }
 
+    // Don't force line breaks and don't indent inside brackets
+    BracketFormatter forceInline() {
+        forceInline = true;
+        forceLineBreaks = false;
+        return this;
+    }
+
     Doc write() {
         SmithyBuilder.requiredState("open", open);
         SmithyBuilder.requiredState("close", close);
         SmithyBuilder.requiredState("children", children);
         if (forceLineBreaks) {
             return FormatVisitor.renderBlock(open, close, Doc.intersperse(Doc.line(), children));
+        }
+
+        int indent = forceInline ? 0 : 4;
+        Doc lineBreakDoc = getBracketLineBreakDoc();
+        return Doc.intersperse(Formatter.LINE_OR_COMMA, children).bracket(indent, lineBreakDoc, open, close);
+    }
+
+    private Doc getBracketLineBreakDoc() {
+        Doc lineDoc;
+        if (!children.isEmpty() && open == Formatter.LBRACE) {
+            // Make flattened objects have space padding inside the braces
+            lineDoc = Doc.lineOrSpace();
+        } else if (children.size() > 1) {
+            // Multiple children may be separated on to different lines
+            lineDoc = Doc.lineOrEmpty();
         } else {
-            return Doc.intersperse(Formatter.LINE_OR_COMMA, children).bracket(4, Doc.lineOrEmpty(), open, close);
+            // Only one child or less doesn't need line separation
+            lineDoc = Doc.empty();
         }
+        return lineDoc;
     }
 
     private static final class Extractor implements Function<TreeCursor, Stream<Doc>> {
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 59d2995ff..2df51058e 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
@@ -271,8 +271,7 @@ final class FormatVisitor {
                                         .close(Formatter.RBRACKET)
                                         .extractChildren(cursor, BracketFormatter.extractByType(TreeType.SHAPE_ID,
                                                                                                 this::visit))
-                                        .detectHardLines(cursor)
-                                        .forceLineBreaks(true) // always put each error on separate lines.
+                                        .forceLineBreaks() // always put each error on separate lines.
                                         .write());
             }
 
@@ -327,30 +326,46 @@ final class FormatVisitor {
                             }))
                             .detectHardLines(cursor)
                             .write();
-                } else {
-                    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 (cursor.getFirstChild(TreeType.TRAIT_NODE) != null) {
+                    TreeCursor traitNode = cursor.getFirstChild(TreeType.TRAIT_NODE);
+                    // Check the inner trait node for hard line breaks rather than the wrapper.
+                    TreeCursor actualTraitNodeValue = traitNode
+                            .getFirstChild(TreeType.NODE_VALUE)
+                            .getFirstChild(); // The actual node value.
+
+                    BracketFormatter formatter = 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();
+                                }
+                            }));
+
+                    // TraitBody may have leading comments and TraitNode may have trailing comments
+                    // which both require line break
+                    TreeCursor bodyWs = cursor.getFirstChild(TreeType.WS);
+                    TreeCursor traitNodeWs = traitNode.getFirstChild(TreeType.WS);
+                    if ((bodyWs != null && !bodyWs.findChildrenByType(TreeType.COMMENT).isEmpty())
+                            || (traitNodeWs != null && !traitNodeWs.findChildrenByType(TreeType.COMMENT).isEmpty())) {
+                        // Need to line break if there's a leading comment in the trait body no matter what
+                        formatter.forceLineBreaks();
+                    } else if (actualTraitNodeValue.getTree().getType() == TreeType.NODE_ARRAY
+                            || actualTraitNodeValue.getTree().getType() == TreeType.NODE_OBJECT) {
+                        // Just inline arrays and objects in trait body
+                        formatter.forceInline();
                     } else {
-                        // If the trait node is empty, remove the empty parentheses.
-                        return Doc.text("");
+                        // Any other values can use normal detection
+                        formatter.detectHardLines(cursor);
                     }
+
+                    return formatter.write();
+                } else {
+                    // If the trait node is empty, remove the empty parentheses.
+                    return Doc.text("");
                 }
             }
 
@@ -400,11 +415,17 @@ final class FormatVisitor {
             }
 
             case NODE_OBJECT: {
-                return new BracketFormatter()
+                BracketFormatter formatter = new BracketFormatter()
                         .extractChildren(cursor, BracketFormatter.extractByType(TreeType.NODE_OBJECT_KVP,
-                                                                                this::visit))
-                        .detectHardLines(cursor)
-                        .write();
+                                                                                this::visit));
+                if (cursor.getParent().getParent().getTree().getType() == TreeType.NODE_ARRAY)  {
+                    // Always break objects inside arrays if not empty
+                    formatter.forceLineBreaksIfNotEmpty();
+                } else {
+                    formatter.detectHardLines(cursor);
+                }
+                return formatter.write();
+
             }
 
             case NODE_OBJECT_KVP: {


Test patch:
diff --git a/smithy-aws-protocol-tests/model/awsJson1_0/defaults.smithy b/smithy-aws-protocol-tests/model/awsJson1_0/defaults.smithy
index fbac5fe08..182c36d65 100644
--- a/smithy-aws-protocol-tests/model/awsJson1_0/defaults.smithy
+++ b/smithy-aws-protocol-tests/model/awsJson1_0/defaults.smithy
@@ -37,7 +37,16 @@ apply OperationWithDefaults @httpRequestTests([
                     "defaultDouble": 1.0,
                     "defaultMap": {},
                     "defaultEnum": "FOO",
-                    "defaultIntEnum": 1
+                    "defaultIntEnum": 1,
+                    "emptyString": "",
+                    "falseBoolean": false,
+                    "emptyBlob": "",
+                    "zeroByte": 0,
+                    "zeroShort": 0,
+                    "zeroInteger": 0,
+                    "zeroLong": 0,
+                    "zeroFloat": 0.0,
+                    "zeroDouble": 0.0
                 }
             }"""
         params: {
@@ -91,6 +100,15 @@ apply OperationWithDefaults @httpRequestTests([
                 defaultMap: {name: "Jack"}
                 defaultEnum: "BAR"
                 defaultIntEnum: 2
+                emptyString: "foo"
+                falseBoolean: true
+                emptyBlob: "hi"
+                zeroByte: 1
+                zeroShort: 1
+                zeroInteger: 1
+                zeroLong: 1
+                zeroFloat: 1.0
+                zeroDouble: 1.0
             }
         }
         body: """
@@ -114,7 +132,16 @@ apply OperationWithDefaults @httpRequestTests([
                     "defaultDouble": 2.0,
                     "defaultMap": {"name": "Jack"},
                     "defaultEnum": "BAR",
-                    "defaultIntEnum": 2
+                    "defaultIntEnum": 2,
+                    "emptyString": "foo",
+                    "falseBoolean": true,
+                    "emptyBlob": "aGk=",
+                    "zeroByte": 1,
+                    "zeroShort": 1,
+                    "zeroInteger": 1,
+                    "zeroLong": 1,
+                    "zeroFloat": 1.0,
+                    "zeroDouble": 1.0
                 }
             }"""
     }
@@ -152,8 +179,56 @@ apply OperationWithDefaults @httpRequestTests([
                 defaultMap: {}
                 defaultEnum: "FOO"
                 defaultIntEnum: 1
+                emptyString: ""
+                falseBoolean: false
+                emptyBlob: ""
+                zeroByte: 0
+                zeroShort: 0
+                zeroInteger: 0
+                zeroLong: 0
+                zeroFloat: 0.0
+                zeroDouble: 0.0
             },
             topLevelDefault: "hi"
+            otherTopLevelDefault: 0
+        }
+    }
+    {
+        id: "AwsJson10ClientUsesExplicitlyProvidedValuesInTopLevel"
+        documentation: "Any time a value is provided for a member in the top level of input, it is used, regardless of if its the default."
+        appliesTo: "client"
+        tags: ["defaults"]
+        protocol: awsJson1_0
+        method: "POST"
+        bodyMediaType: "application/json"
+        uri: "/"
+        headers: {"Content-Type": "application/x-amz-json-1.0"}
+        body: """
+            {
+                "topLevelDefault": "hi",
+                "otherTopLevelDefault": 0
+            }"""
+        params: {
+            topLevelDefault: "hi"
+            otherTopLevelDefault: 0
+        }
+    }
+    {
+        id: "AwsJson10ClientIgnoresNonTopLevelDefaultsOnMembersWithClientOptional"
+        documentation: "Typically, non top-level members would have defaults filled in, but if they have the clientOptional trait, the defaults should be ignored."
+        appliesTo: "client"
+        tags: ["defaults"]
+        protocol: awsJson1_0
+        method: "POST"
+        bodyMediaType: "application/json"
+        uri: "/"
+        headers: {"Content-Type": "application/x-amz-json-1.0"}
+        body: """
+            {
+                "clientOptionalDefaults": {}
+            }"""
+        params: {
+            clientOptionalDefaults: {}
         }
     }
 ])
@@ -188,6 +263,15 @@ apply OperationWithDefaults @httpResponseTests([
             defaultMap: {}
             defaultEnum: "FOO"
             defaultIntEnum: 1
+            emptyString: ""
+            falseBoolean: false
+            emptyBlob: ""
+            zeroByte: 0
+            zeroShort: 0
+            zeroInteger: 0
+            zeroLong: 0
+            zeroFloat: 0.0
+            zeroDouble: 0.0
         }
     }
     {
@@ -219,7 +303,16 @@ apply OperationWithDefaults @httpResponseTests([
                 "defaultDouble": 2.0,
                 "defaultMap": {"name": "Jack"},
                 "defaultEnum": "BAR",
-                "defaultIntEnum": 2
+                "defaultIntEnum": 2,
+                "emptyString": "foo",
+                "falseBoolean": true,
+                "emptyBlob": "aGk=",
+                "zeroByte": 1,
+                "zeroShort": 1,
+                "zeroInteger": 1,
+                "zeroLong": 1,
+                "zeroFloat": 1.0,
+                "zeroDouble": 1.0
             }"""
         params: {
             defaultString: "bye"
@@ -241,6 +334,15 @@ apply OperationWithDefaults @httpResponseTests([
             defaultMap: {name: "Jack"}
             defaultEnum: "BAR"
             defaultIntEnum: 2
+            emptyString: "foo"
+            falseBoolean: true
+            emptyBlob: "hi"
+            zeroByte: 1
+            zeroShort: 1
+            zeroInteger: 1
+            zeroLong: 1
+            zeroFloat: 1.0
+            zeroDouble: 1.0
         }
     }
     {
@@ -271,7 +373,16 @@ apply OperationWithDefaults @httpResponseTests([
                 "defaultDouble": 1.0,
                 "defaultMap": {},
                 "defaultEnum": "FOO",
-                "defaultIntEnum": 1
+                "defaultIntEnum": 1,
+                "emptyString": "",
+                "falseBoolean": false,
+                "emptyBlob": "",
+                "zeroByte": 0,
+                "zeroShort": 0,
+                "zeroInteger": 0,
+                "zeroLong": 0,
+                "zeroFloat": 0.0,
+                "zeroDouble": 0.0
             }"""
         params: {}
     }
@@ -280,8 +391,9 @@ apply OperationWithDefaults @httpResponseTests([
 operation OperationWithDefaults {
     input := {
         defaults: Defaults
-
+        clientOptionalDefaults: ClientOptionalDefaults
         topLevelDefault: String = "hi" // Client should ignore default values in input shape
+        otherTopLevelDefault: Integer = 0
     }
 
     output := with [DefaultsMixin] {}
@@ -289,6 +401,11 @@ operation OperationWithDefaults {
 
 structure Defaults with [DefaultsMixin] {}
 
+structure ClientOptionalDefaults {
+    @clientOptional
+    member: Integer = 0
+}
+
 @mixin
 structure DefaultsMixin {
     defaultString: String = "hi"
@@ -310,6 +427,15 @@ structure DefaultsMixin {
     defaultMap: TestStringMap = {}
     defaultEnum: TestEnum = "FOO"
     defaultIntEnum: TestIntEnum = 1
+    emptyString: String = ""
+    falseBoolean: Boolean = false
+    emptyBlob: Blob = ""
+    zeroByte: Byte = 0
+    zeroShort: Short = 0
+    zeroInteger: Integer = 0
+    zeroLong: Long = 0
+    zeroFloat: Float = 0.0
+    zeroDouble: Double = 0.0
 }
 
 list TestStringList {
diff --git a/smithy-aws-protocol-tests/model/restJson1/http-query.smithy b/smithy-aws-protocol-tests/model/restJson1/http-query.smithy
index dea2c2a7f..995b18904 100644
--- a/smithy-aws-protocol-tests/model/restJson1/http-query.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/http-query.smithy
@@ -218,6 +218,22 @@ apply AllQueryStringTypes @httpRequestTests([
             }
         }
     },
+    {
+        id: "RestJsonZeroAndFalseQueryValues"
+        documentation: "Query values of 0 and false are serialized"
+        protocol: restJson1
+        method: "GET"
+        uri: "/AllQueryStringTypesInput"
+        body: ""
+        queryParams: [
+            "Integer=0"
+            "Boolean=false"
+        ]
+        params: {
+            queryInteger: 0
+            queryBoolean: false
+        }
+    }
 ])
 
 @suppress(["HttpQueryParamsTrait"])
diff --git a/smithy-aws-protocol-tests/model/restXml/http-query.smithy b/smithy-aws-protocol-tests/model/restXml/http-query.smithy
index 98f57de8e..017003b41 100644
--- a/smithy-aws-protocol-tests/model/restXml/http-query.smithy
+++ b/smithy-aws-protocol-tests/model/restXml/http-query.smithy
@@ -180,6 +180,22 @@ apply AllQueryStringTypes @httpRequestTests([
             queryDouble: "-Infinity",
         }
     },
+    {
+        id: "RestXmlZeroAndFalseQueryValues"
+        documentation: "Query values of 0 and false are serialized"
+        protocol: restXml
+        method: "GET"
+        uri: "/AllQueryStringTypesInput"
+        body: ""
+        queryParams: [
+            "Integer=0"
+            "Boolean=false"
+        ]
+        params: {
+            queryInteger: 0
+            queryBoolean: false
+        }
+    }
 ])
 
 @suppress(["HttpQueryParamsTrait"])
diff --git a/smithy-aws-smoke-test-model/build.gradle b/smithy-aws-smoke-test-model/build.gradle
new file mode 100644
index 000000000..ddc313f67
--- /dev/null
+++ b/smithy-aws-smoke-test-model/build.gradle
@@ -0,0 +1,15 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+description = "Defines shapes used by AWS for modeling smoke tests"
+
+ext {
+    displayName = "Smithy :: AWS :: Smoke Test :: Model"
+    moduleName = "software.amazon.smithy.aws.smoketest.model"
+}
+
+dependencies {
+    api project(":smithy-smoke-test-traits")
+}
diff --git a/smithy-aws-smoke-test-model/src/main/java/software/amazon/smithy/aws/smoketests/model/AwsSmokeTestModel.java b/smithy-aws-smoke-test-model/src/main/java/software/amazon/smithy/aws/smoketests/model/AwsSmokeTestModel.java
new file mode 100644
index 000000000..2e9803b9b
--- /dev/null
+++ b/smithy-aws-smoke-test-model/src/main/java/software/amazon/smithy/aws/smoketests/model/AwsSmokeTestModel.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.aws.smoketests.model;
+
+import java.util.Optional;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.smoketests.traits.SmokeTestCase;
+
+/**
+ * Provides methods for interfacing with Java representations of the different
+ * kinds of vendor params shapes used in smoke tests for AWS services.
+ */
+public final class AwsSmokeTestModel {
+    private AwsSmokeTestModel() {
+    }
+
+    /**
+     * @param testCase The test case to check.
+     * @return {@code true} if the {@code testCase}'s {@link SmokeTestCase#getVendorParams()}
+     * are {@link AwsVendorParams}.
+     */
+    public static boolean hasAwsVendorParams(SmokeTestCase testCase) {
+        return testCase.getVendorParamsShape()
+                .filter(AwsVendorParams.ID::equals)
+                .isPresent();
+    }
+
+    /**
+     * @param testCase The test case to check.
+     * @return {@code true} if the {@code testCase}'s {@link SmokeTestCase#getVendorParams()}
+     * are {@link S3VendorParams}.
+     */
+    public static boolean hasS3VendorParams(SmokeTestCase testCase) {
+        return testCase.getVendorParamsShape()
+                .filter(S3VendorParams.ID::equals)
+                .isPresent();
+    }
+
+    /**
+     * Gets the vendor params for the given {@code forTestCase} as {@link AwsVendorParams}.
+     *
+     * <p>The vendor params will be present if {@link #hasAwsVendorParams(SmokeTestCase)}
+     * was {@code true} for the given {@code forTestCase}.
+     *
+     * @param forTestCase The test case to get vendor params for.
+     * @return The optionally present vendor params as {@link S3VendorParams}.
+     */
+    public static Optional<AwsVendorParams> getAwsVendorParams(SmokeTestCase forTestCase) {
+        if (!hasAwsVendorParams(forTestCase)) {
+            return Optional.empty();
+        }
+
+        ObjectNode vendorParams = forTestCase.getVendorParams().orElse(Node.objectNode());
+        return Optional.of(new AwsVendorParams(vendorParams));
+    }
+
+    /**
+     * Gets the vendor params for the given {@code forTestCase} as {@link S3VendorParams}.
+     *
+     * <p>The vendor params will be present if {@link #hasS3VendorParams(SmokeTestCase)}
+     * was {@code true} for the given {@code forTestCase}.
+     *
+     * @param forTestCase The test case to get vendor params for.
+     * @return The optionally present vendor params as {@link S3VendorParams}.
+     */
+    public static Optional<S3VendorParams> getS3VendorParams(SmokeTestCase forTestCase) {
+        if (!hasS3VendorParams(forTestCase)) {
+            return Optional.empty();
+        }
+
+        ObjectNode vendorParams = forTestCase.getVendorParams().orElse(Node.objectNode());
+        return Optional.of(new S3VendorParams(vendorParams));
+    }
+}
diff --git a/smithy-aws-smoke-test-model/src/main/java/software/amazon/smithy/aws/smoketests/model/AwsVendorParams.java b/smithy-aws-smoke-test-model/src/main/java/software/amazon/smithy/aws/smoketests/model/AwsVendorParams.java
new file mode 100644
index 000000000..d0c4747e6
--- /dev/null
+++ b/smithy-aws-smoke-test-model/src/main/java/software/amazon/smithy/aws/smoketests/model/AwsVendorParams.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.aws.smoketests.model;
+
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.shapes.ShapeId;
+
+/**
+ * Concrete vendor params to apply to AWS services by default.
+ */
+public final class AwsVendorParams extends BaseAwsVendorParams {
+    public static final ShapeId ID = ShapeId.from("aws.test#AwsVendorParams");
+
+    AwsVendorParams(ObjectNode node) {
+        super(node);
+    }
+}
diff --git a/smithy-aws-smoke-test-model/src/main/java/software/amazon/smithy/aws/smoketests/model/BaseAwsVendorParams.java b/smithy-aws-smoke-test-model/src/main/java/software/amazon/smithy/aws/smoketests/model/BaseAwsVendorParams.java
new file mode 100644
index 000000000..fdddbebfb
--- /dev/null
+++ b/smithy-aws-smoke-test-model/src/main/java/software/amazon/smithy/aws/smoketests/model/BaseAwsVendorParams.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.aws.smoketests.model;
+
+import java.util.List;
+import java.util.Optional;
+import software.amazon.smithy.model.node.ObjectNode;
+
+/**
+ * Base vendor params for all AWS services.
+ */
+public abstract class BaseAwsVendorParams {
+    private static final String DEFAULT_REGION = "us-west-2";
+    private static final boolean DEFAULT_USE_FIPS = false;
+    private static final boolean DEFAULT_USE_DUALSTACK = false;
+    private static final boolean DEFAULT_USE_ACCOUNT_ID_ROUTING = true;
+
+    private final String region;
+    private final List<String> sigv4aRegionSet;
+    private final String uri;
+    private final boolean useFips;
+    private final boolean useDualstack;
+    private final boolean useAccountIdRouting;
+
+    BaseAwsVendorParams(ObjectNode node) {
+        this.region = node.getStringMemberOrDefault("region", DEFAULT_REGION);
+        this.sigv4aRegionSet = node.getArrayMember("sigv4aRegionSet")
+                .map(a -> a.getElementsAs(el -> el.expectStringNode().getValue()))
+                .orElse(null);
+        this.uri = node.getStringMemberOrDefault("uri", null);
+        this.useFips = node.getBooleanMemberOrDefault("useFips", DEFAULT_USE_FIPS);
+        this.useDualstack = node.getBooleanMemberOrDefault("useDualstack", DEFAULT_USE_DUALSTACK);
+        this.useAccountIdRouting = node.getBooleanMemberOrDefault(
+                "useAccountIdRouting", DEFAULT_USE_ACCOUNT_ID_ROUTING);
+    }
+
+    /**
+     * @return The AWS region to sign the request for and to resolve the default
+     * endpoint with. Defaults to {@code us-west-2}.
+     */
+    public String getRegion() {
+        return region;
+    }
+
+    /**
+     * @return The set of regions to sign a sigv4a request with, if present.
+     */
+    public Optional<List<String>> getSigv4aRegionSet() {
+        return Optional.ofNullable(sigv4aRegionSet);
+    }
+
+    /**
+     * @return The static endpoint to send the request to, if present.
+     */
+    public Optional<String> getUri() {
+        return Optional.ofNullable(uri);
+    }
+
+    /**
+     * @return Whether to resolve a FIPS compliant endpoint or not. Defaults to
+     * {@code false}.
+     */
+    public boolean useFips() {
+        return useFips;
+    }
+
+    /**
+     * @return Whether to resolve a dualstack endpoint or not. Defaults to
+     * {@code false}.
+     */
+    public boolean useDualstack() {
+        return useDualstack;
+    }
+
+    /**
+     * @return Whether to use account ID based routing where applicable.
+     * Defaults to {@code true}.
+     */
+    public boolean useAccountIdRouting() {
+        return useAccountIdRouting;
+    }
+}
diff --git a/smithy-aws-smoke-test-model/src/main/java/software/amazon/smithy/aws/smoketests/model/S3VendorParams.java b/smithy-aws-smoke-test-model/src/main/java/software/amazon/smithy/aws/smoketests/model/S3VendorParams.java
new file mode 100644
index 000000000..845ac4f33
--- /dev/null
+++ b/smithy-aws-smoke-test-model/src/main/java/software/amazon/smithy/aws/smoketests/model/S3VendorParams.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.aws.smoketests.model;
+
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.shapes.ShapeId;
+
+/**
+ * Vendor params for S3.
+ */
+public final class S3VendorParams extends BaseAwsVendorParams {
+    public static final ShapeId ID = ShapeId.from("aws.test#S3VendorParams");
+
+    private static final boolean DEFAULT_USE_ACCELERATE = false;
+    private static final boolean DEFAULT_USE_GLOBAL_ENDPOINT = false;
+    private static final boolean DEFAULT_FORCE_PATH_STYLE = false;
+    private static final boolean DEFAULT_USE_ARN_REGION = true;
+    private static final boolean DEFAULT_USE_MULTI_REGION_ACCESS_POINTS = true;
+
+    private final boolean useAccelerate;
+    private final boolean useGlobalEndpoint;
+    private final boolean forcePathStyle;
+    private final boolean useArnRegion;
+    private final boolean useMultiRegionAccessPoints;
+
+    S3VendorParams(ObjectNode node) {
+        super(node);
+        this.useAccelerate = node.getBooleanMemberOrDefault("useAccelerate", DEFAULT_USE_ACCELERATE);
+        this.useGlobalEndpoint = node.getBooleanMemberOrDefault("useGlobalEndpoint", DEFAULT_USE_GLOBAL_ENDPOINT);
+        this.forcePathStyle = node.getBooleanMemberOrDefault("forcePathStyle", DEFAULT_FORCE_PATH_STYLE);
+        this.useArnRegion = node.getBooleanMemberOrDefault("useArnRegion", DEFAULT_USE_ARN_REGION);
+        this.useMultiRegionAccessPoints = node.getBooleanMemberOrDefault(
+                "useMultiRegionAccessPoints", DEFAULT_USE_MULTI_REGION_ACCESS_POINTS);
+    }
+
+    /**
+     * @return Whether to resolve an accelerate endpoint or not. Defaults to
+     * {@code false}.
+     */
+    public boolean useAccelerate() {
+        return useAccelerate;
+    }
+
+    /**
+     * @return Whether to use the global endpoint for {@code us-east-1}.
+     * Defaults to {@code false}.
+     */
+    public boolean useGlobalEndpoint() {
+        return useGlobalEndpoint;
+    }
+
+    /**
+     * @return Whether to force path-style addressing. Defaults to {@code false}.
+     */
+    public boolean forcePathStyle() {
+        return forcePathStyle;
+    }
+
+    /**
+     * @return Whether to use the region in the bucket ARN to override the set
+     * region. Defaults to {@code true}.
+     */
+    public boolean useArnRegion() {
+        return useArnRegion;
+    }
+
+    /**
+     * @return Whether to use S3's multi-region access points. Defaults to
+     * {@code true}.
+     */
+    public boolean useMultiRegionAccessPoints() {
+        return useMultiRegionAccessPoints;
+    }
+}
diff --git a/smithy-aws-smoke-test-model/src/main/resources/META-INF/smithy/aws.test.smithy b/smithy-aws-smoke-test-model/src/main/resources/META-INF/smithy/aws.test.smithy
new file mode 100644
index 000000000..2a1050de5
--- /dev/null
+++ b/smithy-aws-smoke-test-model/src/main/resources/META-INF/smithy/aws.test.smithy
@@ -0,0 +1,58 @@
+$version: "2.0"
+
+namespace aws.test
+
+/// Base vendor params for all aws services.
+@mixin
+@suppress(["UnreferencedShape"])
+structure BaseAwsVendorParams {
+    /// The AWS region to sign the request for and to resolve the default
+    /// endpoint with.
+    region: String = "us-west-2"
+
+    /// The set of regions to sign a sigv4a request with.
+    sigv4aRegionSet: NonEmptyStringList
+
+    /// A static endpoint to send the request to.
+    uri: String
+
+    /// Whether to resolve a FIPS compliant endpoint or not.
+    useFips: Boolean = false
+
+    /// Whether to resolve a dualstack endpoint or not.
+    useDualstack: Boolean = false
+
+    /// Whether to use account ID based routing where applicable.
+    useAccountIdRouting: Boolean = true
+}
+
+@private
+@length(min: 1)
+@suppress(["UnreferencedShape"])
+list NonEmptyStringList {
+    member: String
+}
+
+/// Concrete vendor params to apply to AWS services by default.
+@suppress(["UnreferencedShape"])
+structure AwsVendorParams with [BaseAwsVendorParams] {}
+
+/// Vendor params for S3.
+@suppress(["UnreferencedShape"])
+structure S3VendorParams with [BaseAwsVendorParams] {
+    /// Whether to resolve an accelerate endpoint or not.
+    useAccelerate: Boolean = false
+
+    /// Whether to use the global endpoint for us-east-1.
+    useGlobalEndpoint: Boolean = false
+
+    /// Whether to force path-style addressing.
+    forcePathStyle: Boolean = false
+
+    /// Whether to use the region in the bucket arn to override the set
+    /// region.
+    useArnRegion: Boolean = true
+
+    /// Whether to use S3's multi-region access points.
+    useMultiRegionAccessPoints: Boolean = true
+}
diff --git a/smithy-aws-smoke-test-model/src/main/resources/META-INF/smithy/manifest b/smithy-aws-smoke-test-model/src/main/resources/META-INF/smithy/manifest
new file mode 100644
index 000000000..7d0b1ba0b
--- /dev/null
+++ b/smithy-aws-smoke-test-model/src/main/resources/META-INF/smithy/manifest
@@ -0,0 +1,1 @@
+aws.test.smithy
diff --git a/smithy-aws-smoke-test-model/src/test/java/software/amazon/smithy/aws/smoketests/model/AwsSmokeTestModelTest.java b/smithy-aws-smoke-test-model/src/test/java/software/amazon/smithy/aws/smoketests/model/AwsSmokeTestModelTest.java
new file mode 100644
index 000000000..fba8d8ff6
--- /dev/null
+++ b/smithy-aws-smoke-test-model/src/test/java/software/amazon/smithy/aws/smoketests/model/AwsSmokeTestModelTest.java
@@ -0,0 +1,169 @@
+package software.amazon.smithy.aws.smoketests.model;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.smoketests.traits.Expectation;
+import software.amazon.smithy.smoketests.traits.SmokeTestCase;
+import software.amazon.smithy.smoketests.traits.SmokeTestsTrait;
+
+public class AwsSmokeTestModelTest {
+    @Test
+    public void determinesIfTestCaseHasAwsVendorParams() {
+        SmokeTestCase withAwsVendorParamsShape = SmokeTestCase.builder()
+                .id("foo")
+                .expectation(Expectation.success())
+                .vendorParamsShape(AwsVendorParams.ID)
+                .build();
+        SmokeTestCase withNoVendorParamsShape = withAwsVendorParamsShape.toBuilder()
+                .vendorParamsShape(null)
+                .build();
+        SmokeTestCase withOtherVendorParamsShape = withNoVendorParamsShape.toBuilder()
+                .vendorParamsShape(S3VendorParams.ID)
+                .build();
+
+        assertThat(AwsSmokeTestModel.hasAwsVendorParams(withAwsVendorParamsShape), is(true));
+        assertThat(AwsSmokeTestModel.hasAwsVendorParams(withNoVendorParamsShape), is(false));
+        assertThat(AwsSmokeTestModel.hasAwsVendorParams(withOtherVendorParamsShape), is(false));
+    }
+
+    @Test
+    public void determinesIfTestCaseHasS3VendorParams() {
+        SmokeTestCase withS3VendorParamsShape = SmokeTestCase.builder()
+                .id("foo")
+                .expectation(Expectation.success())
+                .vendorParamsShape(S3VendorParams.ID)
+                .build();
+        SmokeTestCase withNoVendorParamsShape = withS3VendorParamsShape.toBuilder()
+                .vendorParamsShape(null)
+                .build();
+        SmokeTestCase withOtherVendorParamsShape = withNoVendorParamsShape.toBuilder()
+                .vendorParamsShape(AwsVendorParams.ID)
+                .build();
+
+        assertThat(AwsSmokeTestModel.hasS3VendorParams(withS3VendorParamsShape), is(true));
+        assertThat(AwsSmokeTestModel.hasS3VendorParams(withNoVendorParamsShape), is(false));
+        assertThat(AwsSmokeTestModel.hasS3VendorParams(withOtherVendorParamsShape), is(false));
+    }
+
+    @Test
+    public void canGetAwsVendorParamsFromTestCase() {
+        SmokeTestCase withoutVendorParams = SmokeTestCase.builder()
+                .id("foo")
+                .expectation(Expectation.success())
+                .vendorParamsShape(AwsVendorParams.ID)
+                .build();
+        SmokeTestCase withNoVendorParamsShape = withoutVendorParams.toBuilder()
+                .vendorParamsShape(null)
+                .build();
+        SmokeTestCase withOtherVendorParamsShape = withNoVendorParamsShape.toBuilder()
+                .vendorParamsShape(S3VendorParams.ID)
+                .build();
+
+        assertThat(AwsSmokeTestModel.getAwsVendorParams(withoutVendorParams), not(equalTo(Optional.empty())));
+        assertThat(AwsSmokeTestModel.getAwsVendorParams(withNoVendorParamsShape), equalTo(Optional.empty()));
+        assertThat(AwsSmokeTestModel.getAwsVendorParams(withOtherVendorParamsShape), equalTo(Optional.empty()));
+    }
+
+    @Test
+    public void fillsInDefaultsForAwsVendorParams() {
+        AwsVendorParams vendorParams = AwsSmokeTestModel.getAwsVendorParams(SmokeTestCase.builder()
+                .id("foo")
+                .expectation(Expectation.success())
+                .vendorParamsShape(AwsVendorParams.ID)
+                .build()).get();
+
+        assertThat(vendorParams.getRegion(), equalTo("us-west-2"));
+        assertThat(vendorParams.getSigv4aRegionSet(), equalTo(Optional.empty()));
+        assertThat(vendorParams.getUri(), equalTo(Optional.empty()));
+        assertThat(vendorParams.useFips(), is(false));
+        assertThat(vendorParams.useDualstack(), is(false));
+        assertThat(vendorParams.useAccountIdRouting(), is(true));
+    }
+
+    @Test
+    public void fillsInDefaultsForS3VendorParams() {
+        S3VendorParams vendorParams = AwsSmokeTestModel.getS3VendorParams(SmokeTestCase.builder()
+                .id("foo")
+                .expectation(Expectation.success())
+                .vendorParamsShape(S3VendorParams.ID)
+                .build()).get();
+
+        assertThat(vendorParams.getRegion(), equalTo("us-west-2"));
+        assertThat(vendorParams.getSigv4aRegionSet(), equalTo(Optional.empty()));
+        assertThat(vendorParams.getUri(), equalTo(Optional.empty()));
+        assertThat(vendorParams.useFips(), is(false));
+        assertThat(vendorParams.useDualstack(), is(false));
+        assertThat(vendorParams.useAccountIdRouting(), is(true));
+
+        assertThat(vendorParams.useAccelerate(), is(false));
+        assertThat(vendorParams.useGlobalEndpoint(), is(false));
+        assertThat(vendorParams.forcePathStyle(), is(false));
+        assertThat(vendorParams.useArnRegion(), is(true));
+        assertThat(vendorParams.useMultiRegionAccessPoints(), is(true));
+    }
+
+    @Test
+    public void loadsAwsVendorParamsFromModel() {
+        Model model = Model.assembler()
+                .discoverModels()
+                .addImport(getClass().getResource("vendor-params.smithy"))
+                .assemble()
+                .unwrap();
+        SmokeTestsTrait trait = model.expectShape(ShapeId.from("com.foo#GetFoo")).expectTrait(SmokeTestsTrait.class);
+        SmokeTestCase awsCase = trait.getTestCases().stream()
+                .filter(testCase -> testCase.getId().equals("AwsVendorParamsCase"))
+                .findAny()
+                .get();
+
+        assertThat(AwsSmokeTestModel.hasAwsVendorParams(awsCase), is(true));
+        assertThat(AwsSmokeTestModel.getAwsVendorParams(awsCase), not(equalTo(Optional.empty())));
+        AwsVendorParams vendorParams = AwsSmokeTestModel.getAwsVendorParams(awsCase).get();
+        assertThat(vendorParams.getRegion(), equalTo("us-east-1"));
+        assertThat(vendorParams.getSigv4aRegionSet(), not(equalTo(Optional.empty())));
+        assertThat(vendorParams.getSigv4aRegionSet().get(), containsInAnyOrder("us-east-1"));
+        assertThat(vendorParams.getUri(), equalTo(Optional.of("foo")));
+        assertThat(vendorParams.useFips(), is(true));
+        assertThat(vendorParams.useDualstack(), is(true));
+        assertThat(vendorParams.useAccountIdRouting(), is(false));
+    }
+
+    @Test
+    public void loadsS3VendorParamsFromModel() {
+        Model model = Model.assembler()
+                .discoverModels()
+                .addImport(getClass().getResource("vendor-params.smithy"))
+                .assemble()
+                .unwrap();
+        SmokeTestCase s3Case = model.expectShape(ShapeId.from("com.foo#GetFoo"))
+                .expectTrait(SmokeTestsTrait.class)
+                .getTestCases()
+                .stream()
+                .filter(testCase -> testCase.getId().equals("S3VendorParamsCase"))
+                .findAny()
+                .get();
+
+        assertThat(AwsSmokeTestModel.hasS3VendorParams(s3Case), is(true));
+        assertThat(AwsSmokeTestModel.getS3VendorParams(s3Case), not(equalTo(Optional.empty())));
+        S3VendorParams vendorParams = AwsSmokeTestModel.getS3VendorParams(s3Case).get();
+        assertThat(vendorParams.getRegion(), equalTo("us-east-2"));
+        assertThat(vendorParams.getSigv4aRegionSet(), not(equalTo(Optional.empty())));
+        assertThat(vendorParams.getSigv4aRegionSet().get(), containsInAnyOrder("us-east-2"));
+        assertThat(vendorParams.getUri(), equalTo(Optional.of("bar")));
+        assertThat(vendorParams.useFips(), is(true));
+        assertThat(vendorParams.useDualstack(), is(true));
+        assertThat(vendorParams.useAccountIdRouting(), is(false));
+        assertThat(vendorParams.useAccelerate(), is(true));
+        assertThat(vendorParams.useGlobalEndpoint(), is(true));
+        assertThat(vendorParams.forcePathStyle(), is(true));
+        assertThat(vendorParams.useArnRegion(), is(false));
+        assertThat(vendorParams.useMultiRegionAccessPoints(), is(false));
+    }
+}
diff --git a/smithy-aws-smoke-test-model/src/test/resources/software/amazon/smithy/aws/smoketests/model/vendor-params.smithy b/smithy-aws-smoke-test-model/src/test/resources/software/amazon/smithy/aws/smoketests/model/vendor-params.smithy
new file mode 100644
index 000000000..2d3d7b786
--- /dev/null
+++ b/smithy-aws-smoke-test-model/src/test/resources/software/amazon/smithy/aws/smoketests/model/vendor-params.smithy
@@ -0,0 +1,48 @@
+$version: "2.0"
+
+namespace com.foo
+
+use smithy.test#smokeTests
+use aws.test#AwsVendorParams
+use aws.test#S3VendorParams
+
+@smokeTests([
+    {
+        id: "AwsVendorParamsCase"
+        params: {}
+        vendorParams: {
+            region: "us-east-1"
+            sigv4aRegionSet: ["us-east-1"]
+            uri: "foo"
+            useFips: true
+            useDualstack: true
+            useAccountIdRouting: false
+        }
+        vendorParamsShape: AwsVendorParams
+        expect: {
+            success: {}
+        }
+    }
+    {
+        id: "S3VendorParamsCase"
+        params: {}
+        vendorParams: {
+            region: "us-east-2"
+            sigv4aRegionSet: ["us-east-2"]
+            uri: "bar"
+            useFips: true
+            useDualstack: true
+            useAccountIdRouting: false
+            useAccelerate: true
+            useGlobalEndpoint: true
+            forcePathStyle: true
+            useArnRegion: false
+            useMultiRegionAccessPoints: false
+        }
+        vendorParamsShape: S3VendorParams
+        expect: {
+            success: {}
+        }
+    }
+])
+operation GetFoo {}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enums/enum-with-duplicates.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enums/enum-with-duplicates.smithy
new file mode 100644
index 000000000..950969b08
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enums/enum-with-duplicates.smithy
@@ -0,0 +1,9 @@
+// smithy.example#Foo: Syntax error at line 8, column 5: Found conflicting member name, `BAR`
+$version: "2.0"
+
+namespace smithy.example
+
+enum Foo {
+    BAR = "bar"
+    BAR = "bar"
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enums/intEnum-with-duplicates.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enums/intEnum-with-duplicates.smithy
new file mode 100644
index 000000000..178d28e94
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enums/intEnum-with-duplicates.smithy
@@ -0,0 +1,9 @@
+// smithy.example#Foo: Syntax error at line 8, column 5: Found conflicting member name, `BAR`
+$version: "2.0"
+
+namespace smithy.example
+
+intEnum Foo {
+    BAR = 10
+    BAR = 10
+}
diff --git a/smithy-syntax/src/test/java/software/amazon/smithy/syntax/ParseAndFormatTest.java b/smithy-syntax/src/test/java/software/amazon/smithy/syntax/ParseAndFormatTest.java
index e97c6dc60..3eb26da41 100644
--- a/smithy-syntax/src/test/java/software/amazon/smithy/syntax/ParseAndFormatTest.java
+++ b/smithy-syntax/src/test/java/software/amazon/smithy/syntax/ParseAndFormatTest.java
@@ -14,6 +14,7 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.loader.IdlTokenizer;
+import software.amazon.smithy.model.loader.ModelAssembler;
 import software.amazon.smithy.utils.IoUtils;
 
 // A parameterized test that finds models in corpus, parses them, skipping files that end with ".formatted.smithy".
@@ -32,9 +33,19 @@ public class ParseAndFormatTest {
         }
 
         // Ensure that the tests can be parsed by smithy-model too.
-        Model.assembler().addImport(filename).disableValidation().assemble().unwrap();
+        Model.assembler()
+                .addImport(filename)
+                .putProperty(ModelAssembler.ALLOW_UNKNOWN_TRAITS, true)
+                .disableValidation()
+                .assemble()
+                .unwrap();
         if (!formattedFile.equals(filename)) {
-            Model.assembler().addImport(formattedFile).disableValidation().assemble().unwrap();
+            Model.assembler()
+                    .addImport(formattedFile)
+                    .putProperty(ModelAssembler.ALLOW_UNKNOWN_TRAITS, true)
+                    .disableValidation()
+                    .assemble()
+                    .unwrap();
         }
 
         String model = IoUtils.readUtf8File(filename);
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/array-of-objects.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/array-of-objects.formatted.smithy
new file mode 100644
index 000000000..2a32fbdb5
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/array-of-objects.formatted.smithy
@@ -0,0 +1,15 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@examples([
+    {
+        title: "Foo"
+        documentation: "Foo"
+    }
+    {
+        title: "Bar"
+        documentation: "Bar"
+    }
+])
+operation Foo {}
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/array-of-objects.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/array-of-objects.smithy
new file mode 100644
index 000000000..768b31d0b
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/array-of-objects.smithy
@@ -0,0 +1,17 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@examples(
+    [
+        {
+            title: "Foo"
+            documentation: "Foo"
+        }
+        {
+            title: "Bar"
+            documentation: "Bar"
+        }
+    ]
+)
+operation Foo {}
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/brackets.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/brackets.formatted.smithy
new file mode 100644
index 000000000..4f79946e1
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/brackets.formatted.smithy
@@ -0,0 +1,192 @@
+$version: "2.0"
+
+// Single line
+metadata a = { a: "b" }
+
+// Multiline
+metadata b = {
+    a: "foobarbazbux"
+    b: "foobarbazbux"
+    c: "foobarbazbux"
+    d: "foobarbazbux"
+    e: "foobarbazbux"
+    f: "foobarbazbux"
+}
+
+// Leading and trailing WS
+metadata c = {
+    // Foo
+    a: "b"
+}
+
+metadata d = {
+    a: "b"
+    // Foo
+}
+
+// Parent is array
+metadata e = [
+    {
+        foo: "bar"
+    }
+]
+
+// Line breaking values
+metadata f = [
+    """
+    foo
+    """
+]
+
+metadata g = [
+    "abc
+def"
+]
+
+metadata h = {
+    a: [
+        {
+            foo: "bar"
+        }
+    ]
+}
+
+namespace smithy.example
+
+// TraitStructure
+// Single line
+@externalDocumentation(a: "http://foo.com", c: "http://foo.com")
+structure A {}
+
+// Multi-line
+@externalDocumentation(
+    a: "http://foo.com"
+    b: "http://foo.com"
+    c: "http://foo.com"
+    d: "http://foo.com"
+    e: "http://foo.com"
+    f: "http://foo.com"
+    g: "http://foo.com"
+    h: "http://foo.com"
+    i: "http://foo.com"
+    j: "http://foo.com"
+)
+structure B {}
+
+// Leading WS
+@externalDocumentation(
+    // Foo
+    a: "http://foo.com"
+)
+structure C {}
+
+// Trailing WS
+@externalDocumentation(
+    a: "http://foo.com"
+    // Foo
+)
+structure D {}
+
+// Object values
+@foo(
+    a: { bar: "baz" }
+)
+structure E {}
+
+// Array values
+@foo(
+    a: ["bar", "baz"]
+)
+structure F {}
+
+// TraitNode
+// Single line
+@foo(["foo", "bar"])
+structure G {}
+
+@foo({ foo: "bar", baz: "foo" })
+structure H {}
+
+// Multiline
+@foo([
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+])
+structure I {}
+
+@foo({
+    a: "foobarbaz"
+    b: "foobarbaz"
+    c: "foobarbaz"
+    d: "foobarbaz"
+    e: "foobarbaz"
+    f: "foobarbaz"
+    g: "foobarbaz"
+    h: "foobarbaz"
+    i: "foobarbaz"
+})
+structure J {}
+
+// Leading WS
+@foo(
+    // Foo
+    ["a"]
+)
+structure K {}
+
+// Trailing WS
+@foo(
+    ["a"]
+    // Foo
+)
+structure L {}
+
+// Line breaking NodeValues
+@foo(
+    """
+    foo
+    """
+)
+structure M {}
+
+@foo(
+    "a
+bc"
+)
+structure N {}
+
+// Nested comments
+@foo([
+    "abc"
+    // Foo
+    "def"
+    // Bar
+])
+structure O {}
+
+// Nested objects
+@foo([
+    {}
+])
+structure P {}
+
+@foo([
+    {
+        foo: "bar"
+    }
+])
+structure Q {}
+
+@foo([
+    [
+        []
+    ]
+])
+structure R {}
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/brackets.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/brackets.smithy
new file mode 100644
index 000000000..ec25f1878
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/brackets.smithy
@@ -0,0 +1,184 @@
+$version: "2.0"
+
+// Single line
+metadata a = { a: "b" }
+
+// Multiline
+metadata b = {
+    a: "foobarbazbux"
+    b: "foobarbazbux"
+    c: "foobarbazbux"
+    d: "foobarbazbux"
+    e: "foobarbazbux"
+    f: "foobarbazbux"
+}
+
+// Leading and trailing WS
+metadata c = {
+    // Foo
+    a: "b"
+}
+
+metadata d = {
+    a: "b" // Foo
+}
+
+// Parent is array
+metadata e = [
+    {
+        foo: "bar"
+    }
+]
+
+// Line breaking values
+metadata f = [
+    """
+    foo
+    """
+]
+
+metadata g = [
+    "abc
+def"
+]
+
+metadata h = {
+    a: [
+        {
+            foo: "bar"
+        }
+    ]
+
+}
+
+namespace smithy.example
+
+// TraitStructure
+// Single line
+@externalDocumentation(a: "http://foo.com", c: "http://foo.com")
+structure A {}
+
+// Multi-line
+@externalDocumentation(
+    a: "http://foo.com"
+    b: "http://foo.com"
+    c: "http://foo.com"
+    d: "http://foo.com"
+    e: "http://foo.com"
+    f: "http://foo.com"
+    g: "http://foo.com"
+    h: "http://foo.com"
+    i: "http://foo.com"
+    j: "http://foo.com"
+)
+structure B {}
+
+// Leading WS
+@externalDocumentation(
+    // Foo
+    a: "http://foo.com"
+)
+structure C {}
+
+// Trailing WS
+@externalDocumentation(
+    a: "http://foo.com" // Foo
+)
+structure D {}
+
+// Object values
+@foo(
+    a: { bar: "baz" }
+)
+structure E {}
+
+// Array values
+@foo(
+    a: ["bar", "baz"]
+)
+structure F {}
+
+// TraitNode
+// Single line
+@foo(["foo", "bar"])
+structure G {}
+
+@foo({ foo: "bar", baz: "foo" })
+structure H {}
+
+// Multiline
+@foo([
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+    "foobarbaz"
+])
+structure I {}
+
+@foo({
+    a: "foobarbaz"
+    b: "foobarbaz"
+    c: "foobarbaz"
+    d: "foobarbaz"
+    e: "foobarbaz"
+    f: "foobarbaz"
+    g: "foobarbaz"
+    h: "foobarbaz"
+    i: "foobarbaz"
+})
+structure J {}
+
+// Leading WS
+@foo(
+    // Foo
+    [
+        "a"
+    ]
+)
+structure K {}
+
+// Trailing WS
+@foo(
+    [
+        "a"
+    ] // Foo
+)
+structure L {}
+
+// Line breaking NodeValues
+@foo(
+    """
+    foo
+    """
+)
+structure M {}
+
+@foo(
+    "a
+bc"
+)
+structure N {}
+
+// Nested comments
+@foo([
+    "abc" // Foo
+    "def" // Bar
+])
+structure O {}
+
+// Nested objects
+@foo([{}])
+structure P {}
+
+@foo([{
+    foo: "bar"
+      }])
+structure Q {}
+
+@foo([[[]]])
+structure R {}
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/entity-shapes-line-break.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/entity-shapes-line-break.formatted.smithy
index f4aac3766..76a25c8f7 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/entity-shapes-line-break.formatted.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/entity-shapes-line-break.formatted.smithy
@@ -30,7 +30,7 @@ operation GetTime2 {
 
 @http(method: "X", uri: "/foo", code: 200)
 resource Sprocket2 {
-    identifiers: {username: String, id: String, otherId: String}
+    identifiers: { username: String, id: String, otherId: String }
 }
 
 @error("client")
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/entity-shapes.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/entity-shapes.smithy
index 585e574ef..ce49d2b3a 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/entity-shapes.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/entity-shapes.smithy
@@ -33,12 +33,12 @@ operation GetTime2 {
 }
 
 resource Sprocket1 {
-    identifiers: {username: String}
+    identifiers: { username: String }
 }
 
 @http(method: "X", uri: "/foo", code: 200)
 resource Sprocket2 {
-    identifiers: {username: String, id: String, otherId: String}
+    identifiers: { username: String, id: String, otherId: String }
     collectionOperations: [
         SomeOperation
     ]
@@ -54,7 +54,7 @@ structure SomeOperationFoo {}
 
 @http(method: "X", uri: "/foo3", code: 200)
 resource Sprocket3 {
-    identifiers: {username: String, id: String, otherId: String}
+    identifiers: { username: String, id: String, otherId: String }
     // It's empty, so on a single line.
     collectionOperations: []
 }
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/nested-arrays-and-objects.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/nested-arrays-and-objects.formatted.smithy
new file mode 100644
index 000000000..5c55a0567
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/nested-arrays-and-objects.formatted.smithy
@@ -0,0 +1,24 @@
+$version: "2.0"
+
+metadata foo = {
+    bar: ["baz", "foo", "bar", "baz"]
+    baz: {
+        foo: ["bar"]
+    }
+}
+
+metadata bar = [
+    {
+        foo: [
+            {
+                bar: ["baz", "foo"]
+                baz: ["foo", "bar"]
+            }
+            {
+                foo: "bar"
+            }
+        ]
+    }
+]
+
+namespace smithy.example
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/nested-arrays-and-objects.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/nested-arrays-and-objects.smithy
new file mode 100644
index 000000000..405c2933f
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/nested-arrays-and-objects.smithy
@@ -0,0 +1,7 @@
+$version: "2.0"
+
+metadata foo = {bar: ["baz", "foo", "bar", "baz"], baz: {foo: ["bar"]}}
+
+metadata bar = [{foo: [{bar: ["baz", "foo"], baz: ["foo", "bar"]}, {foo: "bar"}]}]
+
+namespace smithy.example
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/section-statement-blank-lines.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/section-statement-blank-lines.formatted.smithy
index ff3b73ab8..a5bf0f95a 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/section-statement-blank-lines.formatted.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/section-statement-blank-lines.formatted.smithy
@@ -2,7 +2,7 @@
 // newline.
 $version: "2.0"
 $foo: 100
-$baz: {abc: 123}
+$baz: { abc: 123 }
 
 $bar: {
     aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1"
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/single-line-object.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/single-line-object.formatted.smithy
new file mode 100644
index 000000000..01e6f7860
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/single-line-object.formatted.smithy
@@ -0,0 +1,9 @@
+$version: "2.0"
+
+metadata foo = [
+    {
+        bar: { baz: "2" }
+    }
+]
+
+namespace smithy.example
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/single-line-object.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/single-line-object.smithy
new file mode 100644
index 000000000..7bd08106e
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/single-line-object.smithy
@@ -0,0 +1,9 @@
+$version: "2.0"
+
+metadata foo = [
+    {
+        bar: {baz: "2"}
+    }
+]
+
+namespace smithy.example
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
index 8fa2be8e6..899acc3ee 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
@@ -24,3 +24,11 @@ string Bar
     """
 )
 string Baz
+
+@documentation(
+    """
+    This is the documentation for Bux.
+    Lorem ipsum dolor.
+    """
+)
+string Bux
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
index 99efba0d8..834b3579b 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
@@ -24,3 +24,9 @@ Lorem ipsum dolor.
 """
 )
 string Baz
+
+@documentation("""
+    This is the documentation for Bux.
+    Lorem ipsum dolor.
+""")
+string Bux
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/traits-lots-of-tags.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/traits-lots-of-tags.formatted.smithy
new file mode 100644
index 000000000..29b9d4ded
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/traits-lots-of-tags.formatted.smithy
@@ -0,0 +1,20 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@tags([
+    "abcdefghijklmnopqrstuvwxyz"
+    "abcdefghijklmnopqrstuvwxyz"
+    "abcdefghijklmnopqrstuvwxyz"
+    "abcdefghijklmnopqrstuvwxyz"
+    "abcdefghijklmnopqrstuvwxyz"
+    "abcdefghijklmnopqrstuvwxyz"
+    "abcdefghijklmnopqrstuvwxyz"
+    "abcdefghijklmnopqrstuvwxyz"
+    "abcdefghijklmnopqrstuvwxyz"
+    "abcdefghijklmnopqrstuvwxyz"
+    "abcdefghijklmnopqrstuvwxyz"
+    "abcdefghijklmnopqrstuvwxyz"
+    "abcdefghijklmnopqrstuvwxyz"
+])
+string Foo