Repository: smithy-lang/smithy
Pull request #2030 (2023-11-06 21:22:43)

Resolve dupe apply to mixed members
Fixes https://github.com/smithy-lang/smithy/issues/2004.

When an apply statement targets a mixed in member, the trait application is stored until the mixed in member has been synthesized. However, if you load the same model file multiple times in the same loader, the trait application is only stored once, and removed when it is claimed, so when de-conflicting the duplicate mixed-in member definitions, only one of them will have the trait applied, resulting in a conflict.

This commit updates the loader to keep track of when these unclaimed traits have been claimed, rather than just removing them when claimed. As long as the traits have been claimed once, we know they aren't being applied to a non-existent shape. This allows the traits to be claimed multiple times if necessary.

Test cases were added for two different situations:
1. If the exact same model is loaded multiple times, so there are multiple instances where the traits need to be claimed. In this case there shouldn't be conflicts like in the issue.
2. If the apply statement is loaded multiple times, but the member it targets is only loaded once. In this case there shouldn't be any unclaimed traits, even though there are technically multiple of the same apply
statements.

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/docs/source-2.0/aws/aws-iam.rst b/docs/source-2.0/aws/aws-iam.rst
index 5ba48d570..d1556ead3 100644
--- a/docs/source-2.0/aws/aws-iam.rst
+++ b/docs/source-2.0/aws/aws-iam.rst
@@ -139,6 +139,10 @@ Each condition key structure supports the following members:
     * - externalDocumentation
       - ``string``
       - A valid URL that defines more information about the condition key.
+    * - relativeDocumentation
+      - ``string``
+      - A relative URL path that defines more information about the condition key
+        within a set of IAM-related documentation.
 
 .. code-block:: smithy
 
@@ -410,6 +414,10 @@ members:
     * - name
       - ``string``
       - The name of the resource in AWS IAM.
+    * - relativeDocumentation
+      - ``string``
+      - A relative URL path that defines more information about the resource
+        within a set of IAM-related documentation.
 
 The following example defines a simple resource with a name in AWS IAM that
 deviates from the :ref:`shape name of the shape ID <shape-id>` of the resource.
diff --git a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeyDefinition.java b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeyDefinition.java
index e0a90c4ac..da0b38d61 100644
--- a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeyDefinition.java
+++ b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeyDefinition.java
@@ -28,15 +28,18 @@ public final class ConditionKeyDefinition implements ToNode, ToSmithyBuilder<Con
     private static final String TYPE = "type";
     private static final String DOCUMENTATION = "documentation";
     private static final String EXTERNAL_DOCUMENTATION = "externalDocumentation";
+    private static final String RELATIVE_DOCUMENTATION = "relativeDocumentation";
 
     private final String type;
     private final String documentation;
     private final String externalDocumentation;
+    private final String relativeDocumentation;
 
     private ConditionKeyDefinition(Builder builder) {
         type = SmithyBuilder.requiredState(TYPE, builder.type);
         documentation = builder.documentation;
         externalDocumentation = builder.externalDocumentation;
+        relativeDocumentation = builder.relativeDocumentation;
     }
 
     public static Builder builder() {
@@ -51,6 +54,8 @@ public final class ConditionKeyDefinition implements ToNode, ToSmithyBuilder<Con
                 .ifPresent(builder::documentation);
         objectNode.getStringMember(EXTERNAL_DOCUMENTATION).map(StringNode::getValue)
                 .ifPresent(builder::externalDocumentation);
+        objectNode.getStringMember(RELATIVE_DOCUMENTATION).map(StringNode::getValue)
+                .ifPresent(builder::relativeDocumentation);
 
         return builder.build();
     }
@@ -76,11 +81,22 @@ public final class ConditionKeyDefinition implements ToNode, ToSmithyBuilder<Con
         return Optional.ofNullable(externalDocumentation);
     }
 
+    /**
+     * Get the relative URL path that defines more information about the condition key
+     * within a set of IAM-related documentation.
+     *
+     * @return A relative URL to the documentation page.
+     */
+    public Optional<String> getRelativeDocumentation() {
+        return Optional.ofNullable(relativeDocumentation);
+    }
+
     @Override
     public SmithyBuilder<ConditionKeyDefinition> toBuilder() {
         return builder()
                 .documentation(documentation)
                 .externalDocumentation(externalDocumentation)
+                .relativeDocumentation(relativeDocumentation)
                 .type(type);
     }
 
