Skip to content

Commit

Permalink
Add event stream media type to OpenAPI
Browse files Browse the repository at this point in the history
The HttpBindingIndex now knows about event streams and can return an
eventstream specific media type when an operation's input or output uses
one. This is now used as part of the OpenAPI converter so that the
generated OpenAPI description uses an appropriate media type.

AbstractRestProtocol was updated to add a method for returning the media
type of an EventStreamInfo object, with a default implementation that
returns application/vnd.amazon.eventstream.

Since event streams are now supported, the UnsupportedTraits mapper was
updated to allow event streams and streams. However, endpoint and
hostLabel traits are not supported, so they were added into the mapper.
  • Loading branch information
mtdowling committed Mar 31, 2020
1 parent bbc49dd commit edeb6c5
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.shapes.ToShapeId;
import software.amazon.smithy.model.traits.ErrorTrait;
import software.amazon.smithy.model.traits.EventStreamTrait;
import software.amazon.smithy.model.traits.HttpErrorTrait;
import software.amazon.smithy.model.traits.HttpHeaderTrait;
import software.amazon.smithy.model.traits.HttpLabelTrait;
Expand Down Expand Up @@ -252,6 +253,20 @@ public TimestampFormatTrait.Format determineTimestampFormat(
});
}

/**
* Returns the expected request Content-Type of the given operation.
*
* <p>See {@link #determineRequestContentType(ToShapeId, String, String)}
* for documentation on how the content-type is resolved.
*
* @param operation Operation to determine the content-type of.
* @param documentContentType Content-Type to use for protocol documents.
* @return Returns the optionally resolved content-type of the request.
*/
public Optional<String> determineRequestContentType(ToShapeId operation, String documentContentType) {
return determineRequestContentType(operation, documentContentType, null);
}

