Repository: smithy-lang/smithy
Pull request #1863 (2023-07-18 13:36:16)

Add documentationIdentifier value to aws.api#service trait
*Description of changes:* Add documentationIdentifier value to aws.api#service trait. This value is used to implement linking between service and sdk documentation for AWS services.


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/config/spotbugs/filter.xml b/config/spotbugs/filter.xml
index 5c40defe0..3e4ed00a8 100644
--- a/config/spotbugs/filter.xml
+++ b/config/spotbugs/filter.xml
@@ -149,6 +149,12 @@
         <Bug pattern="NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"/>
     </Match>
 
+    <!-- This is a false positive. The value is guarded behind an Objects.requireNonNull -->
+    <Match>
+        <Class name="software.amazon.smithy.model.validation.testrunner.SmithyTestCase"/>
+        <Bug pattern="NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"/>
+    </Match>
+
     <!-- We don't care about the return value of putIfAbsent in this cache -->
     <Match>
         <Class name="software.amazon.smithy.model.shapes.ShapeId$ShapeIdFactory"/>
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 7786b761b..998c28219 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,5 +1,5 @@
 sphinx>=4.2.0,<4.3.0
-pygments==2.12.0
+pygments==2.15.0
 furo==2022.6.21
 
 # Used by new docs.
diff --git a/docs/setup.py b/docs/setup.py
index be7efef78..517c9c956 100644
--- a/docs/setup.py
+++ b/docs/setup.py
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
 
 requires = [
     "sphinx==4.2.0",
-    "pygments==2.12.0",
+    "pygments==2.15.0",
     "sphinx_copybutton==0.5.0",
     # Used by new docs.
     "sphinx-inline-tabs==2022.1.2b11",
diff --git a/docs/source-1.0/spec/aws/aws-core.rst b/docs/source-1.0/spec/aws/aws-core.rst
index 81f657826..8ae2c8cf9 100644
--- a/docs/source-1.0/spec/aws/aws-core.rst
+++ b/docs/source-1.0/spec/aws/aws-core.rst
@@ -26,10 +26,12 @@ Value type
     * :ref:`service-cloudformation-name`
     * :ref:`service-arn-namespace`
     * :ref:`service-cloudtrail-event-source`
+    * :ref:`service-doc-id`
     * :ref:`service-endpoint-prefix`
 
 The following example defines an AWS service that uses the default values of
-``cloudFormationService``, ``arnNamespace``, and ``cloudTrailEventSource``:
+``cloudFormationService``, ``arnNamespace``, ``cloudTrailEventSource`` and
+``docId``:
 
 .. tabs::
 
@@ -78,6 +80,7 @@ The following example provides explicit values for all properties:
             cloudFormationName: "FooBaz",
             arnNamespace: "myservice",
             cloudTrailEventSource: "myservice.amazon.aws",
+            docId: "some-value-2018-03-17",
             endpointPrefix: "my-endpoint"
         )
         service FooBaz {
@@ -97,6 +100,7 @@ The following example provides explicit values for all properties:
                             "sdkId": "Some Value",
                             "cloudFormationName": "FooBaz",
                             "arnNamespace": "myservice",
+                            "docId": "some-value-2018-03-17",
                             "cloudTrailEventSource": "myservice.amazon.aws"
                         }
                     }
@@ -233,6 +237,31 @@ Amazon CloudWatch is ``monitoring.amazonaws.com``. Such services will
 need to explicitly configure the ``cloudTrailEventSource`` setting.
 
 
+.. _service-doc-id:
+
+``docId``
+===========================
+
+The ``docId`` property is a ``string`` value that is used to implement linking
+between service and SDK documentation for AWS services.
+
+This will default to the ``sdkId`` value in lower case followed by the service
+``version`` property, separated by dashes. For the example below, the value
+for this property would default to ``some-value-2018-03-17``.
+
+.. code-block:: smithy
+
+    $version: "1.0"
+    namespace aws.fooBaz
+
+    use aws.api#service
+
+    @service(sdkId: "Some Value")
+    service FooBaz {
+        version: "2018-03-17",
+    }
+
+
 .. _service-endpoint-prefix:
 
 ``endpointPrefix``
diff --git a/docs/source-1.0/spec/core/behavior-traits.rst b/docs/source-1.0/spec/core/behavior-traits.rst
index 9aa6966d1..c1b0d1e45 100644
--- a/docs/source-1.0/spec/core/behavior-traits.rst
+++ b/docs/source-1.0/spec/core/behavior-traits.rst
@@ -214,7 +214,8 @@ The ``paginated`` trait is a structure that contains the following members:
       - ``string``
       - The name of an operation input member that limits the maximum number
         of results to include in the operation output. This input member
-        SHOULD NOT be required and MUST target an integer shape.
+        SHOULD NOT be required and SHOULD target an integer shape. It can, but
+        SHOULD NOT target a byte, short, or long shape.
 
         .. warning::
 
diff --git a/docs/source-2.0/aws/aws-core.rst b/docs/source-2.0/aws/aws-core.rst
index 5e082f881..99f6af5db 100644
--- a/docs/source-2.0/aws/aws-core.rst
+++ b/docs/source-2.0/aws/aws-core.rst
@@ -25,10 +25,12 @@ Value type
     * :ref:`service-cloudformation-name`
     * :ref:`service-arn-namespace`
     * :ref:`service-cloudtrail-event-source`
