Repository: smithy-lang/smithy (java)

Issue:
Can't provide error examples for an erorr applied at service level, as opposed to operation level
if I have:

```smithy
operation GetFoo {
    errors: [MyError1]
}

service MtService {
    operations: [
        GetFoo
    ]
    errors: [
        MyError2
    ]
}
```

I can happily provide an example for `MyError1`:

```smithy
apply GetFoo @examples([
    {
        title: "MyError1"
        error: {
            shapeId: MyError1
            content: {...}
        }
    }
])

```

but I can't do it for `MyError2` this validation stops me from doing so:

https://github.com/smithy-lang/smithy/blob/0f2ddf70dc764206dd1e12e64d7555da251a947a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExamplesTraitValidator.java#L78-L86

is this an oversight? 

Pull request:
Allow examples for service level errors
This commit enables validating error examples for operations with errors that are bound to services. All services the operation with said example are bound to must bind the error.

Fixes #2299 

---
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/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExamplesTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExamplesTraitValidator.java
index 5f4913e3b..43ad440f2 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExamplesTraitValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExamplesTraitValidator.java
@@ -18,10 +18,14 @@ package software.amazon.smithy.model.validation.validators;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.knowledge.TopDownIndex;
 import software.amazon.smithy.model.node.ObjectNode;
 import software.amazon.smithy.model.shapes.OperationShape;
+import software.amazon.smithy.model.shapes.ServiceShape;
 import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.traits.ExamplesTrait;
 import software.amazon.smithy.model.validation.AbstractValidator;
 import software.amazon.smithy.model.validation.NodeValidationVisitor;
@@ -75,7 +79,11 @@ public final class ExamplesTraitValidator extends AbstractValidator {
             } else if (isErrorDefined) {
                 ExamplesTrait.ErrorExample errorExample = example.getError().get();
                 Optional<Shape> errorShape = model.getShape(errorExample.getShapeId());
-                if (errorShape.isPresent() && shape.getErrors().contains(errorExample.getShapeId())) {
+                if (errorShape.isPresent() && (
+                        // The error is directly bound to the operation.
+                        shape.getErrors().contains(errorExample.getShapeId())
+                        // The error is bound to all services that contain the operation.
+                        || servicesContainError(model, shape, errorExample.getShapeId()))) {
                     NodeValidationVisitor validator = createVisitor(
                             "error", errorExample.getContent(), model, shape, example);
                     events.addAll(errorShape.get().accept(validator));
@@ -90,6 +98,30 @@ public final class ExamplesTraitValidator extends AbstractValidator {
         return events;
     }
 
+    private boolean servicesContainError(Model model, OperationShape shape, ShapeId errorId) {
+        TopDownIndex topDownIndex = TopDownIndex.of(model);
+
+        Set<ServiceShape> services = model.getServiceShapes();
+        if (services.isEmpty()) {
+            return false;
+        }
+
+        for (ServiceShape service : services) {
+            // Skip if the service doesn't have the operation.
+            if (!topDownIndex.getContainedOperations(service).contains(shape)) {
+                continue;
+            }
+
+            // We've already checked if the operation contains the error,
+            // so a service having no errors means we've failed.
+            if (service.getErrors().isEmpty() || !service.getErrors().contains(errorId)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
     private NodeValidationVisitor createVisitor(
             String name,
             ObjectNode value,


Test patch:
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-service-errors.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-service-errors.errors
new file mode 100644
index 000000000..2af2c1a71
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-service-errors.errors
@@ -0,0 +1,3 @@
+[ERROR] ns.foo#WithError: Error parameters provided for operation without the `ns.foo#ServiceError` error: `Testing 2` | ExamplesTrait
+[ERROR] ns.foo#WithoutError: Error parameters provided for operation without the `ns.foo#OperationError` error: `Testing 3` | ExamplesTrait
+[ERROR] ns.foo#WithoutError: Error parameters provided for operation without the `ns.foo#ServiceError` error: `Testing 4` | ExamplesTrait
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-service-errors.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-service-errors.smithy
new file mode 100644
index 000000000..2af87d9ab
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-service-errors.smithy
@@ -0,0 +1,109 @@
+$version: "2"
+
+namespace ns.foo
+
+service ErrorBound {
+    version: "2020-07-02"
+    operations: [
+        WithError
+        WithoutError
+    ]
+    errors: [ServiceError]
+}
+
+service ErrorBound2 {
+    version: "2020-07-02"
+    operations: [
+        WithoutError2
+    ]
+    errors: [ServiceError2]
+}
+
+service NoError {
+    version: "2020-07-02"
+    operations: [
+        WithError
+        WithoutError
+    ]
+}
+
+@examples([
+    {
+        "title": "Testing 1",
+        "error": {
+            "shapeId": "ns.foo#OperationError"
+            "content": {
+                "foo": "baz"
+            }
+        }
+    }
+    {
+        "title": "Testing 2"
+        "error": {
+            "shapeId": "ns.foo#ServiceError"
+            "content": {
+                "foo": "baz"
+            }
+        }
+    }
+])
+operation WithError {
+    input := {}
+    errors: [OperationError]
+}
+
+@examples([
+    {
+        "title": "Testing 3",
+        "error": {
+            "shapeId": "ns.foo#OperationError"
+            "content": {
+                "foo": "baz"
+            }
+        }
+    }
+    {
+        "title": "Testing 4"
+        "error": {
+            "shapeId": "ns.foo#ServiceError"
+            "content": {
+                "foo": "baz"
+            }
+        }
+    }
+])
+operation WithoutError {
+    input := {}
+    output := {}
+}
+
+@examples([
+    {
+        "title": "Testing 5"
+        "error": {
+            "shapeId": "ns.foo#ServiceError2"
+            "content": {
+                "foo": "baz"
+            }
+        }
+    }
+])
+operation WithoutError2 {
+    input := {}
+    output := {}
+}
+
+@error("client")
+structure ServiceError {
+    foo: String
+}
+
+@error("client")
+structure ServiceError2 {
+    foo: String
+}
+
+@error("client")
+structure OperationError {
+    foo: String
+}