/**
* Returns the expected request Content-Type of the given operation.
*
Expand All @@ -260,26 +275,48 @@ public TimestampFormatTrait.Format determineTimestampFormat(
* to the payload, then the following checks are made:
*
* <ul>
* <li>If the payload has the {@link EventStreamTrait}, then the
* {@code eventStreamContentType} is returned.</li>
* <li>If the targeted shape is a structure or document type, then
* the {@code documentContentType} is returned.</li>
* <li>If the targeted shape has the {@link MediaTypeTrait}, then
* the value of the trait is returned.</li>
* <li>If the targeted shape is a blob, then "application/octet-stream"
* is returned.</li>
* <li>If the targeted shape is a string, then "text/plain" is
* returned.</li>
* <li>If the targeted shape is a structure or document type, then
* the {@code documentContentType} is returned.</li>
* </ul>
*
* <p>If no members are sent in the payload, an empty Optional is
* returned.
*
* @param operation Operation to determine the content-type of.
* @param documentContentType Content-Type to use for protocol documents.
* @param eventStreamContentType Content-Type to use for event streams.
* @return Returns the optionally resolved content-type of the request.
*/
public Optional<String> determineRequestContentType(ToShapeId operation, String documentContentType) {
String contentType = determineContentType(getRequestBindings(operation).values(), documentContentType);
return Optional.ofNullable(contentType);
public Optional<String> determineRequestContentType(
ToShapeId operation,
String documentContentType,
String eventStreamContentType
) {
Collection<HttpBinding> bindings = getRequestBindings(operation).values();
return Optional.ofNullable(determineContentType(bindings, documentContentType, eventStreamContentType));
}

/**
* Returns the expected response Content-Type of the given operation
* or error.
*
* <p>See {@link #determineResponseContentType(ToShapeId, String, String)}
* for documentation on how the content-type is resolved.
*
* @param operationOrError Operation or error to determine the content-type of.
* @param documentContentType Content-Type to use for protocol documents.
* @return Returns the optionally resolved content-type of the response.
*/
public Optional<String> determineResponseContentType(ToShapeId operationOrError, String documentContentType) {
return determineResponseContentType(operationOrError, documentContentType, null);
}

/**
Expand All @@ -291,50 +328,65 @@ public Optional<String> determineRequestContentType(ToShapeId operation, String
* to the payload, then the following checks are made:
*
* <ul>
* <li>If the payload has the {@link EventStreamTrait}, then the
* {@code eventStreamContentType} is returned.</li>
* <li>If the targeted shape is a structure or document type, then
* the {@code documentContentType} is returned.</li>
* <li>If the targeted shape has the {@link MediaTypeTrait}, then
* the value of the trait is returned.</li>
* <li>If the targeted shape is a blob, then "application/octet-stream"
* is returned.</li>
* <li>If the targeted shape is a string, then "text/plain" is
* returned.</li>
* <li>If the targeted shape is a structure or document type, then
* the {@code documentContentType} is returned.</li>
* </ul>
*
* <p>If no members are sent in the payload, an empty Optional is
* returned.
*
* @param operationOrError Operation or error to determine the content-type of.
* @param documentContentType Content-Type to use for protocol documents.
* @param eventStreamContentType Content-Type used for event streams.
* @return Returns the optionally resolved content-type of the response.
*/
public Optional<String> determineResponseContentType(ToShapeId operationOrError, String documentContentType) {
String contentType = determineContentType(getResponseBindings(operationOrError).values(), documentContentType);
return Optional.ofNullable(contentType);
public Optional<String> determineResponseContentType(
ToShapeId operationOrError,
String documentContentType,
String eventStreamContentType
) {
Collection<HttpBinding> bindings = getResponseBindings(operationOrError).values();
return Optional.ofNullable(determineContentType(bindings, documentContentType, eventStreamContentType));
}

private String determineContentType(Collection<HttpBinding> bindings, String documentContentType) {
private String determineContentType(
Collection<HttpBinding> bindings,
String documentContentType,
String eventStreamContentType
) {
for (HttpBinding binding : bindings) {
if (binding.getLocation() == HttpBinding.Location.DOCUMENT) {
return documentContentType;
}

if (binding.getLocation() == HttpBinding.Location.PAYLOAD) {
Shape target = model.getShape(binding.getMember().getTarget()).orElse(null);
if (binding.getMember().hasTrait(EventStreamTrait.class)) {
return eventStreamContentType;
}

Shape target = model.getShape(binding.getMember().getTarget()).orElse(null);
if (target == null) {
// Can't determine the content-type because the model is broken :(
// Let other parts of the validation system point this out.
break;
}

// Use the @mediaType trait if available.
if (target.getTrait(MediaTypeTrait.class).isPresent()) {
} else if (target.isDocumentShape() || target.isStructureShape()) {
// Document type and structure targets are always the document content-type.
return documentContentType;
} else if (target.getTrait(MediaTypeTrait.class).isPresent()) {
// Use the @mediaType trait if available.
return target.getTrait(MediaTypeTrait.class).get().getValue();
} else if (target.isBlobShape()) {
return "application/octet-stream";
} else if (target.isStringShape()) {
return "text/plain";
} else if (target.isDocumentShape() || target.isStructureShape()) {
return documentContentType;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,14 +260,33 @@ public void resolvesBlobBodyContentType() {
}

@Test
public void resolvesMediaTypeContentType() {
public void resolvesMediaType() {
HttpBindingIndex index = model.getKnowledge(HttpBindingIndex.class);
ShapeId operation = ShapeId.from("ns.foo#ServiceOperationWithMediaType");
Optional<String> contentType = index.determineResponseContentType(operation, "application/json");

assertThat(contentType, equalTo(Optional.of("application/xml")));
}

@Test
public void resolvesResponseEventStreamMediaType() {
HttpBindingIndex index = model.getKnowledge(HttpBindingIndex.class);
ShapeId operation = ShapeId.from("ns.foo#ServiceOperationWithEventStream");
String expected = "application/vnd.amazon.eventstream";
Optional<String> contentType = index.determineResponseContentType(operation, "ignore/me", expected);

assertThat(contentType, equalTo(Optional.of(expected)));
}

@Test
public void resolvesDocumentMediaType() {
HttpBindingIndex index = model.getKnowledge(HttpBindingIndex.class);
ShapeId operation = ShapeId.from("ns.foo#ServiceOperationExplicitMembers");
Optional<String> contentType = index.determineResponseContentType(operation, "application/json");

assertThat(contentType, equalTo(Optional.of("application/json")));
}

private static MemberShape expectMember(Model model, String id) {
ShapeId shapeId = ShapeId.from(id);
return model.expectShape(shapeId).asMemberShape().get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
},
{
"target": "ns.foo#WithLabels"
},
{
"target": "ns.foo#ServiceOperationWithEventStream"
}
]
},
Expand Down Expand Up @@ -288,6 +291,39 @@
"value": {
"target": "smithy.api#String"
}
},
"ns.foo#ServiceOperationWithEventStream": {
"type": "operation",
"output": {
"target": "ns.foo#ServiceOperationWithEventStreamOutput"
},
"traits": {
"smithy.api#http": {
"uri": "/ServiceOperationWithEventStream",
"method": "POST",
"code": 200
}
}
},
"ns.foo#ServiceOperationWithEventStreamOutput": {
"type": "structure",
"members": {
"events": {
"target": "ns.foo#EventStream",
"traits": {
"smithy.api#httpPayload": true,
"smithy.api#eventStream": true
}
}
}
},
"ns.foo#EventStream": {
"type": "structure",
"members": {
"foo": {
"target": "smithy.api#String"
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@
*/
public final class UnsupportedTraits implements OpenApiMapper {
private static final Logger LOGGER = Logger.getLogger(UnsupportedTraits.class.getName());
private static final Set<String> TRAITS = SetUtils.of(
"eventStream", "eventPayload", "eventHeader", "streaming");
private static final Set<String> TRAITS = SetUtils.of("endpoint", "hostLabel");

@Override
public byte getOrder() {
Expand Down
Loading

0 comments on commit edeb6c5

Please sign in to comment.