generated from amazon-archives/__template_Apache-2.0
-
Notifications
You must be signed in to change notification settings - Fork 23
Add support for AWS Query protocol in the client #1029
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6c7b464
Add support for AWS Query protocol in the client
adwsingh 7a4d2ac
Add docs and tests for FormUrlEncodedSink
adwsingh d48935b
Perf improvements
adwsingh aee2745
Fix QueryCustomizedError protocol test
adwsingh de51146
Make errorSchemas non-default in the interface
adwsingh c0321dc
Make errorSchemas non-default in the interface
adwsingh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| plugins { | ||
| id("smithy-java.module-conventions") | ||
| id("smithy-java.protocol-testing-conventions") | ||
| } | ||
|
|
||
| description = "This module provides the implementation of AWS Query protocol" | ||
|
|
||
| extra["displayName"] = "Smithy :: Java :: AWS :: Client :: AWS Query" | ||
| extra["moduleName"] = "software.amazon.smithy.java.aws.client.awsquery" | ||
|
|
||
| dependencies { | ||
| api(project(":client:client-http")) | ||
| api(project(":codecs:xml-codec")) | ||
| api(project(":io")) | ||
| api(libs.smithy.aws.traits) | ||
|
|
||
| // Protocol test dependencies | ||
| testImplementation(libs.smithy.aws.protocol.tests) | ||
| } | ||
|
|
||
| val generator = "software.amazon.smithy.java.protocoltests.generators.ProtocolTestGenerator" | ||
| addGenerateSrcsTask(generator, "awsQuery", "aws.protocoltests.query#AwsQuery") |
78 changes: 78 additions & 0 deletions
78
...ry/src/it/java/software/amazon/smithy/java/aws/client/awsquery/AwsQueryProtocolTests.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package software.amazon.smithy.java.aws.client.awsquery; | ||
|
|
||
| import static java.net.URLDecoder.decode; | ||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
|
|
||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Arrays; | ||
| import java.util.Map; | ||
| import java.util.TreeMap; | ||
| import java.util.stream.Collectors; | ||
| import software.amazon.smithy.java.io.ByteBufferUtils; | ||
| import software.amazon.smithy.java.io.datastream.DataStream; | ||
| import software.amazon.smithy.java.protocoltests.harness.HttpClientRequestTests; | ||
| import software.amazon.smithy.java.protocoltests.harness.HttpClientResponseTests; | ||
| import software.amazon.smithy.java.protocoltests.harness.ProtocolTest; | ||
| import software.amazon.smithy.java.protocoltests.harness.ProtocolTestFilter; | ||
| import software.amazon.smithy.java.protocoltests.harness.TestType; | ||
|
|
||
| @ProtocolTest( | ||
| service = "aws.protocoltests.query#AwsQuery", | ||
| testType = TestType.CLIENT) | ||
| public class AwsQueryProtocolTests { | ||
|
|
||
| @HttpClientRequestTests | ||
| @ProtocolTestFilter( | ||
| skipTests = { | ||
| "SDKAppliedContentEncoding_awsQuery", | ||
| "SDKAppendsGzipAndIgnoresHttpProvidedEncoding_awsQuery", | ||
| }) | ||
| public void requestTest(DataStream expected, DataStream actual) { | ||
| String expectedStr = new String( | ||
| ByteBufferUtils.getBytes(expected.asByteBuffer()), | ||
| StandardCharsets.UTF_8); | ||
| String actualStr = new String( | ||
| ByteBufferUtils.getBytes(actual.asByteBuffer()), | ||
| StandardCharsets.UTF_8); | ||
|
|
||
| Map<String, String> expectedParams = parseFormUrlEncoded(expectedStr); | ||
| Map<String, String> actualParams = parseFormUrlEncoded(actualStr); | ||
|
|
||
| assertEquals(expectedParams, actualParams); | ||
| } | ||
|
|
||
| @HttpClientResponseTests | ||
| @ProtocolTestFilter( | ||
| skipTests = { | ||
| "AwsQueryClientPopulatesDefaultsValuesWhenMissingInResponse", | ||
| }) | ||
| public void responseTest(Runnable test) { | ||
| test.run(); | ||
| } | ||
|
|
||
| private Map<String, String> parseFormUrlEncoded(String body) { | ||
| if (body == null || body.isEmpty()) { | ||
| return new TreeMap<>(); | ||
| } | ||
| return Arrays.stream(body.split("&")) | ||
| .map(pair -> pair.split("=", 2)) | ||
| .collect(Collectors.toMap( | ||
| parts -> urlDecode(parts[0]), | ||
| parts -> parts.length > 1 ? urlDecode(parts[1]) : "", | ||
| (a, b) -> b, | ||
| TreeMap::new)); | ||
| } | ||
|
|
||
| private String urlDecode(String value) { | ||
| try { | ||
| return decode(value, StandardCharsets.UTF_8); | ||
| } catch (Exception e) { | ||
| return value; | ||
| } | ||
| } | ||
| } |
197 changes: 197 additions & 0 deletions
197
...src/main/java/software/amazon/smithy/java/aws/client/awsquery/AwsQueryClientProtocol.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package software.amazon.smithy.java.aws.client.awsquery; | ||
|
|
||
| import java.net.URI; | ||
| import java.nio.ByteBuffer; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import software.amazon.smithy.aws.traits.protocols.AwsQueryErrorTrait; | ||
| import software.amazon.smithy.aws.traits.protocols.AwsQueryTrait; | ||
| import software.amazon.smithy.java.client.core.ClientProtocol; | ||
| import software.amazon.smithy.java.client.core.ClientProtocolFactory; | ||
| import software.amazon.smithy.java.client.core.ProtocolSettings; | ||
| import software.amazon.smithy.java.client.http.HttpClientProtocol; | ||
| import software.amazon.smithy.java.client.http.HttpErrorDeserializer; | ||
| import software.amazon.smithy.java.context.Context; | ||
| import software.amazon.smithy.java.core.error.CallException; | ||
| import software.amazon.smithy.java.core.error.ModeledException; | ||
| import software.amazon.smithy.java.core.schema.ApiOperation; | ||
| import software.amazon.smithy.java.core.schema.Schema; | ||
| import software.amazon.smithy.java.core.schema.SerializableStruct; | ||
| import software.amazon.smithy.java.core.schema.ShapeBuilder; | ||
| import software.amazon.smithy.java.core.schema.TraitKey; | ||
| import software.amazon.smithy.java.core.schema.Unit; | ||
| import software.amazon.smithy.java.core.serde.Codec; | ||
| import software.amazon.smithy.java.core.serde.TypeRegistry; | ||
| import software.amazon.smithy.java.core.serde.document.Document; | ||
| import software.amazon.smithy.java.http.api.HttpHeaders; | ||
| import software.amazon.smithy.java.http.api.HttpRequest; | ||
| import software.amazon.smithy.java.http.api.HttpResponse; | ||
| import software.amazon.smithy.java.io.datastream.DataStream; | ||
| import software.amazon.smithy.java.xml.XmlCodec; | ||
| import software.amazon.smithy.java.xml.XmlUtil; | ||
| import software.amazon.smithy.model.shapes.ShapeId; | ||
|
|
||
| public final class AwsQueryClientProtocol extends HttpClientProtocol { | ||
|
|
||
| private static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; | ||
| private static final List<String> CONTENT_TYPE_LIST = List.of(CONTENT_TYPE); | ||
| public static final HttpHeaders CONTENT_TYPE_HEADERS = HttpHeaders.of(Map.of("Content-Type", CONTENT_TYPE_LIST)); | ||
|
|
||
| private final ShapeId service; | ||
| private final String version; | ||
| private final HttpErrorDeserializer errorDeserializer; | ||
| private final XmlCodec codec = XmlCodec.builder().build(); | ||
|
|
||
| public AwsQueryClientProtocol(ShapeId service, String version) { | ||
| super(AwsQueryTrait.ID); | ||
| this.service = Objects.requireNonNull(service, "service is required"); | ||
| this.version = Objects.requireNonNull(version, "version is required"); | ||
| this.errorDeserializer = HttpErrorDeserializer.builder() | ||
| .codec(codec) | ||
| .serviceId(service) | ||
| .errorPayloadParser(XML_ERROR_PAYLOAD_PARSER) | ||
| .knownErrorFactory(new XmlKnownErrorFactory()) | ||
| .build(); | ||
| } | ||
|
|
||
| @Override | ||
| public Codec payloadCodec() { | ||
| return codec; | ||
| } | ||
|
|
||
| @Override | ||
| public <I extends SerializableStruct, O extends SerializableStruct> HttpRequest createRequest( | ||
| ApiOperation<I, O> operation, | ||
| I input, | ||
| Context context, | ||
| URI endpoint | ||
| ) { | ||
| String operationName = operation.schema().id().getName(); | ||
| AwsQueryFormSerializer serializer = new AwsQueryFormSerializer(operationName, version); | ||
|
|
||
| if (!Unit.ID.equals(operation.inputSchema().id())) { | ||
| input.serializeMembers(serializer); | ||
| } | ||
|
|
||
| ByteBuffer body = serializer.finish(); | ||
|
|
||
| return HttpRequest.builder() | ||
| .method("POST") | ||
| .uri(endpoint) | ||
| .headers(CONTENT_TYPE_HEADERS) | ||
| .body(DataStream.ofByteBuffer(body, CONTENT_TYPE)) | ||
| .build(); | ||
| } | ||
|
|
||
| @Override | ||
| public <I extends SerializableStruct, O extends SerializableStruct> O deserializeResponse( | ||
| ApiOperation<I, O> operation, | ||
| Context context, | ||
| TypeRegistry typeRegistry, | ||
| HttpRequest request, | ||
| HttpResponse response | ||
| ) { | ||
| if (response.statusCode() >= 300) { | ||
| throw errorDeserializer.createError(context, operation, typeRegistry, response); | ||
| } | ||
|
|
||
| var builder = operation.outputBuilder(); | ||
| var content = response.body(); | ||
|
|
||
| if (content.contentLength() == 0) { | ||
| return builder.build(); | ||
| } | ||
|
|
||
| var operationName = operation.schema().id().getName(); | ||
| try (var codec = XmlCodec.builder() | ||
| .wrapperElements(List.of(operationName + "Response", operationName + "Result")) | ||
| .build()) { | ||
| return codec.deserializeShape(response.body().asByteBuffer(), builder); | ||
| } | ||
| } | ||
|
|
||
| private static final HttpErrorDeserializer.ErrorPayloadParser XML_ERROR_PAYLOAD_PARSER = | ||
| new HttpErrorDeserializer.ErrorPayloadParser() { | ||
| @Override | ||
| public CallException parsePayload( | ||
| Context context, | ||
| Codec codec, | ||
| HttpErrorDeserializer.KnownErrorFactory knownErrorFactory, | ||
| ShapeId serviceId, | ||
| TypeRegistry typeRegistry, | ||
| ApiOperation<?, ?> operation, | ||
| HttpResponse response, | ||
| ByteBuffer buffer | ||
| ) { | ||
| var deserializer = codec.createDeserializer(buffer); | ||
| String code = XmlUtil.parseErrorCodeName(deserializer); | ||
|
|
||
| // First, resolve @awsQueryError custom codes | ||
| ShapeBuilder<ModeledException> builder = null; | ||
| for (Schema errorSchema : operation.errorSchemas()) { | ||
| var trait = errorSchema.getTrait(TraitKey.get(AwsQueryErrorTrait.class)); | ||
| if (trait != null && code.equals(trait.getCode())) { | ||
| builder = typeRegistry.createBuilder(errorSchema.id(), ModeledException.class); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // Fallback: resolve by shape ID | ||
| if (builder == null) { | ||
| var id = ShapeId.fromOptionalNamespace(serviceId.getNamespace(), code); | ||
| builder = typeRegistry.createBuilder(id, ModeledException.class); | ||
| } | ||
|
|
||
| if (builder != null) { | ||
| return knownErrorFactory.createError(context, codec, response, builder); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| @Override | ||
| public ShapeId extractErrorType( | ||
| Document document, | ||
| String namespace | ||
| ) { | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| private static final class XmlKnownErrorFactory implements HttpErrorDeserializer.KnownErrorFactory { | ||
| @Override | ||
| public ModeledException createError( | ||
| Context context, | ||
| Codec codec, | ||
| HttpResponse response, | ||
| ShapeBuilder<ModeledException> builder | ||
| ) { | ||
| ByteBuffer bytes = DataStream.ofPublisher( | ||
| response.body(), | ||
| response.contentType(), | ||
| response.contentLength(-1)).asByteBuffer(); | ||
| return codec.deserializeShape(bytes, builder); | ||
| } | ||
| } | ||
|
|
||
| public static final class Factory implements ClientProtocolFactory<AwsQueryTrait> { | ||
|
|
||
| @Override | ||
| public ShapeId id() { | ||
| return AwsQueryTrait.ID; | ||
| } | ||
|
|
||
| @Override | ||
| public ClientProtocol<?, ?> createProtocol(ProtocolSettings settings, AwsQueryTrait trait) { | ||
| return new AwsQueryClientProtocol( | ||
| Objects.requireNonNull(settings.service(), "service is a required protocol setting"), | ||
| Objects.requireNonNull(settings.serviceVersion(), | ||
| "serviceVersion is a required protocol setting for AWS Query.")); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.