From 233c35d9360984db3dc31223af179b8d41c3d6cb Mon Sep 17 00:00:00 2001 From: ysaito1001 Date: Thu, 2 Oct 2025 20:46:41 -0500 Subject: [PATCH 1/7] Try different protocols against union in question --- .../client/smithy/protocols/AwsQueryTest.kt | 24 ++++--- .../client/smithy/protocols/RestJsonTest.kt | 32 ++++----- .../client/smithy/protocols/RestXmlTest.kt | 65 ++++--------------- 3 files changed, 45 insertions(+), 76 deletions(-) diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt index 9f811158951..249c5b94d15 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt @@ -18,19 +18,25 @@ class AwsQueryTest { @awsQuery @xmlNamespace(uri: "https://example.com/") service TestService { - version: "2019-12-16", - operations: [SomeOperation] + version: "1.0", + operations: [TestOperation] } - operation SomeOperation { - input: SomeOperationInputOutput, - output: SomeOperationInputOutput, + union ObjectEncryptionFilter { + sses3: SSES3Filter, } - structure SomeOperationInputOutput { - payload: String, - a: String, - b: Integer + structure SSES3Filter { + // Empty structure - no members + } + + @input + structure TestInput { + filter: ObjectEncryptionFilter + } + + operation TestOperation { + input: TestInput, } """.asSmithyModel() diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt index 200019adb12..bae6d67c7d7 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt @@ -15,27 +15,29 @@ internal class RestJsonTest { namespace test use aws.protocols#restJson1 use aws.api#service - use smithy.test#httpRequestTests - use smithy.test#httpResponseTests - /// A REST JSON service that sends JSON requests and responses. - @service(sdkId: "Rest Json Protocol") @restJson1 - service RestJsonExtras { - version: "2019-12-16", - operations: [StringPayload] + service TestService { + version: "1.0", + operations: [TestOperation] } - @http(uri: "/StringPayload", method: "POST") - operation StringPayload { - input: StringPayloadInput, - output: StringPayloadInput + union ObjectEncryptionFilter { + sses3: SSES3Filter, } - structure StringPayloadInput { - payload: String, - a: String, - b: Integer + structure SSES3Filter { + // Empty structure - no members + } + + @input + structure TestInput { + filter: ObjectEncryptionFilter + } + + @http(uri: "/test", method: "POST") + operation TestOperation { + input: TestInput, } """.asSmithyModel() diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt index 279bf964edd..dad7abae326 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt @@ -20,67 +20,28 @@ internal class RestXmlTest { /// A REST XML service that sends XML requests and responses. @service(sdkId: "Rest XML UT") @restXml - service RestXmlExtras { - version: "2019-12-16", - operations: [Op] + service TestService { + version: "1.0", + operations: [TestOperation] } - - @http(uri: "/top", method: "POST") - operation Op { - input: Top, - output: Top - } - union Choice { - @xmlFlattened - @xmlName("Hi") - flatMap: MyMap, - - deepMap: MyMap, - - @xmlFlattened - flatList: SomeList, - - deepList: SomeList, - - s: String, - - enum: FooEnum, - - date: Timestamp, - - number: Double, - - top: Top, - - blob: Blob + union ObjectEncryptionFilter { + sses3: SSES3Filter, } - @enum([{name: "FOO", value: "FOO"}]) - string FooEnum - - map MyMap { - @xmlName("Name") - key: String, - - @xmlName("Setting") - value: Choice, + structure SSES3Filter { + // Empty structure - no members } - list SomeList { - member: Choice + @input + structure TestInput { + filter: ObjectEncryptionFilter } - structure Top { - choice: Choice, - - @xmlAttribute - extra: Long, - - @xmlName("prefix:local") - renamedWithPrefix: String + @http(uri: "/test", method: "POST") + operation TestOperation { + input: TestInput, } - """.asSmithyModel() @Test From 773ee8263df2c59ba8dcf250fbe1ce007d8a71cb Mon Sep 17 00:00:00 2001 From: vcjana Date: Mon, 6 Oct 2025 10:41:29 -0700 Subject: [PATCH 2/7] Fix unused variable warnings in QuerySerializerGenerator for unions with empty structs - Add isEmptyStruct() helper to detect empty struct shapes - Use _inner for empty structs to avoid unused variable warnings - Use inner for non-empty structs where variable is used - Add comprehensive tests for RestXml, AwsQuery, and RestJson protocols - Tests enforce -D warnings via clientIntegrationTest --- .../client/smithy/protocols/AwsQueryTest.kt | 47 +++++++++++++++++ .../client/smithy/protocols/RestJsonTest.kt | 51 +++++++++++++++++++ .../client/smithy/protocols/RestXmlTest.kt | 49 ++++++++++++++++++ .../serialize/QuerySerializerGenerator.kt | 18 ++++++- 4 files changed, 163 insertions(+), 2 deletions(-) diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt index 249c5b94d15..490a472ec6c 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt @@ -44,4 +44,51 @@ class AwsQueryTest { fun `generate an aws query service that compiles`() { clientIntegrationTest(model) { _, _ -> } } + + @Test + fun `union with empty struct generates warning-free code`() { + val modelWithEmptyStruct = + """ + namespace test + use aws.protocols#awsQuery + + @awsQuery + @xmlNamespace(uri: "https://example.com/") + service TestService { + version: "2019-12-16", + operations: [TestOp] + } + + operation TestOp { + input: TestInput, + output: TestOutput + } + + structure TestInput { + testUnion: TestUnion + } + + structure TestOutput { + testUnion: TestUnion + } + + union TestUnion { + // Empty struct - should generate _inner to avoid unused variable warning + emptyStruct: EmptyStruct, + + // Normal struct - should generate inner (without underscore) + normalStruct: NormalStruct + } + + structure EmptyStruct {} + + structure NormalStruct { + value: String + } + """.asSmithyModel() + + // This test will fail with unused variable warnings if the fix is not applied + // clientIntegrationTest enforces -D warnings via codegenIntegrationTest + clientIntegrationTest(modelWithEmptyStruct) { _, _ -> } + } } diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt index bae6d67c7d7..8511c4176f8 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt @@ -45,4 +45,55 @@ internal class RestJsonTest { fun `generate a rest json service that compiles`() { clientIntegrationTest(model) { _, _ -> } } + + @Test + fun `union with empty struct always uses inner variable`() { + val modelWithEmptyStruct = + """ + namespace test + use aws.protocols#restJson1 + use aws.api#service + + @service(sdkId: "Rest Json Empty Struct") + @restJson1 + service RestJsonEmptyStruct { + version: "2019-12-16", + operations: [TestOp] + } + + @http(uri: "/test", method: "POST") + operation TestOp { + input: TestInput, + output: TestOutput + } + + structure TestInput { + testUnion: TestUnion + } + + structure TestOutput { + testUnion: TestUnion + } + + union TestUnion { + // Empty struct - RestJson ALWAYS uses inner variable, no warning + emptyStruct: EmptyStruct, + + // Normal struct - RestJson uses inner variable + normalStruct: NormalStruct + } + + structure EmptyStruct {} + + structure NormalStruct { + value: String + } + """.asSmithyModel() + + // This test documents that RestJson protocol is immune to unused variable issues. + // Unlike RestXml/AwsQuery, RestJson serializers always reference the inner variable + // even for empty structs, so no underscore prefix is needed. + // This test passes without any code changes, proving RestJson immunity. + clientIntegrationTest(modelWithEmptyStruct) { _, _ -> } + } } diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt index dad7abae326..f677a4b5e04 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt @@ -48,4 +48,53 @@ internal class RestXmlTest { fun `generate a rest xml service that compiles`() { clientIntegrationTest(model) { _, _ -> } } + + @Test + fun `union with empty struct generates warning-free code`() { + val modelWithEmptyStruct = + """ + namespace test + use aws.protocols#restXml + use aws.api#service + + @service(sdkId: "Rest XML Empty Struct") + @restXml + service RestXmlEmptyStruct { + version: "2019-12-16", + operations: [TestOp] + } + + @http(uri: "/test", method: "POST") + operation TestOp { + input: TestInput, + output: TestOutput + } + + structure TestInput { + testUnion: TestUnion + } + + structure TestOutput { + testUnion: TestUnion + } + + union TestUnion { + // Empty struct - should generate _inner to avoid unused variable warning + emptyStruct: EmptyStruct, + + // Normal struct - should generate inner (without underscore) + normalStruct: NormalStruct + } + + structure EmptyStruct {} + + structure NormalStruct { + value: String + } + """.asSmithyModel() + + // This test will fail with unused variable warnings if the fix is not applied + // clientIntegrationTest enforces -D warnings via codegenIntegrationTest + clientIntegrationTest(modelWithEmptyStruct) { _, _ -> } + } } 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 65bf813c3e2..980b36ed28f 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 @@ -345,6 +345,16 @@ abstract class QuerySerializerGenerator(private val codegenContext: CodegenConte } } + /** + * Determines if a struct shape is empty (has no members). + * Empty structs result in unused variables in union match arms since the inner value is never referenced. + */ + private fun isEmptyStruct(shape: Shape): Boolean = + when (shape) { + is StructureShape -> shape.members().isEmpty() + else -> false + } + private fun RustWriter.serializeUnion(context: Context) { val unionSymbol = symbolProvider.toSymbol(context.shape) val unionSerializer = @@ -357,17 +367,21 @@ abstract class QuerySerializerGenerator(private val codegenContext: CodegenConte ) { rustBlock("match input") { for (member in context.shape.members()) { + val targetShape = model.expectShape(member.target) + // Use underscore prefix for empty structs to avoid unused variable warnings + val innerVarName = if (isEmptyStruct(targetShape)) "_inner" else "inner" + val variantName = if (member.isTargetUnit()) { "${symbolProvider.toMemberName(member)}" } else { - "${symbolProvider.toMemberName(member)}(inner)" + "${symbolProvider.toMemberName(member)}($innerVarName)" } withBlock("#T::$variantName => {", "},", unionSymbol) { serializeMember( MemberContext.unionMember( context.copy(writerExpression = "writer"), - "inner", + innerVarName, member, ), ) From a4afcec2a73fa6a68a4799a398630dc4afa4b6b0 Mon Sep 17 00:00:00 2001 From: vcjana Date: Mon, 6 Oct 2025 13:15:55 -0700 Subject: [PATCH 3/7] Fix unused writer parameter in union serializer for empty structs Prefix writer with underscore when union contains only empty structs --- .../protocols/serialize/QuerySerializerGenerator.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 980b36ed28f..ef9cfa52b6f 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 @@ -357,11 +357,19 @@ abstract class QuerySerializerGenerator(private val codegenContext: CodegenConte private fun RustWriter.serializeUnion(context: Context) { val unionSymbol = symbolProvider.toSymbol(context.shape) + + // Check if any union member uses the writer (non-empty structs) + val hasNonEmptyMember = + context.shape.members().any { member -> + !member.isTargetUnit() && !isEmptyStruct(model.expectShape(member.target)) + } + val writerVarName = if (hasNonEmptyMember) "writer" else "_writer" + val unionSerializer = protocolFunctions.serializeFn(context.shape) { fnName -> Attribute.AllowUnusedMut.render(this) rustBlockTemplate( - "pub fn $fnName(mut writer: #{QueryValueWriter}, input: &#{Input}) -> #{Result}<(), #{Error}>", + "pub fn $fnName(mut $writerVarName: #{QueryValueWriter}, input: &#{Input}) -> #{Result}<(), #{Error}>", "Input" to unionSymbol, *codegenScope, ) { @@ -380,7 +388,7 @@ abstract class QuerySerializerGenerator(private val codegenContext: CodegenConte withBlock("#T::$variantName => {", "},", unionSymbol) { serializeMember( MemberContext.unionMember( - context.copy(writerExpression = "writer"), + context.copy(writerExpression = writerVarName), innerVarName, member, ), From bd419180944439829c478ad143d121e7bd098072 Mon Sep 17 00:00:00 2001 From: vcjana Date: Tue, 7 Oct 2025 14:19:39 -0700 Subject: [PATCH 4/7] Fix XML union serialization for empty struct members When building tests, discovered that serializeUnion was using incorrect variable name for empty struct members. The pattern match correctly used '_inner' but serializeMember was called with 'inner', causing compilation errors. Fixed by passing the correct variable name based on whether the struct is empty. --- .../client/smithy/protocols/AwsQueryTest.kt | 80 +-- .../client/smithy/protocols/RestJsonTest.kt | 86 ++-- .../client/smithy/protocols/RestXmlTest.kt | 84 ++-- .../XmlBindingTraitSerializerGenerator.kt | 464 ++++++++++-------- 4 files changed, 377 insertions(+), 337 deletions(-) diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt index 490a472ec6c..9dfb342fd0b 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt @@ -40,6 +40,46 @@ class AwsQueryTest { } """.asSmithyModel() + private val modelWithEmptyStruct = + """ + namespace test + use aws.protocols#awsQuery + + @awsQuery + @xmlNamespace(uri: "https://example.com/") + service TestService { + version: "2019-12-16", + operations: [TestOp] + } + + operation TestOp { + input: TestInput, + output: TestOutput + } + + structure TestInput { + testUnion: TestUnion + } + + structure TestOutput { + testUnion: TestUnion + } + + union TestUnion { + // Empty struct - should generate _inner to avoid unused variable warning + emptyStruct: EmptyStruct, + + // Normal struct - should generate inner (without underscore) + normalStruct: NormalStruct + } + + structure EmptyStruct {} + + structure NormalStruct { + value: String + } + """.asSmithyModel() + @Test fun `generate an aws query service that compiles`() { clientIntegrationTest(model) { _, _ -> } @@ -47,46 +87,6 @@ class AwsQueryTest { @Test fun `union with empty struct generates warning-free code`() { - val modelWithEmptyStruct = - """ - namespace test - use aws.protocols#awsQuery - - @awsQuery - @xmlNamespace(uri: "https://example.com/") - service TestService { - version: "2019-12-16", - operations: [TestOp] - } - - operation TestOp { - input: TestInput, - output: TestOutput - } - - structure TestInput { - testUnion: TestUnion - } - - structure TestOutput { - testUnion: TestUnion - } - - union TestUnion { - // Empty struct - should generate _inner to avoid unused variable warning - emptyStruct: EmptyStruct, - - // Normal struct - should generate inner (without underscore) - normalStruct: NormalStruct - } - - structure EmptyStruct {} - - structure NormalStruct { - value: String - } - """.asSmithyModel() - // This test will fail with unused variable warnings if the fix is not applied // clientIntegrationTest enforces -D warnings via codegenIntegrationTest clientIntegrationTest(modelWithEmptyStruct) { _, _ -> } diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt index 8511c4176f8..91770a8ed35 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt @@ -11,7 +11,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel internal class RestJsonTest { val model = - """ + """ namespace test use aws.protocols#restJson1 use aws.api#service @@ -41,6 +41,48 @@ internal class RestJsonTest { } """.asSmithyModel() + private val modelWithEmptyStruct = + """ + namespace test + use aws.protocols#restJson1 + use aws.api#service + + @service(sdkId: "Rest Json Empty Struct") + @restJson1 + service RestJsonEmptyStruct { + version: "2019-12-16", + operations: [TestOp] + } + + @http(uri: "/test", method: "POST") + operation TestOp { + input: TestInput, + output: TestOutput + } + + structure TestInput { + testUnion: TestUnion + } + + structure TestOutput { + testUnion: TestUnion + } + + union TestUnion { + // Empty struct - RestJson ALWAYS uses inner variable, no warning + emptyStruct: EmptyStruct, + + // Normal struct - RestJson uses inner variable + normalStruct: NormalStruct + } + + structure EmptyStruct {} + + structure NormalStruct { + value: String + } + """.asSmithyModel() + @Test fun `generate a rest json service that compiles`() { clientIntegrationTest(model) { _, _ -> } @@ -48,48 +90,6 @@ internal class RestJsonTest { @Test fun `union with empty struct always uses inner variable`() { - val modelWithEmptyStruct = - """ - namespace test - use aws.protocols#restJson1 - use aws.api#service - - @service(sdkId: "Rest Json Empty Struct") - @restJson1 - service RestJsonEmptyStruct { - version: "2019-12-16", - operations: [TestOp] - } - - @http(uri: "/test", method: "POST") - operation TestOp { - input: TestInput, - output: TestOutput - } - - structure TestInput { - testUnion: TestUnion - } - - structure TestOutput { - testUnion: TestUnion - } - - union TestUnion { - // Empty struct - RestJson ALWAYS uses inner variable, no warning - emptyStruct: EmptyStruct, - - // Normal struct - RestJson uses inner variable - normalStruct: NormalStruct - } - - structure EmptyStruct {} - - structure NormalStruct { - value: String - } - """.asSmithyModel() - // This test documents that RestJson protocol is immune to unused variable issues. // Unlike RestXml/AwsQuery, RestJson serializers always reference the inner variable // even for empty structs, so no underscore prefix is needed. diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt index f677a4b5e04..11ffba5ac07 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt @@ -44,6 +44,48 @@ internal class RestXmlTest { } """.asSmithyModel() + private val modelWithEmptyStruct = + """ + namespace test + use aws.protocols#restXml + use aws.api#service + + @service(sdkId: "Rest XML Empty Struct") + @restXml + service RestXmlEmptyStruct { + version: "2019-12-16", + operations: [TestOp] + } + + @http(uri: "/test", method: "POST") + operation TestOp { + input: TestInput, + output: TestOutput + } + + structure TestInput { + testUnion: TestUnion + } + + structure TestOutput { + testUnion: TestUnion + } + + union TestUnion { + // Empty struct - should generate _inner to avoid unused variable warning + emptyStruct: EmptyStruct, + + // Normal struct - should generate inner (without underscore) + normalStruct: NormalStruct + } + + structure EmptyStruct {} + + structure NormalStruct { + value: String + } + """.asSmithyModel() + @Test fun `generate a rest xml service that compiles`() { clientIntegrationTest(model) { _, _ -> } @@ -51,48 +93,6 @@ internal class RestXmlTest { @Test fun `union with empty struct generates warning-free code`() { - val modelWithEmptyStruct = - """ - namespace test - use aws.protocols#restXml - use aws.api#service - - @service(sdkId: "Rest XML Empty Struct") - @restXml - service RestXmlEmptyStruct { - version: "2019-12-16", - operations: [TestOp] - } - - @http(uri: "/test", method: "POST") - operation TestOp { - input: TestInput, - output: TestOutput - } - - structure TestInput { - testUnion: TestUnion - } - - structure TestOutput { - testUnion: TestUnion - } - - union TestUnion { - // Empty struct - should generate _inner to avoid unused variable warning - emptyStruct: EmptyStruct, - - // Normal struct - should generate inner (without underscore) - normalStruct: NormalStruct - } - - structure EmptyStruct {} - - structure NormalStruct { - value: String - } - """.asSmithyModel() - // This test will fail with unused variable warnings if the fix is not applied // clientIntegrationTest enforces -D warnings via codegenIntegrationTest clientIntegrationTest(modelWithEmptyStruct) { _, _ -> } 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 96e1524109c..c15dc5d6d1c 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 @@ -54,8 +54,8 @@ import software.amazon.smithy.rust.codegen.core.util.letIf import software.amazon.smithy.rust.codegen.core.util.outputShape class XmlBindingTraitSerializerGenerator( - codegenContext: CodegenContext, - private val httpBindingResolver: HttpBindingResolver, + codegenContext: CodegenContext, + private val httpBindingResolver: HttpBindingResolver, ) : StructuredDataSerializerGenerator { private val symbolProvider = codegenContext.symbolProvider private val runtimeConfig = codegenContext.runtimeConfig @@ -63,13 +63,15 @@ class XmlBindingTraitSerializerGenerator( private val codegenTarget = codegenContext.target private val protocolFunctions = ProtocolFunctions(codegenContext) private val codegenScope = - arrayOf( - "XmlWriter" to RuntimeType.smithyXml(runtimeConfig).resolve("encode::XmlWriter"), - "ElementWriter" to RuntimeType.smithyXml(runtimeConfig).resolve("encode::ElWriter"), - "SdkBody" to RuntimeType.sdkBody(runtimeConfig), - "Error" to runtimeConfig.serializationError(), - *RuntimeType.preludeScope, - ) + arrayOf( + "XmlWriter" to + RuntimeType.smithyXml(runtimeConfig).resolve("encode::XmlWriter"), + "ElementWriter" to + RuntimeType.smithyXml(runtimeConfig).resolve("encode::ElWriter"), + "SdkBody" to RuntimeType.sdkBody(runtimeConfig), + "Error" to runtimeConfig.serializationError(), + *RuntimeType.preludeScope, + ) private val xmlIndex = XmlNameIndex.of(model) private val rootNamespace = codegenContext.serviceShape.getTrait() @@ -86,22 +88,22 @@ class XmlBindingTraitSerializerGenerator( // Kotlin doesn't have a "This" type @Suppress("UNCHECKED_CAST") fun updateInput( - input: T, - newInput: String, + input: T, + newInput: String, ): T = - when (input) { - is Element -> input.copy(input = newInput) as T - is Scope -> input.copy(input = newInput) as T - else -> TODO() - } + when (input) { + is Element -> input.copy(input = newInput) as T + is Scope -> input.copy(input = newInput) as T + else -> TODO() + } } } private fun Ctx.Element.scopedTo(member: MemberShape) = - this.copy(input = "$input.${symbolProvider.toMemberName(member)}") + this.copy(input = "$input.${symbolProvider.toMemberName(member)}") private fun Ctx.Scope.scopedTo(member: MemberShape) = - this.copy(input = "$input.${symbolProvider.toMemberName(member)}") + this.copy(input = "$input.${symbolProvider.toMemberName(member)}") override fun operationInputSerializer(operationShape: OperationShape): RuntimeType? { val inputShape = operationShape.inputShape(model) @@ -110,12 +112,13 @@ class XmlBindingTraitSerializerGenerator( return null } val operationXmlName = - xmlIndex.operationInputShapeName(operationShape) - ?: throw CodegenException("operation must have a name if it has members") + xmlIndex.operationInputShapeName(operationShape) + ?: throw CodegenException("operation must have a name if it has members") return protocolFunctions.serializeFn(operationShape, fnNameSuffix = "op_input") { fnName -> rustBlockTemplate( - "pub fn $fnName(input: &#{target}) -> #{Result}<#{SdkBody}, #{Error}>", - *codegenScope, "target" to symbolProvider.toSymbol(inputShape), + "pub fn $fnName(input: &#{target}) -> #{Result}<#{SdkBody}, #{Error}>", + *codegenScope, + "target" to symbolProvider.toSymbol(inputShape), ) { rust("let mut out = String::new();") // Create a scope for writer. This ensures that: @@ -123,16 +126,21 @@ class XmlBindingTraitSerializerGenerator( // - All closing tags get written rustBlock("") { rustTemplate( - """ + """ let mut writer = #{XmlWriter}::new(&mut out); ##[allow(unused_mut)] let mut root = writer.start_el(${operationXmlName.dq()})${ inputShape.xmlNamespace(root = true).apply() }; """, - *codegenScope, + *codegenScope, + ) + serializeStructure( + inputShape, + xmlMembers, + Ctx.Element("root", "input"), + fnNameSuffix = "input" ) - serializeStructure(inputShape, xmlMembers, Ctx.Element("root", "input"), fnNameSuffix = "input") } rustTemplate("Ok(#{SdkBody}::from(out))", *codegenScope) } @@ -146,10 +154,15 @@ class XmlBindingTraitSerializerGenerator( override fun payloadSerializer(member: MemberShape): RuntimeType { val target = model.expectShape(member.target) return protocolFunctions.serializeFn(member, fnNameSuffix = "payload") { fnName -> - val t = symbolProvider.toSymbol(member).rustType().stripOuter().render(true) + val t = + symbolProvider + .toSymbol(member) + .rustType() + .stripOuter() + .render(true) rustBlockTemplate( - "pub fn $fnName(input: &$t) -> std::result::Result, #{Error}>", - *codegenScope, + "pub fn $fnName(input: &$t) -> std::result::Result, #{Error}>", + *codegenScope, ) { rust("let mut out = String::new();") // Create a scope for writer. This ensures that: @@ -157,25 +170,27 @@ class XmlBindingTraitSerializerGenerator( // - All closing tags get written rustBlock("") { rustTemplate( - """ + """ let mut writer = #{XmlWriter}::new(&mut out); ##[allow(unused_mut)] let mut root = writer.start_el(${xmlIndex.payloadShapeName(member).dq()})${ target.xmlNamespace(root = true).apply() }; """, - *codegenScope, + *codegenScope, ) when (target) { is StructureShape -> - serializeStructure( - target, - XmlMemberIndex.fromMembers(target.members().toList()), - Ctx.Element("root", "input"), - ) - + serializeStructure( + target, + XmlMemberIndex.fromMembers(target.members().toList()), + Ctx.Element("root", "input"), + ) is UnionShape -> serializeUnion(target, Ctx.Element("root", "input")) - else -> throw IllegalStateException("xml payloadSerializer only supports structs and unions") + else -> + throw IllegalStateException( + "xml payloadSerializer only supports structs and unions" + ) } } rustTemplate("Ok(out.into_bytes())", *codegenScope) @@ -184,25 +199,25 @@ class XmlBindingTraitSerializerGenerator( } override fun unsetStructure(structure: StructureShape): RuntimeType = - ProtocolFunctions.crossOperationFn("rest_xml_unset_struct_payload") { fnName -> - rustTemplate( - """ + ProtocolFunctions.crossOperationFn("rest_xml_unset_struct_payload") { fnName -> + rustTemplate( + """ pub fn $fnName() -> #{ByteSlab} { Vec::new() } """, - "ByteSlab" to RuntimeType.ByteSlab, - ) - } + "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() }", - *codegenScope, - "ByteSlab" to RuntimeType.ByteSlab, - ) - } + ProtocolFunctions.crossOperationFn("rest_xml_unset_union_payload") { fnName -> + rustTemplate( + "pub fn $fnName() -> #{ByteSlab} { #{Vec}::new() }", + *codegenScope, + "ByteSlab" to RuntimeType.ByteSlab, + ) + } override fun operationOutputSerializer(operationShape: OperationShape): RuntimeType? { val outputShape = operationShape.outputShape(model) @@ -211,12 +226,13 @@ class XmlBindingTraitSerializerGenerator( return null } val operationXmlName = - xmlIndex.operationOutputShapeName(operationShape) - ?: throw CodegenException("operation must have a name if it has members") + xmlIndex.operationOutputShapeName(operationShape) + ?: throw CodegenException("operation must have a name if it has members") return protocolFunctions.serializeFn(operationShape, fnNameSuffix = "output") { fnName -> rustBlockTemplate( - "pub fn $fnName(output: &#{target}) -> #{Result}", - *codegenScope, "target" to symbolProvider.toSymbol(outputShape), + "pub fn $fnName(output: &#{target}) -> #{Result}", + *codegenScope, + "target" to symbolProvider.toSymbol(outputShape), ) { rust("let mut out = String::new();") // Create a scope for writer. This ensures that: @@ -224,14 +240,14 @@ class XmlBindingTraitSerializerGenerator( // - All closing tags get written rustBlock("") { rustTemplate( - """ + """ let mut writer = #{XmlWriter}::new(&mut out); ##[allow(unused_mut)] let mut root = writer.start_el(${operationXmlName.dq()})${ outputShape.xmlNamespace(root = true).apply() }; """, - *codegenScope, + *codegenScope, ) serializeStructure(outputShape, xmlMembers, Ctx.Element("root", "output")) } @@ -243,13 +259,15 @@ class XmlBindingTraitSerializerGenerator( override fun serverErrorSerializer(shape: ShapeId): RuntimeType { val errorShape = model.expectShape(shape, StructureShape::class.java) val xmlMembers = - httpBindingResolver.errorResponseBindings(shape) - .filter { it.location == HttpLocation.DOCUMENT } - .map { it.member } + httpBindingResolver + .errorResponseBindings(shape) + .filter { it.location == HttpLocation.DOCUMENT } + .map { it.member } return protocolFunctions.serializeFn(errorShape, fnNameSuffix = "error") { fnName -> rustBlockTemplate( - "pub fn $fnName(error: &#{target}) -> #{Result}", - *codegenScope, "target" to symbolProvider.toSymbol(errorShape), + "pub fn $fnName(error: &#{target}) -> #{Result}", + *codegenScope, + "target" to symbolProvider.toSymbol(errorShape), ) { rust("let mut out = String::new();") // Create a scope for writer. This ensures that: @@ -257,17 +275,17 @@ class XmlBindingTraitSerializerGenerator( // - All closing tags get written rustBlock("") { rustTemplate( - """ + """ let mut writer = #{XmlWriter}::new(&mut out); ##[allow(unused_mut)] let mut root = writer.start_el("Error")${errorShape.xmlNamespace(root = true).apply()}; """, - *codegenScope, + *codegenScope, ) serializeStructure( - errorShape, - XmlMemberIndex.fromMembers(xmlMembers), - Ctx.Element("root", "error"), + errorShape, + XmlMemberIndex.fromMembers(xmlMembers), + Ctx.Element("root", "error"), ) } rustTemplate("Ok(out)", *codegenScope) @@ -282,17 +300,18 @@ class XmlBindingTraitSerializerGenerator( } private fun RustWriter.structureInner( - members: XmlMemberIndex, - ctx: Ctx.Element, + members: XmlMemberIndex, + ctx: Ctx.Element, ) { if (members.attributeMembers.isNotEmpty()) { rust("let mut ${ctx.elementWriter} = ${ctx.elementWriter};") } members.attributeMembers.forEach { member -> handleOptional(member, ctx.scopedTo(member)) { ctx -> - withBlock("${ctx.elementWriter}.write_attribute(${xmlIndex.memberName(member).dq()},", ");") { - serializeRawMember(member, ctx.input) - } + withBlock( + "${ctx.elementWriter}.write_attribute(${xmlIndex.memberName(member).dq()},", + ");" + ) { serializeRawMember(member, ctx.input) } } } Attribute.AllowUnusedMut.render(this) @@ -305,80 +324,91 @@ class XmlBindingTraitSerializerGenerator( } private fun RustWriter.serializeRawMember( - member: MemberShape, - input: String, + member: MemberShape, + input: String, ) { when (model.expectShape(member.target)) { is StringShape -> { - // The `input` expression always evaluates to a reference type at this point, but if it does so because + // The `input` expression always evaluates to a reference type at this point, but if + // it does so because // it's preceded by the `&` operator, calling `as_str()` on it will upset Clippy. val dereferenced = - if (input.startsWith("&")) { - autoDeref(input) - } else { - input - } + if (input.startsWith("&")) { + autoDeref(input) + } else { + input + } rust("$dereferenced.as_str()") } - is BooleanShape, is NumberShape -> { rust( - "#T::from(${autoDeref(input)}).encode()", - RuntimeType.smithyTypes(runtimeConfig).resolve("primitive::Encoder"), + "#T::from(${autoDeref(input)}).encode()", + RuntimeType.smithyTypes(runtimeConfig).resolve("primitive::Encoder"), ) } - - is BlobShape -> rust("#T($input.as_ref()).as_ref()", RuntimeType.base64Encode(runtimeConfig)) + is BlobShape -> + rust("#T($input.as_ref()).as_ref()", RuntimeType.base64Encode(runtimeConfig)) is TimestampShape -> { val timestampFormat = - httpBindingResolver.timestampFormat( - member, - HttpLocation.DOCUMENT, - TimestampFormatTrait.Format.DATE_TIME, model, - ) + httpBindingResolver.timestampFormat( + member, + HttpLocation.DOCUMENT, + TimestampFormatTrait.Format.DATE_TIME, + model, + ) val timestampFormatType = - RuntimeType.parseTimestampFormat(codegenTarget, runtimeConfig, timestampFormat) + RuntimeType.parseTimestampFormat( + codegenTarget, + runtimeConfig, + timestampFormat + ) rust("$input.fmt(#T)?.as_ref()", timestampFormatType) } - else -> TODO(member.toString()) } } @Suppress("NAME_SHADOWING") private fun RustWriter.serializeMember( - memberShape: MemberShape, - ctx: Ctx.Scope, - rootNameOverride: String? = null, + memberShape: MemberShape, + ctx: Ctx.Scope, + rootNameOverride: String? = null, ) { val target = model.expectShape(memberShape.target) val xmlName = rootNameOverride ?: xmlIndex.memberName(memberShape) val ns = memberShape.xmlNamespace(root = false).apply() handleOptional(memberShape, ctx) { ctx -> when (target) { - is StringShape, is BooleanShape, is NumberShape, is TimestampShape, is BlobShape -> { - rust("let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();") + is StringShape, + is BooleanShape, + is NumberShape, + is TimestampShape, + is BlobShape -> { + rust( + "let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();" + ) withBlock("inner_writer.data(", ");") { serializeRawMember(memberShape, ctx.input) } } - is CollectionShape -> - if (memberShape.hasTrait()) { - serializeFlatList(memberShape, target, ctx) - } else { - rust("let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();") - serializeList(target, Ctx.Scope("inner_writer", ctx.input)) - } - + if (memberShape.hasTrait()) { + serializeFlatList(memberShape, target, ctx) + } else { + rust( + "let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();" + ) + serializeList(target, Ctx.Scope("inner_writer", ctx.input)) + } is MapShape -> - if (memberShape.hasTrait()) { - serializeMap(target, xmlIndex.memberName(memberShape), ctx) - } else { - rust("let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();") - serializeMap(target, "entry", Ctx.Scope("inner_writer", ctx.input)) - } - + if (memberShape.hasTrait()) { + serializeMap(target, xmlIndex.memberName(memberShape), ctx) + } else { + rust( + "let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();" + ) + serializeMap(target, "entry", Ctx.Scope("inner_writer", ctx.input)) + } is StructureShape -> { // We call serializeStructure only when target.members() is nonempty. // If it were empty, serializeStructure would generate the following code: @@ -392,108 +422,115 @@ class XmlBindingTraitSerializerGenerator( // scope.finish(); // Ok(()) // } - // However, this would cause a compilation error at a call site because it cannot - // extract data out of the Unit type that corresponds to the variable "input" above. + // However, this would cause a compilation error at a call site because it + // cannot + // extract data out of the Unit type that corresponds to the variable "input" + // above. if (target.members().isEmpty()) { rust("${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();") } else { rust("let inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns;") serializeStructure( - target, - XmlMemberIndex.fromMembers(target.members().toList()), - Ctx.Element("inner_writer", ctx.input), + target, + XmlMemberIndex.fromMembers(target.members().toList()), + Ctx.Element("inner_writer", ctx.input), ) } } - is UnionShape -> { rust("let inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns;") serializeUnion(target, Ctx.Element("inner_writer", ctx.input)) } - else -> TODO(target.toString()) } } } private fun RustWriter.serializeStructure( - structureShape: StructureShape, - members: XmlMemberIndex, - ctx: Ctx.Element, - fnNameSuffix: String? = null, + structureShape: StructureShape, + members: XmlMemberIndex, + ctx: Ctx.Element, + fnNameSuffix: String? = null, ) { val structureSymbol = symbolProvider.toSymbol(structureShape) val structureSerializer = - protocolFunctions.serializeFn(structureShape, fnNameSuffix = fnNameSuffix) { fnName -> - rustBlockTemplate( - "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter}) -> #{Result}<(), #{Error}>", - "Input" to structureSymbol, - *codegenScope, - ) { - if (!members.isNotEmpty()) { - // removed unused warning if there are no fields we're going to read - rust("let _ = input;") + protocolFunctions.serializeFn(structureShape, fnNameSuffix = fnNameSuffix) { fnName + -> + rustBlockTemplate( + "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter}) -> #{Result}<(), #{Error}>", + "Input" to structureSymbol, + *codegenScope, + ) { + if (!members.isNotEmpty()) { + // removed unused warning if there are no fields we're going to read + rust("let _ = input;") + } + structureInner(members, Ctx.Element("writer", "&input")) + rust("Ok(())") } - structureInner(members, Ctx.Element("writer", "&input")) - rust("Ok(())") } - } rust("#T(${ctx.input}, ${ctx.elementWriter})?", structureSerializer) } private fun RustWriter.serializeUnion( - unionShape: UnionShape, - ctx: Ctx.Element, + unionShape: UnionShape, + ctx: Ctx.Element, ) { val unionSymbol = symbolProvider.toSymbol(unionShape) val structureSerializer = - protocolFunctions.serializeFn(unionShape) { fnName -> - rustBlockTemplate( - "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter}) -> #{Result}<(), #{Error}>", - "Input" to unionSymbol, - *codegenScope, - ) { - rust("let mut scope_writer = writer.finish();") - rustBlock("match input") { - val members = unionShape.members() - - members.forEach { member -> - val memberShape = model.expectShape(member.target) - val memberName = symbolProvider.toMemberName(member) - val variantName = - if (member.isTargetUnit()) { - "$memberName" - } else if (memberShape.isStructureShape && - memberShape.asStructureShape() - .get().allMembers.isEmpty() - ) { - // Unit structs don't serialize inner, so it is never accessed - "$memberName(_inner)" - } else { - "$memberName(inner)" + protocolFunctions.serializeFn(unionShape) { fnName -> + rustBlockTemplate( + "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter}) -> #{Result}<(), #{Error}>", + "Input" to unionSymbol, + *codegenScope, + ) { + rust("let mut scope_writer = writer.finish();") + rustBlock("match input") { + val members = unionShape.members() + + members.forEach { member -> + val memberShape = model.expectShape(member.target) + val memberName = symbolProvider.toMemberName(member) + val isEmptyStruct = + memberShape.isStructureShape && + memberShape + .asStructureShape() + .get() + .allMembers + .isEmpty() + val variantName = + if (member.isTargetUnit()) { + "$memberName" + } else if (isEmptyStruct) { + // Unit structs don't serialize inner, so it is never + // accessed + "$memberName(_inner)" + } else { + "$memberName(inner)" + } + val innerVarName = if (isEmptyStruct) "_inner" else "inner" + withBlock("#T::$variantName =>", ",", unionSymbol) { + serializeMember(member, Ctx.Scope("scope_writer", innerVarName)) } - withBlock("#T::$variantName =>", ",", unionSymbol) { - serializeMember(member, Ctx.Scope("scope_writer", "inner")) } - } - if (codegenTarget.renderUnknownVariant()) { - rustTemplate( - "#{Union}::${UnionGenerator.UNKNOWN_VARIANT_NAME} => return Err(#{Error}::unknown_variant(${unionSymbol.name.dq()}))", - "Union" to unionSymbol, - *codegenScope, - ) + if (codegenTarget.renderUnknownVariant()) { + rustTemplate( + "#{Union}::${UnionGenerator.UNKNOWN_VARIANT_NAME} => return Err(#{Error}::unknown_variant(${unionSymbol.name.dq()}))", + "Union" to unionSymbol, + *codegenScope, + ) + } } + rust("Ok(())") } - rust("Ok(())") } - } rust("#T(${ctx.input}, ${ctx.elementWriter})?", structureSerializer) } private fun RustWriter.serializeList( - listShape: CollectionShape, - ctx: Ctx.Scope, + listShape: CollectionShape, + ctx: Ctx.Scope, ) { val itemName = safeName("list_item") rustBlock("for $itemName in ${ctx.input}") { @@ -502,20 +539,24 @@ class XmlBindingTraitSerializerGenerator( } private fun RustWriter.serializeFlatList( - member: MemberShape, - listShape: CollectionShape, - ctx: Ctx.Scope, + member: MemberShape, + listShape: CollectionShape, + ctx: Ctx.Scope, ) { val itemName = safeName("list_item") rustBlock("for $itemName in ${ctx.input}") { - serializeMember(listShape.member, ctx.copy(input = itemName), xmlIndex.memberName(member)) + serializeMember( + listShape.member, + ctx.copy(input = itemName), + xmlIndex.memberName(member) + ) } } private fun RustWriter.serializeMap( - mapShape: MapShape, - entryName: String, - ctx: Ctx.Scope, + mapShape: MapShape, + entryName: String, + ctx: Ctx.Scope, ) { val key = safeName("key") val value = safeName("value") @@ -534,57 +575,56 @@ class XmlBindingTraitSerializerGenerator( * } * ``` * - * If [member] is not an optional shape, generate code like: - * `{ .. BLOCK }` + * If [member] is not an optional shape, generate code like: `{ .. BLOCK }` * - * [inner] is passed a new `ctx` object to use for code generation which handles the - * potentially new name of the input. + * [inner] is passed a new `ctx` object to use for code generation which handles the potentially + * new name of the input. */ private fun RustWriter.handleOptional( - member: MemberShape, - ctx: T, - inner: RustWriter.(T) -> Unit, + member: MemberShape, + ctx: T, + inner: RustWriter.(T) -> Unit, ) { val memberSymbol = symbolProvider.toSymbol(member) if (memberSymbol.isOptional()) { val tmp = safeName() val target = model.expectShape(member.target) val pattern = - if (target.isStructureShape && target.members().isEmpty()) { - // In this case, we mark a variable captured in the if-let - // expression as unused to prevent the warning coming - // from the following code generated by handleOptional: - // if let Some(var_2) = &input.input { - // scope.start_el("input").finish(); - // } - // where var_2 above is unused. - "Some(_$tmp)" - } else { - "Some($tmp)" - } - rustBlock("if let $pattern = ${ctx.input}") { - inner(Ctx.updateInput(ctx, tmp)) - } + if (target.isStructureShape && target.members().isEmpty()) { + // In this case, we mark a variable captured in the if-let + // expression as unused to prevent the warning coming + // from the following code generated by handleOptional: + // if let Some(var_2) = &input.input { + // scope.start_el("input").finish(); + // } + // where var_2 above is unused. + "Some(_$tmp)" + } else { + "Some($tmp)" + } + rustBlock("if let $pattern = ${ctx.input}") { inner(Ctx.updateInput(ctx, tmp)) } } else { with(util) { val valueExpression = - if (ctx.input.startsWith("&")) { - ValueExpression.Reference(ctx.input) - } else { - ValueExpression.Value(ctx.input) - } - ignoreDefaultsForNumbersAndBools(member, valueExpression) { - inner(ctx) - } + if (ctx.input.startsWith("&")) { + ValueExpression.Reference(ctx.input) + } else { + ValueExpression.Value(ctx.input) + } + ignoreDefaultsForNumbersAndBools(member, valueExpression) { inner(ctx) } } } } private fun OperationShape.requestBodyMembers(): XmlMemberIndex = - XmlMemberIndex.fromMembers(httpBindingResolver.requestMembers(this, HttpLocation.DOCUMENT)) + XmlMemberIndex.fromMembers( + httpBindingResolver.requestMembers(this, HttpLocation.DOCUMENT) + ) private fun OperationShape.responseBodyMembers(): XmlMemberIndex = - XmlMemberIndex.fromMembers(httpBindingResolver.responseMembers(this, HttpLocation.DOCUMENT)) + XmlMemberIndex.fromMembers( + httpBindingResolver.responseMembers(this, HttpLocation.DOCUMENT) + ) private fun Shape.xmlNamespace(root: Boolean): XmlNamespaceTrait? { return this.getTrait().letIf(root) { it ?: rootNamespace } From 29e541e4be96ad568e9a857ba7257f6d5155e637 Mon Sep 17 00:00:00 2001 From: vcjana Date: Tue, 7 Oct 2025 15:12:03 -0700 Subject: [PATCH 5/7] Apply ktlint formatting and bump codegenVersion to 0.1.4 --- .../client/smithy/protocols/RestJsonTest.kt | 4 +- .../XmlBindingTraitSerializerGenerator.kt | 453 +++++++++--------- 2 files changed, 229 insertions(+), 228 deletions(-) diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt index 91770a8ed35..07b050bef00 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt @@ -11,7 +11,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel internal class RestJsonTest { val model = - """ + """ namespace test use aws.protocols#restJson1 use aws.api#service @@ -42,7 +42,7 @@ internal class RestJsonTest { """.asSmithyModel() private val modelWithEmptyStruct = - """ + """ namespace test use aws.protocols#restJson1 use aws.api#service 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 c15dc5d6d1c..2cfd7674a85 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 @@ -54,8 +54,8 @@ import software.amazon.smithy.rust.codegen.core.util.letIf import software.amazon.smithy.rust.codegen.core.util.outputShape class XmlBindingTraitSerializerGenerator( - codegenContext: CodegenContext, - private val httpBindingResolver: HttpBindingResolver, + codegenContext: CodegenContext, + private val httpBindingResolver: HttpBindingResolver, ) : StructuredDataSerializerGenerator { private val symbolProvider = codegenContext.symbolProvider private val runtimeConfig = codegenContext.runtimeConfig @@ -63,15 +63,15 @@ class XmlBindingTraitSerializerGenerator( private val codegenTarget = codegenContext.target private val protocolFunctions = ProtocolFunctions(codegenContext) private val codegenScope = - arrayOf( - "XmlWriter" to - RuntimeType.smithyXml(runtimeConfig).resolve("encode::XmlWriter"), - "ElementWriter" to - RuntimeType.smithyXml(runtimeConfig).resolve("encode::ElWriter"), - "SdkBody" to RuntimeType.sdkBody(runtimeConfig), - "Error" to runtimeConfig.serializationError(), - *RuntimeType.preludeScope, - ) + arrayOf( + "XmlWriter" to + RuntimeType.smithyXml(runtimeConfig).resolve("encode::XmlWriter"), + "ElementWriter" to + RuntimeType.smithyXml(runtimeConfig).resolve("encode::ElWriter"), + "SdkBody" to RuntimeType.sdkBody(runtimeConfig), + "Error" to runtimeConfig.serializationError(), + *RuntimeType.preludeScope, + ) private val xmlIndex = XmlNameIndex.of(model) private val rootNamespace = codegenContext.serviceShape.getTrait() @@ -88,22 +88,22 @@ class XmlBindingTraitSerializerGenerator( // Kotlin doesn't have a "This" type @Suppress("UNCHECKED_CAST") fun updateInput( - input: T, - newInput: String, + input: T, + newInput: String, ): T = - when (input) { - is Element -> input.copy(input = newInput) as T - is Scope -> input.copy(input = newInput) as T - else -> TODO() - } + when (input) { + is Element -> input.copy(input = newInput) as T + is Scope -> input.copy(input = newInput) as T + else -> TODO() + } } } private fun Ctx.Element.scopedTo(member: MemberShape) = - this.copy(input = "$input.${symbolProvider.toMemberName(member)}") + this.copy(input = "$input.${symbolProvider.toMemberName(member)}") private fun Ctx.Scope.scopedTo(member: MemberShape) = - this.copy(input = "$input.${symbolProvider.toMemberName(member)}") + this.copy(input = "$input.${symbolProvider.toMemberName(member)}") override fun operationInputSerializer(operationShape: OperationShape): RuntimeType? { val inputShape = operationShape.inputShape(model) @@ -112,13 +112,13 @@ class XmlBindingTraitSerializerGenerator( return null } val operationXmlName = - xmlIndex.operationInputShapeName(operationShape) - ?: throw CodegenException("operation must have a name if it has members") + xmlIndex.operationInputShapeName(operationShape) + ?: throw CodegenException("operation must have a name if it has members") return protocolFunctions.serializeFn(operationShape, fnNameSuffix = "op_input") { fnName -> rustBlockTemplate( - "pub fn $fnName(input: &#{target}) -> #{Result}<#{SdkBody}, #{Error}>", - *codegenScope, - "target" to symbolProvider.toSymbol(inputShape), + "pub fn $fnName(input: &#{target}) -> #{Result}<#{SdkBody}, #{Error}>", + *codegenScope, + "target" to symbolProvider.toSymbol(inputShape), ) { rust("let mut out = String::new();") // Create a scope for writer. This ensures that: @@ -126,20 +126,20 @@ class XmlBindingTraitSerializerGenerator( // - All closing tags get written rustBlock("") { rustTemplate( - """ + """ let mut writer = #{XmlWriter}::new(&mut out); ##[allow(unused_mut)] let mut root = writer.start_el(${operationXmlName.dq()})${ inputShape.xmlNamespace(root = true).apply() }; """, - *codegenScope, + *codegenScope, ) serializeStructure( - inputShape, - xmlMembers, - Ctx.Element("root", "input"), - fnNameSuffix = "input" + inputShape, + xmlMembers, + Ctx.Element("root", "input"), + fnNameSuffix = "input", ) } rustTemplate("Ok(#{SdkBody}::from(out))", *codegenScope) @@ -155,14 +155,14 @@ class XmlBindingTraitSerializerGenerator( val target = model.expectShape(member.target) return protocolFunctions.serializeFn(member, fnNameSuffix = "payload") { fnName -> val t = - symbolProvider - .toSymbol(member) - .rustType() - .stripOuter() - .render(true) + symbolProvider + .toSymbol(member) + .rustType() + .stripOuter() + .render(true) rustBlockTemplate( - "pub fn $fnName(input: &$t) -> std::result::Result, #{Error}>", - *codegenScope, + "pub fn $fnName(input: &$t) -> std::result::Result, #{Error}>", + *codegenScope, ) { rust("let mut out = String::new();") // Create a scope for writer. This ensures that: @@ -170,27 +170,27 @@ class XmlBindingTraitSerializerGenerator( // - All closing tags get written rustBlock("") { rustTemplate( - """ + """ let mut writer = #{XmlWriter}::new(&mut out); ##[allow(unused_mut)] let mut root = writer.start_el(${xmlIndex.payloadShapeName(member).dq()})${ target.xmlNamespace(root = true).apply() }; """, - *codegenScope, + *codegenScope, ) when (target) { is StructureShape -> - serializeStructure( - target, - XmlMemberIndex.fromMembers(target.members().toList()), - Ctx.Element("root", "input"), - ) + serializeStructure( + target, + XmlMemberIndex.fromMembers(target.members().toList()), + Ctx.Element("root", "input"), + ) is UnionShape -> serializeUnion(target, Ctx.Element("root", "input")) else -> - throw IllegalStateException( - "xml payloadSerializer only supports structs and unions" - ) + throw IllegalStateException( + "xml payloadSerializer only supports structs and unions", + ) } } rustTemplate("Ok(out.into_bytes())", *codegenScope) @@ -199,25 +199,25 @@ class XmlBindingTraitSerializerGenerator( } override fun unsetStructure(structure: StructureShape): RuntimeType = - ProtocolFunctions.crossOperationFn("rest_xml_unset_struct_payload") { fnName -> - rustTemplate( - """ + ProtocolFunctions.crossOperationFn("rest_xml_unset_struct_payload") { fnName -> + rustTemplate( + """ pub fn $fnName() -> #{ByteSlab} { Vec::new() } """, - "ByteSlab" to RuntimeType.ByteSlab, - ) - } + "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() }", - *codegenScope, - "ByteSlab" to RuntimeType.ByteSlab, - ) - } + ProtocolFunctions.crossOperationFn("rest_xml_unset_union_payload") { fnName -> + rustTemplate( + "pub fn $fnName() -> #{ByteSlab} { #{Vec}::new() }", + *codegenScope, + "ByteSlab" to RuntimeType.ByteSlab, + ) + } override fun operationOutputSerializer(operationShape: OperationShape): RuntimeType? { val outputShape = operationShape.outputShape(model) @@ -226,13 +226,13 @@ class XmlBindingTraitSerializerGenerator( return null } val operationXmlName = - xmlIndex.operationOutputShapeName(operationShape) - ?: throw CodegenException("operation must have a name if it has members") + xmlIndex.operationOutputShapeName(operationShape) + ?: throw CodegenException("operation must have a name if it has members") return protocolFunctions.serializeFn(operationShape, fnNameSuffix = "output") { fnName -> rustBlockTemplate( - "pub fn $fnName(output: &#{target}) -> #{Result}", - *codegenScope, - "target" to symbolProvider.toSymbol(outputShape), + "pub fn $fnName(output: &#{target}) -> #{Result}", + *codegenScope, + "target" to symbolProvider.toSymbol(outputShape), ) { rust("let mut out = String::new();") // Create a scope for writer. This ensures that: @@ -240,14 +240,14 @@ class XmlBindingTraitSerializerGenerator( // - All closing tags get written rustBlock("") { rustTemplate( - """ + """ let mut writer = #{XmlWriter}::new(&mut out); ##[allow(unused_mut)] let mut root = writer.start_el(${operationXmlName.dq()})${ outputShape.xmlNamespace(root = true).apply() }; """, - *codegenScope, + *codegenScope, ) serializeStructure(outputShape, xmlMembers, Ctx.Element("root", "output")) } @@ -259,15 +259,15 @@ class XmlBindingTraitSerializerGenerator( override fun serverErrorSerializer(shape: ShapeId): RuntimeType { val errorShape = model.expectShape(shape, StructureShape::class.java) val xmlMembers = - httpBindingResolver - .errorResponseBindings(shape) - .filter { it.location == HttpLocation.DOCUMENT } - .map { it.member } + httpBindingResolver + .errorResponseBindings(shape) + .filter { it.location == HttpLocation.DOCUMENT } + .map { it.member } return protocolFunctions.serializeFn(errorShape, fnNameSuffix = "error") { fnName -> rustBlockTemplate( - "pub fn $fnName(error: &#{target}) -> #{Result}", - *codegenScope, - "target" to symbolProvider.toSymbol(errorShape), + "pub fn $fnName(error: &#{target}) -> #{Result}", + *codegenScope, + "target" to symbolProvider.toSymbol(errorShape), ) { rust("let mut out = String::new();") // Create a scope for writer. This ensures that: @@ -275,17 +275,17 @@ class XmlBindingTraitSerializerGenerator( // - All closing tags get written rustBlock("") { rustTemplate( - """ + """ let mut writer = #{XmlWriter}::new(&mut out); ##[allow(unused_mut)] let mut root = writer.start_el("Error")${errorShape.xmlNamespace(root = true).apply()}; """, - *codegenScope, + *codegenScope, ) serializeStructure( - errorShape, - XmlMemberIndex.fromMembers(xmlMembers), - Ctx.Element("root", "error"), + errorShape, + XmlMemberIndex.fromMembers(xmlMembers), + Ctx.Element("root", "error"), ) } rustTemplate("Ok(out)", *codegenScope) @@ -300,8 +300,8 @@ class XmlBindingTraitSerializerGenerator( } private fun RustWriter.structureInner( - members: XmlMemberIndex, - ctx: Ctx.Element, + members: XmlMemberIndex, + ctx: Ctx.Element, ) { if (members.attributeMembers.isNotEmpty()) { rust("let mut ${ctx.elementWriter} = ${ctx.elementWriter};") @@ -309,8 +309,8 @@ class XmlBindingTraitSerializerGenerator( members.attributeMembers.forEach { member -> handleOptional(member, ctx.scopedTo(member)) { ctx -> withBlock( - "${ctx.elementWriter}.write_attribute(${xmlIndex.memberName(member).dq()},", - ");" + "${ctx.elementWriter}.write_attribute(${xmlIndex.memberName(member).dq()},", + ");", ) { serializeRawMember(member, ctx.input) } } } @@ -324,8 +324,8 @@ class XmlBindingTraitSerializerGenerator( } private fun RustWriter.serializeRawMember( - member: MemberShape, - input: String, + member: MemberShape, + input: String, ) { when (model.expectShape(member.target)) { is StringShape -> { @@ -333,35 +333,35 @@ class XmlBindingTraitSerializerGenerator( // it does so because // it's preceded by the `&` operator, calling `as_str()` on it will upset Clippy. val dereferenced = - if (input.startsWith("&")) { - autoDeref(input) - } else { - input - } + if (input.startsWith("&")) { + autoDeref(input) + } else { + input + } rust("$dereferenced.as_str()") } is BooleanShape, is NumberShape -> { rust( - "#T::from(${autoDeref(input)}).encode()", - RuntimeType.smithyTypes(runtimeConfig).resolve("primitive::Encoder"), + "#T::from(${autoDeref(input)}).encode()", + RuntimeType.smithyTypes(runtimeConfig).resolve("primitive::Encoder"), ) } is BlobShape -> - rust("#T($input.as_ref()).as_ref()", RuntimeType.base64Encode(runtimeConfig)) + rust("#T($input.as_ref()).as_ref()", RuntimeType.base64Encode(runtimeConfig)) is TimestampShape -> { val timestampFormat = - httpBindingResolver.timestampFormat( - member, - HttpLocation.DOCUMENT, - TimestampFormatTrait.Format.DATE_TIME, - model, - ) + httpBindingResolver.timestampFormat( + member, + HttpLocation.DOCUMENT, + TimestampFormatTrait.Format.DATE_TIME, + model, + ) val timestampFormatType = - RuntimeType.parseTimestampFormat( - codegenTarget, - runtimeConfig, - timestampFormat - ) + RuntimeType.parseTimestampFormat( + codegenTarget, + runtimeConfig, + timestampFormat, + ) rust("$input.fmt(#T)?.as_ref()", timestampFormatType) } else -> TODO(member.toString()) @@ -370,9 +370,9 @@ class XmlBindingTraitSerializerGenerator( @Suppress("NAME_SHADOWING") private fun RustWriter.serializeMember( - memberShape: MemberShape, - ctx: Ctx.Scope, - rootNameOverride: String? = null, + memberShape: MemberShape, + ctx: Ctx.Scope, + rootNameOverride: String? = null, ) { val target = model.expectShape(memberShape.target) val xmlName = rootNameOverride ?: xmlIndex.memberName(memberShape) @@ -383,32 +383,33 @@ class XmlBindingTraitSerializerGenerator( is BooleanShape, is NumberShape, is TimestampShape, - is BlobShape -> { + is BlobShape, + -> { rust( - "let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();" + "let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();", ) withBlock("inner_writer.data(", ");") { serializeRawMember(memberShape, ctx.input) } } is CollectionShape -> - if (memberShape.hasTrait()) { - serializeFlatList(memberShape, target, ctx) - } else { - rust( - "let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();" - ) - serializeList(target, Ctx.Scope("inner_writer", ctx.input)) - } + if (memberShape.hasTrait()) { + serializeFlatList(memberShape, target, ctx) + } else { + rust( + "let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();", + ) + serializeList(target, Ctx.Scope("inner_writer", ctx.input)) + } is MapShape -> - if (memberShape.hasTrait()) { - serializeMap(target, xmlIndex.memberName(memberShape), ctx) - } else { - rust( - "let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();" - ) - serializeMap(target, "entry", Ctx.Scope("inner_writer", ctx.input)) - } + if (memberShape.hasTrait()) { + serializeMap(target, xmlIndex.memberName(memberShape), ctx) + } else { + rust( + "let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();", + ) + serializeMap(target, "entry", Ctx.Scope("inner_writer", ctx.input)) + } is StructureShape -> { // We call serializeStructure only when target.members() is nonempty. // If it were empty, serializeStructure would generate the following code: @@ -431,9 +432,9 @@ class XmlBindingTraitSerializerGenerator( } else { rust("let inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns;") serializeStructure( - target, - XmlMemberIndex.fromMembers(target.members().toList()), - Ctx.Element("inner_writer", ctx.input), + target, + XmlMemberIndex.fromMembers(target.members().toList()), + Ctx.Element("inner_writer", ctx.input), ) } } @@ -447,90 +448,90 @@ class XmlBindingTraitSerializerGenerator( } private fun RustWriter.serializeStructure( - structureShape: StructureShape, - members: XmlMemberIndex, - ctx: Ctx.Element, - fnNameSuffix: String? = null, + structureShape: StructureShape, + members: XmlMemberIndex, + ctx: Ctx.Element, + fnNameSuffix: String? = null, ) { val structureSymbol = symbolProvider.toSymbol(structureShape) val structureSerializer = - protocolFunctions.serializeFn(structureShape, fnNameSuffix = fnNameSuffix) { fnName - -> - rustBlockTemplate( - "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter}) -> #{Result}<(), #{Error}>", - "Input" to structureSymbol, - *codegenScope, - ) { - if (!members.isNotEmpty()) { - // removed unused warning if there are no fields we're going to read - rust("let _ = input;") - } - structureInner(members, Ctx.Element("writer", "&input")) - rust("Ok(())") + protocolFunctions.serializeFn(structureShape, fnNameSuffix = fnNameSuffix) { fnName, + -> + rustBlockTemplate( + "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter}) -> #{Result}<(), #{Error}>", + "Input" to structureSymbol, + *codegenScope, + ) { + if (!members.isNotEmpty()) { + // removed unused warning if there are no fields we're going to read + rust("let _ = input;") } + structureInner(members, Ctx.Element("writer", "&input")) + rust("Ok(())") } + } rust("#T(${ctx.input}, ${ctx.elementWriter})?", structureSerializer) } private fun RustWriter.serializeUnion( - unionShape: UnionShape, - ctx: Ctx.Element, + unionShape: UnionShape, + ctx: Ctx.Element, ) { val unionSymbol = symbolProvider.toSymbol(unionShape) val structureSerializer = - protocolFunctions.serializeFn(unionShape) { fnName -> - rustBlockTemplate( - "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter}) -> #{Result}<(), #{Error}>", - "Input" to unionSymbol, - *codegenScope, - ) { - rust("let mut scope_writer = writer.finish();") - rustBlock("match input") { - val members = unionShape.members() - - members.forEach { member -> - val memberShape = model.expectShape(member.target) - val memberName = symbolProvider.toMemberName(member) - val isEmptyStruct = - memberShape.isStructureShape && - memberShape - .asStructureShape() - .get() - .allMembers - .isEmpty() - val variantName = - if (member.isTargetUnit()) { - "$memberName" - } else if (isEmptyStruct) { - // Unit structs don't serialize inner, so it is never - // accessed - "$memberName(_inner)" - } else { - "$memberName(inner)" - } - val innerVarName = if (isEmptyStruct) "_inner" else "inner" - withBlock("#T::$variantName =>", ",", unionSymbol) { - serializeMember(member, Ctx.Scope("scope_writer", innerVarName)) + protocolFunctions.serializeFn(unionShape) { fnName -> + rustBlockTemplate( + "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter}) -> #{Result}<(), #{Error}>", + "Input" to unionSymbol, + *codegenScope, + ) { + rust("let mut scope_writer = writer.finish();") + rustBlock("match input") { + val members = unionShape.members() + + members.forEach { member -> + val memberShape = model.expectShape(member.target) + val memberName = symbolProvider.toMemberName(member) + val isEmptyStruct = + memberShape.isStructureShape && + memberShape + .asStructureShape() + .get() + .allMembers + .isEmpty() + val variantName = + if (member.isTargetUnit()) { + "$memberName" + } else if (isEmptyStruct) { + // Unit structs don't serialize inner, so it is never + // accessed + "$memberName(_inner)" + } else { + "$memberName(inner)" } + val innerVarName = if (isEmptyStruct) "_inner" else "inner" + withBlock("#T::$variantName =>", ",", unionSymbol) { + serializeMember(member, Ctx.Scope("scope_writer", innerVarName)) } + } - if (codegenTarget.renderUnknownVariant()) { - rustTemplate( - "#{Union}::${UnionGenerator.UNKNOWN_VARIANT_NAME} => return Err(#{Error}::unknown_variant(${unionSymbol.name.dq()}))", - "Union" to unionSymbol, - *codegenScope, - ) - } + if (codegenTarget.renderUnknownVariant()) { + rustTemplate( + "#{Union}::${UnionGenerator.UNKNOWN_VARIANT_NAME} => return Err(#{Error}::unknown_variant(${unionSymbol.name.dq()}))", + "Union" to unionSymbol, + *codegenScope, + ) } - rust("Ok(())") } + rust("Ok(())") } + } rust("#T(${ctx.input}, ${ctx.elementWriter})?", structureSerializer) } private fun RustWriter.serializeList( - listShape: CollectionShape, - ctx: Ctx.Scope, + listShape: CollectionShape, + ctx: Ctx.Scope, ) { val itemName = safeName("list_item") rustBlock("for $itemName in ${ctx.input}") { @@ -539,24 +540,24 @@ class XmlBindingTraitSerializerGenerator( } private fun RustWriter.serializeFlatList( - member: MemberShape, - listShape: CollectionShape, - ctx: Ctx.Scope, + member: MemberShape, + listShape: CollectionShape, + ctx: Ctx.Scope, ) { val itemName = safeName("list_item") rustBlock("for $itemName in ${ctx.input}") { serializeMember( - listShape.member, - ctx.copy(input = itemName), - xmlIndex.memberName(member) + listShape.member, + ctx.copy(input = itemName), + xmlIndex.memberName(member), ) } } private fun RustWriter.serializeMap( - mapShape: MapShape, - entryName: String, - ctx: Ctx.Scope, + mapShape: MapShape, + entryName: String, + ctx: Ctx.Scope, ) { val key = safeName("key") val value = safeName("value") @@ -581,50 +582,50 @@ class XmlBindingTraitSerializerGenerator( * new name of the input. */ private fun RustWriter.handleOptional( - member: MemberShape, - ctx: T, - inner: RustWriter.(T) -> Unit, + member: MemberShape, + ctx: T, + inner: RustWriter.(T) -> Unit, ) { val memberSymbol = symbolProvider.toSymbol(member) if (memberSymbol.isOptional()) { val tmp = safeName() val target = model.expectShape(member.target) val pattern = - if (target.isStructureShape && target.members().isEmpty()) { - // In this case, we mark a variable captured in the if-let - // expression as unused to prevent the warning coming - // from the following code generated by handleOptional: - // if let Some(var_2) = &input.input { - // scope.start_el("input").finish(); - // } - // where var_2 above is unused. - "Some(_$tmp)" - } else { - "Some($tmp)" - } + if (target.isStructureShape && target.members().isEmpty()) { + // In this case, we mark a variable captured in the if-let + // expression as unused to prevent the warning coming + // from the following code generated by handleOptional: + // if let Some(var_2) = &input.input { + // scope.start_el("input").finish(); + // } + // where var_2 above is unused. + "Some(_$tmp)" + } else { + "Some($tmp)" + } rustBlock("if let $pattern = ${ctx.input}") { inner(Ctx.updateInput(ctx, tmp)) } } else { with(util) { val valueExpression = - if (ctx.input.startsWith("&")) { - ValueExpression.Reference(ctx.input) - } else { - ValueExpression.Value(ctx.input) - } + if (ctx.input.startsWith("&")) { + ValueExpression.Reference(ctx.input) + } else { + ValueExpression.Value(ctx.input) + } ignoreDefaultsForNumbersAndBools(member, valueExpression) { inner(ctx) } } } } private fun OperationShape.requestBodyMembers(): XmlMemberIndex = - XmlMemberIndex.fromMembers( - httpBindingResolver.requestMembers(this, HttpLocation.DOCUMENT) - ) + XmlMemberIndex.fromMembers( + httpBindingResolver.requestMembers(this, HttpLocation.DOCUMENT), + ) private fun OperationShape.responseBodyMembers(): XmlMemberIndex = - XmlMemberIndex.fromMembers( - httpBindingResolver.responseMembers(this, HttpLocation.DOCUMENT) - ) + XmlMemberIndex.fromMembers( + httpBindingResolver.responseMembers(this, HttpLocation.DOCUMENT), + ) private fun Shape.xmlNamespace(root: Boolean): XmlNamespaceTrait? { return this.getTrait().letIf(root) { it ?: rootNamespace } From 8103a6c87207e6e721a1e34802cf5498e96a2ae5 Mon Sep 17 00:00:00 2001 From: vcjana Date: Wed, 8 Oct 2025 14:52:09 -0700 Subject: [PATCH 6/7] Preserves original test models and adding new tests for empty struct variants --- .../client/smithy/protocols/AwsQueryTest.kt | 24 +++---- .../client/smithy/protocols/RestJsonTest.kt | 32 +++++---- .../client/smithy/protocols/RestXmlTest.kt | 66 +++++++++++++++---- 3 files changed, 76 insertions(+), 46 deletions(-) diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt index 9dfb342fd0b..10c9146722f 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt @@ -18,25 +18,19 @@ class AwsQueryTest { @awsQuery @xmlNamespace(uri: "https://example.com/") service TestService { - version: "1.0", - operations: [TestOperation] - } - - union ObjectEncryptionFilter { - sses3: SSES3Filter, - } - - structure SSES3Filter { - // Empty structure - no members + version: "2019-12-16", + operations: [SomeOperation] } - @input - structure TestInput { - filter: ObjectEncryptionFilter + operation SomeOperation { + input: SomeOperationInputOutput, + output: SomeOperationInputOutput, } - operation TestOperation { - input: TestInput, + structure SomeOperationInputOutput { + payload: String, + a: String, + b: Integer } """.asSmithyModel() diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt index 07b050bef00..1b8a7e6462f 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt @@ -15,29 +15,27 @@ internal class RestJsonTest { namespace test use aws.protocols#restJson1 use aws.api#service + use smithy.test#httpRequestTests + use smithy.test#httpResponseTests + /// A REST JSON service that sends JSON requests and responses. + @service(sdkId: "Rest Json Protocol") @restJson1 - service TestService { - version: "1.0", - operations: [TestOperation] - } - - union ObjectEncryptionFilter { - sses3: SSES3Filter, - } - - structure SSES3Filter { - // Empty structure - no members + service RestJsonExtras { + version: "2019-12-16", + operations: [StringPayload] } - @input - structure TestInput { - filter: ObjectEncryptionFilter + @http(uri: "/StringPayload", method: "POST") + operation StringPayload { + input: StringPayloadInput, + output: StringPayloadInput } - @http(uri: "/test", method: "POST") - operation TestOperation { - input: TestInput, + structure StringPayloadInput { + payload: String, + a: String, + b: Integer } """.asSmithyModel() diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt index 11ffba5ac07..91679ede2b1 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt @@ -20,28 +20,67 @@ internal class RestXmlTest { /// A REST XML service that sends XML requests and responses. @service(sdkId: "Rest XML UT") @restXml - service TestService { - version: "1.0", - operations: [TestOperation] + service RestXmlExtras { + version: "2019-12-16", + operations: [Op] } - union ObjectEncryptionFilter { - sses3: SSES3Filter, + + @http(uri: "/top", method: "POST") + operation Op { + input: Top, + output: Top } + union Choice { + @xmlFlattened + @xmlName("Hi") + flatMap: MyMap, + + deepMap: MyMap, + + @xmlFlattened + flatList: SomeList, + + deepList: SomeList, + + s: String, - structure SSES3Filter { - // Empty structure - no members + enum: FooEnum, + + date: Timestamp, + + number: Double, + + top: Top, + + blob: Blob } - @input - structure TestInput { - filter: ObjectEncryptionFilter + @enum([{name: "FOO", value: "FOO"}]) + string FooEnum + + map MyMap { + @xmlName("Name") + key: String, + + @xmlName("Setting") + value: Choice, } - @http(uri: "/test", method: "POST") - operation TestOperation { - input: TestInput, + list SomeList { + member: Choice + } + + structure Top { + choice: Choice, + + @xmlAttribute + extra: Long, + + @xmlName("prefix:local") + renamedWithPrefix: String } + """.asSmithyModel() private val modelWithEmptyStruct = @@ -74,7 +113,6 @@ internal class RestXmlTest { union TestUnion { // Empty struct - should generate _inner to avoid unused variable warning emptyStruct: EmptyStruct, - // Normal struct - should generate inner (without underscore) normalStruct: NormalStruct } From 74b95a7eeb27f47526d22bec6bd39eff5a0674f0 Mon Sep 17 00:00:00 2001 From: vcjana Date: Thu, 9 Oct 2025 12:25:43 -0700 Subject: [PATCH 7/7] Simplify test model names - Rename modelWithEmptyStruct to inputUnionWithEmptyStructure - Remove output parameter and TestOutput structure - Focus tests on input serialization only --- .../codegen/client/smithy/protocols/AwsQueryTest.kt | 11 +++-------- .../codegen/client/smithy/protocols/RestJsonTest.kt | 11 +++-------- .../codegen/client/smithy/protocols/RestXmlTest.kt | 11 +++-------- 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt index 10c9146722f..31941ffd377 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryTest.kt @@ -34,7 +34,7 @@ class AwsQueryTest { } """.asSmithyModel() - private val modelWithEmptyStruct = + private val inputUnionWithEmptyStructure = """ namespace test use aws.protocols#awsQuery @@ -47,18 +47,13 @@ class AwsQueryTest { } operation TestOp { - input: TestInput, - output: TestOutput + input: TestInput } structure TestInput { testUnion: TestUnion } - structure TestOutput { - testUnion: TestUnion - } - union TestUnion { // Empty struct - should generate _inner to avoid unused variable warning emptyStruct: EmptyStruct, @@ -83,6 +78,6 @@ class AwsQueryTest { fun `union with empty struct generates warning-free code`() { // This test will fail with unused variable warnings if the fix is not applied // clientIntegrationTest enforces -D warnings via codegenIntegrationTest - clientIntegrationTest(modelWithEmptyStruct) { _, _ -> } + clientIntegrationTest(inputUnionWithEmptyStructure) { _, _ -> } } } diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt index 1b8a7e6462f..6bd6df229d4 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestJsonTest.kt @@ -39,7 +39,7 @@ internal class RestJsonTest { } """.asSmithyModel() - private val modelWithEmptyStruct = + private val inputUnionWithEmptyStructure = """ namespace test use aws.protocols#restJson1 @@ -54,18 +54,13 @@ internal class RestJsonTest { @http(uri: "/test", method: "POST") operation TestOp { - input: TestInput, - output: TestOutput + input: TestInput } structure TestInput { testUnion: TestUnion } - structure TestOutput { - testUnion: TestUnion - } - union TestUnion { // Empty struct - RestJson ALWAYS uses inner variable, no warning emptyStruct: EmptyStruct, @@ -92,6 +87,6 @@ internal class RestJsonTest { // Unlike RestXml/AwsQuery, RestJson serializers always reference the inner variable // even for empty structs, so no underscore prefix is needed. // This test passes without any code changes, proving RestJson immunity. - clientIntegrationTest(modelWithEmptyStruct) { _, _ -> } + clientIntegrationTest(inputUnionWithEmptyStructure) { _, _ -> } } } diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt index 91679ede2b1..4ba4e16ca31 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/RestXmlTest.kt @@ -83,7 +83,7 @@ internal class RestXmlTest { """.asSmithyModel() - private val modelWithEmptyStruct = + private val inputUnionWithEmptyStructure = """ namespace test use aws.protocols#restXml @@ -98,18 +98,13 @@ internal class RestXmlTest { @http(uri: "/test", method: "POST") operation TestOp { - input: TestInput, - output: TestOutput + input: TestInput } structure TestInput { testUnion: TestUnion } - structure TestOutput { - testUnion: TestUnion - } - union TestUnion { // Empty struct - should generate _inner to avoid unused variable warning emptyStruct: EmptyStruct, @@ -133,6 +128,6 @@ internal class RestXmlTest { fun `union with empty struct generates warning-free code`() { // This test will fail with unused variable warnings if the fix is not applied // clientIntegrationTest enforces -D warnings via codegenIntegrationTest - clientIntegrationTest(modelWithEmptyStruct) { _, _ -> } + clientIntegrationTest(inputUnionWithEmptyStructure) { _, _ -> } } }