Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ crash-*
mise.toml

.claude/settings.local.json

**/bin
3 changes: 1 addition & 2 deletions client/client-rpcv2-cbor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ extra["displayName"] = "Smithy :: Java :: Client :: RPCv2 CBOR"
extra["moduleName"] = "software.amazon.smithy.java.client.rpcv2cbor"

dependencies {
api(project(":client:client-http"))
api(project(":client:client-rpcv2"))
api(project(":codecs:cbor-codec"))
api(project(":aws:aws-event-streams"))
api(libs.smithy.aws.traits)

implementation(libs.smithy.protocol.traits)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,170 +5,36 @@

package software.amazon.smithy.java.client.rpcv2;

import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import software.amazon.smithy.java.aws.events.AwsEventDecoderFactory;
import software.amazon.smithy.java.aws.events.AwsEventEncoderFactory;
import software.amazon.smithy.java.aws.events.AwsEventFrame;
import software.amazon.smithy.java.aws.events.RpcEventStreamsUtil;
import software.amazon.smithy.java.cbor.Rpcv2CborCodec;
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.ErrorTypeUtils;
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.schema.ApiOperation;
import software.amazon.smithy.java.core.schema.SerializableStruct;
import software.amazon.smithy.java.core.schema.TraitKey;
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.core.serde.document.DocumentDeserializer;
import software.amazon.smithy.java.core.serde.event.EventDecoderFactory;
import software.amazon.smithy.java.core.serde.event.EventEncoderFactory;
import software.amazon.smithy.java.core.serde.event.EventStreamingException;
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.http.api.HttpVersion;
import software.amazon.smithy.java.io.ByteBufferOutputStream;
import software.amazon.smithy.java.io.datastream.DataStream;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.protocol.traits.Rpcv2CborTrait;

/**
* Implements smithy.protocols#rpcv2Cbor.
* Client protocol implementation for {@code smithy.protocols#rpcv2Cbor}.
*/
public final class RpcV2CborProtocol extends HttpClientProtocol {
private static final Codec CBOR_CODEC = Rpcv2CborCodec.builder().build();
public final class RpcV2CborProtocol extends AbstractRpcV2ClientProtocol {
private static final String PAYLOAD_MEDIA_TYPE = "application/cbor";
private static final List<String> CONTENT_TYPE = List.of(PAYLOAD_MEDIA_TYPE);
private static final List<String> SMITHY_PROTOCOL = List.of("rpc-v2-cbor");

private final ShapeId service;
private final HttpErrorDeserializer errorDeserializer;
private static final Codec CBOR_CODEC = Rpcv2CborCodec.builder().build();

public RpcV2CborProtocol(ShapeId service) {
super(Rpcv2CborTrait.ID);
this.service = service;
this.errorDeserializer = HttpErrorDeserializer.builder()
.codec(CBOR_CODEC)
.serviceId(service)
.errorPayloadParser(RpcV2CborProtocol::extractErrorType)
.build();
super(Rpcv2CborTrait.ID, service, PAYLOAD_MEDIA_TYPE);
}

@Override
public Codec payloadCodec() {
protected Codec codec() {
return CBOR_CODEC;
}

@Override
public <I extends SerializableStruct, O extends SerializableStruct> HttpRequest createRequest(
ApiOperation<I, O> operation,
I input,
Context context,
URI endpoint
) {
var target = "/service/" + service.getName() + "/operation/" + operation.schema().id().getName();
var builder = HttpRequest.builder().method("POST").uri(endpoint.resolve(target));

protected void customizeRequestBuilder(HttpRequest.Builder builder) {
builder.httpVersion(HttpVersion.HTTP_2);
if (operation.inputSchema().hasTrait(TraitKey.UNIT_TYPE_TRAIT)) {
// Top-level Unit types do not get serialized
builder.headers(HttpHeaders.of(headersForEmptyBody()))
.body(DataStream.ofEmpty());
} else if (operation.inputEventBuilderSupplier() != null) {
// Event streaming
var encoderFactory = getEventEncoderFactory(operation);
var body = RpcEventStreamsUtil.bodyForEventStreaming(encoderFactory, input);
builder.headers(HttpHeaders.of(headersForEventStreaming()))
.body(body);
} else {
// Regular request
builder.headers(HttpHeaders.of(headers()))
.body(getBody(input));
}
return builder.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() != 200) {
throw errorDeserializer.createError(context, operation, typeRegistry, response);
}

if (operation.outputEventBuilderSupplier() != null) {
var eventDecoderFactory = getEventDecoderFactory(operation);
return RpcEventStreamsUtil.deserializeResponse(eventDecoderFactory, bodyDataStream(response));
}

var builder = operation.outputBuilder();
var content = response.body();
if (content.contentLength() == 0) {
return builder.build();
}

var bytes = content.asByteBuffer();
return CBOR_CODEC.deserializeShape(bytes, builder);
}

private static DataStream bodyDataStream(HttpResponse response) {
var contentType = response.headers().contentType();
var contentLength = response.headers().contentLength();
return DataStream.withMetadata(response.body(), contentType, contentLength, null);
}

private DataStream getBody(SerializableStruct input) {
var sink = new ByteBufferOutputStream();
try (var serializer = CBOR_CODEC.createSerializer(sink)) {
input.serialize(serializer);
}
return DataStream.ofByteBuffer(sink.toByteBuffer(), PAYLOAD_MEDIA_TYPE);
}

private Map<String, List<String>> headers() {
return Map.of("smithy-protocol", SMITHY_PROTOCOL, "Content-Type", CONTENT_TYPE, "Accept", CONTENT_TYPE);
}

private Map<String, List<String>> headersForEmptyBody() {
return Map.of("smithy-protocol", SMITHY_PROTOCOL, "Accept", CONTENT_TYPE);
}

private Map<String, List<String>> headersForEventStreaming() {
return Map.of("smithy-protocol",
SMITHY_PROTOCOL,
"Content-Type",
List.of("application/vnd.amazon.eventstream"),
"Accept",
CONTENT_TYPE);
}

private EventEncoderFactory<AwsEventFrame> getEventEncoderFactory(ApiOperation<?, ?> operation) {
return AwsEventEncoderFactory.forInputStream(operation,
payloadCodec(),
PAYLOAD_MEDIA_TYPE,
(e) -> new EventStreamingException("InternalServerException", "Internal Server Error"));
}

private EventDecoderFactory<AwsEventFrame> getEventDecoderFactory(ApiOperation<?, ?> operation) {
return AwsEventDecoderFactory.forOutputStream(operation, payloadCodec(), f -> f);
}

private static ShapeId extractErrorType(Document document, String namespace) {
return DocumentDeserializer.parseDiscriminator(
ErrorTypeUtils.removeUri(ErrorTypeUtils.readType(document)),
namespace);
}

public static final class Factory implements ClientProtocolFactory<Rpcv2CborTrait> {
Expand Down
23 changes: 23 additions & 0 deletions client/client-rpcv2-json/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
plugins {
id("smithy-java.module-conventions")
id("smithy-java.protocol-testing-conventions")
}

description = "This module provides the implementation of the client RpcV2 JSON protocol"

extra["displayName"] = "Smithy :: Java :: Client :: RPCv2 JSON"
extra["moduleName"] = "software.amazon.smithy.java.client.rpcv2json"

dependencies {
api(project(":client:client-rpcv2"))
api(project(":codecs:json-codec", configuration = "shadow"))
api(libs.smithy.aws.traits)

implementation(libs.smithy.protocol.traits)

// Protocol test dependencies
testImplementation(libs.smithy.protocol.tests)
}

val generator = "software.amazon.smithy.java.protocoltests.generators.ProtocolTestGenerator"
addGenerateSrcsTask(generator, "rpcv2Json", "smithy.protocoltests.rpcv2Json#RpcV2JsonProtocol")
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.client.rpcv2json;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.nio.charset.StandardCharsets;
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.StringBuildingSubscriber;
import software.amazon.smithy.java.protocoltests.harness.TestType;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;

@ProtocolTest(
service = "smithy.protocoltests.rpcv2Json#RpcV2JsonProtocol",
testType = TestType.CLIENT)
public class RpcV2JsonProtocolTests {
@HttpClientRequestTests
@ProtocolTestFilter(
skipTests = {
// clientOptional is not respected for client-generated shapes yet
"RpcV2JsonRequestClientSkipsTopLevelDefaultValuesInInput",
"RpcV2JsonRequestClientPopulatesDefaultValuesInInput",
"RpcV2JsonRequestClientUsesExplicitlyProvidedMemberValuesOverDefaults",
"RpcV2JsonRequestClientIgnoresNonTopLevelDefaultsOnMembersWithClientOptional",
})
public void requestTest(DataStream expected, DataStream actual) {
Node expectedNode = ObjectNode.objectNode();
if (expected.contentLength() != 0) {
expectedNode = Node.parse(new String(ByteBufferUtils.getBytes(expected.asByteBuffer()),
StandardCharsets.UTF_8));
}
Node actualNode = ObjectNode.objectNode();
if (actual.contentLength() != 0) {
actualNode = Node.parse(new StringBuildingSubscriber(actual).getResult());
}
assertEquals(expectedNode, actualNode);
}

@HttpClientResponseTests
@ProtocolTestFilter(
skipTests = {
"RpcV2JsonResponseClientPopulatesDefaultsValuesWhenMissingInResponse",
})
public void responseTest(Runnable test) {
test.run();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.client.rpcv2json;

import java.util.Objects;
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.rpcv2.AbstractRpcV2ClientProtocol;
import software.amazon.smithy.java.core.serde.Codec;
import software.amazon.smithy.java.json.JsonCodec;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.protocol.traits.Rpcv2JsonTrait;

/**
* Client protocol implementation for {@code smithy.protocols#rpcv2Json}.
*
* <p>BigDecimal and BigInteger values are serialized as JSON strings to preserve
* arbitrary precision.
*/
public final class RpcV2JsonProtocol extends AbstractRpcV2ClientProtocol {
private static final String PAYLOAD_MEDIA_TYPE = "application/json";

private final JsonCodec codec;

public RpcV2JsonProtocol(ShapeId service) {
super(Rpcv2JsonTrait.ID, service, PAYLOAD_MEDIA_TYPE);
this.codec = JsonCodec.builder()
.defaultNamespace(service.getNamespace())
.useStringForArbitraryPrecision(true)
.build();
}

@Override
protected Codec codec() {
return codec;
}

public static final class Factory implements ClientProtocolFactory<Rpcv2JsonTrait> {
@Override
public ShapeId id() {
return Rpcv2JsonTrait.ID;
}

@Override
public ClientProtocol<?, ?> createProtocol(ProtocolSettings settings, Rpcv2JsonTrait trait) {
return new RpcV2JsonProtocol(
Objects.requireNonNull(settings.service(), "service is a required protocol setting"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
software.amazon.smithy.java.client.rpcv2json.RpcV2JsonProtocol$Factory
13 changes: 13 additions & 0 deletions client/client-rpcv2/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins {
id("smithy-java.module-conventions")
}

description = "This module provides the shared base implementation for client RpcV2 protocols"

extra["displayName"] = "Smithy :: Java :: Client :: RPCv2"
extra["moduleName"] = "software.amazon.smithy.java.client.rpcv2"

dependencies {
api(project(":client:client-http"))
api(project(":aws:aws-event-streams"))
}
Loading
Loading