Repository: smithy-lang/smithy
Pull request #2418 (2024-10-10 05:58:10)

Add validator for identifiers missing references
This commit introduces a new validator that emits warnings when it detects a member that matches a resource identifier without the relevant `@references` trait configuration.

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/.gitignore b/.gitignore
index ed1c10be1..f39ff7525 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,9 @@ docs/src
 *.iml
 *.iws
 
+# Fleet
+.fleet/
+
 # VSCode
 bin/
 
diff --git a/docs/source-2.0/additional-specs/protocols/smithy-rpc-v2.rst b/docs/source-2.0/additional-specs/protocols/smithy-rpc-v2.rst
index f4abe4669..315589b40 100644
--- a/docs/source-2.0/additional-specs/protocols/smithy-rpc-v2.rst
+++ b/docs/source-2.0/additional-specs/protocols/smithy-rpc-v2.rst
@@ -214,10 +214,16 @@ Buffered RPC requests for the ``rpcv2Cbor`` protocol SHOULD include a
 ``Content-Length`` header. Event streaming requests MUST NOT specify a content
 length (instead using ``Transfer-Encoding: chunked`` on HTTP/1.1).
 
-Requests with event stream responses for the ``rpcv2Cbor`` protocol MUST
-include an ``Accept`` header set to the value
-``application/vnd.amazon.eventstream``. Other forms of content streaming MAY be
-added in the future, utilizing different values for ``Accept``.
+Requests for the ``rpcv2Cbor`` protocol MUST use the following behavior for
+setting an ``Accept`` header:
+
+* For requests with event streaming responses: the value of the ``Accept``
+    header MUST be ``application/vnd.amazon.eventstream``.
+* For requests with all other response types: the value of the ``Accept``
+    header MUST be ``application/cbor``.
+
+Other forms of content streaming MAY be added in the future, utilizing
+different values for ``Accept``.
 
 In summary, the ``rpcv2Cbor`` protocol defines behavior for the following
 headers for requests:
@@ -241,9 +247,9 @@ headers for requests:
       - The standard ``Content-Length`` header defined by :rfc:`9110#section-8.6`.
         For event streaming requests, this MUST NOT be set.
     * - ``Accept``
-      - Conditional
-      - For requests with event streaming responses, this is
-        ``application/vnd.amazon.eventstream``.
+      - Required
+      - The value of ``application/cbor``. For requests with event streaming
+        responses, this is ``application/vnd.amazon.eventstream``.
 
 
 ~~~~~~~~~
