Skip to content

Commit 36b50b3

Browse files
authored
Add client-support for RPC v2 CBOR (#3767)
## Motivation and Context Follow-up on #2544 to add client-side support for the protocol ## Description The client implementation mainly focuses on a sub-section [Requests](https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html#requests) in the spec. To that end, this PR addresses `TODO` for the client to fill in the blanks and includes additional adjustments/refactoring to pass client protocol tests. ## Testing - Existing tests in CI - Upstream protocol test `rpcv2Cbor` - Our handwritten protocol test `rpcv2Cbor-extras.smithy` ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent 50148e6 commit 36b50b3

File tree

22 files changed

+409
-121
lines changed

22 files changed

+409
-121
lines changed

CHANGELOG.next.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@ message = "Fix incorrect redaction of `@sensitive` types in maps and lists."
2222
references = ["smithy-rs#3765", "smithy-rs#3757"]
2323
meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "client" }
2424
author = "landonxjames"
25+
26+
[[smithy-rs]]
27+
message = "Fix client error correction to properly parse structure members that target a `Union` containing that structure recursively."
28+
references = ["smithy-rs#3767"]
29+
meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "client" }
30+
author = "ysaito1001"

codegen-client-test/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ val workingDirUnderBuildDir = "smithyprojections/codegen-client-test/"
2424
dependencies {
2525
implementation(project(":codegen-client"))
2626
implementation("software.amazon.smithy:smithy-aws-protocol-tests:$smithyVersion")
27+
implementation("software.amazon.smithy:smithy-protocol-tests:$smithyVersion")
2728
implementation("software.amazon.smithy:smithy-protocol-test-traits:$smithyVersion")
2829
implementation("software.amazon.smithy:smithy-aws-traits:$smithyVersion")
2930
}
@@ -72,6 +73,12 @@ val allCodegenTests = listOf(
7273
ClientTest("aws.protocoltests.restxml#RestXml", "rest_xml", addMessageToErrors = false),
7374
ClientTest("aws.protocoltests.query#AwsQuery", "aws_query", addMessageToErrors = false),
7475
ClientTest("aws.protocoltests.ec2#AwsEc2", "ec2_query", addMessageToErrors = false),
76+
ClientTest("smithy.protocoltests.rpcv2Cbor#RpcV2Protocol", "rpcv2Cbor"),
77+
ClientTest(
78+
"smithy.protocoltests.rpcv2Cbor#RpcV2CborService",
79+
"rpcv2Cbor_extras",
80+
dependsOn = listOf("rpcv2Cbor-extras.smithy")
81+
),
7582
ClientTest(
7683
"aws.protocoltests.restxml.xmlns#RestXmlWithNamespace",
7784
"rest_xml_namespace",

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ErrorCorrection.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,18 @@ private fun ClientCodegenContext.errorCorrectedDefault(member: MemberShape): Wri
8787

8888
target is TimestampShape -> instantiator.instantiate(target, Node.from(0)).some()(this)
8989
target is BlobShape -> instantiator.instantiate(target, Node.from("")).some()(this)
90-
target is UnionShape -> rust("Some(#T::Unknown)", targetSymbol)
90+
target is UnionShape ->
91+
rustTemplate(
92+
"Some(#{unknown})", *preludeScope,
93+
"unknown" to
94+
writable {
95+
if (memberSymbol.isRustBoxed()) {
96+
rust("Box::new(#T::Unknown)", targetSymbol)
97+
} else {
98+
rust("#T::Unknown", targetSymbol)
99+
}
100+
},
101+
)
91102
}
92103
}
93104
}

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ClientProtocolTestGenerator.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.Proto
3131
import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ServiceShapeId
3232
import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ServiceShapeId.AWS_JSON_10
3333
import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ServiceShapeId.REST_JSON
34+
import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ServiceShapeId.RPC_V2_CBOR
3435
import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.TestCase
3536
import software.amazon.smithy.rust.codegen.core.util.PANIC
3637
import software.amazon.smithy.rust.codegen.core.util.dq
@@ -78,6 +79,8 @@ class ClientProtocolTestGenerator(
7879
FailingTest.RequestTest(AWS_JSON_10, "AwsJson10ClientPopulatesDefaultValuesInInput"),
7980
FailingTest.RequestTest(REST_JSON, "RestJsonClientPopulatesDefaultValuesInInput"),
8081
FailingTest.RequestTest(REST_JSON, "RestJsonClientUsesExplicitlyProvidedMemberValuesOverDefaults"),
82+
FailingTest.RequestTest(RPC_V2_CBOR, "RpcV2CborClientPopulatesDefaultValuesInInput"),
83+
FailingTest.RequestTest(RPC_V2_CBOR, "RpcV2CborClientUsesExplicitlyProvidedMemberValuesOverDefaults"),
8184
)
8285

