Skip to content

Commit 9145cc9

Browse files
committed
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 91afa89 commit 9145cc9

24 files changed

Lines changed: 877 additions & 347 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: 6 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -5,170 +5,36 @@
55

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

8-
import java.net.URI;
9-
import java.util.List;
10-
import java.util.Map;
118
import java.util.Objects;
12-
import software.amazon.smithy.java.aws.events.AwsEventDecoderFactory;
13-
import software.amazon.smithy.java.aws.events.AwsEventEncoderFactory;
14-
import software.amazon.smithy.java.aws.events.AwsEventFrame;
15-
import software.amazon.smithy.java.aws.events.RpcEventStreamsUtil;
169
import software.amazon.smithy.java.cbor.Rpcv2CborCodec;
1710
import software.amazon.smithy.java.client.core.ClientProtocol;
1811
import software.amazon.smithy.java.client.core.ClientProtocolFactory;
1912
import software.amazon.smithy.java.client.core.ProtocolSettings;
20-
import software.amazon.smithy.java.client.http.ErrorTypeUtils;
21-
import software.amazon.smithy.java.client.http.HttpClientProtocol;
22-
import software.amazon.smithy.java.client.http.HttpErrorDeserializer;
23-
import software.amazon.smithy.java.context.Context;
24-
import software.amazon.smithy.java.core.schema.ApiOperation;
25-
import software.amazon.smithy.java.core.schema.SerializableStruct;
26-
import software.amazon.smithy.java.core.schema.TraitKey;
2713
import software.amazon.smithy.java.core.serde.Codec;
28-
import software.amazon.smithy.java.core.serde.TypeRegistry;
29-
import software.amazon.smithy.java.core.serde.document.Document;
30-
import software.amazon.smithy.java.core.serde.document.DocumentDeserializer;
31-
import software.amazon.smithy.java.core.serde.event.EventDecoderFactory;
32-
import software.amazon.smithy.java.core.serde.event.EventEncoderFactory;
33-
import software.amazon.smithy.java.core.serde.event.EventStreamingException;
34-
import software.amazon.smithy.java.http.api.HttpHeaders;
3514
import software.amazon.smithy.java.http.api.HttpRequest;
36-
import software.amazon.smithy.java.http.api.HttpResponse;
3715
import software.amazon.smithy.java.http.api.HttpVersion;
38-
import software.amazon.smithy.java.io.ByteBufferOutputStream;
39-
import software.amazon.smithy.java.io.datastream.DataStream;
4016
import software.amazon.smithy.model.shapes.ShapeId;
4117
import software.amazon.smithy.protocol.traits.Rpcv2CborTrait;
4218

