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

Relax http payload constraints #679

Merged
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
@@ -0,0 +1,110 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.smithy.aws.traits.protocols;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.HttpBinding;
import software.amazon.smithy.model.knowledge.HttpBinding.Location;
import software.amazon.smithy.model.knowledge.HttpBindingIndex;
import software.amazon.smithy.model.knowledge.ServiceIndex;
import software.amazon.smithy.model.knowledge.TopDownIndex;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.ShapeType;
import software.amazon.smithy.model.shapes.ToShapeId;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.utils.SetUtils;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* Ensures that the http payload trait is only bound to structures, unions,
* documents, blobs, or strings for AWS protocols.
*/
@SmithyInternalApi
public final class ProtocolHttpPayloadValidator extends AbstractValidator {
private static final Set<ShapeType> VALID_HTTP_PAYLOAD_TYPES = SetUtils.of(
ShapeType.STRUCTURE, ShapeType.UNION, ShapeType.DOCUMENT, ShapeType.BLOB, ShapeType.STRING
);

@Override
public List<ValidationEvent> validate(Model model) {
ServiceIndex serviceIndex = ServiceIndex.of(model);
HttpBindingIndex bindingIndex = HttpBindingIndex.of(model);
TopDownIndex topDownIndex = TopDownIndex.of(model);
return model.shapes(ServiceShape.class)
.filter(service -> usesAwsProtocol(service, serviceIndex))
.flatMap(service -> validateService(model, service, bindingIndex, topDownIndex).stream())
.collect(Collectors.toList());
}

private boolean usesAwsProtocol(ServiceShape service, ServiceIndex index) {
for (Trait protocol : index.getProtocols(service).values()) {
if (protocol instanceof AwsProtocolTrait) {
return true;
}
}
return false;
}

private List<ValidationEvent> validateService(
Model model,
ServiceShape service,
HttpBindingIndex bindingIndex,
TopDownIndex topDownIndex
) {
List<ValidationEvent> events = new ArrayList<>();

for (OperationShape operation : topDownIndex.getContainedOperations(service)) {
List<HttpBinding> requestBindings = bindingIndex.getRequestBindings(operation, Location.PAYLOAD);
validateBindings(model, requestBindings).ifPresent(events::add);

List<HttpBinding> responseBindings = bindingIndex.getResponseBindings(operation, Location.PAYLOAD);
validateBindings(model, responseBindings).ifPresent(events::add);

for (ShapeId error : operation.getErrors()) {
List<HttpBinding> errorBindings = bindingIndex.getResponseBindings(error, Location.PAYLOAD);
validateBindings(model, errorBindings).ifPresent(events::add);
}
}

return events;
}

private Optional<ValidationEvent> validateBindings(Model model, Collection<HttpBinding> payloadBindings) {
for (HttpBinding binding : payloadBindings) {
if (!payloadBoundToValidType(model, binding.getMember().getTarget())) {
return Optional.of(error(binding.getMember(), "AWS Protocols do not support applying the httpPayload "
+ "trait to members that target sets, lists, or maps."));
}
}
return Optional.empty();
}

private boolean payloadBoundToValidType(Model model, ToShapeId payloadShape) {
return model.getShape(payloadShape.toShapeId())
.map(shape -> VALID_HTTP_PAYLOAD_TYPES.contains(shape.getType()))
.orElse(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ software.amazon.smithy.aws.traits.SdkServiceIdValidator
software.amazon.smithy.aws.traits.clientendpointdiscovery.ClientEndpointDiscoveryValidator
software.amazon.smithy.aws.traits.protocols.ProtocolHttpValidator
software.amazon.smithy.aws.traits.EventSourceValidator
software.amazon.smithy.aws.traits.protocols.ProtocolHttpPayloadValidator
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[ERROR] smithy.example#InvalidBindingInput$listBinding: AWS Protocols do not support applying the httpPayload trait to members that target sets, lists, or maps. | ProtocolHttpPayload
[ERROR] smithy.example#InvalidBindingOutput$mapBinding: AWS Protocols do not support applying the httpPayload trait to members that target sets, lists, or maps. | ProtocolHttpPayload
[ERROR] smithy.example#InvalidBindingError$setBinding: AWS Protocols do not support applying the httpPayload trait to members that target sets, lists, or maps. | ProtocolHttpPayload
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// AWS protocols do not currently support applying the http payload trait to
// sets, lists, or maps.

namespace smithy.example

use aws.protocols#restJson1
use smithy.api#http
use smithy.api#httpPayload

@restJson1
service InvalidExample {
version: "2020-12-29",
operations: [InvalidBindingOperation],
}

@http(method: "POST", uri: "/invalid-payload")
operation InvalidBindingOperation {
input: InvalidBindingInput,
output: InvalidBindingOutput,
errors: [InvalidBindingError],
}

structure InvalidBindingInput {
@httpPayload
listBinding: StringList,
}

structure InvalidBindingOutput {
@httpPayload
mapBinding: StringMap,
}

@error("client")
structure InvalidBindingError {
@httpPayload
setBinding: StringSet
}

list StringList {
member: String
}

set StringSet {
member: String
}

map StringMap {
key: String,
value: String,
}
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ string httpHeader
string httpPrefixHeaders

/// Binds a single structure member to the body of an HTTP request.
@trait(selector: "structure > :test(member > :test(string, blob, structure, union, document))",
@trait(selector: "structure > :test(member > :test(string, blob, structure, union, document, list, set, map))",
conflicts: [httpLabel, httpQuery, httpHeader, httpPrefixHeaders, httpResponseCode],
structurallyExclusive: "member")
@tags(["diff.error.const"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@
},
{
"target": "ns.foo#Q"
},
{
"target": "ns.foo#ListPayload"
},
{
"target": "ns.foo#SetPayload"
},
{
"target": "ns.foo#MapPayload"
}
]
},
Expand Down Expand Up @@ -640,6 +649,84 @@
}
}
},
"ns.foo#ListPayload": {
"type": "operation",
"input": {
"target": "ns.foo#ListPayloadInputOutput"
},
"output": {
"target": "ns.foo#ListPayloadInputOutput"
},
"traits": {
"smithy.api#http": {
"method": "POST",
"uri": "/list-payload"
}
}
},
"ns.foo#ListPayloadInputOutput": {
"type": "structure",
"members": {
"listPayload": {
"target": "ns.foo#StringList",
"traits": {
"smithy.api#httpPayload": {}
}
}
}
},
"ns.foo#SetPayload": {
"type": "operation",
"input": {
"target": "ns.foo#SetPayloadInputOutput"
},
"output": {
"target": "ns.foo#SetPayloadInputOutput"
},
"traits": {
"smithy.api#http": {
"method": "POST",
"uri": "/set-payload"
}
}
},
"ns.foo#SetPayloadInputOutput": {
"type": "structure",
"members": {
"setPayload": {
"target": "ns.foo#StringSet",
"traits": {
"smithy.api#httpPayload": {}
}
}
}
},
"ns.foo#MapPayload": {
"type": "operation",
"input": {
"target": "ns.foo#MapPayloadInputOutput"
},
"output": {
"target": "ns.foo#MapPayloadInputOutput"
},
"traits": {
"smithy.api#http": {
"method": "POST",
"uri": "/map-payload"
}
}
},
"ns.foo#MapPayloadInputOutput": {
"type": "structure",
"members": {
"mapPayload": {
"target": "ns.foo#MapOfString",
"traits": {
"smithy.api#httpPayload": {}
}
}
}
},
"ns.foo#Integer": {
"type": "integer"
},
Expand All @@ -655,6 +742,12 @@
"target": "ns.foo#String"
}
},
"ns.foo#StringSet": {
"type": "set",
"member": {
"target": "ns.foo#String"
}
},
"ns.foo#Structure": {
"type": "structure"
},
Expand Down