Skip to content

Commit c98d5fe

Browse files
authored
Fix codegen for unions with the @httpPayload trait (#2969)
This PR incorporates the new test cases in Smithy from smithy-lang/smithy#1908, and adds support to `@restXml` and `@restJson1` for unions with the `@httpPayload` trait. This resolves #1896. This also fixes code generation for the latest `medicalimaging` SDK model. ---- _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 a460bfc commit c98d5fe

File tree

8 files changed

+238
-7
lines changed

8 files changed

+238
-7
lines changed

CHANGELOG.next.toml

+6
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,9 @@ message = "Generate a region setter when a model uses SigV4."
8484
references = ["smithy-rs#2960"]
8585
meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "client" }
8686
author = "jdisanti"
87+
88+
[[smithy-rs]]
89+
message = "Fix code generation for union members with the `@httpPayload` trait."
90+
references = ["smithy-rs#2969", "smithy-rs#1896"]
91+
meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "all" }
92+
author = "jdisanti"

codegen-client-test/model/rest-xml-extras.smithy

+96
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ service RestXmlExtras {
2121
StringHeader,
2222
CreateFoo,
2323
RequiredMember,
24+
// TODO(https://github.com/awslabs/smithy-rs/issues/2968): Remove the following once these tests are included in Smithy
25+
// They're being added in https://github.com/smithy-lang/smithy/pull/1908
26+
HttpPayloadWithUnion,
2427
]
2528
}
2629

@@ -257,3 +260,96 @@ structure RequiredMemberInputOutput {
257260
@required
258261
requiredString: String
259262
}
263+
264+
// TODO(https://github.com/awslabs/smithy-rs/issues/2968): Delete the HttpPayloadWithUnion tests below once Smithy vends them
265+
// They're being added in https://github.com/smithy-lang/smithy/pull/1908
266+
267+
/// This example serializes a union in the payload.
268+
@idempotent
269+
@http(uri: "/HttpPayloadWithUnion", method: "PUT")
270+
operation HttpPayloadWithUnion {
271+
input: HttpPayloadWithUnionInputOutput,
272+
output: HttpPayloadWithUnionInputOutput
273+
}
274+
275+
apply HttpPayloadWithUnion @httpRequestTests([
276+
{
277+
id: "RestXmlHttpPayloadWithUnion",
278+
documentation: "Serializes a union in the payload.",
279+
protocol: restXml,
280+
method: "PUT",
281+
uri: "/HttpPayloadWithUnion",
282+
body: """
283+
<UnionPayload>
284+
<greeting>hello</greeting>
285+
</UnionPayload>""",
286+
bodyMediaType: "application/xml",
287+
headers: {
288+
"Content-Type": "application/xml",
289+
},
290+
requireHeaders: [
291+
"Content-Length"
292+
],
293+
params: {
294+
nested: {
295+
greeting: "hello"
296+
}
297+
}
298+
},
299+
{
300+
id: "RestXmlHttpPayloadWithUnsetUnion",
301+
documentation: "No payload is sent if the union has no value.",
302+
protocol: restXml,
303+
method: "PUT",
304+
uri: "/HttpPayloadWithUnion",
305+
body: "",
306+
headers: {
307+
"Content-Type": "application/xml",
308+
"Content-Length": "0"
309+
},
310+
params: {}
311+
}
312+
])
313+
314+
apply HttpPayloadWithUnion @httpResponseTests([
315+
{
316+
id: "RestXmlHttpPayloadWithUnion",
317+
documentation: "Serializes a union in the payload.",
318+
protocol: restXml,
319+
code: 200,
320+
body: """
321+
<UnionPayload>
322+
<greeting>hello</greeting>
323+
</UnionPayload>""",
324+
bodyMediaType: "application/xml",
325+
headers: {
326+
"Content-Type": "application/xml",
327+
},
328+
params: {
329+
nested: {
330+
greeting: "hello"
331+
}
332+
}
333+
},
334+
{
335+
id: "RestXmlHttpPayloadWithUnsetUnion",
336+
documentation: "No payload is sent if the union has no value.",
337+
protocol: restXml,
338+
code: 200,
339+
body: "",
340+
headers: {
341+
"Content-Type": "application/xml",
342+
"Content-Length": "0"
343+
},
344+
params: {}
345+
}
346+
])
347+
348+
structure HttpPayloadWithUnionInputOutput {
349+
@httpPayload
350+
nested: UnionPayload,
351+
}
352+
353+
union UnionPayload {
354+
greeting: String
355+
}

codegen-core/common-test-models/rest-json-extras.smithy