4319
/**
44-
* Implements smithy.protocols#rpcv2Cbor.
20+
* Client protocol implementation for {@code smithy.protocols#rpcv2Cbor}.
4521
*/
46-
public final class RpcV2CborProtocol extends HttpClientProtocol {
47-
private static final Codec CBOR_CODEC = Rpcv2CborCodec.builder().build();
22+
public final class RpcV2CborProtocol extends AbstractRpcV2ClientProtocol {
4823
private static final String PAYLOAD_MEDIA_TYPE = "application/cbor";
49-
private static final List<String> CONTENT_TYPE = List.of(PAYLOAD_MEDIA_TYPE);
50-
private static final List<String> SMITHY_PROTOCOL = List.of("rpc-v2-cbor");
51-
52-
private final ShapeId service;
53-
private final HttpErrorDeserializer errorDeserializer;
24+
private static final Codec CBOR_CODEC = Rpcv2CborCodec.builder().build();
5425

5526
public RpcV2CborProtocol(ShapeId service) {
56-
super(Rpcv2CborTrait.ID);
57-
this.service = service;
58-
this.errorDeserializer = HttpErrorDeserializer.builder()
59-
.codec(CBOR_CODEC)
60-
.serviceId(service)
61-
.errorPayloadParser(RpcV2CborProtocol::extractErrorType)
62-
.build();
27+
super(Rpcv2CborTrait.ID, service, PAYLOAD_MEDIA_TYPE);
6328
}
6429

6530
@Override
66-
public Codec payloadCodec() {
31+
protected Codec codec() {
6732
return CBOR_CODEC;
6833
}
6934

7035
@Override
71-
public <I extends SerializableStruct, O extends SerializableStruct> HttpRequest createRequest(
72-
ApiOperation<I, O> operation,
73-
I input,
74-
Context context,
75-
URI endpoint
76-
) {
77-
var target = "/service/" + service.getName() + "/operation/" + operation.schema().id().getName();
78-
var builder = HttpRequest.builder().method("POST").uri(endpoint.resolve(target));
79-
36+
protected void customizeRequestBuilder(HttpRequest.Builder builder) {
8037
builder.httpVersion(HttpVersion.HTTP_2);
81-
if (operation.inputSchema().hasTrait(TraitKey.UNIT_TYPE_TRAIT)) {
82-
// Top-level Unit types do not get serialized
83-
builder.headers(HttpHeaders.of(headersForEmptyBody()))
84-
.body(DataStream.ofEmpty());
85-
} else if (operation.inputEventBuilderSupplier() != null) {
86-
// Event streaming
87-
var encoderFactory = getEventEncoderFactory(operation);
88-
var body = RpcEventStreamsUtil.bodyForEventStreaming(encoderFactory, input);
89-
builder.headers(HttpHeaders.of(headersForEventStreaming()))
90-
.body(body);
91-
} else {
92-
// Regular request
93-
builder.headers(HttpHeaders.of(headers()))
94-
.body(getBody(input));
95-
}
96-
return builder.build();
97-
}
98-
99-
@Override
100-
public <I extends SerializableStruct, O extends SerializableStruct> O deserializeResponse(
101-
ApiOperation<I, O> operation,
102-
Context context,
103-
TypeRegistry typeRegistry,
104-
HttpRequest request,
105-
HttpResponse response
106-
) {
107-
if (response.statusCode() != 200) {
108-
throw errorDeserializer.createError(context, operation, typeRegistry, response);
109-
}
110-
111-
if (operation.outputEventBuilderSupplier() != null) {
112-
var eventDecoderFactory = getEventDecoderFactory(operation);
113-
return RpcEventStreamsUtil.deserializeResponse(eventDecoderFactory, bodyDataStream(response));
114-
}
115-
116-
var builder = operation.outputBuilder();
117-
var content = response.body();
118-
if (content.contentLength() == 0) {
119-
return builder.build();
120-
}
121-
122-
var bytes = content.asByteBuffer();
123-
return CBOR_CODEC.deserializeShape(bytes, builder);
124-
}
125-
126-
private static DataStream bodyDataStream(HttpResponse response) {
127-
var contentType = response.headers().contentType();
128-
var contentLength = response.headers().contentLength();
129-
return DataStream.withMetadata(response.body(), contentType, contentLength, null);
130-
}
131-
132-
private DataStream getBody(SerializableStruct input) {
133-
var sink = new ByteBufferOutputStream();
134-
try (var serializer = CBOR_CODEC.createSerializer(sink)) {
135-
input.serialize(serializer);
136-
}
137-
return DataStream.ofByteBuffer(sink.toByteBuffer(), PAYLOAD_MEDIA_TYPE);
138-
}
139-
140-
private Map<String, List<String>> headers() {
141-
return Map.of("smithy-protocol", SMITHY_PROTOCOL, "Content-Type", CONTENT_TYPE, "Accept", CONTENT_TYPE);
142-
}
143-
144-
private Map<String, List<String>> headersForEmptyBody() {
145-
return Map.of("smithy-protocol", SMITHY_PROTOCOL, "Accept", CONTENT_TYPE);
146-
}
147-
148-
private Map<String, List<String>> headersForEventStreaming() {
149-
return Map.of("smithy-protocol",
150-
SMITHY_PROTOCOL,
151-
"Content-Type",
152-
List.of("application/vnd.amazon.eventstream"),
153-
"Accept",
154-
CONTENT_TYPE);
155-
}
156-
157-
private EventEncoderFactory<AwsEventFrame> getEventEncoderFactory(ApiOperation<?, ?> operation) {
158-
return AwsEventEncoderFactory.forInputStream(operation,
159-
payloadCodec(),
160-
PAYLOAD_MEDIA_TYPE,
161-
(e) -> new EventStreamingException("InternalServerException", "Internal Server Error"));
162-
}
163-
164-
private EventDecoderFactory<AwsEventFrame> getEventDecoderFactory(ApiOperation<?, ?> operation) {
165-
return AwsEventDecoderFactory.forOutputStream(operation, payloadCodec(), f -> f);
166-
}
167-
168-
private static ShapeId extractErrorType(Document document, String namespace) {
169-
return DocumentDeserializer.parseDiscriminator(
170-
ErrorTypeUtils.removeUri(ErrorTypeUtils.readType(document)),
171-
namespace);
17238
}
17339

17440
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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
18+
/**
19+
* Client protocol implementation for {@code smithy.protocols#rpcv2Json}.
20+
*
21+
* <p>BigDecimal and BigInteger values are serialized as JSON strings to preserve
22+
* arbitrary precision.
23+
*/
24+
public final class RpcV2JsonProtocol extends AbstractRpcV2ClientProtocol {
25+
private static final String PAYLOAD_MEDIA_TYPE = "application/json";
26+
27+
private final JsonCodec codec;
28+
29+
public RpcV2JsonProtocol(ShapeId service) {
30+
super(Rpcv2JsonTrait.ID, service, PAYLOAD_MEDIA_TYPE);
31+
this.codec = JsonCodec.builder()
32+
.defaultNamespace(service.getNamespace())
33+
.useStringForArbitraryPrecision(true)
34+
.build();
35+
}
36+
37+
@Override
38+
protected Codec codec() {
39+
return codec;
40+
}
41+
42+
public static final class Factory implements ClientProtocolFactory<Rpcv2JsonTrait> {
43+
@Override
44+
public ShapeId id() {
45+
return Rpcv2JsonTrait.ID;
46+
}
47+
48+
@Override
49+
public ClientProtocol<?, ?> createProtocol(ProtocolSettings settings, Rpcv2JsonTrait trait) {
50+
return new RpcV2JsonProtocol(
51+
Objects.requireNonNull(settings.service(), "service is a required protocol setting"));
52+
}
53+
}
54+
}
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)