diff --git a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/protocols/ProtocolHttpPayloadValidator.java b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/protocols/ProtocolHttpPayloadValidator.java new file mode 100644 index 00000000000..6529073473c --- /dev/null +++ b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/protocols/ProtocolHttpPayloadValidator.java @@ -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 VALID_HTTP_PAYLOAD_TYPES = SetUtils.of( + ShapeType.STRUCTURE, ShapeType.UNION, ShapeType.DOCUMENT, ShapeType.BLOB, ShapeType.STRING + ); + + @Override + public List 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 validateService( + Model model, + ServiceShape service, + HttpBindingIndex bindingIndex, + TopDownIndex topDownIndex + ) { + List events = new ArrayList<>(); + + for (OperationShape operation : topDownIndex.getContainedOperations(service)) { + List requestBindings = bindingIndex.getRequestBindings(operation, Location.PAYLOAD); + validateBindings(model, requestBindings).ifPresent(events::add); + + List responseBindings = bindingIndex.getResponseBindings(operation, Location.PAYLOAD); + validateBindings(model, responseBindings).ifPresent(events::add); + + for (ShapeId error : operation.getErrors()) { + List errorBindings = bindingIndex.getResponseBindings(error, Location.PAYLOAD); + validateBindings(model, errorBindings).ifPresent(events::add); + } + } + + return events; + } + + private Optional validateBindings(Model model, Collection 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); + } +} diff --git a/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator index 48cc272f9fc..2d14d35e6ff 100644 --- a/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator +++ b/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator @@ -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 diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.errors b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.errors new file mode 100644 index 00000000000..265c5db3888 --- /dev/null +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.errors @@ -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 diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.smithy new file mode 100644 index 00000000000..cabfe16a331 --- /dev/null +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.smithy @@ -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, +} diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy index 6bc570c7152..b2ccef8dd83 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy @@ -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"]) diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.json index 4ddecd54fc8..8a4d4fe9968 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.json @@ -55,6 +55,15 @@ }, { "target": "ns.foo#Q" + }, + { + "target": "ns.foo#ListPayload" + }, + { + "target": "ns.foo#SetPayload" + }, + { + "target": "ns.foo#MapPayload" } ] }, @@ -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" }, @@ -655,6 +742,12 @@ "target": "ns.foo#String" } }, + "ns.foo#StringSet": { + "type": "set", + "member": { + "target": "ns.foo#String" + } + }, "ns.foo#Structure": { "type": "structure" },