From 28faa492ba373c7863ecd2fa44eb3aa770f8a729 Mon Sep 17 00:00:00 2001 From: kstich Date: Wed, 11 Aug 2021 16:26:49 -0700 Subject: [PATCH] Add error support to examples trait This commit adds the ability to specify an error in the examples trait. The error specified must be bound to the operation the trait is applied to. --- .../1.0/spec/core/documentation-traits.rst | 46 ++++++++- .../smithy/model/traits/ExamplesTrait.java | 97 ++++++++++++++++++- .../validators/ExamplesTraitValidator.java | 21 +++- .../amazon/smithy/model/loader/prelude.smithy | 12 ++- .../model/traits/ExamplesTraitTest.java | 5 +- .../examples-trait-validator.errors | 2 + .../validators/examples-trait-validator.json | 31 +++++- 7 files changed, 203 insertions(+), 11 deletions(-) diff --git a/docs/source/1.0/spec/core/documentation-traits.rst b/docs/source/1.0/spec/core/documentation-traits.rst index 3f8c32bb415..79eefbb786b 100644 --- a/docs/source/1.0/spec/core/documentation-traits.rst +++ b/docs/source/1.0/spec/core/documentation-traits.rst @@ -202,6 +202,10 @@ Each ``example`` trait value is a structure with the following members: - Provides example output parameters for the operation. Each key is the name of a top-level output structure member, and each value is the value of the member. + * - error + - :ref:`examples-ErrorExample-structure` + - Provides an error shape ID and example error parameters for the + operation. The values provided for the ``input`` and ``output`` members MUST be compatible with the shapes and constraints of the corresponding structure. @@ -215,7 +219,8 @@ These values use the same semantics and format as @readonly operation MyOperation { input: MyOperationInput, - output: MyOperationOutput + output: MyOperationOutput, + errors: [MyOperationError] } apply MyOperation @examples([ @@ -237,9 +242,48 @@ These values use the same semantics and format as status: "PENDING", } }, + { + title: "Error example for MyOperation", + input: { + foo: 1, + }, + error: { + shapeId: MyOperationError, + content: { + message: "Invalid 'foo'", + } + } + }, ]) +.. _examples-ErrorExample-structure: + +``ErrorExample`` structure +========================== + +The ``ErrorExample`` structure defines an error example using the following +members: + +.. list-table:: + :header-rows: 1 + :widths: 10 10 80 + + * - Property + - Type + - Description + * - shapeId + - :ref:`shape-id` + - The shape ID of the error in this example. This shape ID MUST be of + a structure shape with the error trait. The structure shape MUST be + bound as an error to the operation this example trait is applied to. + * - content + - ``document`` + - Provides example error parameters for the operation. Each key is + the name of a top-level error structure member, and each value is the + value of the member. + + .. smithy-trait:: smithy.api#externalDocumentation .. _externalDocumentation-trait: 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 187c0a5de1c..2e935b21990 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 @@ -100,12 +100,14 @@ public static final class Example implements ToNode, ToSmithyBuilder { private final String documentation; private final ObjectNode input; private final ObjectNode output; + private final ErrorExample error; private Example(Builder builder) { this.title = Objects.requireNonNull(builder.title, "Example title must not be null"); this.documentation = builder.documentation; this.input = builder.input; this.output = builder.output; + this.error = builder.error; } /** @@ -136,11 +138,19 @@ public ObjectNode getOutput() { return output; } + /** + * @return Gets the error example. + */ + public Optional getError() { + return Optional.ofNullable(error); + } + @Override public Node toNode() { ObjectNode.Builder builder = Node.objectNodeBuilder() .withMember("title", Node.from(title)) - .withOptionalMember("documentation", getDocumentation().map(Node::from)); + .withOptionalMember("documentation", getDocumentation().map(Node::from)) + .withOptionalMember("error", getError().map(ErrorExample::toNode)); if (!input.isEmpty()) { builder.withMember("input", input); @@ -154,7 +164,7 @@ public Node toNode() { @Override public Builder toBuilder() { - return new Builder().documentation(documentation).title(title).input(input).output(output); + return new Builder().documentation(documentation).title(title).input(input).output(output).error(error); } public static Builder builder() { @@ -169,6 +179,7 @@ public static final class Builder implements SmithyBuilder { private String documentation; private ObjectNode input = Node.objectNode(); private ObjectNode output = Node.objectNode(); + private ErrorExample error; @Override public Example build() { @@ -194,6 +205,79 @@ public Builder output(ObjectNode output) { this.output = output; return this; } + + public Builder error(ErrorExample error) { + this.error = error; + return this; + } + } + } + + public static final class ErrorExample implements ToNode, ToSmithyBuilder { + private final ShapeId shapeId; + private final ObjectNode content; + + public ErrorExample(Builder builder) { + this.shapeId = builder.shapeId; + this.content = builder.content; + } + + public static ErrorExample fromNode(ObjectNode node) { + return builder() + .shapeId(node.expectStringMember("shapeId").expectShapeId()) + .content(node.expectObjectMember("content")) + .build(); + } + + /** + * @return Gets the error shape id for the example. + */ + public ShapeId getShapeId() { + return shapeId; + } + + /** + * @return Gets the error object. + */ + public ObjectNode getContent() { + return content; + } + + @Override + public Node toNode() { + return ObjectNode.objectNodeBuilder() + .withMember("shapeId", shapeId.toString()) + .withMember("content", content) + .build(); + } + + @Override + public SmithyBuilder toBuilder() { + return builder().content(content).shapeId(shapeId); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder implements SmithyBuilder { + private ShapeId shapeId; + private ObjectNode content = Node.objectNode(); + + @Override + public ErrorExample build() { + return new ErrorExample(this); + } + + public Builder shapeId(ShapeId shapeId) { + this.shapeId = shapeId; + return this; + } + + public Builder content(ObjectNode content) { + this.content = content; + return this; + } } } @@ -213,12 +297,15 @@ public ExamplesTrait createTrait(ShapeId target, Node value) { } private static Example exampleFromNode(ObjectNode node) { - return Example.builder() + Example.Builder builder = Example.builder() .title(node.expectStringMember("title").getValue()) .documentation(node.getStringMember("documentation").map(StringNode::getValue).orElse(null)) .input(node.getMember("input").map(Node::expectObjectNode).orElseGet(Node::objectNode)) - .output(node.getMember("output").map(Node::expectObjectNode).orElseGet(Node::objectNode)) - .build(); + .output(node.getMember("output").map(Node::expectObjectNode).orElseGet(Node::objectNode)); + + node.getObjectMember("error").map(ErrorExample::fromNode).map(builder::error); + + return builder.build(); } } } 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 4c3279fac93..4d22b548f4d 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 @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.OperationShape; @@ -53,9 +54,11 @@ private List validateExamples(Model model, OperationShape shape events.addAll(input.accept(validator)); }); } else if (!example.getInput().isEmpty()) { - events.add(error(shape, trait, String.format("Input parameters provided for operation with no " - + "input structure members: `%s`", example.getTitle()))); + events.add(error(shape, trait, String.format( + "Input parameters provided for operation with no input structure members: `%s`", + example.getTitle()))); } + if (shape.getOutput().isPresent()) { model.getShape(shape.getOutput().get()).ifPresent(output -> { NodeValidationVisitor validator = createVisitor( @@ -67,6 +70,20 @@ private List validateExamples(Model model, OperationShape shape "Output parameters provided for operation with no output structure members: `%s`", example.getTitle()))); } + + if (example.getError().isPresent()) { + ExamplesTrait.ErrorExample errorExample = example.getError().get(); + Optional errorShape = model.getShape(errorExample.getShapeId()); + if (errorShape.isPresent() && shape.getErrors().contains(errorExample.getShapeId())) { + NodeValidationVisitor validator = createVisitor( + "error", errorExample.getContent(), model, shape, example); + events.addAll(errorShape.get().accept(validator)); + } else { + events.add(error(shape, trait, String.format( + "Error parameters provided for operation without the `%s` error: `%s`", + errorExample.getShapeId(), example.getTitle()))); + } + } } return events; diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy index f4b83d4f2e3..8264cec438b 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy @@ -231,7 +231,17 @@ structure Example { input: Document, - output: Document + output: Document, + + error: ExampleError, +} + +@private +structure ExampleError { + @idRef(selector: "structure[trait|error]") + shapeId: String, + + content: Document, } /// Indicates that a structure shape represents an error. 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 d85111b1431..dfbcca76e92 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 @@ -37,7 +37,10 @@ public void loadsTrait() { .withMember("title", Node.from("qux")) .withMember("documentation", Node.from("docs")) .withMember("input", Node.objectNode().withMember("a", Node.from("b"))) - .withMember("output", Node.objectNode().withMember("c", Node.from("d")))); + .withMember("output", Node.objectNode().withMember("c", Node.from("d"))) + .withMember("error", Node.objectNode() + .withMember(Node.from("shapeId"), Node.from("smithy.example#FooError")) + .withMember(Node.from("content"), Node.objectNode().withMember("e", Node.from("f"))))); Optional trait = provider.createTrait( ShapeId.from("smithy.api#examples"), ShapeId.from("ns.qux#foo"), node); diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-validator.errors index 0be6d518cf5..e5e6a1ca75a 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-validator.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-validator.errors @@ -1,5 +1,7 @@ [ERROR] ns.foo#Operation2: Input parameters provided for operation with no input structure members: `Testing 3` | ExamplesTrait [ERROR] ns.foo#Operation2: Output parameters provided for operation with no output structure members: `Testing 3` | ExamplesTrait +[ERROR] ns.foo#Operation2: Error parameters provided for operation without the `ns.foo#OperationError` error: `Testing 3` | ExamplesTrait [ERROR] ns.foo#Operation: Example input of `Testing 2`: Missing required structure member `foo` for `ns.foo#OperationInput` | ExamplesTrait [WARNING] ns.foo#Operation: Example output of `Testing 2`: Invalid structure member `additional` found for `ns.foo#OperationOutput` | ExamplesTrait [ERROR] ns.foo#Operation: Example output of `Testing 2`: Missing required structure member `bam` for `ns.foo#OperationOutput` | ExamplesTrait +[WARNING] ns.foo#Operation: Example error of `Testing 1`: Invalid structure member `extra` found for `ns.foo#OperationError` | ExamplesTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-validator.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-validator.json index d548c89db33..1de6c6a622c 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-validator.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/examples-trait-validator.json @@ -9,6 +9,11 @@ "output": { "target": "ns.foo#OperationOutput" }, + "errors": [ + { + "target": "ns.foo#OperationError" + } + ], "traits": { "smithy.api#readonly": {}, "smithy.api#examples": [ @@ -19,6 +24,13 @@ }, "output": { "bam": "value2" + }, + "error": { + "shapeId": "ns.foo#OperationError", + "content": { + "bat": "baz", + "extra": "field" + } } }, { @@ -33,6 +45,17 @@ ] } }, + "ns.foo#OperationError": { + "type": "structure", + "members": { + "bat": { + "target": "ns.foo#String" + } + }, + "traits": { + "smithy.api#error": "client" + } + }, "ns.foo#OperationInput": { "type": "structure", "members": { @@ -69,7 +92,13 @@ "foo": "baz" }, "output": { - "foo": "baz" + "bam": "baz" + }, + "error": { + "shapeId": "ns.foo#OperationError", + "content": { + "bat": "baz" + } } } ]