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 event stream media type to OpenAPI #334

Merged
merged 1 commit into from
Mar 31, 2020
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
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