Repository: smithy-lang/smithy
Pull request #2052 (2023-11-24 15:25:03)

Add `equals` and `hashCode` to `ErrorExample`
*Issue #, if available:*

We looked into this because we were having some false negatives while comparing values of the `@examples` trait. Part of it was using an old version of smithy which didn't have an `equals` for `Example`, but this could happen if the trait also has an `ErrorExample`.

*Description of changes:*

Adds a simple implementation and a test.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.


kubukoz: Can we have this reviewed and released?

Related: https://github.com/smithy-lang/smithy/pull/2009
gosar: > This branch cannot be rebased due to conflicts

Hi @miguel-vila, can you rebase? I can merge after that. Thanks.

Patch:
diff --git a/docs/source-2.0/aws/aws-auth.rst b/docs/source-2.0/aws/aws-auth.rst
index cad432300..30a7f581e 100644
--- a/docs/source-2.0/aws/aws-auth.rst
+++ b/docs/source-2.0/aws/aws-auth.rst
@@ -33,7 +33,8 @@ Trait value
           - **Required**. The signature version 4 service signing name to use
             in the `credential scope`_ when signing requests. This value MUST
             NOT be empty. This value SHOULD match the ``arnNamespace`` property
-            of the :ref:`aws.api#service-trait`.
+            of the :ref:`aws.api#service-trait` if present and the ``name``
+            property of the :ref:`aws.auth#sigv4a-trait` if present.
 
 If a request contains the ``Authorization`` header or a query string parameter
 with the name of ``X-Amz-Algorithm`` containing the value ``AWS4-HMAC-SHA256``,
@@ -59,6 +60,65 @@ unauthenticated request.
     }
 
 