+    * :ref:`service-doc-id`
     * :ref:`service-endpoint-prefix`
 
 The following example defines an AWS service that uses the default values of
-``cloudFormationService``, ``arnNamespace``, and ``cloudTrailEventSource``:
+``cloudFormationService``, ``arnNamespace``, ``cloudTrailEventSource`` and
+``docId``:
 
 .. code-block:: smithy
 
@@ -58,6 +60,7 @@ The following example provides explicit values for all properties:
         cloudFormationName: "FooBaz"
         arnNamespace: "myservice"
         cloudTrailEventSource: "myservice.amazon.aws"
+        docId: "some-value-2018-03-17"
         endpointPrefix: "my-endpoint"
     )
     service FooBaz {
@@ -194,6 +197,32 @@ Amazon CloudWatch is ``monitoring.amazonaws.com``. Such services will
 need to explicitly configure the ``cloudTrailEventSource`` setting.
 
 
+.. _service-doc-id:
+
+``docId``
+===========================
+
+The ``docId`` property is a ``string`` value that is used to implement linking
+between service and SDK documentation for AWS services.
+
+This will default to the ``sdkId`` value in lower case followed by the service
+``version`` property, separated by dashes. For the example below, the value
+for this property would default to ``some-value-2018-03-17``.
+
+.. code-block:: smithy
+
+    $version: "2"
+
+    namespace aws.fooBaz
+
+    use aws.api#service
+
+    @service(sdkId: "Some Value")
+    service FooBaz {
+        version: "2018-03-17"
+    }
+
+
 .. _service-endpoint-prefix:
 
 ``endpointPrefix``
diff --git a/docs/source-2.0/spec/behavior-traits.rst b/docs/source-2.0/spec/behavior-traits.rst
index b885845b5..7ada59fea 100644
--- a/docs/source-2.0/spec/behavior-traits.rst
+++ b/docs/source-2.0/spec/behavior-traits.rst
@@ -225,7 +225,8 @@ The ``paginated`` trait is a structure that contains the following members:
       - ``string``
       - The name of an operation input member that limits the maximum number
         of results to include in the operation output. This input member
-        SHOULD NOT be required and MUST target an integer shape.
+        SHOULD NOT be required and SHOULD target an integer shape. It can, but
+        SHOULD NOT target a byte, short, or long shape.
 
         .. warning::
 
diff --git a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/ServiceTrait.java b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/ServiceTrait.java
index 705abc16f..b6f27c09a 100644
--- a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/ServiceTrait.java
+++ b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/ServiceTrait.java
@@ -19,10 +19,12 @@ import java.util.Locale;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.logging.Logger;
+import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.SourceException;
 import software.amazon.smithy.model.node.Node;
 import software.amazon.smithy.model.node.ObjectNode;
 import software.amazon.smithy.model.node.StringNode;
+import software.amazon.smithy.model.shapes.ServiceShape;
 import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.traits.AbstractTrait;
 import software.amazon.smithy.model.traits.AbstractTraitBuilder;
@@ -38,19 +40,23 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
     public static final ShapeId ID = ShapeId.from("aws.api#service");
     private static final Logger LOGGER = Logger.getLogger(ServiceTrait.class.getName());
 
+    private final ShapeId target;
     private final String cloudFormationName;
     private final String arnNamespace;
     private final String sdkId;
     private final String cloudTrailEventSource;
+    private final String docId;
     private final String endpointPrefix;
 
     private ServiceTrait(Builder builder) {
         super(ID, builder.getSourceLocation());
+        this.target = SmithyBuilder.requiredState("target", builder.target);
         this.sdkId = SmithyBuilder.requiredState("sdkId", builder.sdkId);
         this.arnNamespace = SmithyBuilder.requiredState("arnNamespace", builder.arnNamespace);
         this.cloudFormationName = SmithyBuilder.requiredState("cloudFormationName", builder.cloudFormationName);
         this.cloudTrailEventSource = SmithyBuilder.requiredState(
                 "cloudTrailEventSource", builder.cloudTrailEventSource);
+        this.docId = builder.docId;
         this.endpointPrefix = SmithyBuilder.requiredState("endpointPrefix", builder.endpointPrefix);
     }
 
@@ -77,6 +83,8 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
                     .ifPresent(builder::cloudFormationName);
             objectNode.getStringMember("cloudTrailEventSource").map(StringNode::getValue)
                     .ifPresent(builder::cloudTrailEventSource);
+            objectNode.getStringMember("docId").map(StringNode::getValue)
+                    .ifPresent(builder::docId);
             objectNode.getStringMember("endpointPrefix")
                     .map(StringNode::getValue)
                     .ifPresent(builder::endpointPrefix);
@@ -140,6 +148,28 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
         return cloudTrailEventSource;
     }
 
