diff --git a/docs/source/1.0/spec/aws/aws-restjson1-protocol.rst b/docs/source/1.0/spec/aws/aws-restjson1-protocol.rst index 54a7a693fa1..d90b7764a95 100644 --- a/docs/source/1.0/spec/aws/aws-restjson1-protocol.rst +++ b/docs/source/1.0/spec/aws/aws-restjson1-protocol.rst @@ -180,6 +180,8 @@ that affect serialization: prefixed HTTP headers. * - :ref:`httpQuery ` - Binds a top-level input structure member to a query string parameter. + * - :ref:`httpQueryParams ` + - Binds a map of key-value pairs to query string parameters. * - :ref:`jsonName ` - By default, the JSON property names used in serialized structures are the same as a structure member name. The ``jsonName`` trait changes diff --git a/docs/source/1.0/spec/aws/aws-restxml-protocol.rst b/docs/source/1.0/spec/aws/aws-restxml-protocol.rst index dbda6b1d6b0..7bb587e1238 100644 --- a/docs/source/1.0/spec/aws/aws-restxml-protocol.rst +++ b/docs/source/1.0/spec/aws/aws-restxml-protocol.rst @@ -175,6 +175,8 @@ that affect serialization: prefixed HTTP headers. * - :ref:`httpQuery ` - Binds a top-level input structure member to a query string parameter. + * - :ref:`httpQueryParams ` + - Binds a map of key-value pairs to query string parameters. * - :ref:`xmlAttribute ` - Serializes an object property as an XML attribute rather than a nested XML element. diff --git a/docs/source/1.0/spec/core/http-traits.rst b/docs/source/1.0/spec/core/http-traits.rst index 9e21cd8e5bb..3700d3e0d74 100644 --- a/docs/source/1.0/spec/core/http-traits.rst +++ b/docs/source/1.0/spec/core/http-traits.rst @@ -511,6 +511,7 @@ Value type Conflicts with :ref:`httpLabel-trait`, :ref:`httpQuery-trait`, + :ref:`httpQueryParams-trait`, :ref:`httpPrefixHeaders-trait`, :ref:`httpPayload-trait`, :ref:`httpResponseCode-trait` @@ -609,6 +610,7 @@ Value type Conflicts with :ref:`httpHeader-trait`, :ref:`httpQuery-trait`, + :ref:`httpQueryParams-trait`, :ref:`httpPrefixHeaders-trait`, :ref:`httpPayload-trait`, :ref:`httpResponseCode-trait` @@ -693,7 +695,7 @@ Trait selector Value type Annotation trait. Conflicts with - :ref:`httpLabel-trait`, :ref:`httpQuery-trait`, + :ref:`httpLabel-trait`, :ref:`httpQuery-trait`, :ref:`httpQueryParams-trait`, :ref:`httpHeader-trait`, :ref:`httpPrefixHeaders-trait`, :ref:`httpResponseCode-trait` Structurally exclusive @@ -739,7 +741,7 @@ be bound to ``httpPayload``. If the ``httpPayload`` trait is present on the structure referenced by the input of an operation, then all other structure members MUST be bound with the :ref:`httpLabel-trait`, :ref:`httpHeader-trait`, -:ref:`httpPrefixHeaders-trait`, or :ref:`httpQuery-trait`. +:ref:`httpPrefixHeaders-trait`, :ref:`httpQueryParams-trait`, or :ref:`httpQuery-trait`. If the ``httpPayload`` trait is present on the structure referenced by the output of an operation or a structure targeted by the :ref:`error-trait`, @@ -778,7 +780,7 @@ Value type of of "X-Amz-Meta-" and a map key entry of "Baz", the resulting header field name serialized in the message is "X-Amz-Meta-Baz". Conflicts with - :ref:`httpLabel-trait`, :ref:`httpQuery-trait`, + :ref:`httpLabel-trait`, :ref:`httpQuery-trait`, :ref:`httpQueryParams-trait`, :ref:`httpHeader-trait`, :ref:`httpPayload-trait`, :ref:`httpResponseCode-trait` Structurally exclusive @@ -858,7 +860,7 @@ Value type parameter. The query string parameter name MUST be case-sensitively unique across all other members marked with the ``httpQuery`` trait. Conflicts with - :ref:`httpLabel-trait`, :ref:`httpHeader-trait`, + :ref:`httpLabel-trait`, :ref:`httpHeader-trait`, :ref:`httpQueryParams-trait`, :ref:`httpPrefixHeaders-trait`, :ref:`httpPayload-trait`, :ref:`httpResponseCode-trait` @@ -932,6 +934,69 @@ many HTTP client and server implementations enforce limits in practice. Carefully consider the maximum allowed length of each member that is bound to an HTTP query string or path. +.. _httpQueryParams-trait: + +``httpQueryParams`` trait +========================= + +Summary + Binds a map of key-value pairs to query string parameters. +Trait selector + .. code-block:: none + + structure > member + :test(> map > member[id|member=value] > :test(string, collection > member > string)) + + The ``httpQueryParams`` trait can be applied to ``structure`` members + that target a ``map`` of ``string``, or a ``map`` of ``list``/``set`` of + ``string``. + +Value type + Annotation trait. +Conflicts with + :ref:`httpLabel-trait`, :ref:`httpHeader-trait`, :ref:`httpQuery-trait`, + :ref:`httpPrefixHeaders-trait`, :ref:`httpPayload-trait`, + :ref:`httpResponseCode-trait` + +The following example defines an operation that optionally sends the +target input map as query string parameters in an HTTP request: + +.. tabs:: + + .. code-tab:: smithy + + @readonly + @http(method: "GET", uri: "/things") + operation ListThings { + input: ListThingsInput, + output: ListThingsOutput, // omitted for brevity + } + + structure ListThingsInput { + @httpQueryParams() + myParams: MapOfStrings, + } + + map MapOfStrings { + key: String, + value: String + } + +.. rubric:: ``httpQueryParams`` is only used on input + +``httpQueryParams`` is ignored when resolving the HTTP bindings of an operation's +output or an error. This means that if a structure that contains members +marked with the ``httpQueryParams`` trait is used as the top-level output structure +of an operation, then those members are sent as part of the +:ref:`protocol-specific document ` sent in +the body of the response. + +.. rubric:: Serialization rules + +See the :ref:`httpQuery-trait` serialization rules that define how the keys and values of the +target map will be serialized in the request query string. Key-value pairs in the target map +are treated like they were explicitly bound using the :ref:`httpQuery-trait`, including the +requirement that reserved characters MUST be percent-encoded_. .. _httpResponseCode-trait: @@ -954,7 +1019,7 @@ Value type Conflicts with :ref:`httpLabel-trait`, :ref:`httpHeader-trait`, :ref:`httpPrefixHeaders-trait`, :ref:`httpPayload-trait`, - :ref:`httpQuery-trait` + :ref:`httpQuery-trait`, :ref:`httpQueryParams-trait`, .. rubric:: ``httpResponseCode`` use cases @@ -1046,13 +1111,15 @@ parameters: 1. If the member has the ``httpLabel`` trait, expand the value into the URI. 2. If the member has the ``httpQuery`` trait, serialize the value into the HTTP request as a query string parameter. - 3. If the member has the ``httpHeader`` trait, serialize the value in an + 3. If the member has the ``httpQueryParams`` trait, serialize the values into + the HTTP request as query string parameters. + 4. If the member has the ``httpHeader`` trait, serialize the value in an HTTP header using the value of the ``httpHeader`` trait. - 4. If the member has the ``httpPrefixHeaders`` trait and the value is a map, + 5. If the member has the ``httpPrefixHeaders`` trait and the value is a map, serialize the map key value pairs as prefixed HTTP headers. - 5. If the member has the ``httpPayload`` trait, serialize the value as the + 6. If the member has the ``httpPayload`` trait, serialize the value as the body of the request. - 6. If the member has no bindings, serialize the key-value pair as part of a + 7. If the member has no bindings, serialize the key-value pair as part of a protocol-specific document sent in the body of the request. The following steps are taken to serialize an HTTP response given a map of diff --git a/smithy-aws-protocol-tests/model/restJson1/http-query.smithy b/smithy-aws-protocol-tests/model/restJson1/http-query.smithy index cb8c45c5395..1e389f886a5 100644 --- a/smithy-aws-protocol-tests/model/restJson1/http-query.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/http-query.smithy @@ -1,5 +1,6 @@ // This file defines test cases that test HTTP query string bindings. -// See: https://awslabs.github.io/smithy/1.0/spec/http.html#httpquery-trait +// See: https://awslabs.github.io/smithy/1.0/spec/http.html#httpquery-trait and +// https://awslabs.github.io/smithy/1.0/spec/http.html#httpqueryparams-trait $version: "1.0" @@ -13,6 +14,8 @@ use aws.protocoltests.shared#FooEnumList use aws.protocoltests.shared#IntegerList use aws.protocoltests.shared#IntegerSet use aws.protocoltests.shared#StringList +use aws.protocoltests.shared#StringListMap +use aws.protocoltests.shared#StringMap use aws.protocoltests.shared#StringSet use aws.protocoltests.shared#TimestampList use smithy.test#httpRequestTests @@ -68,6 +71,8 @@ apply AllQueryStringTypes @httpRequestTests([ "EnumList=Foo", "EnumList=Baz", "EnumList=Bar", + "QueryParamsStringKeyA=Foo", + "QueryParamsStringKeyB=Bar", ], params: { queryString: "Hello there", @@ -88,6 +93,10 @@ apply AllQueryStringTypes @httpRequestTests([ queryTimestampList: [1, 2, 3], queryEnum: "Foo", queryEnumList: ["Foo", "Baz", "Bar"], + queryParamsMapOfStrings: { + "QueryParamsStringKeyA": "Foo", + "QueryParamsStringKeyB": "Bar" + }, } } ]) @@ -146,6 +155,9 @@ structure AllQueryStringTypesInput { @httpQuery("EnumList") queryEnumList: FooEnumList, + + @httpQueryParams + queryParamsMapOfStrings: StringMap, } /// This example uses a constant query string parameters and a label. @@ -365,3 +377,119 @@ structure QueryIdempotencyTokenAutoFillInput { @idempotencyToken token: String, } + +// Clients must make named query members take precedence over unnamed members +// and servers must use all query params in the unnamed map. +@http(uri: "/Precedence", method: "POST") +operation QueryPrecedence { + input: QueryPrecedenceInput +} + +apply QueryPrecedence @httpRequestTests([ + { + id: "RestJsonQueryPrecedence", + documentation: "Prefer named query parameters when serializing", + protocol: restJson1, + method: "POST", + uri: "/Precedence", + body: "", + queryParams: [ + "bar=named", + "qux=alsoFromMap" + ], + params: { + foo: "named", + baz: { + bar: "fromMap", + qux: "alsoFromMap" + } + }, + appliesTo: "client", + }, + { + id: "RestJsonServersPutAllQueryParamsInMap", + documentation: "Servers put all query params in map", + protocol: restJson1, + method: "POST", + uri: "/Precedence", + body: "", + queryParams: [ + "bar=named", + "qux=fromMap" + ], + params: { + foo: "named", + baz: { + bar: "named", + qux: "fromMap" + } + }, + appliesTo: "server", + } +]) + +structure QueryPrecedenceInput { + @httpQuery("bar") + foo: String, + + @httpQueryParams + baz: StringMap +} + +// httpQueryParams as Map of ListStrings +@http(uri: "/StringListMap", method: "POST") +operation QueryParamsAsStringListMap { + input: QueryParamsAsStringListMapInput +} + +apply QueryParamsAsStringListMap @httpRequestTests([ + { + id: "RestJsonQueryParamsStringListMap", + documentation: "Serialize query params from map of list strings", + protocol: restJson1, + method: "POST", + uri: "/StringListMap", + body: "", + queryParams: [ + "corge=named", + "baz=bar", + "baz=qux" + ], + params: { + qux: "named", + foo: { + "baz": ["bar", "qux"] + } + }, + appliesTo: "client" + }, + { + id: "RestJsonServersQueryParamsStringListMap", + documentation: "Servers put all query params in map", + protocol: restJson1, + method: "POST", + uri: "/StringListMap", + body: "", + queryParams: [ + "corge=named", + "baz=bar", + "baz=qux" + ], + params: { + qux: "named", + foo: { + "corge": ["named"], + "baz": ["bar", "qux"] + } + }, + appliesTo: "server" + } +]) + +structure QueryParamsAsStringListMapInput { + @httpQuery("corge") + qux: String, + + @httpQueryParams + foo: StringListMap +} diff --git a/smithy-aws-protocol-tests/model/restJson1/main.smithy b/smithy-aws-protocol-tests/model/restJson1/main.smithy index 8e2a923124d..95c0feac4cc 100644 --- a/smithy-aws-protocol-tests/model/restJson1/main.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/main.smithy @@ -30,13 +30,15 @@ service RestJson { HttpRequestWithLabelsAndTimestampFormat, HttpRequestWithGreedyLabelInPath, - // @httpQuery tests + // @httpQuery and @httpQueryParams tests AllQueryStringTypes, ConstantQueryString, ConstantAndVariableQueryString, IgnoreQueryParamsInResponse, OmitsNullSerializesEmptyString, QueryIdempotencyTokenAutoFill, + QueryPrecedence, + QueryParamsAsStringListMap, // @httpPrefixHeaders tests HttpPrefixHeaders, diff --git a/smithy-aws-protocol-tests/model/restXml/http-query.smithy b/smithy-aws-protocol-tests/model/restXml/http-query.smithy index e8cbaac55e1..4809f85a46c 100644 --- a/smithy-aws-protocol-tests/model/restXml/http-query.smithy +++ b/smithy-aws-protocol-tests/model/restXml/http-query.smithy @@ -1,5 +1,6 @@ // This file defines test cases that test HTTP query string bindings. -// See: https://awslabs.github.io/smithy/1.0/spec/http.html#httpquery-trait +// See: https://awslabs.github.io/smithy/1.0/spec/http.html#httpquery-trait and +// https://awslabs.github.io/smithy/1.0/spec/http.html#httpqueryparams-trait $version: "1.0" @@ -13,6 +14,8 @@ use aws.protocoltests.shared#FooEnumList use aws.protocoltests.shared#IntegerList use aws.protocoltests.shared#IntegerSet use aws.protocoltests.shared#StringList +use aws.protocoltests.shared#StringListMap +use aws.protocoltests.shared#StringMap use aws.protocoltests.shared#StringSet use aws.protocoltests.shared#TimestampList use smithy.test#httpRequestTests @@ -68,6 +71,8 @@ apply AllQueryStringTypes @httpRequestTests([ "EnumList=Foo", "EnumList=Baz", "EnumList=Bar", + "QueryParamsStringKeyA=Foo", + "QueryParamsStringKeyB=Bar", ], params: { queryString: "Hello there", @@ -88,6 +93,10 @@ apply AllQueryStringTypes @httpRequestTests([ queryTimestampList: [1, 2, 3], queryEnum: "Foo", queryEnumList: ["Foo", "Baz", "Bar"], + queryParamsMapOfStrings: { + "QueryParamsStringKeyA": "Foo", + "QueryParamsStringKeyB": "Bar" + }, } } ]) @@ -146,6 +155,9 @@ structure AllQueryStringTypesInput { @httpQuery("EnumList") queryEnumList: FooEnumList, + + @httpQueryParams + queryParamsMapOfStrings: StringMap, } /// This example uses a constant query string parameters and a label. @@ -351,3 +363,119 @@ structure QueryIdempotencyTokenAutoFillInput { @idempotencyToken token: String, } + +// Clients must make named query members take precedence over unnamed members +// and servers must use all query params in the unnamed map. +@http(uri: "/Precedence", method: "POST") +operation QueryPrecedence { + input: QueryPrecedenceInput +} + +apply QueryPrecedence @httpRequestTests([ + { + id: "RestXmlQueryPrecedence", + documentation: "Prefer named query parameters when serializing", + protocol: restXml, + method: "POST", + uri: "/Precedence", + body: "", + queryParams: [ + "bar=named", + "qux=alsoFromMap" + ], + params: { + foo: "named", + baz: { + bar: "fromMap", + qux: "alsoFromMap" + } + }, + appliesTo: "client", + }, + { + id: "RestXmlServersPutAllQueryParamsInMap", + documentation: "Servers put all query params in map", + protocol: restXml, + method: "POST", + uri: "/Precedence", + body: "", + queryParams: [ + "bar=named", + "qux=fromMap" + ], + params: { + foo: "named", + baz: { + bar: "named", + qux: "fromMap" + } + }, + appliesTo: "server", + } +]) + +structure QueryPrecedenceInput { + @httpQuery("bar") + foo: String, + + @httpQueryParams + baz: StringMap +} + +// httpQueryParams as Map of ListStrings +@http(uri: "/StringListMap", method: "POST") +operation QueryParamsAsStringListMap { + input: QueryParamsAsStringListMapInput +} + +apply QueryParamsAsStringListMap @httpRequestTests([ + { + id: "RestXmlQueryParamsStringListMap", + documentation: "Serialize query params from map of list strings", + protocol: restXml, + method: "POST", + uri: "/StringListMap", + body: "", + queryParams: [ + "corge=named", + "baz=bar", + "baz=qux" + ], + params: { + qux: "named", + foo: { + "baz": ["bar", "qux"] + } + }, + appliesTo: "client" + }, + { + id: "RestXmlServersQueryParamsStringListMap", + documentation: "Servers put all query params in map", + protocol: restXml, + method: "POST", + uri: "/StringListMap", + body: "", + queryParams: [ + "corge=named", + "baz=bar", + "baz=qux" + ], + params: { + qux: "named", + foo: { + "corge": ["named"], + "baz": ["bar", "qux"] + } + }, + appliesTo: "server" + } +]) + +structure QueryParamsAsStringListMapInput { + @httpQuery("corge") + qux: String, + + @httpQueryParams + foo: StringListMap +} diff --git a/smithy-aws-protocol-tests/model/restXml/main.smithy b/smithy-aws-protocol-tests/model/restXml/main.smithy index 649040abd59..ea0438188bf 100644 --- a/smithy-aws-protocol-tests/model/restXml/main.smithy +++ b/smithy-aws-protocol-tests/model/restXml/main.smithy @@ -32,13 +32,15 @@ service RestXml { HttpRequestWithLabelsAndTimestampFormat, HttpRequestWithGreedyLabelInPath, - // @httpQuery tests + // @httpQuery and @httpQueryParams tests AllQueryStringTypes, ConstantQueryString, ConstantAndVariableQueryString, IgnoreQueryParamsInResponse, OmitsNullSerializesEmptyString, QueryIdempotencyTokenAutoFill, + QueryPrecedence, + QueryParamsAsStringListMap, // @httpPrefixHeaders tests HttpPrefixHeaders, diff --git a/smithy-aws-protocol-tests/model/shared-types.smithy b/smithy-aws-protocol-tests/model/shared-types.smithy index 457cb5413ff..82686772d8f 100644 --- a/smithy-aws-protocol-tests/model/shared-types.smithy +++ b/smithy-aws-protocol-tests/model/shared-types.smithy @@ -37,6 +37,11 @@ map StringMap { value: String, } +map StringListMap { + key: String, + value: StringList +} + @sparse map SparseStringMap { key: String, diff --git a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.json b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.json index 18db45c7205..153bfb127a8 100644 --- a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.json +++ b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.json @@ -27,6 +27,7 @@ "smithy.api#httpPayload", "smithy.api#httpPrefixHeaders", "smithy.api#httpQuery", + "smithy.api#httpQueryParams", "smithy.api#httpResponseCode", "smithy.api#jsonName", "smithy.api#timestampFormat" @@ -69,6 +70,7 @@ "smithy.api#httpPayload", "smithy.api#httpPrefixHeaders", "smithy.api#httpQuery", + "smithy.api#httpQueryParams", "smithy.api#httpResponseCode", "smithy.api#xmlAttribute", "smithy.api#xmlFlattened", diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/HttpBinding.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/HttpBinding.java index 5fd61f559e2..de2a6a6fbea 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/HttpBinding.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/HttpBinding.java @@ -39,7 +39,8 @@ public final class HttpBinding { } /** HTTP binding types. */ - public enum Location { LABEL, DOCUMENT, PAYLOAD, HEADER, PREFIX_HEADERS, QUERY, RESPONSE_CODE, UNBOUND } + public enum Location { LABEL, DOCUMENT, PAYLOAD, HEADER, PREFIX_HEADERS, QUERY, QUERY_PARAMS, RESPONSE_CODE, + UNBOUND } public MemberShape getMember() { return member; diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/HttpBindingIndex.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/HttpBindingIndex.java index b480d40a6fe..00f4dea30e4 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/HttpBindingIndex.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/HttpBindingIndex.java @@ -37,6 +37,7 @@ import software.amazon.smithy.model.traits.HttpLabelTrait; import software.amazon.smithy.model.traits.HttpPayloadTrait; import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait; +import software.amazon.smithy.model.traits.HttpQueryParamsTrait; import software.amazon.smithy.model.traits.HttpQueryTrait; import software.amazon.smithy.model.traits.HttpResponseCodeTrait; import software.amazon.smithy.model.traits.HttpTrait; @@ -99,6 +100,7 @@ public static boolean hasHttpRequestBindings(Shape shape) { || shape.hasTrait(HttpPrefixHeadersTrait.class) || shape.hasTrait(HttpPayloadTrait.class) || shape.hasTrait(HttpQueryTrait.class) + || shape.hasTrait(HttpQueryParamsTrait.class) || shape.hasTrait(HttpLabelTrait.class); } @@ -426,6 +428,9 @@ private List createStructureBindings(StructureShape struct, boolean } else if (isRequest && member.getTrait(HttpQueryTrait.class).isPresent()) { HttpQueryTrait trait = member.getTrait(HttpQueryTrait.class).get(); bindings.add(new HttpBinding(member, HttpBinding.Location.QUERY, trait.getValue(), trait)); + } else if (isRequest && member.getTrait(HttpQueryParamsTrait.class).isPresent()) { + HttpQueryParamsTrait trait = member.getTrait(HttpQueryParamsTrait.class).get(); + bindings.add(new HttpBinding(member, HttpBinding.Location.QUERY_PARAMS, member.getMemberName(), trait)); } else if (member.getTrait(HttpPayloadTrait.class).isPresent()) { foundPayload = true; HttpPayloadTrait trait = member.getTrait(HttpPayloadTrait.class).get(); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/HttpQueryParamsTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/HttpQueryParamsTrait.java new file mode 100644 index 00000000000..38b5cb3656e --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/HttpQueryParamsTrait.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 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.model.traits; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; + +/** + * Binds a map structure member to the HTTP query string. + */ +public class HttpQueryParamsTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("smithy.api#httpQueryParams"); + + + public HttpQueryParamsTrait(ObjectNode node) { + super(ID, node); + } + + public HttpQueryParamsTrait() { + this(Node.objectNode()); + } + + public static final class Provider extends AnnotationTrait.Provider { + public Provider() { + super(ID, HttpQueryParamsTrait::new); + } + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpQueryParamsTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpQueryParamsTraitValidator.java new file mode 100644 index 00000000000..204c198989e --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpQueryParamsTraitValidator.java @@ -0,0 +1,82 @@ +/* + * Copyright 2021 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.model.validation.validators; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.HttpQueryParamsTrait; +import software.amazon.smithy.model.traits.HttpQueryTrait; +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.model.validation.ValidationUtils; + +/** + * When the `httpQueryParams` trait is used, this validator emits a NOTE when another member of the container shape + * applies the `httpQuery` trait which may result in a conflict within the query string. + */ +public final class HttpQueryParamsTraitValidator extends AbstractValidator { + @Override + public List validate(Model model) { + if (!model.isTraitApplied(HttpQueryParamsTrait.class)) { + return Collections.emptyList(); + } else { + return validateQueryTraitUsage(model); + } + } + + private List validateQueryTraitUsage(Model model) { + List events = new ArrayList<>(); + + for (Shape shape : model.getShapesWithTrait(HttpQueryParamsTrait.class)) { + shape.asMemberShape() + .flatMap(member -> model.getShape(member.getContainer()) + .flatMap(Shape::asStructureShape)) + .ifPresent(structure -> { + // Gather the names of member shapes, as strings, that apply HttpQuery traits + List queryShapes = getMembersWithTrait(structure, HttpQueryTrait.class); + if (queryShapes.size() > 0) { + events.add(createNote(structure, shape.toShapeId().getMember().get(), queryShapes)); + } + }); + } + + return events; + } + + private List getMembersWithTrait(StructureShape structure, Class trait) { + List members = new ArrayList<>(); + for (MemberShape member : structure.members()) { + if (member.hasTrait(trait)) { + members.add(member.getMemberName()); + } + } + return members; + } + + private ValidationEvent createNote(Shape target, String queryParamsShape, List queryShapes) { + return note(target, String.format("Structure member `%s` is marked with the `httpQueryParams` trait, and " + + "`httpQuery` traits are applied to the following members: %s. The service will not be able to " + + "disambiguate between query string parameters intended for the `%s` member and those explicitly " + + "bound to the `httpQuery` members.", queryParamsShape, ValidationUtils.tickedList(queryShapes), + queryParamsShape)); + } +} diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService index c2a2915237a..59d6be134b8 100644 --- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService +++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -17,6 +17,7 @@ software.amazon.smithy.model.traits.HttpHeaderTrait$Provider software.amazon.smithy.model.traits.HttpLabelTrait$Provider software.amazon.smithy.model.traits.HttpPayloadTrait$Provider software.amazon.smithy.model.traits.HttpPrefixHeadersTrait$Provider +software.amazon.smithy.model.traits.HttpQueryParamsTrait$Provider software.amazon.smithy.model.traits.HttpQueryTrait$Provider software.amazon.smithy.model.traits.HttpResponseCodeTrait$Provider software.amazon.smithy.model.traits.HttpTrait$Provider diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator index f8d772407f4..f38bcfab372 100644 --- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator +++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator @@ -11,6 +11,7 @@ software.amazon.smithy.model.validation.validators.HttpLabelTraitValidator software.amazon.smithy.model.validation.validators.HttpMethodSemanticsValidator software.amazon.smithy.model.validation.validators.HttpPayloadValidator software.amazon.smithy.model.validation.validators.HttpPrefixHeadersTraitValidator +software.amazon.smithy.model.validation.validators.HttpQueryParamsTraitValidator software.amazon.smithy.model.validation.validators.HttpQueryTraitValidator software.amazon.smithy.model.validation.validators.HttpResponseCodeSemanticsValidator software.amazon.smithy.model.validation.validators.HttpUriConflictValidator 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 8569fbe2bf2..49f9396334b 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 @@ -551,7 +551,7 @@ structure http { /// Binds an operation input structure member to an HTTP label. @trait(selector: "structure > member[trait|required] :test(> :test(string, number, boolean, timestamp))", - conflicts: [httpHeader, httpQuery, httpPrefixHeaders, httpPayload, httpResponseCode]) + conflicts: [httpHeader, httpQuery, httpPrefixHeaders, httpPayload, httpResponseCode, httpQueryParams]) @tags(["diff.error.const"]) structure httpLabel {} @@ -560,16 +560,25 @@ structure httpLabel {} structure > member :test(> :test(string, number, boolean, timestamp), > collection > member > :test(string, number, boolean, timestamp))""", - conflicts: [httpLabel, httpHeader, httpPrefixHeaders, httpPayload, httpResponseCode]) + conflicts: [httpLabel, httpHeader, httpPrefixHeaders, httpPayload, httpResponseCode, httpQueryParams]) @length(min: 1) @tags(["diff.error.const"]) string httpQuery +/// Binds an operation input structure member to the HTTP query string. +@trait(selector: """ + structure > member + :test(> map > member[id|member=value] > :test(string, collection > member > string))""", + structurallyExclusive: "member", + conflicts: [httpLabel, httpQuery, httpHeader, httpPayload, httpResponseCode, httpPrefixHeaders]) +@tags(["diff.error.const"]) +structure httpQueryParams {} + /// Binds a structure member to an HTTP header. @trait(selector: """ structure > :test(member > :test(boolean, number, string, timestamp, collection > member > :test(boolean, number, string, timestamp)))""", - conflicts: [httpLabel, httpQuery, httpPrefixHeaders, httpPayload, httpResponseCode]) + conflicts: [httpLabel, httpQuery, httpPrefixHeaders, httpPayload, httpResponseCode, httpQueryParams]) @length(min: 1) @tags(["diff.error.const"]) string httpHeader @@ -579,13 +588,13 @@ string httpHeader structure > member :test(> map > member[id|member=value] > :test(string, collection > member > string))""", structurallyExclusive: "member", - conflicts: [httpLabel, httpQuery, httpHeader, httpPayload, httpResponseCode]) + conflicts: [httpLabel, httpQuery, httpHeader, httpPayload, httpResponseCode, httpQueryParams]) @tags(["diff.error.const"]) 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, list, set, map))", - conflicts: [httpLabel, httpQuery, httpHeader, httpPrefixHeaders, httpResponseCode], + conflicts: [httpLabel, httpQuery, httpHeader, httpPrefixHeaders, httpResponseCode, httpQueryParams], structurallyExclusive: "member") @tags(["diff.error.const"]) structure httpPayload {} @@ -600,7 +609,7 @@ integer httpError /// on the response. @trait(selector: "structure > member :test(> integer)", structurallyExclusive: "member", - conflicts: [httpLabel, httpQuery, httpHeader, httpPrefixHeaders, httpPayload]) + conflicts: [httpLabel, httpQuery, httpHeader, httpPrefixHeaders, httpPayload, httpQueryParams]) @tags(["diff.error.const"]) structure httpResponseCode {} diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/HttpBindingIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/HttpBindingIndexTest.java index c8d64888a3c..74779fb5caa 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/HttpBindingIndexTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/HttpBindingIndexTest.java @@ -43,6 +43,8 @@ import software.amazon.smithy.model.traits.HttpLabelTrait; import software.amazon.smithy.model.traits.HttpPayloadTrait; import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait; +import software.amazon.smithy.model.traits.HttpQueryParamsTrait; +import software.amazon.smithy.model.traits.HttpQueryTrait; import software.amazon.smithy.model.traits.HttpResponseCodeTrait; import software.amazon.smithy.model.traits.HttpTrait; import software.amazon.smithy.model.traits.TimestampFormatTrait; @@ -187,6 +189,31 @@ public void findsLabelBindings() { HttpBinding.Location.LABEL, "baz", null))); } + @Test + public void findsQueryBindings() { + HttpBindingIndex index = HttpBindingIndex.of(model); + ShapeId id = ShapeId.from("ns.foo#ServiceOperationExplicitMembers"); + Map bindings = index.getRequestBindings(id); + + assertThat(bindings.get("baz"), equalTo(new HttpBinding( + expectMember(model, "ns.foo#ServiceOperationExplicitMembersInput$baz"), + HttpBinding.Location.QUERY, "baz", new HttpQueryTrait("baz")))); + assertThat(bindings.get("bar"), equalTo(new HttpBinding( + expectMember(model, "ns.foo#ServiceOperationExplicitMembersInput$bar"), + HttpBinding.Location.QUERY, "bar", new HttpQueryTrait("bar")))); + } + + @Test + public void findsQueryParamsBindings() { + HttpBindingIndex index = HttpBindingIndex.of(model); + ShapeId id = ShapeId.from("ns.foo#ServiceOperationExplicitMembers"); + Map bindings = index.getRequestBindings(id); + + assertThat(bindings.get("corge"), equalTo(new HttpBinding( + expectMember(model, "ns.foo#ServiceOperationExplicitMembersInput$corge"), + HttpBinding.Location.QUERY_PARAMS, "corge", new HttpQueryParamsTrait()))); + } + @Test public void findsUnboundMembers() { ServiceShape service = ServiceShape.builder() @@ -260,6 +287,30 @@ public void checksForHttpResponseCodeBindings() { assertThat(HttpBindingIndex.hasHttpResponseBindings(shape), is(true)); } + @Test + public void checksForRequestQueryBindings() { + Shape queryShape = MemberShape.builder() + .target("smithy.api#Timestamp") + .id("smithy.example#Baz$bar") + .addTrait(new HttpQueryTrait("foo")) + .build(); + + assertThat(HttpBindingIndex.hasHttpRequestBindings(queryShape), is(true)); + assertThat(HttpBindingIndex.hasHttpResponseBindings(queryShape), is(false)); + } + + @Test + public void checksForRequestQueryParamsBindings() { + Shape queryParamsShape = MemberShape.builder() + .target("smithy.api#Timestamp") + .id("smithy.example#Baz$bar") + .addTrait(new HttpQueryParamsTrait()) + .build(); + + assertThat(HttpBindingIndex.hasHttpRequestBindings(queryParamsShape), is(true)); + assertThat(HttpBindingIndex.hasHttpResponseBindings(queryParamsShape), is(false)); + } + @Test public void resolvesStructureBodyContentType() { HttpBindingIndex index = HttpBindingIndex.of(model); diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors index de3259a325e..c5659de11a3 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors @@ -29,3 +29,4 @@ [ERROR] ns.foo#HInput$a: Trait `httpHeader` cannot be applied to `ns.foo#HInput$a`. This trait may only be applied to shapes that match the following selector: structure > :test(member > :test(boolean, number, string, timestamp, collection > member > :test(boolean, number, string, timestamp))) | TraitTarget [ERROR] ns.foo#EInput$label2: Trait `httpLabel` cannot be applied to `ns.foo#EInput$label2`. This trait may only be applied to shapes that match the following selector: structure > member[trait|required] :test(> :test(string, number, boolean, timestamp)) | TraitTarget [ERROR] ns.foo#OInput$a: Trait `httpLabel` cannot be applied to `ns.foo#OInput$a`. This trait may only be applied to shapes that match the following selector: structure > member[trait|required] :test(> :test(string, number, boolean, timestamp)) | TraitTarget +[NOTE] ns.foo#QueryParamsInput: Structure member `queryParams` is marked with the `httpQueryParams` trait, and `httpQuery` traits are applied to the following members: `namedQuery`, `otherNamedQuery`. The service will not be able to disambiguate between query string parameters intended for the `queryParams` member and those explicitly bound to the `httpQuery` members. | HttpQueryParamsTrait 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 8a4d4fe9968..998d220f288 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 @@ -64,6 +64,9 @@ }, { "target": "ns.foo#MapPayload" + }, + { + "target": "ns.foo#QueryParams" } ] }, @@ -727,6 +730,52 @@ } } }, + "ns.foo#QueryParams": { + "type": "operation", + "input": { + "target": "ns.foo#QueryParamsInput" + }, + "output": { + "target": "ns.foo#QueryParamsOutput" + }, + "traits": { + "smithy.api#http": { + "method": "POST", + "uri": "/query-params" + } + } + }, + "ns.foo#QueryParamsInput": { + "type": "structure", + "members": { + "namedQuery": { + "target": "ns.foo#String", + "traits": { + "smithy.api#httpQuery": "named" + } + }, + "otherNamedQuery": { + "target": "ns.foo#String", + "traits": { + "smithy.api#httpQuery": "otherNamed" + } + }, + "queryParams": { + "target": "ns.foo#MapOfString", + "traits": { + "smithy.api#httpQueryParams": {} + } + } + } + }, + "ns.foo#QueryParamsOutput": { + "type": "structure", + "members": { + "foo": { + "target": "ns.foo#String" + } + } + }, "ns.foo#Integer": { "type": "integer" }, diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/http-index.json b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/http-index.json index c0453dc542d..22b2758c1ac 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/http-index.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/http-index.json @@ -107,6 +107,12 @@ "smithy.api#httpQuery": "bar" } }, + "corge": { + "target": "ns.foo#StringMap", + "traits": { + "smithy.api#httpQueryParams": {} + } + }, "bam": { "target": "ns.foo#String", "traits": { diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/protocols/AbstractRestProtocol.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/protocols/AbstractRestProtocol.java index abab25cecc0..57a36af4914 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/protocols/AbstractRestProtocol.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/protocols/AbstractRestProtocol.java @@ -189,13 +189,17 @@ private boolean needsInlineTimestampSchema(Context context, Mem } // Creates parameters that appear in the query string. Each input member - // bound to the QUERY location will generate a new ParameterObject that - // has a location of "query". + // bound to the QUERY or QUERY_PARAMS location will generate a new + // ParameterObject that has a location of "query". private List createQueryParameters(Context context, OperationShape operation) { HttpBindingIndex httpBindingIndex = HttpBindingIndex.of(context.getModel()); List result = new ArrayList<>(); - for (HttpBinding binding : httpBindingIndex.getRequestBindings(operation, HttpBinding.Location.QUERY)) { + List bindings = new ArrayList<>(); + bindings.addAll(httpBindingIndex.getRequestBindings(operation, HttpBinding.Location.QUERY)); + bindings.addAll(httpBindingIndex.getRequestBindings(operation, HttpBinding.Location.QUERY_PARAMS)); + + for (HttpBinding binding : bindings) { MemberShape member = binding.getMember(); ParameterObject.Builder param = ModelUtils.createParameterMember(context, member) .in("query") @@ -209,16 +213,33 @@ private List createQueryParameters(Context context, Operatio param.style("form").explode(true); } - // Create the appropriate schema based on the shape type. - Schema refSchema = context.inlineOrReferenceSchema(member); - QuerySchemaVisitor visitor = new QuerySchemaVisitor<>(context, refSchema, member); - param.schema(target.accept(visitor)); + // To allow undefined parameters of a specific type, the style is set to `form`. This is set in conjunction + // with a schema of the `object` type. + if (binding.getLocation().equals(HttpBinding.Location.QUERY_PARAMS)) { + param.style("form"); + + // QUERY_PARAMS necessarily target maps. If the map value is a list or set, the query string are + // repeated and must also be set to "explode". + Shape shape = context.getModel().expectShape(target.asMapShape().get().getValue().getTarget()); + if (shape instanceof CollectionShape) { + param.explode(true); + } + } + + param.schema(createQuerySchema(context, member, target)); result.add(param.build()); } return result; } + private Schema createQuerySchema(Context context, MemberShape member, Shape target) { + // Create the appropriate schema based on the shape type. + Schema refSchema = context.inlineOrReferenceSchema(member); + QuerySchemaVisitor visitor = new QuerySchemaVisitor<>(context, refSchema, member); + return target.accept(visitor); + } + private Collection createRequestHeaderParameters(Context context, OperationShape operation) { List bindings = HttpBindingIndex.of(context.getModel()) .getRequestBindings(operation, HttpBinding.Location.HEADER); diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service-integer.openapi.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service-integer.openapi.json index 74e9189add8..86210095860 100644 --- a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service-integer.openapi.json +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service-integer.openapi.json @@ -35,6 +35,14 @@ "description": "Query list docs!" }, "explode": true + }, + { + "name": "queryParams", + "in": "query", + "style": "form", + "schema": { + "$ref": "#/components/schemas/QueryMap" + } } ], "responses": { @@ -51,6 +59,43 @@ } } }, + "/foo": { + "post": { + "operationId": "CreateFoo", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFooRequestContent" + } + } + } + }, + "parameters": [ + { + "name": "queryParams", + "in": "query", + "style": "form", + "schema": { + "$ref": "#/components/schemas/QueryStringListMap" + }, + "explode": true + } + ], + "responses": { + "200": { + "description": "CreateFoo 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFooResponseContent" + } + } + } + } + } + } + }, "/payload/{path}": { "put": { "operationId": "PutPayload", @@ -186,6 +231,22 @@ "time" ] }, + "CreateFooRequestContent": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + }, + "CreateFooResponseContent": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + }, "EnumString": { "type": "string", "enum": [ @@ -208,6 +269,21 @@ "type": "string", "format": "byte" }, + "QueryMap": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "QueryStringListMap": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, "TaggedUnion": { "oneOf": [ { diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service.json index fec89bb4b61..64f6b011cda 100644 --- a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service.json +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service.json @@ -10,6 +10,9 @@ }, { "target": "example.rest#PutPayload" + }, + { + "target": "example.rest#CreateFoo" } ], "traits": { @@ -149,6 +152,12 @@ "traits": { "smithy.api#required": {} } + }, + "queryParams": { + "target": "example.rest#QueryMap", + "traits": { + "smithy.api#httpQueryParams": {} + } } } }, @@ -175,12 +184,56 @@ } } }, + "example.rest#CreateFoo": { + "type": "operation", + "input": { + "target": "example.rest#CreateFooInput" + }, + "output": { + "target": "example.rest#CreateFooOutput" + }, + "traits": { + "smithy.api#http": { + "uri": "/foo", + "method": "POST" + } + } + }, + "example.rest#CreateFooInput": { + "type": "structure", + "members": { + "queryParams": { + "target": "example.rest#QueryStringListMap", + "traits": { + "smithy.api#httpQueryParams": {} + } + }, + "foo": { + "target": "example.rest#String" + } + } + }, + "example.rest#CreateFooOutput": { + "type": "structure", + "members": { + "foo": { + "target": "example.rest#String" + } + } + + }, "example.rest#Blob": { "type": "blob" }, "example.rest#String": { "type": "string" }, + "example.rest#StringList": { + "type": "list", + "member": { + "target": "example.rest#String" + } + }, "example.rest#Integer": { "type": "integer" }, @@ -230,6 +283,24 @@ } } }, + "example.rest#QueryMap": { + "type": "map", + "key": { + "target": "example.rest#String" + }, + "value": { + "target": "example.rest#String" + } + }, + "example.rest#QueryStringListMap": { + "type": "map", + "key": { + "target": "example.rest#String" + }, + "value": { + "target": "example.rest#StringList" + } + }, "example.rest#Timestamp": { "type": "timestamp" }, diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service.openapi.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service.openapi.json index 5cc3ed43cd3..53fa4291179 100644 --- a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service.openapi.json +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service.openapi.json @@ -35,6 +35,14 @@ "description": "Query list docs!" }, "explode": true + }, + { + "name": "queryParams", + "in": "query", + "style": "form", + "schema": { + "$ref": "#/components/schemas/QueryMap" + } } ], "responses": { @@ -51,6 +59,43 @@ } } }, + "/foo": { + "post": { + "operationId": "CreateFoo", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFooRequestContent" + } + } + } + }, + "parameters": [ + { + "name": "queryParams", + "in": "query", + "style": "form", + "schema": { + "$ref": "#/components/schemas/QueryStringListMap" + }, + "explode": true + } + ], + "responses": { + "200": { + "description": "CreateFoo 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFooResponseContent" + } + } + } + } + } + } + }, "/payload/{path}": { "put": { "operationId": "PutPayload", @@ -186,6 +231,22 @@ "time" ] }, + "CreateFooRequestContent": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + }, + "CreateFooResponseContent": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + }, "EnumString": { "type": "string", "enum": [ @@ -208,6 +269,21 @@ "type": "string", "format": "byte" }, + "QueryMap": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "QueryStringListMap": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, "TaggedUnion": { "oneOf": [ {