@@ -90,6 +106,7 @@ public final class ConditionKeyDefinition implements ToNode, ToSmithyBuilder<Con
                 .withMember(TYPE, Node.from(type))
                 .withOptionalMember(DOCUMENTATION, getDocumentation().map(Node::from))
                 .withOptionalMember(EXTERNAL_DOCUMENTATION, getExternalDocumentation().map(Node::from))
+                .withOptionalMember(RELATIVE_DOCUMENTATION, getRelativeDocumentation().map(Node::from))
                 .build();
     }
 
@@ -104,18 +121,20 @@ public final class ConditionKeyDefinition implements ToNode, ToSmithyBuilder<Con
         ConditionKeyDefinition that = (ConditionKeyDefinition) o;
         return Objects.equals(type, that.type)
                && Objects.equals(documentation, that.documentation)
-               && Objects.equals(externalDocumentation, that.externalDocumentation);
+               && Objects.equals(externalDocumentation, that.externalDocumentation)
+               && Objects.equals(relativeDocumentation, that.relativeDocumentation);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(type, documentation, externalDocumentation);
+        return Objects.hash(type, documentation, externalDocumentation, relativeDocumentation);
     }
 
     public static final class Builder implements SmithyBuilder<ConditionKeyDefinition> {
         private String type;
         private String documentation;
         private String externalDocumentation;
+        private String relativeDocumentation;
 
         @Override
         public ConditionKeyDefinition build() {
@@ -136,5 +155,10 @@ public final class ConditionKeyDefinition implements ToNode, ToSmithyBuilder<Con
             this.externalDocumentation = externalDocumentation;
             return this;
         }
+
+        public Builder relativeDocumentation(String relativeDocumentation) {
+            this.relativeDocumentation = relativeDocumentation;
+            return this;
+        }
     }
 }
diff --git a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTrait.java b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTrait.java
index baed36536..2e3bed2fb 100644
--- a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTrait.java
+++ b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTrait.java
@@ -33,10 +33,12 @@ public final class IamResourceTrait extends AbstractTrait
     public static final ShapeId ID = ShapeId.from("aws.iam#iamResource");
 
     public final String name;
+    public final String relativeDocumentation;
 
     private IamResourceTrait(Builder builder) {
         super(ID, builder.getSourceLocation());
         name = builder.name;
+        relativeDocumentation = builder.relativeDocumentation;
     }
 
     /**
@@ -48,6 +50,16 @@ public final class IamResourceTrait extends AbstractTrait
         return Optional.of(name);
     }
 
+    /**
+     * Get the relative URL path that defines more information about the resource
+     * within a set of IAM-related documentation.
+     *
+     * @return A relative URL to the documentation page.
+     */
+    public Optional<String> getRelativeDocumentation() {
+        return Optional.ofNullable(relativeDocumentation);
+    }
+
     public static Builder builder() {
         return new Builder();
     }
@@ -80,6 +92,7 @@ public final class IamResourceTrait extends AbstractTrait
 
     public static final class Builder extends AbstractTraitBuilder<IamResourceTrait, Builder> {
         private String name;
+        private String relativeDocumentation;
 
         private Builder() {}
 
@@ -92,5 +105,10 @@ public final class IamResourceTrait extends AbstractTrait
             this.name = name;
             return this;
         }
+
+        public Builder relativeDocumentation(String relativeDocumentation) {
+            this.relativeDocumentation = relativeDocumentation;
+            return this;
+        }
     }
 }
diff --git a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.smithy b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.smithy
index 3b5f585d8..ae2ca3b5e 100644
--- a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.smithy
+++ b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.smithy
@@ -39,6 +39,10 @@ structure disableConditionKeyInference {}
 structure iamResource {
     /// The name of the resource in AWS IAM.
     name: String
+
+    ///  A relative URL path that defines more information about the resource
+    ///  within a set of IAM-related documentation.
+    relativeDocumentation: String
 }
 
 /// Other actions that the invoker must be authorized to perform when executing the targeted operation.
