Skip to content

Commit

Permalink
Add error support to examples trait
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kstich committed Aug 12, 2021
1 parent 9cfa328 commit 28faa49
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 11 deletions.
46 changes: 45 additions & 1 deletion docs/source/1.0/spec/core/documentation-traits.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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([
Expand All @@ -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:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,14 @@ public static final class Example implements ToNode, ToSmithyBuilder<Example> {
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;
}

/**
Expand Down Expand Up @@ -136,11 +138,19 @@ public ObjectNode getOutput() {
return output;
}

/**
* @return Gets the error example.
*/
public Optional<ErrorExample> 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);
Expand All @@ -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() {
Expand All @@ -169,6 +179,7 @@ public static final class Builder implements SmithyBuilder<Example> {
private String documentation;
private ObjectNode input = Node.objectNode();
private ObjectNode output = Node.objectNode();
private ErrorExample error;

@Override
public Example build() {
Expand All @@ -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<ErrorExample> {
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<ErrorExample> toBuilder() {
return builder().content(content).shapeId(shapeId);
}

public static Builder builder() {
return new Builder();
}

public static final class Builder implements SmithyBuilder<ErrorExample> {
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;
}
}
}

Expand All @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,9 +54,11 @@ private List<ValidationEvent> 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(
Expand All @@ -67,6 +70,20 @@ private List<ValidationEvent> 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<Shape> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> trait = provider.createTrait(
ShapeId.from("smithy.api#examples"), ShapeId.from("ns.qux#foo"), node);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"output": {
"target": "ns.foo#OperationOutput"
},
"errors": [
{
"target": "ns.foo#OperationError"
}
],
"traits": {
"smithy.api#readonly": {},
"smithy.api#examples": [
Expand All @@ -19,6 +24,13 @@
},
"output": {
"bam": "value2"
},
"error": {
"shapeId": "ns.foo#OperationError",
"content": {
"bat": "baz",
"extra": "field"
}
}
},
{
Expand All @@ -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": {
Expand Down Expand Up @@ -69,7 +92,13 @@
"foo": "baz"
},
"output": {
"foo": "baz"
"bam": "baz"
},
"error": {
"shapeId": "ns.foo#OperationError",
"content": {
"bat": "baz"
}
}
}
]
Expand Down

0 comments on commit 28faa49

Please sign in to comment.