diff --git a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/HttpChecksumTrait.java b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/HttpChecksumTrait.java
index 50fdf5bbb..679792d5c 100644
--- a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/HttpChecksumTrait.java
+++ b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/HttpChecksumTrait.java
@@ -38,6 +38,7 @@ import software.amazon.smithy.utils.ToSmithyBuilder;
 public final class HttpChecksumTrait extends AbstractTrait implements ToSmithyBuilder<HttpChecksumTrait> {
     public static final ShapeId ID = ShapeId.from("aws.protocols#httpChecksum");
     public static final String CHECKSUM_PREFIX = "x-amz-checksum-";
+    // This list should be in sync with the trait definition in `aws.protocols.smithy`.
     public static final List<String> CHECKSUM_ALGORITHMS = ListUtils.of("CRC64NVME", "CRC32C", "CRC32",
             "SHA1", "SHA256");
     public static final List<String> VALIDATION_MODES = ListUtils.of("ENABLED");
diff --git a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.smithy b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.smithy
index d4a7be98f..16fbbfb2b 100644
--- a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.smithy
+++ b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.smithy
@@ -253,8 +253,12 @@ list ChecksumAlgorithmSet {
     member: ChecksumAlgorithm
 }
 
+// This enum should be in sync with the `HttpChecksumTrait` list.
 @private
 enum ChecksumAlgorithm {
+    /// CRC64NVME
+    CRC64NVME
+
     /// CRC32C
     CRC32C
 
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/MemberShouldReferenceResourceValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/MemberShouldReferenceResourceValidator.java
new file mode 100644
index 000000000..e70449bdf
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/MemberShouldReferenceResourceValidator.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.validation.validators;
+
+import static java.lang.String.format;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.selector.PathFinder;
+import software.amazon.smithy.model.shapes.MemberShape;
+import software.amazon.smithy.model.shapes.ResourceShape;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.traits.ReferencesTrait;
+import software.amazon.smithy.model.validation.AbstractValidator;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidationUtils;
+import software.amazon.smithy.utils.ListUtils;
+
+/**
+ * Validates if a member matches a resource identifier without the
+ * proper configuration of a `@references` trait.
+ */
+public final class MemberShouldReferenceResourceValidator extends AbstractValidator {
+    @Override
+    public List<ValidationEvent> validate(Model model) {
+        // There are usually far fewer resources than members, precompute the identifiers
+        // so various short circuits can be added.
+        Set<String> identifierNames = getAllIdentifierNames(model);
+        // Short circuit validating all the members if we don't have any resources to test.
+        if (identifierNames.isEmpty()) {
+            return ListUtils.of();
+        }
+
+        // Check every member to see if it's a potential reference.
+        List<ValidationEvent> events = new ArrayList<>();
+        for (MemberShape member : model.getMemberShapes()) {
+            // Only the known identifier names can match for this, skip names that we don't know.
+            if (!identifierNames.contains(member.getMemberName())) {
+                continue;
+            }
+            // Only strings can be identifiers, so skip non-String targets.
+            if (!model.expectShape(member.getTarget()).isStringShape()) {
+                continue;
+            }
+
+            Set<ShapeId> potentialReferences = computePotentialReferences(model, member);
+            if (!potentialReferences.isEmpty()) {
+                events.add(warning(member, format("This member appears to reference the following resources without "
+                        + "being included in a `@references` trait: [%s]",
+                        ValidationUtils.tickedList(potentialReferences))));
+            }
+        }
+
+        return events;
+    }
+
+    private Set<String> getAllIdentifierNames(Model model) {
+        Set<String> identifierNames = new HashSet<>();
+        for (ResourceShape resource : model.getResourceShapes()) {
+            identifierNames.addAll(resource.getIdentifiers().keySet());
+        }
+        return identifierNames;
+    }
+
+    private Set<ShapeId> computePotentialReferences(Model model, MemberShape member) {
+        // Exclude any resources already in `@references` on the member or container structure.
+        Set<ShapeId> resourcesToIgnore = new HashSet<>();
+        ignoreReferencedResources(member, resourcesToIgnore);
+        ignoreReferencedResources(model.expectShape(member.getContainer()), resourcesToIgnore);
+
+        // Check each resource in the model for something missed.
+        Set<ShapeId> potentialResources = new HashSet<>();
+        for (ResourceShape resource : model.getResourceShapes()) {
+            // We'll want to ignore some resources based on the member -> resource path.
+            computeResourcesToIgnore(model, member, resource, resourcesToIgnore);
+
+            // Exclude members bound to resource hierarchies from generating events,
+            // including for resources that are within the same hierarchy.
+            if (resourcesToIgnore.contains(resource.getId())) {
+                continue;
+            }
+
+            // This member matches the identifier for the resource we're checking, add it to a list.
+            if (isIdentifierMatch(resource, member)) {
+                potentialResources.add(resource.getId());
+            }
+        }
+
+        // Clean up any resources added through other paths that should be ignored.
+        potentialResources.removeAll(resourcesToIgnore);
+        return potentialResources;
+    }
+
+    private void computeResourcesToIgnore(Model model, MemberShape member, ResourceShape resource,
+            Set<ShapeId> resourcesToIgnore) {
+        // Exclude actually bound members via searching with a PathFinder.
+        List<PathFinder.Path> resourceMemberPaths = PathFinder.create(model)
+                .search(resource, ListUtils.of(member));
+        if (!resourceMemberPaths.isEmpty()) {
+            // This member is already bound to a resource, so we don't need a references trait for it.
+            // In addition, we should not tell users to add a references trait for other resources that
+            // are children in that hierarchy - any parent resources or other children of those parents.
+            for (PathFinder.Path path : resourceMemberPaths) {
+                for (Shape pathShape : path.getShapes()) {
+                    if (pathShape.isResourceShape()) {
+                        ResourceShape resourceShape = (ResourceShape) pathShape;
+                        resourcesToIgnore.add(resourceShape.getId());
+                        resourcesToIgnore.addAll(resourceShape.getResources());
+                    }
+                }
+            }
+        }
+    }
+
+    private void ignoreReferencedResources(Shape shape, Set<ShapeId> resourcesToIgnore) {
+        if (shape.hasTrait(ReferencesTrait.class)) {
+            for (ReferencesTrait.Reference reference : shape.expectTrait(ReferencesTrait.class)
+                    .getReferences()) {
+                resourcesToIgnore.add(reference.getResource());
+            }
+        }
+    }
+
+    private boolean isIdentifierMatch(ResourceShape resource, MemberShape member) {
+        Map<String, ShapeId> identifiers = resource.getIdentifiers();
+        return identifiers.containsKey(member.getMemberName())
+                && identifiers.get(member.getMemberName()).equals(member.getTarget());
+    }
+}
diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
index b3072519f..a43408752 100644
--- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
+++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
@@ -26,6 +26,7 @@ software.amazon.smithy.model.validation.validators.IdempotencyTokenIgnoredValida
 software.amazon.smithy.model.validation.validators.JsonNameValidator
 software.amazon.smithy.model.validation.validators.LengthTraitValidator
 software.amazon.smithy.model.validation.validators.MediaTypeValidator
+software.amazon.smithy.model.validation.validators.MemberShouldReferenceResourceValidator
 software.amazon.smithy.model.validation.validators.NoInlineDocumentSupportValidator
 software.amazon.smithy.model.validation.validators.OperationValidator
 software.amazon.smithy.model.validation.validators.PaginatedTraitValidator
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java
index 6529039b0..a9dee31e3 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java
@@ -272,14 +272,14 @@ final class BuilderGenerator implements Runnable {
 
             // Clear all
             writer.openBlock("public Builder clear$U() {", "}", memberName, () -> {
-                        writer.write("$L.get().clear();", memberName);
+                        writer.write("this.$L.get().clear();", memberName);
                         writer.writeWithNoFormatting("return this;");
                     }).newLine();
 
             // Set one
             writer.openBlock("public Builder add$U($T value) {", "}",
                     memberName, symbolProvider.toSymbol(shape.getMember()), () -> {
-                        writer.write("$L.get().add(value);", memberName);
+                        writer.write("this.$L.get().add(value);", memberName);
                         writer.write("return this;");
                     }).newLine();
 
@@ -287,7 +287,7 @@ final class BuilderGenerator implements Runnable {
             writer.openBlock("public Builder remove$U($T value) {", "}",
                     memberName, symbolProvider.toSymbol(shape.getMember()),
                     () -> {
-                        writer.write("$L.get().remove(value);", memberName);
+                        writer.write("this.$L.get().remove(value);", memberName);
                         writer.write("return this;");
                     }).newLine();
             return null;
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java
index 5920c7ba1..475884629 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java
@@ -36,6 +36,7 @@ import software.amazon.smithy.model.shapes.StructureShape;
 import software.amazon.smithy.model.shapes.TimestampShape;
 import software.amazon.smithy.model.shapes.UnionShape;
 import software.amazon.smithy.model.traits.TimestampFormatTrait;
+import software.amazon.smithy.traitcodegen.SymbolProperties;
 import software.amazon.smithy.traitcodegen.TraitCodegenUtils;
 import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
 import software.amazon.smithy.utils.StringUtils;
@@ -106,6 +107,34 @@ final class FromNodeGenerator extends TraitVisitor<Void> implements Runnable {
         return null;
     }
 
+    @Override
+    public Void enumShape(EnumShape shape) {
+        // Enum traits do not need this method, only nested enums.
+        if (symbol.getProperty(SymbolProperties.BASE_SYMBOL).isPresent())  {
+            return null;
+        }
+        writeFromNodeJavaDoc();
+        writer.openBlock("public static $T fromNode($T node) {", "}", symbol, Node.class, () -> {
+            writer.write("return from(node.expectStringNode().getValue());");
+        });
+        writer.newLine();
+        return null;
+    }
+
+    @Override
+    public Void intEnumShape(IntEnumShape shape) {
+        // Enum traits do not need this method, only nested enums.
+        if (symbol.getProperty(SymbolProperties.BASE_SYMBOL).isPresent())  {
+            return null;
+        }
+        writeFromNodeJavaDoc();
+        writer.openBlock("public static $T fromNode($T node) {", "}", symbol, Node.class, () -> {
+            writer.writeWithNoFormatting("return from(node.expectNumberNode().getValue().intValue());");
+        });
+        writer.newLine();
+        return null;
+    }
+
     @Override
     protected Void numberShape(NumberShape shape) {
         // Number shapes do not create a from node method


Test patch:
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/references/missing-member-references.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/references/missing-member-references.errors
new file mode 100644
index 000000000..6b086ce8c
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/references/missing-member-references.errors
@@ -0,0 +1,1 @@
+[WARNING] ns.foo#Operation1Input$fooId: This member appears to reference the following resources without being included in a `@references` trait: [`ns.foo#FooResource`] | MemberShouldReferenceResource
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/references/missing-member-references.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/references/missing-member-references.smithy
new file mode 100644
index 000000000..4da9d4a65
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/references/missing-member-references.smithy
@@ -0,0 +1,39 @@
+$version: "2.0"
+
+namespace ns.foo
+
+resource FooResource {
+    identifiers: {
+        fooId: String
+    }
+}
+
+operation Operation1 {
+    input := {
+        fooId: String
+    }
+}
+
+operation Operation2 {
+    input := @references([{
+        resource: FooResource
+    }]) {
+        fooId: String
+    }
+}
+
+resource BarResource {
+    identifiers: {
+        barId: BarId
+    }
+}
+
+operation Operation3 {
+    input := @references([{
+        resource: BarResource
+    }]) {
+        barId: BarId
+    }
+}
+
+string BarId
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/references/valid-implicit-references.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/references/valid-implicit-references.json
index 281143360..31dbfd792 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/references/valid-implicit-references.json
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/references/valid-implicit-references.json
@@ -19,9 +19,6 @@
             },
             "traits": {
                 "smithy.api#references": [
-                    {
-                        "resource": "ns.foo#MyResource"
-                    },
                     {
                         "resource": "ns.foo#MyResource"
                     },
@@ -48,6 +45,9 @@
                 "smithy.api#references": [
                     {
                         "resource": "ns.foo#MyResource"
+                    },
+                    {
+                        "resource": "ns.foo#MyResource2"
                     }
                 ]
             }
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-identifiers/detects-misbound-operations.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-identifiers/detects-misbound-operations.errors
index e649b0549..8a60535dd 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-identifiers/detects-misbound-operations.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-identifiers/detects-misbound-operations.errors
@@ -1,1 +1,2 @@
 [WARNING] smithy.example#GetFoo: The `smithy.example#GetFoo` operation is bound to the `smithy.example#FooService` service but has members that match identifiers of the following resource shapes: [`smithy.example#Foo`]. It may be more accurately bound to one of them than directly to the service. | ServiceBoundResourceOperation.smithy.example#FooService.GetFoo
+[WARNING] smithy.example#GetFooInput$fooId: This member appears to reference the following resources without being included in a `@references` trait: [`smithy.example#Foo`] | MemberShouldReferenceResource
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-operation-binding-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-operation-binding-validator.errors
index 8a249199f..705ea9cbd 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-operation-binding-validator.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-operation-binding-validator.errors
@@ -1,2 +1,2 @@
-[ERROR] ns.foo#InvalidResourceOperation1: This operation does not form a valid instance operation when bound to resource `ns.foo#InvalidResource`. All of the identifiers of the resource were not implicitly or explicitly bound to the input of the operation. Expected the following identifier bindings: [required member named `foo` that targets `smithy.api#String`]. Found the following identifier bindings: [] | ResourceIdentifierBinding
+[ERROR] ns.foo#InvalidResourceOperation1: This operation does not form a valid instance operation when bound to resource `ns.foo#InvalidResource`. All of the identifiers of the resource were not implicitly or explicitly bound to the input of the operation. Expected the following identifier bindings: [required member named `bar` that targets `smithy.api#String`]. Found the following identifier bindings: [] | ResourceIdentifierBinding
 [ERROR] ns.foo#ValidResourceOperation2: This operation does not form a valid instance operation when bound to resource `ns.foo#ValidResource`. All of the identifiers of the resource were not implicitly or explicitly bound to the input of the operation. Expected the following identifier bindings: [required member named `foo` that targets `smithy.api#String`]. Found the following identifier bindings: [] | ResourceIdentifierBinding
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-operation-binding-validator.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-operation-binding-validator.json
index 9bb2b0f1c..9508ffe81 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-operation-binding-validator.json
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-operation-binding-validator.json
@@ -83,7 +83,7 @@
         "ns.foo#InvalidResource": {
             "type": "resource",
             "identifiers": {
-                "foo": {
+                "bar": {
                     "target": "smithy.api#String"
                 }
             },
@@ -129,7 +129,7 @@
         "ns.foo#InvalidResourceOperation2Input": {
             "type": "structure",
             "members": {
-                "foo": {
+                "bar": {
                     "target": "smithy.api#String",
                     "traits": {
                         "smithy.api#required": {}
diff --git a/smithy-protocol-tests/model/rpcv2Cbor/cbor-lists.smithy b/smithy-protocol-tests/model/rpcv2Cbor/cbor-lists.smithy
index be0817cfc..b7d99291f 100644
--- a/smithy-protocol-tests/model/rpcv2Cbor/cbor-lists.smithy
+++ b/smithy-protocol-tests/model/rpcv2Cbor/cbor-lists.smithy
@@ -45,7 +45,8 @@ apply RpcV2CborLists @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -116,7 +117,8 @@ apply RpcV2CborLists @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -136,7 +138,8 @@ apply RpcV2CborLists @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -155,7 +158,8 @@ apply RpcV2CborLists @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -175,7 +179,8 @@ apply RpcV2CborLists @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -343,7 +348,8 @@ structure StructureListMember {
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -364,7 +370,8 @@ structure StructureListMember {
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
diff --git a/smithy-protocol-tests/model/rpcv2Cbor/cbor-maps.smithy b/smithy-protocol-tests/model/rpcv2Cbor/cbor-maps.smithy
index 0f4a5223d..6013433f0 100644
--- a/smithy-protocol-tests/model/rpcv2Cbor/cbor-maps.smithy
+++ b/smithy-protocol-tests/model/rpcv2Cbor/cbor-maps.smithy
@@ -29,7 +29,8 @@ apply RpcV2CborDenseMaps @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -55,7 +56,8 @@ apply RpcV2CborDenseMaps @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -79,7 +81,8 @@ apply RpcV2CborDenseMaps @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -222,7 +225,8 @@ apply RpcV2CborSparseMaps @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -248,7 +252,8 @@ apply RpcV2CborSparseMaps @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -278,7 +283,8 @@ apply RpcV2CborSparseMaps @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -300,7 +306,8 @@ apply RpcV2CborSparseMaps @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -323,7 +330,8 @@ apply RpcV2CborSparseMaps @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
diff --git a/smithy-protocol-tests/model/rpcv2Cbor/cbor-structs.smithy b/smithy-protocol-tests/model/rpcv2Cbor/cbor-structs.smithy
index 7ea41d9cf..81ae08360 100644
--- a/smithy-protocol-tests/model/rpcv2Cbor/cbor-structs.smithy
+++ b/smithy-protocol-tests/model/rpcv2Cbor/cbor-structs.smithy
@@ -13,7 +13,8 @@ use smithy.test#httpResponseTests
         documentation: "Serializes simple scalar properties",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         }
         requireHeaders: [
             "Content-Length"
@@ -44,7 +45,8 @@ use smithy.test#httpResponseTests
             a key encoded using an indefinite length string.""",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         }
         requireHeaders: [
             "Content-Length"
@@ -77,7 +79,8 @@ use smithy.test#httpResponseTests
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         }
         requireHeaders: [
             "Content-Length"
@@ -97,7 +100,8 @@ use smithy.test#httpResponseTests
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         }
         requireHeaders: [
             "Content-Length"
@@ -111,7 +115,8 @@ use smithy.test#httpResponseTests
         documentation: "Supports handling NaN float values.",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         }
         requireHeaders: [
             "Content-Length"
@@ -131,7 +136,8 @@ use smithy.test#httpResponseTests
         documentation: "Supports handling Infinity float values.",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         }
         requireHeaders: [
             "Content-Length"
@@ -151,7 +157,8 @@ use smithy.test#httpResponseTests
         documentation: "Supports handling Infinity float values.",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         }
         requireHeaders: [
             "Content-Length"
@@ -171,7 +178,8 @@ use smithy.test#httpResponseTests
         documentation: "The server should be capable of deserializing indefinite length text strings.",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         }
         requireHeaders: [
             "Content-Length"
@@ -191,7 +199,8 @@ use smithy.test#httpResponseTests
         documentation: "The server should be capable of deserializing indefinite length byte strings.",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         }
         requireHeaders: [
             "Content-Length"
@@ -211,7 +220,8 @@ use smithy.test#httpResponseTests
         documentation: "Supports upcasting from a smaller byte representation of the same data type.",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         }
         requireHeaders: [
             "Content-Length"
@@ -238,7 +248,8 @@ use smithy.test#httpResponseTests
             generated against an older Smithy model.""",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         }
         requireHeaders: [
             "Content-Length"
@@ -260,6 +271,35 @@ use smithy.test#httpResponseTests
             blobValue: "foo"
         },
         appliesTo: "server"
+    },
+    {
+        id: "RpcV2CborServersShouldHandleNoAcceptHeader",
+        protocol: rpcv2Cbor,
+        documentation: "Servers should tolerate requests without an Accept header set.",
+        headers: {
+            "smithy-protocol": "rpc-v2-cbor",
+            "Content-Type": "application/cbor"
+        }
+        requireHeaders: [
+            "Content-Length"
+        ],
+        method: "POST",
+        bodyMediaType: "application/cbor",
+        uri: "/service/RpcV2Protocol/operation/SimpleScalarProperties",
+        body: "v2lieXRlVmFsdWUFa2RvdWJsZVZhbHVl+z/+OVgQYk3TcWZhbHNlQm9vbGVhblZhbHVl9GpmbG9hdFZhbHVl+kD0AABsaW50ZWdlclZhbHVlGQEAaWxvbmdWYWx1ZRkmkWpzaG9ydFZhbHVlGSaqa3N0cmluZ1ZhbHVlZnNpbXBsZXB0cnVlQm9vbGVhblZhbHVl9WlibG9iVmFsdWVDZm9v/w==",
+        params: {
+            byteValue: 5,
+            doubleValue: 1.889,
+            falseBooleanValue: false,
+            floatValue: 7.625,
+            integerValue: 256,
+            longValue: 9873,
+            shortValue: 9898,
+            stringValue: "simple",
+            trueBooleanValue: true,
+            blobValue: "foo"
+        },
+        appliesTo: "server"
     }
 ])
 @httpResponseTests([
@@ -453,7 +493,8 @@ apply RecursiveShapes @httpRequestTests([
         bodyMediaType: "application/cbor",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
diff --git a/smithy-protocol-tests/model/rpcv2Cbor/defaults.smithy b/smithy-protocol-tests/model/rpcv2Cbor/defaults.smithy
index bf1e44834..f2d969bca 100644
--- a/smithy-protocol-tests/model/rpcv2Cbor/defaults.smithy
+++ b/smithy-protocol-tests/model/rpcv2Cbor/defaults.smithy
@@ -18,7 +18,8 @@ apply OperationWithDefaults @httpRequestTests([
         uri: "/service/RpcV2Protocol/operation/OperationWithDefaults",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -40,7 +41,8 @@ apply OperationWithDefaults @httpRequestTests([
         uri: "/service/RpcV2Protocol/operation/OperationWithDefaults",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -60,7 +62,8 @@ apply OperationWithDefaults @httpRequestTests([
         uri: "/service/RpcV2Protocol/operation/OperationWithDefaults",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -105,7 +108,8 @@ apply OperationWithDefaults @httpRequestTests([
         uri: "/service/RpcV2Protocol/operation/OperationWithDefaults",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -152,7 +156,8 @@ apply OperationWithDefaults @httpRequestTests([
         uri: "/service/RpcV2Protocol/operation/OperationWithDefaults",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -174,7 +179,8 @@ apply OperationWithDefaults @httpRequestTests([
         uri: "/service/RpcV2Protocol/operation/OperationWithDefaults",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
diff --git a/smithy-protocol-tests/model/rpcv2Cbor/empty-input-output.smithy b/smithy-protocol-tests/model/rpcv2Cbor/empty-input-output.smithy
index c753e062d..b2bb21615 100644
--- a/smithy-protocol-tests/model/rpcv2Cbor/empty-input-output.smithy
+++ b/smithy-protocol-tests/model/rpcv2Cbor/empty-input-output.smithy
@@ -14,6 +14,7 @@ use smithy.test#httpResponseTests
         documentation: "Body is empty and no Content-Type header if no input",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
+            "Accept": "application/cbor"
         },
         forbidHeaders: [
             "Content-Type",
@@ -108,7 +109,8 @@ operation NoInputOutput {}
         documentation: "When Input structure is empty we write CBOR equivalent of {}",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         requireHeaders: [
             "Content-Length"
@@ -127,6 +129,7 @@ operation NoInputOutput {}
         documentation: "When Input structure is empty the server should accept an empty body",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
+            "Accept": "application/cbor",
             "Content-Type": "application/cbor"
         },
         method: "POST",
@@ -192,7 +195,8 @@ operation EmptyInputOutput {
         documentation: "When input is empty we write CBOR equivalent of {}",
         headers: {
             "smithy-protocol": "rpc-v2-cbor",
-            "Content-Type": "application/cbor"
+            "Content-Type": "application/cbor",
+            "Accept": "application/cbor"
         },
         forbidHeaders: [
             "X-Amz-Target"
diff --git a/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/CreatesTraitTest.java b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/CreatesTraitTest.java
index 29f268a06..07be21da4 100644
--- a/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/CreatesTraitTest.java
+++ b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/CreatesTraitTest.java
@@ -6,6 +6,7 @@ import com.example.traits.StringTrait;
 import com.example.traits.defaults.StructDefaultsTrait;
 import com.example.traits.documents.DocumentTrait;
 import com.example.traits.documents.StructWithNestedDocumentTrait;
+import com.example.traits.enums.EnumListMemberTrait;
 import com.example.traits.enums.IntEnumTrait;
 import com.example.traits.enums.StringEnumTrait;
 import com.example.traits.enums.SuitTrait;
@@ -74,6 +75,9 @@ public class CreatesTraitTest {
                 Arguments.of(StringEnumTrait.ID, Node.from("no")),
                 Arguments.of(IntEnumTrait.ID, Node.from(2)),
                 Arguments.of(SuitTrait.ID, Node.from("clubs")),
+                Arguments.of(EnumListMemberTrait.ID, ObjectNode.objectNodeBuilder()
+                        .withMember("value", ArrayNode.fromStrings("some", "none", "some"))
+                        .build()),
                 // Lists
                 Arguments.of(NumberListTrait.ID, ArrayNode.fromNodes(
                         Node.from(1), Node.from(2), Node.from(3))
diff --git a/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/LoadsFromModelTest.java b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/LoadsFromModelTest.java
index 5a14fe995..d0b170332 100644
--- a/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/LoadsFromModelTest.java
+++ b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/LoadsFromModelTest.java
@@ -7,7 +7,9 @@ import com.example.traits.StringTrait;
 import com.example.traits.defaults.StructDefaultsTrait;
 import com.example.traits.documents.DocumentTrait;
 import com.example.traits.documents.StructWithNestedDocumentTrait;
+import com.example.traits.enums.EnumListMemberTrait;
 import com.example.traits.enums.IntEnumTrait;
+import com.example.traits.enums.SomeEnum;
 import com.example.traits.enums.StringEnumTrait;
 import com.example.traits.enums.SuitTrait;
 import com.example.traits.idref.IdRefListTrait;
@@ -95,6 +97,8 @@ public class LoadsFromModelTest {
                         MapUtils.of("getValue", 1, "getEnumValue", IntEnumTrait.IntEnum.YES)),
                 Arguments.of("enums/string-enum-compatibility.smithy", SuitTrait.class,
                         MapUtils.of("getEnumValue", SuitTrait.Suit.CLUB, "getValue", "club")),
+                Arguments.of("enums/enum-list-member-trait.smithy", EnumListMemberTrait.class,
+                    MapUtils.of("getValue", Optional.of(ListUtils.of(SomeEnum.SOME, SomeEnum.NONE, SomeEnum.SOME)))),
                 // Id Refs
                 Arguments.of("idref/idref-string.smithy", IdRefStringTrait.class,
                         MapUtils.of("getValue", TARGET_ONE)),
diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/enums/enum-list-member-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/enums/enum-list-member-trait.smithy
new file mode 100644
index 000000000..2bd82da77
--- /dev/null
+++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/enums/enum-list-member-trait.smithy
@@ -0,0 +1,8 @@
+$version: "2.0"
+
+namespace test.smithy.traitcodegen
+
+use test.smithy.traitcodegen.enums#EnumListMemberTrait
+
+@EnumListMemberTrait(value: ["some", "none", "some"])
+structure myStruct {}
diff --git a/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java
index 1d3bbcc00..99a42f638 100644
--- a/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java
+++ b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java
@@ -28,7 +28,7 @@ import software.amazon.smithy.model.node.ObjectNode;
 
 
 public class TraitCodegenPluginTest {
-    private static final int EXPECTED_NUMBER_OF_FILES = 58;
+    private static final int EXPECTED_NUMBER_OF_FILES = 60;
 
     private MockManifest manifest;
     private Model model;
diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/enums/enum-list-member-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/enums/enum-list-member-trait.smithy
new file mode 100644
index 000000000..a73740a1e
--- /dev/null
+++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/enums/enum-list-member-trait.smithy
@@ -0,0 +1,17 @@
+$version: "2.0"
+
+namespace test.smithy.traitcodegen.enums
+
+@trait(selector: "structure")
+structure EnumListMemberTrait {
+    value: EnumList
+}
+
+list EnumList {
+    member: SomeEnum
+}
+
+enum SomeEnum {
+    SOME = "some"
+    NONE = "none"
+}
diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/manifest b/smithy-trait-codegen/src/test/resources/META-INF/smithy/manifest
index 8e659e2ba..be481772e 100644
--- a/smithy-trait-codegen/src/test/resources/META-INF/smithy/manifest
+++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/manifest
@@ -4,6 +4,7 @@ documents/struct-with-nested-document.smithy
 enums/enum-trait.smithy
 enums/int-enum-trait.smithy
 enums/string-enum-compatibility.smithy
+enums/enum-list-member-trait.smithy
 idref/idref-list.smithy
 idref/idref-map.smithy
 idref/idref-string.smithy