+96
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ service RestJsonExtras {
6565
NullInNonSparse,
6666
CaseInsensitiveErrorOperation,
6767
EmptyStructWithContentOnWireOp,
68+
// TODO(https://github.com/awslabs/smithy-rs/issues/2968): Remove the following once these tests are included in Smithy
69+
// They're being added in https://github.com/smithy-lang/smithy/pull/1908
70+
HttpPayloadWithUnion,
6871
],
6972
errors: [ExtraError]
7073
}
@@ -348,3 +351,96 @@ structure EmptyStructWithContentOnWireOpOutput {
348351
operation EmptyStructWithContentOnWireOp {
349352
output: EmptyStructWithContentOnWireOpOutput,
350353
}
354+
355+
// TODO(https://github.com/awslabs/smithy-rs/issues/2968): Delete the HttpPayloadWithUnion tests below once Smithy vends them
356+
// They're being added in https://github.com/smithy-lang/smithy/pull/1908
357+
358+
/// This examples serializes a union in the payload.
359+
@idempotent
360+
@http(uri: "/HttpPayloadWithUnion", method: "PUT")
361+
operation HttpPayloadWithUnion {
362+
input: HttpPayloadWithUnionInputOutput,
363+
output: HttpPayloadWithUnionInputOutput
364+
}
365+
366+
structure HttpPayloadWithUnionInputOutput {
367+
@httpPayload
368+
nested: UnionPayload,
369+
}
370+
371+
union UnionPayload {
372+
greeting: String
373+
}
374+
375+
apply HttpPayloadWithUnion @httpRequestTests([
376+
{
377+
id: "RestJsonHttpPayloadWithUnion",
378+
documentation: "Serializes a union in the payload.",
379+
protocol: restJson1,
380+
method: "PUT",
381+
uri: "/HttpPayloadWithUnion",
382+
body: """
383+
{
384+
"greeting": "hello"
385+
}""",
386+
bodyMediaType: "application/json",
387+
headers: {
388+
"Content-Type": "application/json"
389+
},
390+
requireHeaders: [
391+
"Content-Length"
392+
],
393+
params: {
394+
nested: {
395+
greeting: "hello"
396+
}
397+
}
398+
},
399+
{
400+
id: "RestJsonHttpPayloadWithUnsetUnion",
401+
documentation: "No payload is sent if the union has no value.",
402+
protocol: restJson1,
403+
method: "PUT",
404+
uri: "/HttpPayloadWithUnion",
405+
body: "",
406+
headers: {
407+
"Content-Type": "application/json",
408+
"Content-Length": "0"
409+
},
410+
params: {}
411+
}
412+
])
413+
414+
apply HttpPayloadWithUnion @httpResponseTests([
415+
{
416+
id: "RestJsonHttpPayloadWithUnion",
417+
documentation: "Serializes a union in the payload.",
418+
protocol: restJson1,
419+
code: 200,
420+
body: """
421+
{
422+
"greeting": "hello"
423+
}""",
424+
bodyMediaType: "application/json",
425+
headers: {
426+
"Content-Type": "application/json"
427+
},
428+
params: {
429+
nested: {
430+
greeting: "hello"
431+
}
432+
}
433+
},
434+
{
435+
id: "RestJsonHttpPayloadWithUnsetUnion",
436+
documentation: "No payload is sent if the union has no value.",
437+
protocol: restJson1,
438+
code: 200,
439+
body: "",
440+
headers: {
441+
"Content-Type": "application/json",
442+
"Content-Length": "0"
443+
},
444+
params: {}
445+
}
446+
])

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ class HttpBoundProtocolPayloadGenerator(
269269
""",
270270
)
271271
is StructureShape -> rust("#T()", serializerGenerator.unsetStructure(targetShape))
272-
is UnionShape -> throw CodegenException("Currently unsupported. Tracking issue: https://github.com/awslabs/smithy-rs/issues/1896")
272+
is UnionShape -> rust("#T()", serializerGenerator.unsetUnion(targetShape))
273273
else -> throw CodegenException("`httpPayload` on member shapes targeting shapes of type ${targetShape.type} is unsupported")
274274
}
275275
}

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

+11-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.withBlock
3636
import software.amazon.smithy.rust.codegen.core.rustlang.writable
3737
import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext
3838
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
39+
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
3940
import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider
4041
import software.amazon.smithy.rust.codegen.core.smithy.customize.NamedCustomization
4142
import software.amazon.smithy.rust.codegen.core.smithy.customize.Section
@@ -170,7 +171,7 @@ class JsonSerializerGenerator(
170171
private val runtimeConfig = codegenContext.runtimeConfig
171172
private val protocolFunctions = ProtocolFunctions(codegenContext)
172173
private val codegenScope = arrayOf(
173-
"String" to RuntimeType.String,
174+
*preludeScope,
174175
"Error" to runtimeConfig.serializationError(),
175176
"SdkBody" to RuntimeType.sdkBody(runtimeConfig),
176177
"JsonObjectWriter" to RuntimeType.smithyJson(runtimeConfig).resolve("serialize::JsonObjectWriter"),
@@ -232,7 +233,7 @@ class JsonSerializerGenerator(
232233
}
233234

234235
override fun unsetStructure(structure: StructureShape): RuntimeType =
235-
ProtocolFunctions.crossOperationFn("rest_json_unsetpayload") { fnName ->
236+
ProtocolFunctions.crossOperationFn("rest_json_unset_struct_payload") { fnName ->
236237
rustTemplate(
237238
"""
238239
pub fn $fnName() -> #{ByteSlab} {
@@ -243,6 +244,14 @@ class JsonSerializerGenerator(
243244
)
244245
}
245246

247+
override fun unsetUnion(union: UnionShape): RuntimeType =
248+
ProtocolFunctions.crossOperationFn("rest_json_unset_union_payload") { fnName ->
249+
rustTemplate(
250+
"pub fn $fnName() -> #{ByteSlab} { #{Vec}::new() }",
251+
*codegenScope,
252+
)
253+
}
254+
246255
override fun operationInputSerializer(operationShape: OperationShape): RuntimeType? {
247256
// Don't generate an operation JSON serializer if there is no JSON body.
248257
val httpDocumentMembers = httpBindingResolver.requestMembers(operationShape, HttpLocation.DOCUMENT)

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

+4
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ abstract class QuerySerializerGenerator(codegenContext: CodegenContext) : Struct
121121
TODO("AwsQuery doesn't support payload serialization")
122122
}
123123

124+
override fun unsetUnion(union: UnionShape): RuntimeType {
125+
TODO("AwsQuery doesn't support payload serialization")
126+
}
127+
124128
override fun operationInputSerializer(operationShape: OperationShape): RuntimeType? {
125129
val inputShape = operationShape.inputShape(model)
126130
return protocolFunctions.serializeFn(inputShape, fnNameSuffix = "input") { fnName ->

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import software.amazon.smithy.model.shapes.MemberShape
99
import software.amazon.smithy.model.shapes.OperationShape
1010
import software.amazon.smithy.model.shapes.ShapeId
1111
import software.amazon.smithy.model.shapes.StructureShape
12+
import software.amazon.smithy.model.shapes.UnionShape
1213
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
1314

1415
interface StructuredDataSerializerGenerator {
@@ -27,12 +28,22 @@ interface StructuredDataSerializerGenerator {
2728
* Generate the correct data when attempting to serialize a structure that is unset
2829
*
2930
* ```rust
30-
* fn rest_json_unsetpayload() -> Vec<u8> {
31+
* fn rest_json_unset_struct_payload() -> Vec<u8> {
3132
* ...
3233
* }
3334
*/
3435
fun unsetStructure(structure: StructureShape): RuntimeType
3536

37+
/**
38+
* Generate the correct data when attempting to serialize a union that is unset
39+
*
40+
* ```rust
41+
* fn rest_json_unset_union_payload() -> Vec<u8> {
42+
* ...
43+
* }
44+
*/
45+
fun unsetUnion(union: UnionShape): RuntimeType
46+
3647
/**
3748
* Generate a serializer for an operation input structure.
3849
* This serializer is only used by clients.

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

+12-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.stripOuter
3535
import software.amazon.smithy.rust.codegen.core.rustlang.withBlock
3636
import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext
3737
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
38+
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
3839
import software.amazon.smithy.rust.codegen.core.smithy.generators.UnionGenerator
3940
import software.amazon.smithy.rust.codegen.core.smithy.generators.renderUnknownVariant
4041
import software.amazon.smithy.rust.codegen.core.smithy.generators.serializationError
@@ -176,8 +177,8 @@ class XmlBindingTraitSerializerGenerator(
176177
}
177178
}
178179

179-
override fun unsetStructure(structure: StructureShape): RuntimeType {
180-
return ProtocolFunctions.crossOperationFn("rest_xml_unset_payload") { fnName ->
180+
override fun unsetStructure(structure: StructureShape): RuntimeType =
181+
ProtocolFunctions.crossOperationFn("rest_xml_unset_struct_payload") { fnName ->
181182
rustTemplate(
182183
"""
183184
pub fn $fnName() -> #{ByteSlab} {
@@ -187,7 +188,15 @@ class XmlBindingTraitSerializerGenerator(
187188
"ByteSlab" to RuntimeType.ByteSlab,
188189
)
189190
}
190-
}
191+
192+
override fun unsetUnion(union: UnionShape): RuntimeType =
193+
ProtocolFunctions.crossOperationFn("rest_xml_unset_union_payload") { fnName ->
194+
rustTemplate(
195+
"pub fn $fnName() -> #{ByteSlab} { #{Vec}::new() }",
196+
*preludeScope,
197+
"ByteSlab" to RuntimeType.ByteSlab,
198+
)
199+
}
191200

192201
override fun operationOutputSerializer(operationShape: OperationShape): RuntimeType? {
193202
val outputShape = operationShape.outputShape(model)

0 commit comments

Comments
 (0)