diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index e538133e78..5f3224f695 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -84,3 +84,9 @@ message = "Generate a region setter when a model uses SigV4." references = ["smithy-rs#2960"] meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "client" } author = "jdisanti" + +[[smithy-rs]] +message = "Fix code generation for union members with the `@httpPayload` trait." +references = ["smithy-rs#2969", "smithy-rs#1896"] +meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "all" } +author = "jdisanti" diff --git a/codegen-client-test/model/rest-xml-extras.smithy b/codegen-client-test/model/rest-xml-extras.smithy index 76a6bbd9fa..0c20c273cd 100644 --- a/codegen-client-test/model/rest-xml-extras.smithy +++ b/codegen-client-test/model/rest-xml-extras.smithy @@ -21,6 +21,9 @@ service RestXmlExtras { StringHeader, CreateFoo, RequiredMember, + // TODO(https://github.com/awslabs/smithy-rs/issues/2968): Remove the following once these tests are included in Smithy + // They're being added in https://github.com/smithy-lang/smithy/pull/1908 + HttpPayloadWithUnion, ] } @@ -257,3 +260,96 @@ structure RequiredMemberInputOutput { @required requiredString: String } + +// TODO(https://github.com/awslabs/smithy-rs/issues/2968): Delete the HttpPayloadWithUnion tests below once Smithy vends them +// They're being added in https://github.com/smithy-lang/smithy/pull/1908 + +/// This example serializes a union in the payload. +@idempotent +@http(uri: "/HttpPayloadWithUnion", method: "PUT") +operation HttpPayloadWithUnion { + input: HttpPayloadWithUnionInputOutput, + output: HttpPayloadWithUnionInputOutput +} + +apply HttpPayloadWithUnion @httpRequestTests([ + { + id: "RestXmlHttpPayloadWithUnion", + documentation: "Serializes a union in the payload.", + protocol: restXml, + method: "PUT", + uri: "/HttpPayloadWithUnion", + body: """ + + hello + """, + bodyMediaType: "application/xml", + headers: { + "Content-Type": "application/xml", + }, + requireHeaders: [ + "Content-Length" + ], + params: { + nested: { + greeting: "hello" + } + } + }, + { + id: "RestXmlHttpPayloadWithUnsetUnion", + documentation: "No payload is sent if the union has no value.", + protocol: restXml, + method: "PUT", + uri: "/HttpPayloadWithUnion", + body: "", + headers: { + "Content-Type": "application/xml", + "Content-Length": "0" + }, + params: {} + } +]) + +apply HttpPayloadWithUnion @httpResponseTests([ + { + id: "RestXmlHttpPayloadWithUnion", + documentation: "Serializes a union in the payload.", + protocol: restXml, + code: 200, + body: """ + + hello + """, + bodyMediaType: "application/xml", + headers: { + "Content-Type": "application/xml", + }, + params: { + nested: { + greeting: "hello" + } + } + }, + { + id: "RestXmlHttpPayloadWithUnsetUnion", + documentation: "No payload is sent if the union has no value.", + protocol: restXml, + code: 200, + body: "", + headers: { + "Content-Type": "application/xml", + "Content-Length": "0" + }, + params: {} + } +]) + +structure HttpPayloadWithUnionInputOutput { + @httpPayload + nested: UnionPayload, +} + +union UnionPayload { + greeting: String +} diff --git a/codegen-core/common-test-models/rest-json-extras.smithy b/codegen-core/common-test-models/rest-json-extras.smithy index ff92f36c6b..d946ab0b5d 100644 --- a/codegen-core/common-test-models/rest-json-extras.smithy +++ b/codegen-core/common-test-models/rest-json-extras.smithy @@ -65,6 +65,9 @@ service RestJsonExtras { NullInNonSparse, CaseInsensitiveErrorOperation, EmptyStructWithContentOnWireOp, + // TODO(https://github.com/awslabs/smithy-rs/issues/2968): Remove the following once these tests are included in Smithy + // They're being added in https://github.com/smithy-lang/smithy/pull/1908 + HttpPayloadWithUnion, ], errors: [ExtraError] } @@ -348,3 +351,96 @@ structure EmptyStructWithContentOnWireOpOutput { operation EmptyStructWithContentOnWireOp { output: EmptyStructWithContentOnWireOpOutput, } + +// TODO(https://github.com/awslabs/smithy-rs/issues/2968): Delete the HttpPayloadWithUnion tests below once Smithy vends them +// They're being added in https://github.com/smithy-lang/smithy/pull/1908 + +/// This examples serializes a union in the payload. +@idempotent +@http(uri: "/HttpPayloadWithUnion", method: "PUT") +operation HttpPayloadWithUnion { + input: HttpPayloadWithUnionInputOutput, + output: HttpPayloadWithUnionInputOutput +} + +structure HttpPayloadWithUnionInputOutput { + @httpPayload + nested: UnionPayload, +} + +union UnionPayload { + greeting: String +} + +apply HttpPayloadWithUnion @httpRequestTests([ + { + id: "RestJsonHttpPayloadWithUnion", + documentation: "Serializes a union in the payload.", + protocol: restJson1, + method: "PUT", + uri: "/HttpPayloadWithUnion", + body: """ + { + "greeting": "hello" + }""", + bodyMediaType: "application/json", + headers: { + "Content-Type": "application/json" + }, + requireHeaders: [ + "Content-Length" + ], + params: { + nested: { + greeting: "hello" + } + } + }, + { + id: "RestJsonHttpPayloadWithUnsetUnion", + documentation: "No payload is sent if the union has no value.", + protocol: restJson1, + method: "PUT", + uri: "/HttpPayloadWithUnion", + body: "", + headers: { + "Content-Type": "application/json", + "Content-Length": "0" + }, + params: {} + } +]) + +apply HttpPayloadWithUnion @httpResponseTests([ + { + id: "RestJsonHttpPayloadWithUnion", + documentation: "Serializes a union in the payload.", + protocol: restJson1, + code: 200, + body: """ + { + "greeting": "hello" + }""", + bodyMediaType: "application/json", + headers: { + "Content-Type": "application/json" + }, + params: { + nested: { + greeting: "hello" + } + } + }, + { + id: "RestJsonHttpPayloadWithUnsetUnion", + documentation: "No payload is sent if the union has no value.", + protocol: restJson1, + code: 200, + body: "", + headers: { + "Content-Type": "application/json", + "Content-Length": "0" + }, + params: {} + } +]) diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/HttpBoundProtocolPayloadGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/HttpBoundProtocolPayloadGenerator.kt index 2c6ffc662c..0ed594fc28 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/HttpBoundProtocolPayloadGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/HttpBoundProtocolPayloadGenerator.kt @@ -269,7 +269,7 @@ class HttpBoundProtocolPayloadGenerator( """, ) is StructureShape -> rust("#T()", serializerGenerator.unsetStructure(targetShape)) - is UnionShape -> throw CodegenException("Currently unsupported. Tracking issue: https://github.com/awslabs/smithy-rs/issues/1896") + is UnionShape -> rust("#T()", serializerGenerator.unsetUnion(targetShape)) else -> throw CodegenException("`httpPayload` on member shapes targeting shapes of type ${targetShape.type} is unsupported") } } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/JsonSerializerGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/JsonSerializerGenerator.kt index 87562dc2ab..981706b566 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/JsonSerializerGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/JsonSerializerGenerator.kt @@ -36,6 +36,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.withBlock import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.customize.NamedCustomization import software.amazon.smithy.rust.codegen.core.smithy.customize.Section @@ -170,7 +171,7 @@ class JsonSerializerGenerator( private val runtimeConfig = codegenContext.runtimeConfig private val protocolFunctions = ProtocolFunctions(codegenContext) private val codegenScope = arrayOf( - "String" to RuntimeType.String, + *preludeScope, "Error" to runtimeConfig.serializationError(), "SdkBody" to RuntimeType.sdkBody(runtimeConfig), "JsonObjectWriter" to RuntimeType.smithyJson(runtimeConfig).resolve("serialize::JsonObjectWriter"), @@ -232,7 +233,7 @@ class JsonSerializerGenerator( } override fun unsetStructure(structure: StructureShape): RuntimeType = - ProtocolFunctions.crossOperationFn("rest_json_unsetpayload") { fnName -> + ProtocolFunctions.crossOperationFn("rest_json_unset_struct_payload") { fnName -> rustTemplate( """ pub fn $fnName() -> #{ByteSlab} { @@ -243,6 +244,14 @@ class JsonSerializerGenerator( ) } + override fun unsetUnion(union: UnionShape): RuntimeType = + ProtocolFunctions.crossOperationFn("rest_json_unset_union_payload") { fnName -> + rustTemplate( + "pub fn $fnName() -> #{ByteSlab} { #{Vec}::new() }", + *codegenScope, + ) + } + override fun operationInputSerializer(operationShape: OperationShape): RuntimeType? { // Don't generate an operation JSON serializer if there is no JSON body. val httpDocumentMembers = httpBindingResolver.requestMembers(operationShape, HttpLocation.DOCUMENT) diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/QuerySerializerGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/QuerySerializerGenerator.kt index 974e112cde..1a5b37bc74 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/QuerySerializerGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/QuerySerializerGenerator.kt @@ -121,6 +121,10 @@ abstract class QuerySerializerGenerator(codegenContext: CodegenContext) : Struct TODO("AwsQuery doesn't support payload serialization") } + override fun unsetUnion(union: UnionShape): RuntimeType { + TODO("AwsQuery doesn't support payload serialization") + } + override fun operationInputSerializer(operationShape: OperationShape): RuntimeType? { val inputShape = operationShape.inputShape(model) return protocolFunctions.serializeFn(inputShape, fnNameSuffix = "input") { fnName -> diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/StructuredDataSerializerGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/StructuredDataSerializerGenerator.kt index 962b9c8720..92b28d89fc 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/StructuredDataSerializerGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/StructuredDataSerializerGenerator.kt @@ -9,6 +9,7 @@ import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType interface StructuredDataSerializerGenerator { @@ -27,12 +28,22 @@ interface StructuredDataSerializerGenerator { * Generate the correct data when attempting to serialize a structure that is unset * * ```rust - * fn rest_json_unsetpayload() -> Vec { + * fn rest_json_unset_struct_payload() -> Vec { * ... * } */ fun unsetStructure(structure: StructureShape): RuntimeType + /** + * Generate the correct data when attempting to serialize a union that is unset + * + * ```rust + * fn rest_json_unset_union_payload() -> Vec { + * ... + * } + */ + fun unsetUnion(union: UnionShape): RuntimeType + /** * Generate a serializer for an operation input structure. * This serializer is only used by clients. diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt index 678768cfa7..fc4f019838 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt @@ -35,6 +35,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.stripOuter import software.amazon.smithy.rust.codegen.core.rustlang.withBlock import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope import software.amazon.smithy.rust.codegen.core.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.core.smithy.generators.renderUnknownVariant import software.amazon.smithy.rust.codegen.core.smithy.generators.serializationError @@ -176,8 +177,8 @@ class XmlBindingTraitSerializerGenerator( } } - override fun unsetStructure(structure: StructureShape): RuntimeType { - return ProtocolFunctions.crossOperationFn("rest_xml_unset_payload") { fnName -> + override fun unsetStructure(structure: StructureShape): RuntimeType = + ProtocolFunctions.crossOperationFn("rest_xml_unset_struct_payload") { fnName -> rustTemplate( """ pub fn $fnName() -> #{ByteSlab} { @@ -187,7 +188,15 @@ class XmlBindingTraitSerializerGenerator( "ByteSlab" to RuntimeType.ByteSlab, ) } - } + + override fun unsetUnion(union: UnionShape): RuntimeType = + ProtocolFunctions.crossOperationFn("rest_xml_unset_union_payload") { fnName -> + rustTemplate( + "pub fn $fnName() -> #{ByteSlab} { #{Vec}::new() }", + *preludeScope, + "ByteSlab" to RuntimeType.ByteSlab, + ) + } override fun operationOutputSerializer(operationShape: OperationShape): RuntimeType? { val outputShape = operationShape.outputShape(model)