@@ -63,14 +67,18 @@ list supportedPrincipalTypes {
 /// A defined condition key to appear within a service in addition to inferred and global condition keys.
 @private
 structure ConditionKeyDefinition {
+    @required
+    type: ConditionKeyType
+
     /// Defines documentation about the condition key.
     documentation: String
 
     /// A valid URL that defines more information about the condition key.
     externalDocumentation: String
 
-    @required
-    type: ConditionKeyType
+    ///  A relative URL path that defines more information about the condition key
+    ///  within a set of IAM-related documentation.
+    relativeDocumentation: String
 }
 
 /// The IAM policy type of the value that will supplied for this context key
diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/PluginContext.java b/smithy-build/src/main/java/software/amazon/smithy/build/PluginContext.java
index a416f7c7c..f2408bd58 100644
--- a/smithy-build/src/main/java/software/amazon/smithy/build/PluginContext.java
+++ b/smithy-build/src/main/java/software/amazon/smithy/build/PluginContext.java
@@ -254,16 +254,17 @@ public final class PluginContext implements ToSmithyBuilder<PluginContext> {
 
     @Override
     public Builder toBuilder() {
-        return builder()
-                .projection(projectionName, projection)
+        Builder builder =  builder()
                 .model(model)
-                .originalModel(originalModel)
                 .events(events)
                 .settings(settings)
                 .fileManifest(fileManifest)
                 .pluginClassLoader(pluginClassLoader)
                 .sources(sources)
                 .artifactName(artifactName);
+        getProjection().ifPresent(config -> builder.projection(projectionName, config));
+        getOriginalModel().ifPresent(builder::originalModel);
+        return builder;
     }
 
     /**
diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/directed/GenerateIntEnumDirective.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/directed/GenerateIntEnumDirective.java
index 1ab87b7f7..dc3518af6 100644
--- a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/directed/GenerateIntEnumDirective.java
+++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/directed/GenerateIntEnumDirective.java
@@ -16,6 +16,8 @@
 package software.amazon.smithy.codegen.core.directed;
 
 import software.amazon.smithy.codegen.core.CodegenContext;
+import software.amazon.smithy.model.node.ExpectationNotMetException;
+import software.amazon.smithy.model.shapes.IntEnumShape;
 import software.amazon.smithy.model.shapes.ServiceShape;
 import software.amazon.smithy.model.shapes.Shape;
 
@@ -29,6 +31,19 @@ import software.amazon.smithy.model.shapes.Shape;
 public final class GenerateIntEnumDirective<C extends CodegenContext<S, ?, ?>, S> extends ShapeDirective<Shape, C, S> {
 
     GenerateIntEnumDirective(C context, ServiceShape service, Shape shape) {
-        super(context, service, shape);
+        super(context, service, validateShape(shape));
+    }
+
+    private static Shape validateShape(Shape shape) {
+        if (shape.isIntEnumShape()) {
+            return shape;
+        }
+        throw new IllegalArgumentException("GenerateIntEnum requires an IntEnum shape");
+    }
+
+
+    public IntEnumShape expectIntEnumShape() {
+        return shape().asIntEnumShape().orElseThrow(() -> new ExpectationNotMetException(
+                "Expected an IntEnum shape, but found " + shape(), shape()));
     }
 }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderTraitMap.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderTraitMap.java
index 1e15e276c..639cebc88 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderTraitMap.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderTraitMap.java
@@ -21,9 +21,11 @@ import static software.amazon.smithy.model.validation.Validator.MODEL_ERROR;
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.logging.Logger;
 import software.amazon.smithy.model.SourceException;
 import software.amazon.smithy.model.SourceLocation;
@@ -47,6 +49,7 @@ final class LoaderTraitMap {
     private final List<ValidationEvent> events;
     private final boolean allowUnknownTraits;
     private final Map<ShapeId, Map<ShapeId, Trait>> unclaimed = new HashMap<>();
+    private final Set<ShapeId> claimed = new HashSet<>();
 
     LoaderTraitMap(TraitFactory traitFactory, List<ValidationEvent> events, boolean allowUnknownTraits) {
         this.traitFactory = traitFactory;
@@ -147,12 +150,19 @@ final class LoaderTraitMap {
     // Traits can be applied to synthesized members inherited from mixins. Applying these traits is deferred until
     // the point in which mixin members are synthesized into shapes.
     Map<ShapeId, Trait> claimTraitsForShape(ShapeId id) {
-        return unclaimed.containsKey(id) ? unclaimed.remove(id) : Collections.emptyMap();
+        if (!unclaimed.containsKey(id)) {
+            return Collections.emptyMap();
+        }
+        claimed.add(id);
+        return unclaimed.get(id);
     }
 
     // Emit events if any traits were applied to shapes that weren't found in the model.
     void emitUnclaimedTraits() {
         for (Map.Entry<ShapeId, Map<ShapeId, Trait>> entry : unclaimed.entrySet()) {
+            if (claimed.contains(entry.getKey())) {
+                continue;
+            }
             for (Map.Entry<ShapeId, Trait> traitEntry : entry.getValue().entrySet()) {
                 events.add(ValidationEvent.builder()
                         .id(Validator.MODEL_ERROR)
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/NodeValidatorPlugin.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/NodeValidatorPlugin.java
index 3556d13ef..5398f3529 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/NodeValidatorPlugin.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/NodeValidatorPlugin.java
@@ -64,7 +64,8 @@ public interface NodeValidatorPlugin {
                 new PatternTraitPlugin(),
                 new RangeTraitPlugin(),
                 new StringEnumPlugin(),
-                new StringLengthPlugin());
+                new StringLengthPlugin(),
+                new UniqueItemsPlugin());
     }
 
     /**
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/UniqueItemsPlugin.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/UniqueItemsPlugin.java
new file mode 100644
index 000000000..99139e5a4
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/UniqueItemsPlugin.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.validation.node;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import software.amazon.smithy.model.node.ArrayNode;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.shapes.CollectionShape;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.traits.UniqueItemsTrait;
+
+/**
+ * Validates that items in lists with the `@uniqueItems` trait are unique.
+ */
+final class UniqueItemsPlugin extends MemberAndShapeTraitPlugin<CollectionShape, ArrayNode, UniqueItemsTrait> {
+    UniqueItemsPlugin() {
+        super(CollectionShape.class, ArrayNode.class, UniqueItemsTrait.class);
+    }
+
+    @Override
+    protected void check(Shape shape, UniqueItemsTrait trait, ArrayNode value, Context context, Emitter emitter) {
+        Set<Node> uniqueNodes = new HashSet<>(value.getElements());
+        if (uniqueNodes.size() != value.size()) {
+            List<Node> duplicateNodes = new ArrayList<>(value.getElements());
+            for (Node uniqueNode : uniqueNodes) {
+                // Collections remove the first encountered entry, so our unique
+                // nodes through a set will leave behind any duplicates.
+                duplicateNodes.remove(uniqueNode);
+            }
+
+            Set<String> duplicateValues = new LinkedHashSet<>();
+            for (Node duplicateNode : duplicateNodes) {
+                duplicateValues.add(duplicateNode.toString());
+            }
+            emitter.accept(value, String.format(
+                    "Value provided for `%s` must have unique items, but the following items had multiple entries: "
+                    + "[`%s`]", shape.getId(), String.join("`, `", duplicateValues)));
+        }
+    }
+}


Test patch:
diff --git a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java
index 9b1d6cfb6..e47054780 100644
--- a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java
+++ b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java
@@ -53,6 +53,9 @@ public class ConditionKeysIndexTest {
         assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#GetResource2")), is(empty()));
 
         // Defined context keys are assembled from the names and mapped with the definitions.
+        assertEquals(index.getDefinedConditionKeys(service).get("foo:baz").getDocumentation().get(), "Foo baz");
+        assertEquals(index.getDefinedConditionKeys(service).get("foo:baz").getRelativeDocumentation().get(),
+            "condition-keys.html");
         assertThat(index.getDefinedConditionKeys(service).get("myservice:Resource1Id1").getDocumentation(),
                 not(Optional.empty()));
         assertEquals(index.getDefinedConditionKeys(service).get("myservice:ResourceTwoId2").getDocumentation().get(),
diff --git a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitTest.java b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitTest.java
index 96556b185..eb7bc1837 100644
--- a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitTest.java
+++ b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitTest.java
@@ -36,5 +36,6 @@ public class IamResourceTraitTest {
 
         assertTrue(superResource.hasTrait(IamResourceTrait.class));
         assertEquals(superResource.expectTrait(IamResourceTrait.class).getName().get(), "super");
+        assertEquals(superResource.expectTrait(IamResourceTrait.class).getRelativeDocumentation().get(), "API-Super.html");
     }
 }
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-resource.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-resource.smithy
index 76bfb1e2d..55fc02b9a 100644
--- a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-resource.smithy
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-resource.smithy
@@ -10,7 +10,10 @@ service MyService {
     resources: [SuperResource]
 }
 
-@aws.iam#iamResource(name: "super")
+@aws.iam#iamResource(
+    name: "super",
+    relativeDocumentation: "API-Super.html"
+)
 @arn(template: "super/{id1}")
 resource SuperResource {
     identifiers: {
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/successful-condition-keys.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/successful-condition-keys.smithy
index 62afdc379..a7c0999c0 100644
--- a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/successful-condition-keys.smithy
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/successful-condition-keys.smithy
@@ -2,7 +2,13 @@ $version: "1.0"
 namespace smithy.example
 
 @aws.api#service(sdkId: "My")
-@aws.iam#defineConditionKeys("foo:baz": {type: "String", documentation: "Foo baz"})
+@aws.iam#defineConditionKeys(
+  "foo:baz": {
+    type: "String",
+    documentation: "Foo baz",
+    relativeDocumentation: "condition-keys.html"
+  }
+)
 service MyService {
   version: "2019-02-20",
   operations: [Operation1],
diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/PluginContextTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/PluginContextTest.java
index aa26b499e..f70e7724b 100644
--- a/smithy-build/src/test/java/software/amazon/smithy/build/PluginContextTest.java
+++ b/smithy-build/src/test/java/software/amazon/smithy/build/PluginContextTest.java
@@ -82,4 +82,16 @@ public class PluginContextTest {
         assertThat(context.getSources(), equalTo(context2.getSources()));
         assertThat(context.getEvents(), equalTo(context2.getEvents()));
     }
+
+    @Test
+    public void roundTripsDefaults() {
+        PluginContext before = PluginContext.builder()
+                .model(Model.builder().build())
+                .fileManifest(new MockManifest())
+                .build();
+        PluginContext after = before.toBuilder().build();
+        assertThat(before.getModel(), equalTo(after.getModel()));
+        assertThat(before.getFileManifest(), equalTo(after.getFileManifest()));
+        assertThat(before.getProjectionName(), equalTo(after.getProjectionName()));
+    }
 }
diff --git a/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/directed/GenerateIntEnumDirectiveTest.java b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/directed/GenerateIntEnumDirectiveTest.java
new file mode 100644
index 000000000..2d582fbab
--- /dev/null
+++ b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/directed/GenerateIntEnumDirectiveTest.java
@@ -0,0 +1,16 @@
+package software.amazon.smithy.codegen.core.directed;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.model.shapes.BooleanShape;
+
+public class GenerateIntEnumDirectiveTest {
+    @Test
+    public void validatesShapeType() {
+        BooleanShape shape = BooleanShape.builder().id("smithy.example#Foo").build();
+
+        Assertions.assertThrows(IllegalArgumentException.class, () -> {
+            new GenerateIntEnumDirective<TestContext, TestSettings>(null, null, shape);
+        });
+    }
+}
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java
index 6f3950fc0..890f4e7f0 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java
@@ -1341,4 +1341,31 @@ public class ModelAssemblerTest {
         assertThat(result.getValidationEvents(Severity.ERROR).get(0).getMessage(),
                    equalTo("Error creating trait `smithy.foo#baz`: Oops!"));
     }
+
+    @Test
+    public void resolvesDuplicateTraitApplicationsToDuplicateMixedInMembers() throws Exception {
+        String model = IoUtils.readUtf8File(Paths.get(getClass().getResource("mixins/apply-to-mixed-member.json").toURI()));
+        // Should be able to de-conflict the apply statements when the same model is loaded multiple times.
+        // See https://github.com/smithy-lang/smithy/issues/2004
+        Model.assembler()
+                .addUnparsedModel("test.json", model)
+                .addUnparsedModel("test2.json", model)
+                .addUnparsedModel("test3.json", model)
+                .assemble()
+                .unwrap();
+    }
+
+    @Test
+    public void resolvesDuplicateTraitApplicationsToSameMixedInMember() throws Exception {
+        String modelToApplyTo = IoUtils.readUtf8File(Paths.get(getClass().getResource("mixins/mixed-member.smithy").toURI()));
+        String modelWithApply = IoUtils.readUtf8File(Paths.get(getClass().getResource("mixins/member-apply-other-namespace.smithy").toURI()));
+        // Should be able to load when you have multiple identical apply statements to the same mixed in member.
+        // See https://github.com/smithy-lang/smithy/issues/2004
+        Model.assembler()
+                .addUnparsedModel("mixed-member.smithy", modelToApplyTo)
+                .addUnparsedModel("member-apply-1.smithy", modelWithApply)
+                .addUnparsedModel("member-apply-2.smithy", modelWithApply)
+                .assemble()
+                .unwrap();
+    }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/validation/NodeValidationVisitorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/validation/NodeValidationVisitorTest.java
index 185173a3f..091284703 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/validation/NodeValidationVisitorTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/validation/NodeValidationVisitorTest.java
@@ -207,6 +207,13 @@ public class NodeValidationVisitorTest {
                 {"ns.foo#List", "[10]", new String[] {"0: Expected string value for string shape, `ns.foo#String`; found number value, `10`"}},
                 {"ns.foo#List", "10", new String[] {"Expected array value for list shape, `ns.foo#List`; found number value, `10`"}},
 
+                // unique list
+                {"ns.foo#UniqueList", "[\"a\"]", null},
+                {"ns.foo#UniqueList", "[\"a\", \"b\"]", null},
+                {"ns.foo#UniqueList", "[\"a\", \"a\"]", new String[] {"Value provided for `ns.foo#UniqueList` must have unique items, but the following items had multiple entries: [`a`]"}},
+                {"ns.foo#UniqueList", "[\"a\", \"a\", \"a\"]", new String[] {"Value provided for `ns.foo#UniqueList` must have unique items, but the following items had multiple entries: [`a`]"}},
+                {"ns.foo#UniqueList", "[\"a\", \"a\", \"b\", \"b\"]", new String[] {"Value provided for `ns.foo#UniqueList` must have unique items, but the following items had multiple entries: [`a`, `b`]"}},
+
                 // map
                 {"ns.foo#Map", "{\"a\":[\"b\"]}", null},
                 {"ns.foo#Map", "{\"a\":[\"b\"], \"c\":[\"d\"]}", null},
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/mixins/apply-to-mixed-member.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/mixins/apply-to-mixed-member.json
new file mode 100644
index 000000000..544456e8b
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/mixins/apply-to-mixed-member.json
@@ -0,0 +1,34 @@
+{
+    "smithy": "2.0",
+    "shapes": {
+        "com.example#Common": {
+            "type": "structure",
+            "members": {
+                "description": {
+                    "target": "smithy.api#String",
+                    "traits": {
+                        "smithy.api#required": {}
+                    }
+                }
+            },
+            "traits": {
+                "smithy.api#mixin": {}
+            }
+        },
+        "com.example#Thing": {
+            "type": "structure",
+            "mixins": [
+                {
+                    "target": "com.example#Common"
+                }
+            ],
+            "members": {}
+        },
+        "com.example#Thing$description": {
+            "type": "apply",
+            "traits": {
+                "smithy.api#default": "test"
+            }
+        }
+    }
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/mixins/member-apply-other-namespace.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/mixins/member-apply-other-namespace.smithy
new file mode 100644
index 000000000..faad3a6ec
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/mixins/member-apply-other-namespace.smithy
@@ -0,0 +1,7 @@
+$version: "2.0"
+
+namespace com.example.other
+
+use com.example#Thing
+
+apply Thing$description @default("test")
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/mixins/mixed-member.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/mixins/mixed-member.smithy
new file mode 100644
index 000000000..864ee0b06
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/mixins/mixed-member.smithy
@@ -0,0 +1,11 @@
+$version: "2.0"
+
+namespace com.example
+
+@mixin
+structure Common {
+    @required
+    description: String
+}
+
+structure Thing with [Common] {}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/node-validator.json b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/node-validator.json
index da895424e..3b3375b42 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/node-validator.json
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/node-validator.json
@@ -133,6 +133,15 @@
                 }
             }
         },
+        "ns.foo#UniqueList": {
+            "type": "list",
+            "member": {
+                "target": "ns.foo#String"
+            },
+            "traits": {
+                "smithy.api#uniqueItems": {}
+            }
+        },
         "ns.foo#String": {
             "type": "string"
         },