+.. smithy-trait:: aws.auth#sigv4a
+.. _aws.auth#sigv4a-trait:
+
+-------------------------
+``aws.auth#sigv4a`` trait
+-------------------------
+
+Trait summary
+    The ``aws.auth#sigv4a`` trait adds support for AWS Signature Version 4
+    Asymmetric (SigV4A), an extension of `AWS signature version 4`_ (SigV4), to
+    a service.
+Trait selector
+    ``service[trait|aws.auth#sigv4]``
+Trait value
+    An ``object`` that supports the following properties:
+
+    .. list-table::
+        :header-rows: 1
+        :widths: 10 20 70
+
+        * - Property
+          - Type
+          - Description
+        * - name
+          - ``string``
+          - **Required**. The signature version 4a service signing name to use
+            in the `credential scope`_ when signing requests. This value MUST
+            NOT be empty. This value SHOULD match the ``arnNamespace`` property
+            of the :ref:`aws.api#service-trait` if present and the ``name``
+            property of the :ref:`aws.auth#sigv4-trait`.
+
+SigV4A is nearly identical to SigV4, but also uses public-private keys and
+asymmetric cryptographic signatures for every request. Most notably, SigV4A
+supports signatures for multi-region API requests.
+
+.. code-block:: smithy
+
+    $version: "2"
+
+    namespace aws.fooBaz
+
+    use aws.api#service
+    use aws.auth#sigv4
+    use aws.auth#sigv4a
+    use aws.protocols#restJson1
+
+    // This service is an AWS service that prioritizes SigV4A
+    // authentication before SigV4 authentication.
+    // Note that services that support SigV4A MUST support SigV4.
+    @service(sdkId: "Some Value")
+    @auth([sigv4a, sigv4])
+    @sigv4(name: "foobaz")
+    @sigv4a(name: "foobaz")
+    @restJson1
+    service FooBaz {
+        version: "2018-03-17"
+    }
+
+
 .. smithy-trait:: aws.auth#unsignedPayload
 .. _aws.auth#unsignedPayload-trait:
 
diff --git a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/auth/SigV4ATrait.java b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/auth/SigV4ATrait.java
new file mode 100644
index 000000000..bdb702f83
--- /dev/null
+++ b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/auth/SigV4ATrait.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.aws.traits.auth;
+
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.traits.AbstractTrait;
+import software.amazon.smithy.model.traits.AbstractTraitBuilder;
+import software.amazon.smithy.model.traits.Trait;
+import software.amazon.smithy.utils.SmithyBuilder;
+import software.amazon.smithy.utils.ToSmithyBuilder;
+
+/**
+ * Adds AWS Signature Version 4 Asymmetric authentication to a service or operation.
+ */
+public final class SigV4ATrait extends AbstractTrait implements ToSmithyBuilder<SigV4ATrait> {
+    public static final ShapeId ID = ShapeId.from("aws.auth#sigv4a");
+    private static final String NAME = "name";
+
+    private final String name;
+
+    private SigV4ATrait(Builder builder) {
+        super(ID, builder.getSourceLocation());
+        this.name = SmithyBuilder.requiredState(NAME, builder.name);
+    }
+
+    /**
+     * Gets the service signing name.
+     *
+     * @return the service signing name
+     */
+    public String getName() {
+        return name;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @Override
+    public Builder toBuilder() {
+        return builder()
+            .sourceLocation(getSourceLocation())
+            .name(getName());
+    }
+
+    @Override
+    protected Node createNode() {
+        return Node.objectNodeBuilder()
+            .sourceLocation(getSourceLocation())
+            .withMember(NAME, getName())
+            .build();
+    }
+
+    public static final class Builder extends AbstractTraitBuilder<SigV4ATrait, Builder> {
+        private String name;
+
+        private Builder() {}
+
+        @Override
+        public SigV4ATrait build() {
+            return new SigV4ATrait(this);
+        }
+
+        public Builder name(String name) {
+            this.name = name;
+            return this;
+        }
+    }
+
+    public static final class Provider extends AbstractTrait.Provider {
+        public Provider() {
+            super(ID);
+        }
+
+        @Override
+        public Trait createTrait(ShapeId target, Node value) {
+            ObjectNode objectNode = value.expectObjectNode();
+            Builder builder = builder().sourceLocation(value);
+            builder.name(objectNode.expectStringMember(NAME).getValue());
+            SigV4ATrait result = builder.build();
+            result.setNodeCache(objectNode);
+            return result;
+        }
+    }
+}
diff --git a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/auth/SigV4TraitsValidator.java b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/auth/SigV4TraitsValidator.java
new file mode 100644
index 000000000..ed64f87ec
--- /dev/null
+++ b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/auth/SigV4TraitsValidator.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.aws.traits.auth;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import software.amazon.smithy.aws.traits.ServiceTrait;
+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.AbstractValidator;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.utils.SmithyInternalApi;
+
+/**
+ * Validates AWS Service, SigV4, and SigV4A traits.
+ */
+@SmithyInternalApi
+public final class SigV4TraitsValidator extends AbstractValidator {
+    private static final ShapeId SERVICE_ARN_NAMESPACE = ServiceTrait.ID.withMember("arnNamespace");
+    private static final ShapeId SIGV4_NAME = SigV4Trait.ID.withMember("name");
+    private static final ShapeId SIGV4A_NAME = SigV4ATrait.ID.withMember("name");
+
+    @Override
+    public List<ValidationEvent> validate(Model model) {
+        List<ValidationEvent> events = new ArrayList<>();
+        for (ServiceShape service : model.getServiceShapes()) {
+            events.addAll(validateService(model, service));
+        }
+        return events;
+    }
+
+    /**
+     * Validates Service and SigV4 traits.
+     *
+     * - service$arnNamespace, sigv4$name, and sigv4a$name SHOULD be equal. Otherwise, emits warnings.
+     */
+    private List<ValidationEvent> validateService(Model model, ServiceShape service) {
+        List<ValidationEvent> events = new ArrayList<>();
+        Optional<ServiceTrait> serviceTraitOptional = service.getTrait(ServiceTrait.class);
+        Optional<SigV4Trait> sigv4TraitOptional = service.getTrait(SigV4Trait.class);
+        Optional<SigV4ATrait> sigv4aTraitOptional = service.getTrait(SigV4ATrait.class);
+        if (serviceTraitOptional.isPresent()) {
+            String serviceArnNamespace = serviceTraitOptional.get().getArnNamespace();
+            // Check service$arnNamespace with sigv4$name
+            if (sigv4TraitOptional.isPresent()) {
+                String sigv4Name = sigv4TraitOptional.get().getName();
+                if (!serviceArnNamespace.equals(sigv4Name)) {
+                    events.add(createValuesShouldMatchWarning(
+                        service,
+                        SERVICE_ARN_NAMESPACE, serviceArnNamespace,
+                        SIGV4_NAME, sigv4Name));
+                }
+            }
+            // Check service$arnNamespace with sigv4a$name
+            if (sigv4aTraitOptional.isPresent()) {
+                String sigv4aName = sigv4aTraitOptional.get().getName();
+                if (!serviceArnNamespace.equals(sigv4aName)) {
+                    events.add(createValuesShouldMatchWarning(
+                        service,
+                        SERVICE_ARN_NAMESPACE, serviceArnNamespace,
+                        SIGV4A_NAME, sigv4aName));
+                }
+            }
+        }
+        // Check sigv4$name with sigv4a$name
+        if (sigv4TraitOptional.isPresent() && sigv4aTraitOptional.isPresent()) {
+            String sigv4Name = sigv4TraitOptional.get().getName();
+            String sigv4aName = sigv4aTraitOptional.get().getName();
+            if (!sigv4Name.equals(sigv4aName)) {
+                events.add(createValuesShouldMatchWarning(
+                    service,
+                    SIGV4_NAME, sigv4Name,
+                    SIGV4A_NAME, sigv4aName));
+            }
+        }
+        return events;
+    }
+
+    private ValidationEvent createValuesShouldMatchWarning(
+        ServiceShape service,
+        ShapeId member1,
+        String value1,
+        ShapeId member2,
+        String value2
+    ) {
+        return warning(service, String.format(
+            "Value for `%s` \"%s\" and value for `%s` \"%s\" SHOULD match.",
+            member1.toString(), value1, member2.toString(), value2));
+    }
+}
diff --git a/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService
index 77e89519f..6f5b0ca5c 100644
--- a/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService
+++ b/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService
@@ -21,5 +21,6 @@ software.amazon.smithy.aws.traits.protocols.RestXmlTrait$Provider
 software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait$Provider
 software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait$Provider
 software.amazon.smithy.aws.traits.auth.SigV4Trait$Provider
+software.amazon.smithy.aws.traits.auth.SigV4ATrait$Provider
 software.amazon.smithy.aws.traits.tagging.TagEnabledTrait$Provider
 software.amazon.smithy.aws.traits.tagging.TaggableTrait$Provider
diff --git a/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
index eba9e65bd..80a840407 100644
--- a/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
+++ b/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
@@ -11,3 +11,4 @@ software.amazon.smithy.aws.traits.tagging.TaggableResourceValidator
 software.amazon.smithy.aws.traits.tagging.TagResourcePropertyTypeValidator
 software.amazon.smithy.aws.traits.tagging.TagResourcePropertyNameValidator
 software.amazon.smithy.aws.traits.ErrorRenameValidator
+software.amazon.smithy.aws.traits.auth.SigV4TraitsValidator
diff --git a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.auth.smithy b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.auth.smithy
index 7c67bb404..155960783 100644
--- a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.auth.smithy
+++ b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.auth.smithy
@@ -19,18 +19,40 @@ structure cognitoUserPools {
 /// security, most requests to AWS must be signed with an access key, which consists
 /// of an access key ID and secret access key. These two keys are commonly referred to
 /// as your security credentials.
-@authDefinition(traits: [unsignedPayload])
-@externalDocumentation(
-    Reference: "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html"
+@authDefinition(
+    traits: [unsignedPayload]
 )
+@externalDocumentation(Reference: "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html")
 @trait(selector: "service")
 structure sigv4 {
     /// The signature version 4 service signing name to use in the credential
     /// scope when signing requests. This value SHOULD match the `arnNamespace`
-    /// property of the `aws.api#service-trait`.
-    @externalDocumentation(
-        Reference: "https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html"
-    )
+    /// property of the `aws.api#service` trait if present and the `name`
+    /// property of the `aws.api#sigv4a` trait if present.
+    @externalDocumentation(Reference: "https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html")
+    @length(min: 1)
+    @required
+    name: String
+}
+
+/// Signature Version 4 Asymmetric (SigV4A), an extension of Signature Version 4 (SigV4), is the
+/// process to add authentication information to AWS requests sent by HTTP. SigV4A is nearly
+/// identical to SigV4, but also uses public-private keys and asymmetric cryptographic signatures
+/// for every request. Most notably, SigV4A supports signatures for multi-region API requests.
+@authDefinition(
+    traits: [unsignedPayload]
+)
+@externalDocumentation(
+    Reference: "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html"
+    Examples: "https://github.com/aws-samples/sigv4a-signing-examples"
+)
+@trait(selector: "service[trait|aws.auth#sigv4]")
+structure sigv4a {
+    /// The signature version 4a service signing name to use in the credential
+    /// scope when signing requests. This value SHOULD match the `arnNamespace`
+    /// property of the `aws.api#service` trait if present and the `name`
+    /// property of the `aws.api#sigv4` trait.
+    @externalDocumentation(Reference: "https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html")
     @length(min: 1)
     @required
     name: String
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/ExamplesTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/ExamplesTrait.java
index 5dbac8017..486c51fcd 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/ExamplesTrait.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/ExamplesTrait.java
@@ -337,6 +337,23 @@ public final class ExamplesTrait extends AbstractTrait implements ToSmithyBuilde
                 return this;
             }
         }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(shapeId, content);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other == this) {
+                return true;
+            }
+            if (other == null || other.getClass() != this.getClass()) {
+                return false;
+            }
+            ErrorExample otherExample = (ErrorExample) other;
+            return Objects.equals(shapeId, otherExample.shapeId) && Objects.equals(content, otherExample.content);
+        }
     }
 
     public static final class Provider implements TraitService {


Test patch:
diff --git a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/auth/SigV4ATraitTest.java b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/auth/SigV4ATraitTest.java
new file mode 100644
index 000000000..5a2b0abc4
--- /dev/null
+++ b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/auth/SigV4ATraitTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.aws.traits.auth;
+
+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.assertTrue;
+
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+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.ShapeId;
+import software.amazon.smithy.model.traits.Trait;
+import software.amazon.smithy.model.traits.TraitFactory;
+
+public class SigV4ATraitTest {
+    private static final String MOCK_SIGNING_NAME = "mocksigningname";
+    private static final ShapeId MOCK_TARGET = ShapeId.from("ns.qux#foo");
+
+    @Test
+    public void loadsTrait() {
+        Node node = ObjectNode.builder()
+            .withMember("name", StringNode.from(MOCK_SIGNING_NAME))
+            .build();
+        TraitFactory provider = TraitFactory.createServiceFactory();
+        Optional<Trait> trait = provider.createTrait(SigV4ATrait.ID, MOCK_TARGET, node);
+
+        assertTrue(trait.isPresent());
+        assertThat(trait.get(), instanceOf(SigV4ATrait.class));
+        SigV4ATrait sigv4aTrait = (SigV4ATrait) trait.get();
+        assertFalse(sigv4aTrait.getName().isEmpty());
+        assertThat(sigv4aTrait.getName(), equalTo(MOCK_SIGNING_NAME));
+        assertThat(sigv4aTrait.toNode(), equalTo(node));
+        assertThat(sigv4aTrait.toBuilder().build(), equalTo(sigv4aTrait));
+    }
+}
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4-service-mismatch.errors b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4-service-mismatch.errors
new file mode 100644
index 000000000..4c070e372
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4-service-mismatch.errors
@@ -0,0 +1,1 @@
+[WARNING] smithy.example#InvalidService: Value for `aws.api#service$arnNamespace` "invalidservice" and value for `aws.auth#sigv4$name` "signingname" SHOULD match. | SigV4Traits
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4-service-mismatch.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4-service-mismatch.smithy
new file mode 100644
index 000000000..50b9e51b6
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4-service-mismatch.smithy
@@ -0,0 +1,12 @@
+$version: "2.0"
+
+namespace smithy.example
+
+use aws.api#service
+use aws.auth#sigv4
+
+@service(sdkId: "servicename")
+@sigv4(name: "signingname")
+service InvalidService {
+    version: "2020-07-02"
+}
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4-sigv4a-mismatch.errors b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4-sigv4a-mismatch.errors
new file mode 100644
index 000000000..0e4a9d570
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4-sigv4a-mismatch.errors
@@ -0,0 +1,1 @@
+[WARNING] smithy.example#InvalidService: Value for `aws.auth#sigv4$name` "sigv4signingname" and value for `aws.auth#sigv4a$name` "sigv4asigningname" SHOULD match. | SigV4Traits
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4-sigv4a-mismatch.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4-sigv4a-mismatch.smithy
new file mode 100644
index 000000000..803527de3
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4-sigv4a-mismatch.smithy
@@ -0,0 +1,13 @@
+$version: "2.0"
+
+namespace smithy.example
+
+use aws.auth#sigv4
+use aws.auth#sigv4a
+
+@auth([sigv4a, sigv4])
+@sigv4(name: "sigv4signingname")
+@sigv4a(name: "sigv4asigningname")
+service InvalidService {
+    version: "2020-07-02"
+}
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-empty-name.errors b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-empty-name.errors
new file mode 100644
index 000000000..6965fa031
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-empty-name.errors
@@ -0,0 +1,2 @@
+[ERROR] smithy.example#InvalidService: Error validating trait `aws.auth#sigv4a`.name: String value provided for `aws.auth#sigv4a$name` must be >= 1 characters, but the provided value is only 0 characters. | TraitValue
+[WARNING] smithy.example#InvalidService: Value for `aws.auth#sigv4$name` "signingname" and value for `aws.auth#sigv4a$name` "" SHOULD match. | SigV4Traits
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-empty-name.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-empty-name.smithy
new file mode 100644
index 000000000..ef73c04f0
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-empty-name.smithy
@@ -0,0 +1,13 @@
+$version: "2.0"
+
+namespace smithy.example
+
+use aws.auth#sigv4
+use aws.auth#sigv4a
+
+@auth([sigv4a, sigv4])
+@sigv4(name: "signingname")
+@sigv4a(name: "")
+service InvalidService {
+    version: "2020-07-02"
+}
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-no-sigv4.errors b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-no-sigv4.errors
new file mode 100644
index 000000000..cef5bbdd1
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-no-sigv4.errors
@@ -0,0 +1,1 @@
+[ERROR] smithy.example#InvalidService: Trait `aws.auth#sigv4a` cannot be applied to `smithy.example#InvalidService`. This trait may only be applied to shapes that match the following selector: service[trait|aws.auth#sigv4] | TraitTarget
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-no-sigv4.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-no-sigv4.smithy
new file mode 100644
index 000000000..13b5f3644
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-no-sigv4.smithy
@@ -0,0 +1,10 @@
+$version: "2.0"
+
+namespace smithy.example
+
+use aws.auth#sigv4a
+
+@sigv4a(name: "signingname")
+service InvalidService {
+    version: "2020-07-02"
+}
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-service-mismatch.errors b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-service-mismatch.errors
new file mode 100644
index 000000000..469c9815d
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-service-mismatch.errors
@@ -0,0 +1,2 @@
+[WARNING] smithy.example#InvalidService: Value for `aws.api#service$arnNamespace` "invalidservice" and value for `aws.auth#sigv4a$name` "signingname" SHOULD match. | SigV4Traits
+[WARNING] smithy.example#InvalidService: Value for `aws.auth#sigv4$name` "invalidservice" and value for `aws.auth#sigv4a$name` "signingname" SHOULD match. | SigV4Traits
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-service-mismatch.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-service-mismatch.smithy
new file mode 100644
index 000000000..1c7c3a864
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/auth/sigv4a-service-mismatch.smithy
@@ -0,0 +1,15 @@
+$version: "2.0"
+
+namespace smithy.example
+
+use aws.api#service
+use aws.auth#sigv4
+use aws.auth#sigv4a
+
+@service(sdkId: "servicename")
+@auth([sigv4a, sigv4])
+@sigv4(name: "invalidservice")
+@sigv4a(name: "signingname")
+service InvalidService {
+    version: "2020-07-02"
+}
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/traits/ExamplesTraitTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/traits/ExamplesTraitTest.java
index 37c3dab47..761e0fd99 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/traits/ExamplesTraitTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/traits/ExamplesTraitTest.java
@@ -17,6 +17,7 @@ package software.amazon.smithy.model.traits;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.instanceOf;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -27,6 +28,8 @@ import software.amazon.smithy.model.node.ArrayNode;
 import software.amazon.smithy.model.node.Node;
 import software.amazon.smithy.model.node.ObjectNode;
 import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.traits.ExamplesTrait.ErrorExample;
+import software.amazon.smithy.model.traits.ExamplesTrait.Example;
 
 public class ExamplesTraitTest {
     @Test
@@ -81,4 +84,24 @@ public class ExamplesTraitTest {
         assertFalse(serialized.get(1).get().asObjectNode().get().getMember("allowConstraintErrors").isPresent());
         assertTrue(serialized.get(2).get().asObjectNode().get().getMember("allowConstraintErrors").isPresent());
     }
+
+    @Test
+    public void exampleEqualsWorks() {
+        ObjectNode input = Node.objectNode().withMember("a", Node.from("b"));
+        ObjectNode output = Node.objectNode().withMember("c", Node.from("d"));
+        ErrorExample errorExample1 = ErrorExample.builder().shapeId(ShapeId.from("smithy.example#FooError")).content(Node.objectNode()
+                .withMember("e", Node.from("f"))).build();
+        ErrorExample errorExample2 = ErrorExample.builder().shapeId(ShapeId.from("smithy.example#FooError")).content(Node.objectNode()
+                .withMember("e", Node.from("f"))).build();
+        ErrorExample errorExample3 = ErrorExample.builder().shapeId(ShapeId.from("smithy.example#FooError")).content(Node.objectNode()
+                .withMember("g", Node.from("h"))).build();
+        Example example1 = Example.builder().title("foo").documentation("docs").input(input).output(output).error(errorExample1).build();
+        Example example2 = Example.builder().title("foo").documentation("docs").input(input).output(output).error(errorExample2).build();
+        Example example3 = Example.builder().title("foo").documentation("docs").input(input).output(output).error(errorExample3).build();
+        assertThat(errorExample1, equalTo(errorExample2));
+        assertThat(errorExample1, not(equalTo(errorExample3)));
+        assertThat(example1, equalTo(example2));
+        assertThat(example1, not(equalTo(example3)));
+    }
+
 }