8386
private val BrokenTests:
@@ -268,6 +271,7 @@ class ClientProtocolTestGenerator(
268271
""",
269272
RT.sdkBody(runtimeConfig = rc),
270273
)
274+
val mediaType = testCase.bodyMediaType.orNull()
271275
rustTemplate(
272276
"""
273277
use #{DeserializeResponse};
@@ -280,19 +284,19 @@ class ClientProtocolTestGenerator(
280284
let parsed = de.deserialize_streaming(&mut http_response);
281285
let parsed = parsed.unwrap_or_else(|| {
282286
let http_response = http_response.map(|body| {
283-
#{SdkBody}::from(#{copy_from_slice}(body.bytes().unwrap()))
287+
#{SdkBody}::from(#{copy_from_slice}(&#{decode_body_data}(body.bytes().unwrap(), #{MediaType}::from(${(mediaType ?: "unknown").dq()}))))
284288
});
285289
de.deserialize_nonstreaming(&http_response)
286290
});
287291
""",
288292
"copy_from_slice" to RT.Bytes.resolve("copy_from_slice"),
289-
"SharedResponseDeserializer" to
290-
RT.smithyRuntimeApiClient(rc)
291-
.resolve("client::ser_de::SharedResponseDeserializer"),
292-
"Operation" to codegenContext.symbolProvider.toSymbol(operationShape),
293+
"decode_body_data" to RT.protocolTest(rc, "decode_body_data"),
293294
"DeserializeResponse" to RT.smithyRuntimeApiClient(rc).resolve("client::ser_de::DeserializeResponse"),
295+
"MediaType" to RT.protocolTest(rc, "MediaType"),
296+
"Operation" to codegenContext.symbolProvider.toSymbol(operationShape),
294297
"RuntimePlugin" to RT.runtimePlugin(rc),
295298
"SdkBody" to RT.sdkBody(rc),
299+
"SharedResponseDeserializer" to RT.smithyRuntimeApiClient(rc).resolve("client::ser_de::SharedResponseDeserializer"),
296300
)
297301
if (expectedShape.hasTrait<ErrorTrait>()) {
298302
val errorSymbol = codegenContext.symbolProvider.symbolForOperationError(operationShape)

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/RequestSerializerGenerator.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import software.amazon.smithy.rust.codegen.core.rustlang.writable
1919
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
2020
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
2121
import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ProtocolPayloadGenerator
22-
import software.amazon.smithy.rust.codegen.core.smithy.protocols.HttpLocation
2322
import software.amazon.smithy.rust.codegen.core.smithy.protocols.Protocol
2423
import software.amazon.smithy.rust.codegen.core.util.dq
2524
import software.amazon.smithy.rust.codegen.core.util.findStreamingMember
@@ -125,10 +124,8 @@ class RequestSerializerGenerator(
125124
)
126125
}
127126

128-
private fun needsContentLength(operationShape: OperationShape): Boolean {
129-
return protocol.httpBindingResolver.requestBindings(operationShape)
130-
.any { it.location == HttpLocation.DOCUMENT || it.location == HttpLocation.PAYLOAD }
131-
}
127+
private fun needsContentLength(operationShape: OperationShape): Boolean =
128+
protocol.needsRequestContentLength(operationShape)
132129

133130
private fun createHttpRequest(operationShape: OperationShape): Writable =
134131
writable {

codegen-core/common-test-models/rpcv2Cbor-extras.smithy

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ apply ErrorSerializationOperation @httpMalformedRequestTests([
130130
"Content-Type": "application/cbor"
131131
}
132132
// An empty CBOR map. We're missing a lot of `@required` members!
133-
body: "oA=="
133+
body: "oA==",
134+
bodyMediaType: "application/cbor"
134135
},
135136
response: {
136137
code: 400,
@@ -149,9 +150,9 @@ apply ErrorSerializationOperation @httpResponseTests([
149150
id: "OperationOutputSerializationQuestionablyIncludesTypeField",
150151
documentation: """
151152
Despite the operation output being a structure shape with the `@error` trait,
152-
`__type` field should, in a strict interpretation of the spec, not be included,
153-
because we're not serializing a server error response. However, we do, because
154-
there shouldn't™️ be any harm in doing so, and it greatly simplifies the
153+
`__type` field should, in a strict interpretation of the spec, not be included,
154+
because we're not serializing a server error response. However, we do, because
155+
there shouldn't™️ be any harm in doing so, and it greatly simplifies the
155156
code generator. This test just pins this behavior in case we ever modify it.""",
156157
protocol: rpcv2Cbor,
157158
code: 200,
@@ -170,6 +171,12 @@ apply SimpleStructOperation @httpResponseTests([
170171
id: "SimpleStruct",
171172
protocol: rpcv2Cbor,
172173
code: 200, // Not used.
174+
body: "v2RibG9iS2Jsb2JieSBibG9iZ2Jvb2xlYW70ZnN0cmluZ3hwVGhlcmUgYXJlIHRocmVlIHRoaW5ncyBhbGwgd2lzZSBtZW4gZmVhcjogdGhlIHNlYSBpbiBzdG9ybSwgYSBuaWdodCB3aXRoIG5vIG1vb24sIGFuZCB0aGUgYW5nZXIgb2YgYSBnZW50bGUgbWFuLmRieXRlGEVlc2hvcnQYRmdpbnRlZ2VyGEdkbG9uZxhIZWZsb2F0+j8wo9dmZG91Ymxl+z/mTQE6kqMFaXRpbWVzdGFtcMH7QdcKq2AAAABkZW51bWdESUFNT05EbHJlcXVpcmVkQmxvYktibG9iYnkgYmxvYm9yZXF1aXJlZEJvb2xlYW70bnJlcXVpcmVkU3RyaW5neHBUaGVyZSBhcmUgdGhyZWUgdGhpbmdzIGFsbCB3aXNlIG1lbiBmZWFyOiB0aGUgc2VhIGluIHN0b3JtLCBhIG5pZ2h0IHdpdGggbm8gbW9vbiwgYW5kIHRoZSBhbmdlciBvZiBhIGdlbnRsZSBtYW4ubHJlcXVpcmVkQnl0ZRhFbXJlcXVpcmVkU2hvcnQYRm9yZXF1aXJlZEludGVnZXIYR2xyZXF1aXJlZExvbmcYSG1yZXF1aXJlZEZsb2F0+j8wo9ducmVxdWlyZWREb3VibGX7P+ZNATqSowVxcmVxdWlyZWRUaW1lc3RhbXDB+0HXCqtgAAAAbHJlcXVpcmVkRW51bWdESUFNT05E/w==",
175+
bodyMediaType: "application/cbor",
176+
headers: {
177+
"smithy-protocol": "rpc-v2-cbor",
178+
"Content-Type": "application/cbor"
179+
},
173180
params: {
174181
blob: "blobby blob",
175182
boolean: false,
@@ -211,6 +218,12 @@ apply SimpleStructOperation @httpResponseTests([
211218
id: "SimpleStructWithOptionsSetToNone",
212219
protocol: rpcv2Cbor,
213220
code: 200, // Not used.
221+
body: "v2xyZXF1aXJlZEJsb2JLYmxvYmJ5IGJsb2JvcmVxdWlyZWRCb29sZWFu9G5yZXF1aXJlZFN0cmluZ3hwVGhlcmUgYXJlIHRocmVlIHRoaW5ncyBhbGwgd2lzZSBtZW4gZmVhcjogdGhlIHNlYSBpbiBzdG9ybSwgYSBuaWdodCB3aXRoIG5vIG1vb24sIGFuZCB0aGUgYW5nZXIgb2YgYSBnZW50bGUgbWFuLmxyZXF1aXJlZEJ5dGUYRW1yZXF1aXJlZFNob3J0GEZvcmVxdWlyZWRJbnRlZ2VyGEdscmVxdWlyZWRMb25nGEhtcmVxdWlyZWRGbG9hdPo/MKPXbnJlcXVpcmVkRG91Ymxl+z/mTQE6kqMFcXJlcXVpcmVkVGltZXN0YW1wwftB1wqrYAAAAGxyZXF1aXJlZEVudW1nRElBTU9ORP8=",
222+
bodyMediaType: "application/cbor",
223+
headers: {
224+
"smithy-protocol": "rpc-v2-cbor",
225+
"Content-Type": "application/cbor"
226+
},
214227
params: {
215228
requiredBlob: "blobby blob",
216229
requiredBoolean: false,

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ class InlineDependency(
147147
CargoDependency.smithyTypes(runtimeConfig),
148148
)
149149

150+
fun cborErrors(runtimeConfig: RuntimeConfig): InlineDependency =
151+
forInlineableRustFile(
152+
"cbor_errors",
153+
CargoDependency.smithyCbor(runtimeConfig),
154+
CargoDependency.smithyRuntimeApi(runtimeConfig),
155+
CargoDependency.smithyTypes(runtimeConfig),
156+
)
157+
150158
fun ec2QueryErrors(runtimeConfig: RuntimeConfig): InlineDependency =
151159
forInlineableRustFile("ec2_query_errors", CargoDependency.smithyXml(runtimeConfig))
152160

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,8 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null)
519519
)
520520

521521
// inlinable types
522+
fun cborErrors(runtimeConfig: RuntimeConfig) = forInlineDependency(InlineDependency.cborErrors(runtimeConfig))
523+
522524
fun ec2QueryErrors(runtimeConfig: RuntimeConfig) =
523525
forInlineDependency(InlineDependency.ec2QueryErrors(runtimeConfig))
524526

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/Protocol.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ interface Protocol {
7878
* there are no response headers or statuses available to further inform the error parsing.
7979
*/
8080
fun parseEventStreamErrorMetadata(operationShape: OperationShape): RuntimeType
81+
82+
/**
83+
* Determines whether the `Content-Length` header should be set in an HTTP request.
84+
*/
85+
fun needsRequestContentLength(operationShape: OperationShape): Boolean =
86+
httpBindingResolver.requestBindings(operationShape)
87+
.any { it.location == HttpLocation.DOCUMENT || it.location == HttpLocation.PAYLOAD }
8188
}
8289

8390
typealias ProtocolMap<T, C> = Map<ShapeId, ProtocolGeneratorFactory<T, C>>

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/RpcV2Cbor.kt

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,28 @@ package software.amazon.smithy.rust.codegen.core.smithy.protocols
77

88
import software.amazon.smithy.codegen.core.CodegenException
99
import software.amazon.smithy.model.Model
10+
import software.amazon.smithy.model.pattern.UriPattern
1011
import software.amazon.smithy.model.shapes.MemberShape
1112
import software.amazon.smithy.model.shapes.OperationShape
13+
import software.amazon.smithy.model.shapes.ServiceShape
1214
import software.amazon.smithy.model.shapes.ToShapeId
15+
import software.amazon.smithy.model.traits.HttpTrait
1316
import software.amazon.smithy.model.traits.TimestampFormatTrait
17+
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
18+
import software.amazon.smithy.rust.codegen.core.rustlang.writable
1419
import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext
1520
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
1621
import software.amazon.smithy.rust.codegen.core.smithy.protocols.parse.CborParserGenerator
1722
import software.amazon.smithy.rust.codegen.core.smithy.protocols.parse.StructuredDataParserGenerator
1823
import software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize.CborSerializerGenerator
1924
import software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize.StructuredDataSerializerGenerator
2025
import software.amazon.smithy.rust.codegen.core.smithy.transformers.OperationNormalizer
21-
import software.amazon.smithy.rust.codegen.core.util.PANIC
22-
import software.amazon.smithy.rust.codegen.core.util.inputShape
2326
import software.amazon.smithy.rust.codegen.core.util.isStreaming
24-
import software.amazon.smithy.rust.codegen.core.util.outputShape
2527

2628
class RpcV2CborHttpBindingResolver(
2729
private val model: Model,
2830
private val contentTypes: ProtocolContentTypes,
31+
private val serviceShape: ServiceShape,
2932
) : HttpBindingResolver {
3033
private fun bindings(shape: ToShapeId): List<HttpBindingDescriptor> {
3134
val members = shape.let { model.expectShape(it.toShapeId()) }.members()
@@ -47,10 +50,12 @@ class RpcV2CborHttpBindingResolver(
4750
.toList()
4851
}
4952

50-
// TODO(https://github.com/smithy-lang/smithy-rs/issues/3573)
51-
// In the server, this is only used when the protocol actually supports the `@http` trait.
52-
// However, we will have to do this for client support. Perhaps this method deserves a rename.
53-
override fun httpTrait(operationShape: OperationShape) = PANIC("RPC v2 does not support the `@http` trait")
53+
override fun httpTrait(operationShape: OperationShape): HttpTrait =
54+
HttpTrait.builder()
55+
.code(200)
56+
.method("POST")
57+
.uri(UriPattern.parse("/service/${serviceShape.id.name}/operation/${operationShape.id.name}"))
58+
.build()
5459

5560
override fun requestBindings(operationShape: OperationShape) = bindings(operationShape.inputShape)
5661

@@ -87,6 +92,8 @@ class RpcV2CborHttpBindingResolver(
8792
}
8893

8994
open class RpcV2Cbor(val codegenContext: CodegenContext) : Protocol {
95+
private val runtimeConfig = codegenContext.runtimeConfig
96+
9097
override val httpBindingResolver: HttpBindingResolver =
9198
RpcV2CborHttpBindingResolver(
9299
codegenContext.model,
@@ -96,26 +103,50 @@ open class RpcV2Cbor(val codegenContext: CodegenContext) : Protocol {
96103
eventStreamContentType = "application/vnd.amazon.eventstream",
97104
eventStreamMessageContentType = "application/cbor",
98105
),
106+
codegenContext.serviceShape,
99107
)
100108

101109
// Note that [CborParserGenerator] and [CborSerializerGenerator] automatically (de)serialize timestamps
102110
// using floating point seconds from the epoch.
103111
override val defaultTimestampFormat: TimestampFormatTrait.Format = TimestampFormatTrait.Format.EPOCH_SECONDS
104112

113+
override fun additionalRequestHeaders(operationShape: OperationShape): List<Pair<String, String>> =
114+
listOf("smithy-protocol" to "rpc-v2-cbor")
115+
105116
override fun additionalResponseHeaders(operationShape: OperationShape): List<Pair<String, String>> =
106117
listOf("smithy-protocol" to "rpc-v2-cbor")
107118

108119
override fun structuredDataParser(): StructuredDataParserGenerator =
109-
CborParserGenerator(codegenContext, httpBindingResolver)
120+
CborParserGenerator(
121+
codegenContext, httpBindingResolver,
122+
handleNullForNonSparseCollection = { collectionName: String ->
123+
writable {
124+
// The client should drop a null value in a dense collection, see
125+
// https://github.com/smithy-lang/smithy/blob/6466fe77c65b8a17b219f0b0a60c767915205f95/smithy-protocol-tests/model/rpcv2Cbor/cbor-maps.smithy#L158
126+
rustTemplate(
127+
"""
128+
decoder.null()?;
129+
return #{Ok}($collectionName)
130+
""",
131+
*RuntimeType.preludeScope,
132+
)
133+
}
134+
},
135+
)
110136

111137
override fun structuredDataSerializer(): StructuredDataSerializerGenerator =
112138
CborSerializerGenerator(codegenContext, httpBindingResolver)
113139

114-
// TODO(https://github.com/smithy-lang/smithy-rs/issues/3573)
115140
override fun parseHttpErrorMetadata(operationShape: OperationShape): RuntimeType =
116-
TODO("rpcv2Cbor client support has not yet been implemented")
141+
RuntimeType.cborErrors(runtimeConfig).resolve("parse_error_metadata")
117142

118143
// TODO(https://github.com/smithy-lang/smithy-rs/issues/3573)
119144
override fun parseEventStreamErrorMetadata(operationShape: OperationShape): RuntimeType =
120145
TODO("rpcv2Cbor event streams have not yet been implemented")
146+
147+
// Unlike other protocols, the `rpcv2Cbor` protocol requires that `Content-Length` is always set
148+
// unless there is no input or if the operation is an event stream, see
149+
// https://github.com/smithy-lang/smithy/blob/6466fe77c65b8a17b219f0b0a60c767915205f95/smithy-protocol-tests/model/rpcv2Cbor/empty-input-output.smithy#L106
150+
// TODO(https://github.com/smithy-lang/smithy-rs/issues/3772): Do not set `Content-Length` for event stream operations
151+
override fun needsRequestContentLength(operationShape: OperationShape) = operationShape.input.isPresent
121152
}

0 commit comments

Comments
 (0)