Skip to content

Commit

Permalink
Merge pull request #76 from disneystreaming/data-examples-enhancements
Browse files Browse the repository at this point in the history
enhance data examples trait
  • Loading branch information
daddykotex authored Apr 18, 2023
2 parents f3006dc + 13d5977 commit 579c454
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 25 deletions.
20 changes: 18 additions & 2 deletions modules/core/resources/META-INF/smithy/examples.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,23 @@ $version: "2"

namespace alloy

/// A trait for specifying what example data looks like. Differs from the `smithy.api#examples` trait in that
/// it can be used for any shape, not just operations. Below is an explanation of the different example formats
/// that are supported.
/// 1. SMITHY - this means that the examples will be using the `Document` abstraction and will be specified in
/// a protocol agnostic way
/// 2. JSON - this means the examples will use the `Document` abstraction, but will not be validated by the smithy
/// `NodeValidationVisitor` like the first type are. This type can be used to specify protocol specific examples
/// 3. STRING - this is just a string example and anything can be provided inside of the string.
/// This can be helpful for showing e.g. xml or another encoding that isn't JSON and therefore doesn't fit nicely
/// with `Node` semantics
@trait(selector: ":not(:test(service, operation, resource))")
list dataExamples {
member: Document
}
member: DataExample
}

union DataExample {
smithy: Document
json: Document
string: String
}
56 changes: 48 additions & 8 deletions modules/core/src/alloy/DataExamplesTrait.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

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.AbstractTrait;
import software.amazon.smithy.model.traits.AbstractTraitBuilder;
Expand All @@ -26,24 +27,49 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public final class DataExamplesTrait extends AbstractTrait implements ToSmithyBuilder<DataExamplesTrait> {
public static final ShapeId ID = ShapeId.from("alloy#dataExamples");

private final List<Node> examples;
public enum DataExampleType {
STRING, JSON, SMITHY
}

public static final class DataExample {
private final DataExampleType exampleType;
private final Node content;

public DataExample(DataExampleType type, Node content) {
this.exampleType = type;
this.content = content;
}

public DataExampleType getExampleType() {
return exampleType;
}

public Node getContent() {
return content;
}
}

private final List<DataExample> examples;

private DataExamplesTrait(Builder builder) {
super(ID, builder.getSourceLocation());
this.examples = new ArrayList<>(builder.examples);
}

public List<Node> getExamples() {
public List<DataExample> getExamples() {
return examples;
}

@Override
protected Node createNode() {
return examples.stream().collect(ArrayNode.collect(getSourceLocation()));
return examples.stream()
.map(ex -> ObjectNode.builder().withMember(ex.exampleType.name().toLowerCase(), ex.content).build())
.collect(ArrayNode.collect(getSourceLocation()));
}

@Override
Expand All @@ -61,9 +87,9 @@ public static Builder builder() {
}

public static final class Builder extends AbstractTraitBuilder<DataExamplesTrait, Builder> {
private final List<Node> examples = new ArrayList<>();
private final List<DataExample> examples = new ArrayList<>();

public Builder addExample(Node example) {
public Builder addExample(DataExample example) {
examples.add(Objects.requireNonNull(example));
return this;
}
Expand All @@ -81,13 +107,27 @@ public DataExamplesTrait build() {

public static final class Provider implements TraitService {
@Override
public ShapeId getShapeId() {
public ShapeId getShapeId() {
return ID;
}

public DataExamplesTrait createTrait(ShapeId target, Node value) {
public DataExamplesTrait createTrait(ShapeId target, Node value) {
Builder builder = builder().sourceLocation(value);
value.expectArrayNode().forEach(builder::addExample);
value.expectArrayNode().forEach(node -> {
Optional<ObjectNode> maybeNode = node.asObjectNode();
if (maybeNode.isPresent()) {
DataExampleType type;
if (maybeNode.get().containsMember("smithy")) {
type = DataExampleType.SMITHY;
} else if (maybeNode.get().containsMember("json")) {
type = DataExampleType.JSON;
} else {
type = DataExampleType.STRING;
}
Node n = maybeNode.get().expectMember(type.name().toLowerCase());
builder.addExample(new DataExample(type, n));
}
});
DataExamplesTrait result = builder.build();
result.setNodeCache(value);
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ public List<ValidationEvent> validate(Model model) {
List<ValidationEvent> events = new ArrayList<>();
for (Shape shape : model.getShapesWithTrait(DataExamplesTrait.class)) {
DataExamplesTrait trt = shape.getTrait(DataExamplesTrait.class).get();
for (Node example : trt.getExamples()) {
NodeValidationVisitor visitor = createVisitor(example, model, shape);
events.addAll(shape.accept(visitor));
for (DataExamplesTrait.DataExample example : trt.getExamples()) {
if (example.getExampleType() == DataExamplesTrait.DataExampleType.SMITHY) {
NodeValidationVisitor visitor = createVisitor(example.getContent(), model, shape);
events.addAll(shape.accept(visitor));
}
}
}

Expand Down
30 changes: 26 additions & 4 deletions modules/core/test/resources/META-INF/smithy/traits.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,37 @@ union OtherUnion {

@dataExamples([
{
one: "numberOne",
two: 2
smithy: {
one: "numberOne",
two: 2
}
},
{
one: "numberOneAgain",
two: 22
smithy: {
one: "numberOneAgain",
two: 22
}
}
])
structure TestExamples {
one: String
two: Integer
}

@dataExamples([{
json: {
test: "numberOne"
}
}])
structure TestJsonExamples {
one: String
two: Integer
}

@dataExamples([{
string: "test"
}])
structure TestStringExamples {
one: String
two: Integer
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import scala.jdk.CollectionConverters._
import software.amazon.smithy.model.validation.Severity
import software.amazon.smithy.model.node.StringNode
import software.amazon.smithy.model.SourceLocation
import software.amazon.smithy.model.node.NumberNode
import software.amazon.smithy.model.shapes.IntegerShape

final class DataExamplesTraitValidatorSpec extends munit.FunSuite {

Expand All @@ -33,7 +35,12 @@ final class DataExamplesTraitValidatorSpec extends munit.FunSuite {
test("find when node does not match shape") {
val example = DataExamplesTrait
.builder()
.addExample(ObjectNode.builder().withMember("something", false).build())
.addExample(
new DataExamplesTrait.DataExample(
DataExamplesTrait.DataExampleType.SMITHY,
ObjectNode.builder().withMember("something", false).build()
)
)
.build()
val shape = StringShape
.builder()
Expand All @@ -59,7 +66,12 @@ final class DataExamplesTraitValidatorSpec extends munit.FunSuite {
test("no errors when node matches shape") {
val example = DataExamplesTrait
.builder()
.addExample(new StringNode("something", SourceLocation.NONE))
.addExample(
new DataExamplesTrait.DataExample(
DataExamplesTrait.DataExampleType.SMITHY,
new StringNode("something", SourceLocation.NONE)
)
)
.build()
val shape = StringShape
.builder()
Expand All @@ -72,4 +84,46 @@ final class DataExamplesTraitValidatorSpec extends munit.FunSuite {
assertEquals(result, expected)
}

test("no errors when JSON type") {
val example = DataExamplesTrait
.builder()
.addExample(
new DataExamplesTrait.DataExample(
DataExamplesTrait.DataExampleType.JSON,
new NumberNode(1, SourceLocation.NONE)
)
)
.build()
val shape = StringShape
.builder()
.id(ShapeId.fromParts("test", "TestString"))
.addTrait(example)
.build()
val model = Model.builder().addShape(shape).build()
val result = validator.validate(model).asScala.toList
val expected = List.empty
assertEquals(result, expected)
}

test("no errors when STRING type") {
val example = DataExamplesTrait
.builder()
.addExample(
new DataExamplesTrait.DataExample(
DataExamplesTrait.DataExampleType.STRING,
new StringNode("something", SourceLocation.NONE)
)
)
.build()
val shape = IntegerShape
.builder()
.id(ShapeId.fromParts("test", "TestString"))
.addTrait(example)
.build()
val model = Model.builder().addShape(shape).build()
val result = validator.validate(model).asScala.toList
val expected = List.empty
assertEquals(result, expected)
}

}
18 changes: 15 additions & 3 deletions modules/openapi/src/alloy/openapi/DataExamplesMapper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import _root_.software.amazon.smithy.model.shapes.Shape

import scala.jdk.CollectionConverters._
import alloy.DataExamplesTrait
import software.amazon.smithy.model.node.ObjectNode
import software.amazon.smithy.model.node.Node

class DataExamplesMapper() extends JsonSchemaMapper {

Expand All @@ -36,9 +38,19 @@ class DataExamplesMapper() extends JsonSchemaMapper {
.getExamples()
.asScala
.headOption match {
case Some(example) =>
schemaBuilder.putExtension("example", example)
case None => schemaBuilder
case Some(example)
if example.getExampleType != DataExamplesTrait.DataExampleType.STRING =>
schemaBuilder.putExtension("example", example.getContent())
case Some(example)
if example.getExampleType == DataExamplesTrait.DataExampleType.STRING =>
val maybeStrNode = example.getContent().asStringNode()
val res = if (maybeStrNode.isPresent) {
Node.parse(maybeStrNode.get.getValue)
} else {
ObjectNode.builder().build()
}
schemaBuilder.putExtension("example", res)
case _ => schemaBuilder
}
} else schemaBuilder
}
6 changes: 6 additions & 0 deletions modules/openapi/test/resources/foo.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@
"breed": {
"type": "string"
}
},
"example": {
"name": "Woof"
}
},
"DoubleOrFloat": {
Expand Down Expand Up @@ -263,6 +266,9 @@
"$ref": "#/components/schemas/SomeValue"
}
}
},
"example": {
"values": []
}
},
"GreetOutputPayload": {
Expand Down
14 changes: 11 additions & 3 deletions modules/openapi/test/resources/foo.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,28 @@ union DoubleOrFloat {
double: Double
}

@dataExamples([
{
@dataExamples([{
smithy: {
name: "Meow"
}
])
}])
structure Cat {
name: String
}

@dataExamples([{
json: {
name: "Woof"
}
}])
structure Dog {
name: String,
breed: String
}

@dataExamples([{
string: "{\"values\": []}"
}])
structure ValuesResponse {
values: Values
}
Expand Down

0 comments on commit 579c454

Please sign in to comment.