Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error support to examples trait #888

Merged
merged 1 commit into from
Aug 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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