Skip to content

Commit b23eaf7

Browse files
authored
Add support for Smithy RPC v2 JSON
Smithy RPC v2 JSON is a JSON-payload based protocol in the Smithy RPC v2 family. Support is added for both clients and servers. The JSON codec has been updated to allow for transmitting arbitrary precision numbers in JSON strings. A bug in the protocol test implementation was fixed to allow for testing arbitrary precision numbers have been fixed.
1 parent d1860cf commit b23eaf7

28 files changed

Lines changed: 948 additions & 337 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ crash-*
2626
mise.toml
2727

2828
.claude/settings.local.json
29+
30+
**/bin

client/client-rpcv2-cbor/build.gradle.kts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ extra["displayName"] = "Smithy :: Java :: Client :: RPCv2 CBOR"
99
extra["moduleName"] = "software.amazon.smithy.java.client.rpcv2cbor"
1010

1111
dependencies {
12-
api(project(":client:client-http"))
12+
api(project(":client:client-rpcv2"))
1313
api(project(":codecs:cbor-codec"))
14-
api(project(":aws:aws-event-streams"))
1514
api(libs.smithy.aws.traits)
1615

1716
implementation(libs.smithy.protocol.traits)

client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java

Lines changed: 7 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -6,153 +6,35 @@
66
package software.amazon.smithy.java.client.rpcv2;
77

88
import java.util.Objects;
9-
import software.amazon.smithy.java.aws.events.AwsEventDecoderFactory;
10-
import software.amazon.smithy.java.aws.events.AwsEventEncoderFactory;
11-
import software.amazon.smithy.java.aws.events.AwsEventFrame;
12-
import software.amazon.smithy.java.aws.events.RpcEventStreamsUtil;
139
import software.amazon.smithy.java.cbor.Rpcv2CborCodec;
1410
import software.amazon.smithy.java.client.core.ClientProtocol;
1511
import software.amazon.smithy.java.client.core.ClientProtocolFactory;
1612
import software.amazon.smithy.java.client.core.ProtocolSettings;
17-
import software.amazon.smithy.java.client.http.ErrorTypeUtils;
18-
import software.amazon.smithy.java.client.http.HttpClientProtocol;
19-
import software.amazon.smithy.java.client.http.HttpErrorDeserializer;
20-
import software.amazon.smithy.java.context.Context;
21-
import software.amazon.smithy.java.core.schema.ApiOperation;
22-
import software.amazon.smithy.java.core.schema.SerializableStruct;
23-
import software.amazon.smithy.java.core.schema.TraitKey;
2413
import software.amazon.smithy.java.core.serde.Codec;
25-
import software.amazon.smithy.java.core.serde.TypeRegistry;
26-
import software.amazon.smithy.java.core.serde.document.Document;
27-
import software.amazon.smithy.java.core.serde.document.DocumentDeserializer;
28-
import software.amazon.smithy.java.core.serde.event.EventDecoderFactory;
29-
import software.amazon.smithy.java.core.serde.event.EventEncoderFactory;
30-
import software.amazon.smithy.java.core.serde.event.EventStreamingException;
31-
import software.amazon.smithy.java.http.api.HeaderName;
32-
import software.amazon.smithy.java.http.api.HttpRequest;
33-
import software.amazon.smithy.java.http.api.HttpResponse;
3414
import software.amazon.smithy.java.http.api.HttpVersion;
35-
import software.amazon.smithy.java.io.ByteBufferOutputStream;
36-
import software.amazon.smithy.java.io.datastream.DataStream;
37-
import software.amazon.smithy.java.io.uri.SmithyUri;
15+
import software.amazon.smithy.java.http.api.ModifiableHttpRequest;
3816
import software.amazon.smithy.model.shapes.ShapeId;
3917
import software.amazon.smithy.protocol.traits.Rpcv2CborTrait;
4018

4119
/**
42-
* Implements smithy.protocols#rpcv2Cbor.
20+
* Client protocol implementation for {@code smithy.protocols#rpcv2Cbor}.
4321
*/
44-
public final class RpcV2CborProtocol extends HttpClientProtocol {
45-
private static final Codec CBOR_CODEC = Rpcv2CborCodec.builder().build();
22+
public final class RpcV2CborProtocol extends AbstractRpcV2ClientProtocol {
4623
private static final String PAYLOAD_MEDIA_TYPE = "application/cbor";
47-
48-
private final ShapeId service;
49-
private final HttpErrorDeserializer errorDeserializer;
24+
private static final Codec CBOR_CODEC = Rpcv2CborCodec.builder().build();
5025

5126
public RpcV2CborProtocol(ShapeId service) {
52-
super(Rpcv2CborTrait.ID);
53-
this.service = service;
54-
this.errorDeserializer = HttpErrorDeserializer.builder()
55-
.codec(CBOR_CODEC)
56-
.serviceId(service)
57-
.errorPayloadParser(RpcV2CborProtocol::extractErrorType)
58-
.build();
27+
super(Rpcv2CborTrait.ID, service, PAYLOAD_MEDIA_TYPE);
5928
}
6029

6130
@Override
62-
public Codec payloadCodec() {
31+
protected Codec codec() {
6332
return CBOR_CODEC;
6433
}
6534

6635
@Override
67-
public <I extends SerializableStruct, O extends SerializableStruct> HttpRequest createRequest(
68-
ApiOperation<I, O> operation,
69-
I input,
70-
Context context,
71-
SmithyUri endpoint
72-
) {
73-
var target = "/service/" + service.getName() + "/operation/" + operation.schema().id().getName();
74-
var builder = HttpRequest.create().setMethod("POST").setUri(endpoint.withConcatPath(target));
75-
36+
protected void customizeRequestBuilder(ModifiableHttpRequest builder) {
7637
builder.setHttpVersion(HttpVersion.HTTP_2);
77-
if (operation.inputSchema().hasTrait(TraitKey.UNIT_TYPE_TRAIT)) {
78-
// Top-level Unit types do not get serialized
79-
builder.addHeader(HeaderName.SMITHY_PROTOCOL, "rpc-v2-cbor")
80-
.addHeader(HeaderName.ACCEPT, PAYLOAD_MEDIA_TYPE)
81-
.setBody(DataStream.ofEmpty());
82-
} else if (operation.inputEventBuilderSupplier() != null) {
83-
// Event streaming
84-
var encoderFactory = getEventEncoderFactory(operation);
85-
var body = RpcEventStreamsUtil.bodyForEventStreaming(encoderFactory, input);
86-
builder.addHeader(HeaderName.SMITHY_PROTOCOL, "rpc-v2-cbor")
87-
.addHeader(HeaderName.CONTENT_TYPE, "application/vnd.amazon.eventstream")
88-
.addHeader(HeaderName.ACCEPT, PAYLOAD_MEDIA_TYPE)
89-
.setBody(body);
90-
} else {
91-
// Regular request
92-
builder.addHeader(HeaderName.SMITHY_PROTOCOL, "rpc-v2-cbor")
93-
.addHeader(HeaderName.CONTENT_TYPE, PAYLOAD_MEDIA_TYPE)
94-
.addHeader(HeaderName.ACCEPT, PAYLOAD_MEDIA_TYPE)
95-
.setBody(getBody(input));
96-
}
97-
return builder.toUnmodifiable();
98-
}
99-
100-
@Override
101-
public <I extends SerializableStruct, O extends SerializableStruct> O deserializeResponse(
102-
ApiOperation<I, O> operation,
103-
Context context,
104-
TypeRegistry typeRegistry,
105-
HttpRequest request,
106-
HttpResponse response
107-
) {
108-
if (response.statusCode() != 200) {
109-
throw errorDeserializer.createError(context, operation, typeRegistry, response);
110-
}
111-
112-
if (operation.outputEventBuilderSupplier() != null) {
113-
var eventDecoderFactory = getEventDecoderFactory(operation);
114-
return RpcEventStreamsUtil.deserializeResponse(eventDecoderFactory, bodyDataStream(response));
115-
}
116-
117-
var builder = operation.outputBuilder();
118-
var content = response.body();
119-
if (content.contentLength() == 0) {
120-
return builder.build();
121-
}
122-
123-
var bytes = content.asByteBuffer();
124-
return CBOR_CODEC.deserializeShape(bytes, builder);
125-
}
126-
127-
private static DataStream bodyDataStream(HttpResponse response) {
128-
var contentType = response.headers().contentType();
129-
var contentLength = response.headers().contentLength();
130-
return DataStream.withMetadata(response.body(), contentType, contentLength, null);
131-
}
132-
133-
private DataStream getBody(SerializableStruct input) {
134-
var sink = new ByteBufferOutputStream();
135-
try (var serializer = CBOR_CODEC.createSerializer(sink)) {
136-
input.serialize(serializer);
137-
}
138-
return DataStream.ofByteBuffer(sink.toByteBuffer(), PAYLOAD_MEDIA_TYPE);
139-
}
140-
141-
private EventEncoderFactory<AwsEventFrame> getEventEncoderFactory(ApiOperation<?, ?> operation) {
142-
return AwsEventEncoderFactory.forInputStream(operation,
143-
payloadCodec(),
144-
PAYLOAD_MEDIA_TYPE,
145-
(e) -> new EventStreamingException("InternalServerException", "Internal Server Error"));
146-
}
147-
148-
private EventDecoderFactory<AwsEventFrame> getEventDecoderFactory(ApiOperation<?, ?> operation) {
149-
return AwsEventDecoderFactory.forOutputStream(operation, payloadCodec(), f -> f);
150-
}
151-
152-
private static ShapeId extractErrorType(Document document, String namespace) {
153-
return DocumentDeserializer.parseDiscriminator(
154-
ErrorTypeUtils.removeUri(ErrorTypeUtils.readType(document)),
155-
namespace);
15638
}
15739

15840
public static final class Factory implements ClientProtocolFactory<Rpcv2CborTrait> {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
plugins {
2+
id("smithy-java.module-conventions")
3+
id("smithy-java.protocol-testing-conventions")
4+
}
5+
6+
description = "This module provides the implementation of the client RpcV2 JSON protocol"
7+
8+
extra["displayName"] = "Smithy :: Java :: Client :: RPCv2 JSON"
9+
extra["moduleName"] = "software.amazon.smithy.java.client.rpcv2json"
10+
11+
dependencies {
12+
api(project(":client:client-rpcv2"))
13+
api(project(":codecs:json-codec", configuration = "shadow"))
14+
api(libs.smithy.aws.traits)
15+
16+
implementation(libs.smithy.protocol.traits)
17+
18+
// Protocol test dependencies
19+
testImplementation(libs.smithy.protocol.tests)
20+
}
21+
22+
val generator = "software.amazon.smithy.java.protocoltests.generators.ProtocolTestGenerator"
23+
addGenerateSrcsTask(generator, "rpcv2Json", "smithy.protocoltests.rpcv2Json#RpcV2JsonProtocol")
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.client.rpcv2json;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
import java.nio.charset.StandardCharsets;
11+
import software.amazon.smithy.java.io.ByteBufferUtils;
12+
import software.amazon.smithy.java.io.datastream.DataStream;
13+
import software.amazon.smithy.java.protocoltests.harness.HttpClientRequestTests;
14+
import software.amazon.smithy.java.protocoltests.harness.HttpClientResponseTests;
15+
import software.amazon.smithy.java.protocoltests.harness.ProtocolTest;
16+
import software.amazon.smithy.java.protocoltests.harness.ProtocolTestFilter;
17+
import software.amazon.smithy.java.protocoltests.harness.StringBuildingSubscriber;
18+
import software.amazon.smithy.java.protocoltests.harness.TestType;
19+
import software.amazon.smithy.model.node.Node;
20+
import software.amazon.smithy.model.node.ObjectNode;
21+
22+
@ProtocolTest(
23+
service = "smithy.protocoltests.rpcv2Json#RpcV2JsonProtocol",
24+
testType = TestType.CLIENT)
25+
public class RpcV2JsonProtocolTests {
26+
@HttpClientRequestTests
27+
@ProtocolTestFilter(
28+
skipTests = {
29+
// clientOptional is not respected for client-generated shapes yet
30+
"RpcV2JsonRequestClientSkipsTopLevelDefaultValuesInInput",
31+
"RpcV2JsonRequestClientPopulatesDefaultValuesInInput",
32+
"RpcV2JsonRequestClientUsesExplicitlyProvidedMemberValuesOverDefaults",
33+
"RpcV2JsonRequestClientIgnoresNonTopLevelDefaultsOnMembersWithClientOptional",
34+
})
35+
public void requestTest(DataStream expected, DataStream actual) {
36+
Node expectedNode = ObjectNode.objectNode();
37+
if (expected.contentLength() != 0) {
38+
expectedNode = Node.parse(new String(ByteBufferUtils.getBytes(expected.asByteBuffer()),
39+
StandardCharsets.UTF_8));
40+
}
41+
Node actualNode = ObjectNode.objectNode();
42+
if (actual.contentLength() != 0) {
43+
actualNode = Node.parse(new StringBuildingSubscriber(actual).getResult());
44+
}
45+
assertEquals(expectedNode, actualNode);
46+
}
47+
48+
@HttpClientResponseTests
49+
@ProtocolTestFilter(
50+
skipTests = {
51+
"RpcV2JsonResponseClientPopulatesDefaultsValuesWhenMissingInResponse",
52+
})
53+
public void responseTest(Runnable test) {
54+
test.run();
55+
}
56+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.client.rpcv2json;
7+
8+
import java.util.Objects;
9+
import software.amazon.smithy.java.client.core.ClientProtocol;
10+
import software.amazon.smithy.java.client.core.ClientProtocolFactory;
11+
import software.amazon.smithy.java.client.core.ProtocolSettings;
12+
import software.amazon.smithy.java.client.rpcv2.AbstractRpcV2ClientProtocol;
13+
import software.amazon.smithy.java.core.serde.Codec;
14+
import software.amazon.smithy.java.json.JsonCodec;
15+
import software.amazon.smithy.model.shapes.ShapeId;
16+
import software.amazon.smithy.protocol.traits.Rpcv2JsonTrait;
17+
import software.amazon.smithy.utils.SmithyUnstableApi;
18+
19+
/**
20+
* Client protocol implementation for {@code smithy.protocols#rpcv2Json}.
21+
*
22+
* <p>BigDecimal and BigInteger values are serialized as JSON strings to preserve
23+
* arbitrary precision.
24+
*/
25+
@SmithyUnstableApi
26+
public final class RpcV2JsonProtocol extends AbstractRpcV2ClientProtocol {
27+
private static final String PAYLOAD_MEDIA_TYPE = "application/json";
28+
29+
private final JsonCodec codec;
30+
31+
public RpcV2JsonProtocol(ShapeId service) {
32+
super(Rpcv2JsonTrait.ID, service, PAYLOAD_MEDIA_TYPE);
33+
this.codec = JsonCodec.builder()
34+
.defaultNamespace(service.getNamespace())
35+
.useStringForArbitraryPrecision(true)
36+
.build();
37+
}
38+
39+
@Override
40+
protected Codec codec() {
41+
return codec;
42+
}
43+
44+
public static final class Factory implements ClientProtocolFactory<Rpcv2JsonTrait> {
45+
@Override
46+
public ShapeId id() {
47+
return Rpcv2JsonTrait.ID;
48+
}
49+
50+
@Override
51+
public ClientProtocol<?, ?> createProtocol(ProtocolSettings settings, Rpcv2JsonTrait trait) {
52+
return new RpcV2JsonProtocol(
53+
Objects.requireNonNull(settings.service(), "service is a required protocol setting"));
54+
}
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
software.amazon.smithy.java.client.rpcv2json.RpcV2JsonProtocol$Factory
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
plugins {
2+
id("smithy-java.module-conventions")
3+
}
4+
5+
description = "This module provides the shared base implementation for client RpcV2 protocols"
6+
7+
extra["displayName"] = "Smithy :: Java :: Client :: RPCv2"
8+
extra["moduleName"] = "software.amazon.smithy.java.client.rpcv2"
9+
10+
dependencies {
11+
api(project(":client:client-http"))
12+
api(project(":aws:aws-event-streams"))
13+
}

0 commit comments

Comments
 (0)