+    /**
+     * Returns the documentation identifier value for the service.
+     *
+     * <p> When value on trait is not set, this method defaults to the lower
+     * cased value of the sdkId followed by the service version, separated by
+     * dashes.
+     *
+     * @return Returns the documentation identifier value for the service name.
+     */
+    public String getDocId(Model model) {
+        return getDocId().orElse(buildDefaultDocId(model));
+    }
+
+    protected Optional<String> getDocId() {
+        return Optional.ofNullable(docId);
+    }
+
+    private String buildDefaultDocId(Model model) {
+        String version = model.expectShape(target, ServiceShape.class).getVersion();
+        return sdkId.replace(" ", "-").toLowerCase(Locale.US) + "-" + version;
+    }
+
     /**
      * Returns the endpoint prefix for the service.
      *
@@ -162,11 +192,13 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
     @Override
     public Builder toBuilder() {
         return new Builder()
+                .target(target)
                 .sdkId(sdkId)
                 .sourceLocation(getSourceLocation())
                 .cloudFormationName(cloudFormationName)
                 .arnNamespace(arnNamespace)
                 .cloudTrailEventSource(cloudTrailEventSource)
+                .docId(docId)
                 .endpointPrefix(endpointPrefix);
     }
 
@@ -174,10 +206,12 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
     protected Node createNode() {
         return Node.objectNodeBuilder()
                 .sourceLocation(getSourceLocation())
+                .withMember("target", Node.from(target.getName()))
                 .withMember("sdkId", Node.from(sdkId))
                 .withMember("arnNamespace", Node.from(getArnNamespace()))
                 .withMember("cloudFormationName", Node.from(getCloudFormationName()))
                 .withMember("cloudTrailEventSource", Node.from(getCloudTrailEventSource()))
+                .withOptionalMember("docId", getDocId().map(Node::from))
                 .withMember("endpointPrefix", Node.from(getEndpointPrefix()))
                 .build();
     }
@@ -193,25 +227,31 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
         } else {
             ServiceTrait os = (ServiceTrait) other;
             return sdkId.equals(os.sdkId)
+                    && target.equals(os.target)
                     && arnNamespace.equals(os.arnNamespace)
                     && cloudFormationName.equals(os.cloudFormationName)
                     && cloudTrailEventSource.equals(os.cloudTrailEventSource)
+                    && docId == null
+                        ? os.docId == null
+                        : docId.equals(os.docId)
                     && endpointPrefix.equals(os.endpointPrefix);
         }
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(toShapeId(), sdkId, arnNamespace, cloudFormationName,
-                            cloudTrailEventSource, endpointPrefix);
+        return Objects.hash(toShapeId(), target, sdkId, arnNamespace, cloudFormationName,
+                            cloudTrailEventSource, docId, endpointPrefix);
     }
 
     /** Builder for {@link ServiceTrait}. */
     public static final class Builder extends AbstractTraitBuilder<ServiceTrait, Builder> {
+        private ShapeId target;
         private String sdkId;
         private String cloudFormationName;
         private String arnNamespace;
         private String cloudTrailEventSource;
+        private String docId;
         private String endpointPrefix;
 
         private Builder() {}
@@ -226,6 +266,8 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
         }
 
         public ServiceTrait build(ShapeId target) {
+            this.target = target;
+
             // Fill in default values if they weren't set.
             if (arnNamespace == null) {
                 arnNamespace(target.getName().toLowerCase(Locale.US));
@@ -236,7 +278,7 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
             }
 
             if (cloudTrailEventSource == null) {
-                cloudTrailEventSource = arnNamespace + ".amazonaws.com";
+                cloudTrailEventSource(arnNamespace + ".amazonaws.com");
             }
 
             if (endpointPrefix == null) {
@@ -246,6 +288,17 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
             return new ServiceTrait(this);
         }
 
+        /**
+         * Sets the target shape to which the trait is applied.
+         *
+         * @param target the ShapeId targeted by the trait.
+         * @return Returns the builder.
+         */
+        private Builder target(ShapeId target) {
+            this.target = target;
+            return this;
+        }
+
         /**
          * Sets the AWS CloudFormation resource type service name.
          *
@@ -296,6 +349,17 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
             return this;
         }
 
+        /**
+         * Set the documentation identifier for the service.
+         *
+         * @param docId documentation identifier for the service.
+         * @return Returns the builder.
+         */
+        public Builder docId(String docId) {
+            this.docId = docId;
+            return this;
+        }
+
         /**
          * Set the endpoint prefix used to construct client endpoints.
          *
diff --git a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy
index a48ea0d63..baf9cb499 100644
--- a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy
+++ b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy
@@ -186,6 +186,12 @@ structure service {
     /// to the `arnNamespace` plus `.amazonaws.com`.
     cloudTrailEventSource: String
 
+    /// The `docId` property is a string value that defines the identifier
+    /// used to implemention linking between service and SDK documentation for
+    /// AWS services. If not specified, this value defaults to the `sdkId` in
+    /// lower case plus the service `version` property, separated by dashes.
+    docId: String
+
     /// The `endpointPrefix` property is a string value that identifies which
     /// endpoint in a given region should be used to connect to the service.
     /// For example, most services in the AWS standard partition have endpoints
diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuild.java b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuild.java
index 136b03f77..6c6429eca 100644
--- a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuild.java
+++ b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuild.java
@@ -15,8 +15,6 @@
 
 package software.amazon.smithy.build;
 
-import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -199,23 +197,10 @@ public final class SmithyBuild {
     //
     // Ignores and logs when an unsupported model file is encountered.
     private void addSource(Path path) {
-        try {
-            if (Files.isDirectory(path)) {
-                // Pre-emptively crawl the given models to filter them up front into a flat, file-only, list.
-                Files.list(path).filter(Files::isRegularFile).forEach(this::addSourceFile);
-            } else {
-                addSourceFile(path);
-            }
-        } catch (IOException e) {
-            throw new UncheckedIOException(e);
-        }
-    }
-
-    private void addSourceFile(Path file) {
-        if (!VALID_MODELS.matches(file.getFileName())) {
-            LOGGER.warning("Omitting unsupported Smithy model file from model sources: " + file);
+        if (Files.isRegularFile(path) && !VALID_MODELS.matches(path.getFileName())) {
+            LOGGER.warning("Omitting unsupported Smithy model file from model sources: " + path);
         } else {
-            sources.add(file.toAbsolutePath());
+            sources.add(path.toAbsolutePath());
         }
     }
 
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java
index 8404fb9de..08ae9d87f 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java
@@ -477,12 +477,17 @@ public final class SmithyIdlModelSerializer {
 
         private void applyIntroducedTraits(Collection<MemberShape> members) {
             for (MemberShape member : members) {
+                Map<ShapeId, Trait> introducedTraits = new LinkedHashMap<>(member.getIntroducedTraits());
+
+                // The @enumValue trait is serialized using the `=` IDL syntax, so remove it here.
+                introducedTraits.remove(EnumValueTrait.ID);
+
                 // Use short form for a single trait, and block form for multiple traits.
-                if (member.getIntroducedTraits().size() == 1) {
+                if (introducedTraits.size() == 1) {
                     codeWriter.writeInline("apply $I ", member.getId());
                     serializeTraits(member.getIntroducedTraits(), TraitFeature.NO_SPECIAL_DOCS_SYNTAX);
                     codeWriter.write("");
-                } else if (!member.getIntroducedTraits().isEmpty()) {
+                } else if (!introducedTraits.isEmpty()) {
                     codeWriter.openBlock("apply $I {", "}", member.getId(), () -> {
                         // Only serialize local traits, and don't use special documentation syntax here.
                         serializeTraits(member.getIntroducedTraits(), TraitFeature.NO_SPECIAL_DOCS_SYNTAX);
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpMethodSemanticsValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpMethodSemanticsValidator.java
index e34024072..69f38b383 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpMethodSemanticsValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpMethodSemanticsValidator.java
@@ -30,6 +30,7 @@ import software.amazon.smithy.model.traits.ReadonlyTrait;
 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;
 import software.amazon.smithy.utils.MapUtils;
 
 /**
@@ -94,10 +95,19 @@ public final class HttpMethodSemanticsValidator extends AbstractValidator {
             HttpTrait trait
     ) {
         String method = trait.getMethod().toUpperCase(Locale.US);
-        List<ValidationEvent> events = new ArrayList<>();
 
+        // We don't have a standard method, so we can't validate it.
         if (!EXPECTED.containsKey(method)) {
-            return events;
+            return ListUtils.of();
+        }
+
+        List<ValidationEvent> events = new ArrayList<>();
+
+        // Emit a warning if the method is standard, but doesn't match standard casing.
+        if (!EXPECTED.containsKey(trait.getMethod())) {
+            events.add(warning(shape, trait, String.format(
+                    "This operation uses the `%s` method in the `http` trait, but expected `%s`.",
+                    trait.getMethod(), method)));
         }
 
         HttpMethodSemantics semantics = EXPECTED.get(method);
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/PaginatedTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/PaginatedTraitValidator.java
index 08dbdf56e..51f6c0f3c 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/PaginatedTraitValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/PaginatedTraitValidator.java
@@ -45,19 +45,21 @@ import software.amazon.smithy.utils.SetUtils;
  *      operation's properties are optionally merged with a service's.</li>
  *     <li>The items property, if set, must reference a list or map
  *      output member.</li>
- *     <li>The pageSize property, if set, must reference an optional integer
- *      input member.</li>
- *     <li>The inputToken property must reference an optional string input
- *      member.</li>
- *     <li>The outputToken property must reference an optional string output
- *      member.</li>
- *     <li>The pageSize property, if set, must reference an optional input
- *      member that targets an integer.</li>
+ *     <li>The pageSize property, if set, should reference an optional integer
+ *      input member. It may, but should not reference an optional byte, short,
+ *      or long.</li>
+ *     <li>The inputToken property should reference an optional string input
+ *      member. It may, but should not reference an optional map.</li>
+ *     <li>The outputToken property should reference an optional string output
+ *      member. It may, but should not reference an optional map.</li>
  * </ul>
  */
 public final class PaginatedTraitValidator extends AbstractValidator {
     private static final Set<ShapeType> ITEM_SHAPES = SetUtils.of(ShapeType.LIST, ShapeType.MAP);
-    private static final Set<ShapeType> PAGE_SHAPES = SetUtils.of(ShapeType.INTEGER);
+    private static final Set<ShapeType> PAGE_SHAPES = SetUtils.of(ShapeType.BYTE, ShapeType.INTEGER,
+            ShapeType.LONG, ShapeType.SHORT);
+    private static final Set<ShapeType> DANGER_PAGE_SHAPES = SetUtils.of(ShapeType.BYTE, ShapeType.LONG,
+            ShapeType.SHORT);
     private static final Set<ShapeType> TOKEN_SHAPES = SetUtils.of(ShapeType.STRING, ShapeType.MAP);
     private static final Set<ShapeType> DANGER_TOKEN_SHAPES = SetUtils.of(ShapeType.MAP);
     private static final Pattern PATH_PATTERN = Pattern.compile("\\.");
@@ -167,14 +169,14 @@ public final class PaginatedTraitValidator extends AbstractValidator {
                 events.add(error(operation, trait, String.format(
                         "%spaginated trait `%s` member `%s` targets a %s shape, but must target one of "
                                 + "the following: [%s]",
-                        prefix, validator.propertyName(), member.getId().getName(), target.getType(),
+                        prefix, validator.propertyName(), member.getId().getMember().get(), target.getType(),
                         ValidationUtils.tickedList(validator.validTargets()))));
             }
             if (validator.dangerTargets().contains(target.getType())) {
                 Set<ShapeType> preferredTargets = new TreeSet<>(validator.validTargets());
                 preferredTargets.removeAll(validator.dangerTargets());
                 String traitName = validator.propertyName();
-                String memberName = member.getId().getName();
+                String memberName = member.getId().getMember().get();
                 String targetType = target.getType().toString();
                 events.add(danger(operation, trait, String.format(
                             "%spaginated trait `%s` member `%s` targets a %s shape, but this is not recommended. "
@@ -319,6 +321,10 @@ public final class PaginatedTraitValidator extends AbstractValidator {
             return PAGE_SHAPES;
         }
 
+        Set<ShapeType> dangerTargets() {
+            return DANGER_PAGE_SHAPES;
+        }
+
         Optional<String> getMemberPath(OperationIndex opIndex, OperationShape operation, PaginatedTrait trait) {
             return trait.getPageSize();
         }


Test patch:
diff --git a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/EventSourceValidatorTest.java b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/EventSourceValidatorTest.java
index 91906dce3..2515de84e 100644
--- a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/EventSourceValidatorTest.java
+++ b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/EventSourceValidatorTest.java
@@ -10,20 +10,22 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.shapes.ServiceShape;
+import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.validation.Severity;
 import software.amazon.smithy.model.validation.ValidationEvent;
 
 public class EventSourceValidatorTest {
     @Test
     public void detectsWhenEventSourceIsUnexpected() {
+        ShapeId id = ShapeId.from("smithy.example#Foo");
         ServiceTrait trait = ServiceTrait.builder()
                 .sdkId("Foo")
                 .arnNamespace("foo")
                 .cloudTrailEventSource("REPLACE_ME_LATER")
                 .cloudFormationName("AWS::Foo")
-                .build();
+                .build(id);
         ServiceShape service = ServiceShape.builder()
-                .id("smithy.example#Foo")
+                .id(id)
                 .version("123")
                 .addTrait(trait)
                 .build();
@@ -39,14 +41,15 @@ public class EventSourceValidatorTest {
 
     @Test
     public void detectsWhenEventSourceIsPlaceholder() {
+        ShapeId id = ShapeId.from("smithy.example#Foo");
         ServiceTrait trait = ServiceTrait.builder()
                 .sdkId("Foo")
                 .arnNamespace("foo")
                 .cloudTrailEventSource("notfoo.amazonaws.com")
                 .cloudFormationName("AWS::Foo")
-                .build();
+                .build(id);
         ServiceShape service = ServiceShape.builder()
-                .id("smithy.example#Foo")
+                .id(id)
                 .version("123")
                 .addTrait(trait)
                 .build();
@@ -63,14 +66,15 @@ public class EventSourceValidatorTest {
 
     @Test
     public void ignoresKnownExceptions() {
+        ShapeId id = ShapeId.from("smithy.example#Foo");
         ServiceTrait trait = ServiceTrait.builder()
                 .sdkId("Foo")
                 .arnNamespace("cloudwatch")
                 .cloudTrailEventSource("monitoring.amazonaws.com")
                 .cloudFormationName("AWS::Foo")
-                .build();
+                .build(id);
         ServiceShape service = ServiceShape.builder()
-                .id("smithy.example#Foo")
+                .id(id)
                 .version("123")
                 .addTrait(trait)
                 .build();
diff --git a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ServiceTraitTest.java b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ServiceTraitTest.java
index 6009d0b6a..2765bc170 100644
--- a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ServiceTraitTest.java
+++ b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ServiceTraitTest.java
@@ -18,6 +18,7 @@ package software.amazon.smithy.aws.traits;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -47,12 +48,13 @@ public class ServiceTraitTest {
         assertThat(serviceTrait.getCloudTrailEventSource(), equalTo("foo.amazonaws.com"));
         assertThat(serviceTrait.getEndpointPrefix(), equalTo("foo"));
         assertThat(serviceTrait.toBuilder().build(), equalTo(serviceTrait));
+        assertFalse(serviceTrait.getDocId().isPresent());
     }
 
     @Test
     public void loadsTraitWithOptionalValues() {
         Node node = Node.parse("{\"sdkId\": \"Foo\", \"arnNamespace\": \"service\", \"cloudFormationName\": \"Baz\", "
-                        + "\"endpointPrefix\": \"endpoint-prefix\"}");
+                        + "\"endpointPrefix\": \"endpoint-prefix\", \"docId\": \"doc-id\"}");
         TraitFactory provider = TraitFactory.createServiceFactory();
         Optional<Trait> trait = provider.createTrait(ServiceTrait.ID, ShapeId.from("ns.foo#foo"), node);
 
@@ -63,6 +65,8 @@ public class ServiceTraitTest {
         assertThat(serviceTrait.getArnNamespace(), equalTo("service"));
         assertThat(serviceTrait.getCloudFormationName(), equalTo("Baz"));
         assertThat(serviceTrait.getEndpointPrefix(), equalTo("endpoint-prefix"));
+        assertTrue(serviceTrait.getDocId().isPresent());
+        assertThat(serviceTrait.getDocId().get(), equalTo("doc-id"));
     }
 
     @Test
@@ -90,13 +94,14 @@ public class ServiceTraitTest {
                 .assemble()
                 .unwrap();
         ServiceShape service = result
-                .expectShape(ShapeId.from("ns.foo#SomeService"))
-                .asServiceShape().get();
-        ServiceTrait trait = service.getTrait(ServiceTrait.class).get();
+                .expectShape(ShapeId.from("ns.foo#SomeService"), ServiceShape.class);
+        ServiceTrait trait = service.expectTrait(ServiceTrait.class);
 
         assertThat(trait.getSdkId(), equalTo("Some Value"));
         assertThat(trait.getCloudFormationName(), equalTo("SomeService"));
         assertThat(trait.getArnNamespace(), equalTo("service"));
         assertThat(trait.getEndpointPrefix(), equalTo("some-service"));
+        assertFalse(trait.getDocId().isPresent());
+        assertThat(trait.getDocId(result), equalTo("some-value-2018-03-17"));
     }
 }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java
index cbf0c1f34..f18e36892 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java
@@ -15,6 +15,9 @@
 
 package software.amazon.smithy.model.validation.testrunner;
 
+import static java.lang.String.format;
+
+import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -164,16 +167,19 @@ public final class SmithyTestCase {
 
     private static List<ValidationEvent> loadExpectedEvents(String errorsFileLocation) {
         String contents = IoUtils.readUtf8File(errorsFileLocation);
+        String fileName = Objects.requireNonNull(Paths.get(errorsFileLocation).getFileName()).toString();
         return Arrays.stream(contents.split(System.lineSeparator()))
                 .filter(line -> !line.trim().isEmpty())
-                .map(SmithyTestCase::parseValidationEvent)
+                .map(line -> parseValidationEvent(line, fileName))
                 .collect(Collectors.toList());
     }
 
-    static ValidationEvent parseValidationEvent(String event) {
+    static ValidationEvent parseValidationEvent(String event, String fileName) {
         Matcher matcher = EVENT_PATTERN.matcher(event);
         if (!matcher.find()) {
-            throw new IllegalArgumentException("Invalid validation event: " + event);
+            throw new IllegalArgumentException(format("Invalid validation event in file `%s`, the following event did "
+                    + "not match the expected regular expression `%s`: %s",
+                    fileName, EVENT_PATTERN.pattern(), event));
         }
 
         // Construct a dummy source location since we don't validate it.
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java
index 8aa756b55..62dedc3a6 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java
@@ -243,4 +243,20 @@ public class SmithyIdlModelSerializerTest {
 
         assertThat(modelResult, equalTo(IoUtils.readUtf8Url(resource).replace("\r\n", "\n")));
     }
+
+    @Test
+    public void handlesEnumMixins() {
+        URL resource = getClass().getResource("idl-serialization/enum-mixin-input.smithy");
+        Model model = Model.assembler().addImport(resource).assemble().unwrap();
+        Map<Path, String> serialized = SmithyIdlModelSerializer.builder().build().serialize(model);
+
+        if (serialized.size() != 1) {
+            throw new RuntimeException("Exactly one smithy file should be output for generated tests.");
+        }
+
+        String expectedOutput = IoUtils.readUtf8Resource(getClass(), "idl-serialization/enum-mixin-output.smithy")
+                .replaceAll("\\R", "\n");
+        String serializedString = serialized.entrySet().iterator().next().getValue();
+        Assertions.assertEquals(expectedOutput, serializedString);
+    }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCaseTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCaseTest.java
index 065573d98..f185b0884 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCaseTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCaseTest.java
@@ -18,6 +18,7 @@ package software.amazon.smithy.model.validation.testrunner;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.Collections;
 import org.junit.jupiter.api.Assertions;
@@ -32,14 +33,17 @@ import software.amazon.smithy.utils.ListUtils;
 public class SmithyTestCaseTest {
     @Test
     public void validatesThatEventsAreValid() {
-        Assertions.assertThrows(
+        IllegalArgumentException e = Assertions.assertThrows(
                 IllegalArgumentException.class,
-                () -> SmithyTestCase.parseValidationEvent("[ERROR] - m"));
+                () -> SmithyTestCase.parseValidationEvent("[ERROR] - m", "filename"));
+
+        assertTrue(e.getMessage().contains("`filename`"));
+        assertTrue(e.getMessage().contains("SUPPRESSED|NOTE|WARNING|DANGER|ERROR"));
     }
 
     @Test
     public void parsesValidEvents() {
-        SmithyTestCase.parseValidationEvent("[ERROR] -: message | EventId /filename:0:0");
+        SmithyTestCase.parseValidationEvent("[ERROR] -: message | EventId /filename:0:0", "filename");
     }
 
     @Test
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.errors
index e63b8308a..47fe47a07 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.errors
@@ -5,3 +5,4 @@
 [WARNING] ns.foo#H: This operation uses the `DELETE` method in the `http` trait, but is not marked with the idempotent trait | HttpMethodSemantics.MissingIdempotentTrait
 [WARNING] ns.foo#I: This operation uses the `GET` method in the `http` trait, but is not marked with the readonly trait | HttpMethodSemantics.MissingReadonlyTrait
 [WARNING] ns.foo#Options: OPTIONS requests are typically used as part of CORS and should not be modeled explicitly. They are an implementation detail that should not appear in generated client or server code. Instead, tooling should use the `cors` trait on a service to automatically configure CORS on clients and servers. It is the responsibility of service frameworks and API gateways to automatically manage OPTIONS requests. For example, OPTIONS requests are automatically created when using Smithy models with Amazon API Gateway. | HttpMethodSemantics.OPTIONS
+[WARNING] ns.foo#A: This operation uses the `Get` method in the `http` trait, but expected `GET`. | HttpMethodSemantics
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.json
index d55c1449a..b457df117 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.json
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.json
@@ -6,7 +6,7 @@
             "traits": {
                 "smithy.api#readonly": {},
                 "smithy.api#http": {
-                    "method": "GET",
+                    "method": "Get",
                     "uri": "/A"
                 }
             }
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.errors
index 90123a12a..48508ebf0 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.errors
@@ -1,13 +1,13 @@
 [ERROR] ns.foo#Invalid2: paginated trait `inputToken` targets a member `nextToken` that does not exist | PaginatedTrait
 [ERROR] ns.foo#Invalid2: paginated trait `outputToken` targets a member `nextToken` that does not exist | PaginatedTrait
-[ERROR] ns.foo#Invalid3: paginated trait `inputToken` member `InputNotString` targets a integer shape, but must target one of the following: [`map`, `string`] | PaginatedTrait
+[ERROR] ns.foo#Invalid3: paginated trait `inputToken` member `nextToken` targets a integer shape, but must target one of the following: [`map`, `string`] | PaginatedTrait
 [ERROR] ns.foo#Invalid3: paginated trait `items` targets a member `items` that does not exist | PaginatedTrait
-[ERROR] ns.foo#Invalid3: paginated trait `outputToken` member `OutputNotString` targets a integer shape, but must target one of the following: [`map`, `string`] | PaginatedTrait
+[ERROR] ns.foo#Invalid3: paginated trait `outputToken` member `nextToken` targets a integer shape, but must target one of the following: [`map`, `string`] | PaginatedTrait
 [ERROR] ns.foo#Invalid4: paginated trait `inputToken` member `nextToken` must not be required | PaginatedTrait
 [ERROR] ns.foo#Invalid4: paginated trait `outputToken` member `nextToken` must not be required | PaginatedTrait
 [WARNING] ns.foo#Invalid4: paginated trait `pageSize` member `pageSize` should not be required | PaginatedTrait.ShouldNotBeRequired.pageSize
-[ERROR] ns.foo#Invalid5: paginated trait `pageSize` member `Invalid5Input` targets a string shape, but must target one of the following: [`integer`] | PaginatedTrait
-[ERROR] ns.foo#Invalid6: paginated trait `items` member `Invalid6Output` targets a string shape, but must target one of the following: [`list`, `map`] | PaginatedTrait
+[ERROR] ns.foo#Invalid5: paginated trait `pageSize` member `pageSize` targets a string shape, but must target one of the following: [`byte`, `integer`, `long`, `short`] | PaginatedTrait
+[ERROR] ns.foo#Invalid6: paginated trait `items` member `items` targets a string shape, but must target one of the following: [`list`, `map`] | PaginatedTrait
 [ERROR] ns.foo#Invalid7: paginated trait `items` targets a member `items` that does not exist | PaginatedTrait
 [ERROR] ns.foo#Invalid7: paginated trait `outputToken` targets a member `missing` that does not exist | PaginatedTrait
 [ERROR] ns.foo#Invalid8: When bound within the `ns.foo#Service` service, paginated trait `inputToken` is not configured | PaginatedTrait
@@ -18,3 +18,6 @@
 [ERROR] ns.foo#Invalid1: paginated trait `pageSize` targets a member `pageSize` that does not exist | PaginatedTrait
 [ERROR] ns.foo#InvalidNoOutput: paginated trait `items` targets a member `items` that does not exist | PaginatedTrait
 [ERROR] ns.foo#InvalidNoOutput: paginated trait `outputToken` targets a member `nextToken` that does not exist | PaginatedTrait
+[DANGER] ns.foo#DangerSizeByte: paginated trait `pageSize` member `pageSize` targets a byte shape, but this is not recommended. One of [`integer`] SHOULD be targeted. | PaginatedTrait
+[DANGER] ns.foo#DangerSizeShort: paginated trait `pageSize` member `pageSize` targets a short shape, but this is not recommended. One of [`integer`] SHOULD be targeted. | PaginatedTrait
+[DANGER] ns.foo#DangerSizeLong: paginated trait `pageSize` member `pageSize` targets a long shape, but this is not recommended. One of [`integer`] SHOULD be targeted. | PaginatedTrait
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.json
index 5080a86cf..79d3e73dd 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.json
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.json
@@ -34,6 +34,15 @@
                 },
                 {
                     "target": "ns.foo#InvalidNestedInput"
+                },
+                {
+                    "target": "ns.foo#DangerSizeByte"
+                },
+                {
+                    "target": "ns.foo#DangerSizeShort"
+                },
+                {
+                    "target": "ns.foo#DangerSizeLong"
                 }
             ]
         },
@@ -303,6 +312,93 @@
                 }
             }
         },
+        "ns.foo#DangerSizeByte": {
+            "type": "operation",
+            "input": {
+                "target": "ns.foo#DangerSizeByteInput"
+            },
+            "output": {
+                "target": "ns.foo#ValidOutput"
+            },
+            "traits": {
+                "smithy.api#readonly": {},
+                "smithy.api#paginated": {
+                    "inputToken": "nextToken",
+                    "outputToken": "nextToken",
+                    "items": "items",
+                    "pageSize": "pageSize"
+                }
+            }
+        },
+        "ns.foo#DangerSizeByteInput": {
+            "type": "structure",
+            "members": {
+                "nextToken": {
+                    "target": "smithy.api#String"
+                },
+                "pageSize": {
+                    "target": "smithy.api#Byte"
+                }
+            }
+        },
+        "ns.foo#DangerSizeShort": {
+            "type": "operation",
+            "input": {
+                "target": "ns.foo#DangerSizeShortInput"
+            },
+            "output": {
+                "target": "ns.foo#ValidOutput"
+            },
+            "traits": {
+                "smithy.api#readonly": {},
+                "smithy.api#paginated": {
+                    "inputToken": "nextToken",
+                    "outputToken": "nextToken",
+                    "items": "items",
+                    "pageSize": "pageSize"
+                }
+            }
+        },
+        "ns.foo#DangerSizeShortInput": {
+            "type": "structure",
+            "members": {
+                "nextToken": {
+                    "target": "smithy.api#String"
+                },
+                "pageSize": {
+                    "target": "smithy.api#Short"
+                }
+            }
+        },
+        "ns.foo#DangerSizeLong": {
+            "type": "operation",
+            "input": {
+                "target": "ns.foo#DangerSizeLongInput"
+            },
+            "output": {
+                "target": "ns.foo#ValidOutput"
+            },
+            "traits": {
+                "smithy.api#readonly": {},
+                "smithy.api#paginated": {
+                    "inputToken": "nextToken",
+                    "outputToken": "nextToken",
+                    "items": "items",
+                    "pageSize": "pageSize"
+                }
+            }
+        },
+        "ns.foo#DangerSizeLongInput": {
+            "type": "structure",
+            "members": {
+                "nextToken": {
+                    "target": "smithy.api#String"
+                },
+                "pageSize": {
+                    "target": "smithy.api#Long"
+                }
+            }
+        },
         "ns.foo#NestedInput": {
             "type": "structure",
             "members": {
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-map-tokens.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-map-tokens.errors
index 9f6bc968c..30b2459a2 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-map-tokens.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-map-tokens.errors
@@ -1,2 +1,2 @@
-[DANGER] ns.foo#MapTokens: paginated trait `inputToken` member `MapTokensInput` targets a map shape, but this is not recommended. One of [`string`] SHOULD be targeted. | PaginatedTrait.WrongShapeType.inputToken
-[DANGER] ns.foo#MapTokens: paginated trait `outputToken` member `MapTokensOutput` targets a map shape, but this is not recommended. One of [`string`] SHOULD be targeted. | PaginatedTrait.WrongShapeType.outputToken
+[DANGER] ns.foo#MapTokens: paginated trait `inputToken` member `nextToken` targets a map shape, but this is not recommended. One of [`string`] SHOULD be targeted. | PaginatedTrait.WrongShapeType.inputToken
+[DANGER] ns.foo#MapTokens: paginated trait `outputToken` member `nextToken` targets a map shape, but this is not recommended. One of [`string`] SHOULD be targeted. | PaginatedTrait.WrongShapeType.outputToken
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/enum-mixin-input.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/enum-mixin-input.smithy
new file mode 100644
index 000000000..d09dab55b
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/enum-mixin-input.smithy
@@ -0,0 +1,23 @@
+$version: "2.0"
+
+namespace ns.foo
+
+@mixin
+intEnum IntMixin {
+    FOO = 1
+    BAR = 2
+}
+
+intEnum IntEnum with [IntMixin] {
+    FOO = 1
+    BAR = 2
+}
+
+@mixin
+enum StringMixin {
+    BAZ = "baz"
+}
+
+enum StringEnum with [StringMixin] {
+    BAZ = "baz"
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/enum-mixin-output.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/enum-mixin-output.smithy
new file mode 100644
index 000000000..560cb6b03
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/enum-mixin-output.smithy
@@ -0,0 +1,18 @@
+$version: "2.0"
+
+namespace ns.foo
+
+intEnum IntEnum with [IntMixin] {}
+
+@mixin
+intEnum IntMixin {
+    FOO = 1
+    BAR = 2
+}
+
+enum StringEnum with [StringMixin] {}
+
+@mixin
+enum StringMixin {
+    BAZ = "baz"
+}