diff --git a/codegen/protocol-tests/model/error-correction-tests.smithy b/codegen/protocol-tests/model/error-correction-tests.smithy index 13ad29f92..74e614bdc 100644 --- a/codegen/protocol-tests/model/error-correction-tests.smithy +++ b/codegen/protocol-tests/model/error-correction-tests.smithy @@ -125,7 +125,7 @@ apply SayHello @httpResponseTests([ mapValue: {}, nestedListValue: [], document: null, - nested: { a: "" }, + nested: null, timestampValue: 0 }, code: 200, @@ -147,7 +147,7 @@ apply SayHelloXml @httpResponseTests([ listValue: [], mapValue: {}, nestedListValue: [], - nested: { a: "" }, + nested: null, timestampValue: 0 }, code: 200, diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/BoxServices.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/BoxServices.kt deleted file mode 100644 index f04e7faa5..000000000 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/BoxServices.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package aws.sdk.kotlin.codegen.customization - -import software.amazon.smithy.codegen.core.CodegenException -import software.amazon.smithy.kotlin.codegen.KotlinSettings -import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration -import software.amazon.smithy.kotlin.codegen.model.isNumberShape -import software.amazon.smithy.model.Model -import software.amazon.smithy.model.neighbor.Walker -import software.amazon.smithy.model.shapes.* -import software.amazon.smithy.model.transform.ModelTransformer -import software.amazon.smithy.utils.ToSmithyBuilder - -/** - * Integration that pre-processes the model to box all unboxed primitives. - * - * See: https://github.com/awslabs/aws-sdk-kotlin/issues/261 - * - * EC2 incorrectly models primitive shapes as unboxed when they actually - * need to be boxed for the API to work properly (e.g. sending default values). The - * rest of these services are at risk of similar behavior because they aren't true coral services - */ -class BoxServices : KotlinIntegration { - override val order: Byte = -127 - - private val serviceIds = listOf( - "com.amazonaws.ec2#AmazonEC2", - "com.amazonaws.nimble#nimble", - "com.amazonaws.amplifybackend#AmplifyBackend", - "com.amazonaws.apigatewaymanagementapi#ApiGatewayManagementApi", - "com.amazonaws.apigatewayv2#ApiGatewayV2", - "com.amazonaws.dataexchange#DataExchange", - "com.amazonaws.greengrass#Greengrass", - "com.amazonaws.iot1clickprojects#AWSIoT1ClickProjects", - "com.amazonaws.kafka#Kafka", - "com.amazonaws.macie2#Macie2", - "com.amazonaws.mediaconnect#MediaConnect", - "com.amazonaws.mediaconvert#MediaConvert", - "com.amazonaws.medialive#MediaLive", - "com.amazonaws.mediapackage#MediaPackage", - "com.amazonaws.mediapackagevod#MediaPackageVod", - "com.amazonaws.mediatailor#MediaTailor", - "com.amazonaws.pinpoint#Pinpoint", - "com.amazonaws.pinpointsmsvoice#PinpointSMSVoice", - "com.amazonaws.serverlessapplicationrepository#ServerlessApplicationRepository", - "com.amazonaws.mq#mq", - "com.amazonaws.schemas#schemas", - ).map(ShapeId::from) - - override fun enabledForService(model: Model, settings: KotlinSettings): Boolean = - serviceIds.any { it == settings.service } - - override fun preprocessModel(model: Model, settings: KotlinSettings): Model { - val serviceClosure = Walker(model).walkShapes(model.expectShape(settings.service)) - - return ModelTransformer.create().mapShapes(model) { - if (it in serviceClosure && !it.id.namespace.startsWith("smithy.api")) { - boxPrimitives(model, it) - } else { - it - } - } - } - - private fun boxPrimitives(model: Model, shape: Shape): Shape { - val target = when (shape) { - is MemberShape -> model.expectShape(shape.target) - else -> shape - } - - return when { - shape is MemberShape && target.isPrimitiveShape -> box(shape) - shape is NumberShape -> boxNumber(shape) - shape is BooleanShape -> box(shape) - else -> shape - } - } - - private val Shape.isPrimitiveShape: Boolean - get() = isBooleanShape || isNumberShape - - private fun box(shape: T): Shape where T : Shape, T : ToSmithyBuilder = - (shape.toBuilder() as AbstractShapeBuilder<*, T>) - .addTrait(@Suppress("DEPRECATION") software.amazon.smithy.model.traits.BoxTrait()) - .build() - - private fun boxNumber(shape: NumberShape): Shape = when (shape) { - is ByteShape -> box(shape) - is IntegerShape -> box(shape) - is LongShape -> box(shape) - is ShortShape -> box(shape) - is FloatShape -> box(shape) - is DoubleShape -> box(shape) - is BigDecimalShape -> box(shape) - is BigIntegerShape -> box(shape) - else -> throw CodegenException("unhandled numeric shape: $shape") - } -} diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/ec2/EC2MakePrimitivesOptional.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/ec2/EC2MakePrimitivesOptional.kt new file mode 100644 index 000000000..564f9e5ff --- /dev/null +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/ec2/EC2MakePrimitivesOptional.kt @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.codegen.customization.ec2 +import software.amazon.smithy.kotlin.codegen.KotlinSettings +import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.traits.ClientOptionalTrait +import software.amazon.smithy.model.transform.ModelTransformer + +/** + * EC2 incorrectly models primitive shapes as unboxed when they actually + * need to be boxed for the API to work properly (e.g. sending default values). + * This integration pre-processes the model to make all members optional. + * + * See: https://github.com/awslabs/aws-sdk-kotlin/issues/261 + */ +class EC2MakePrimitivesOptional : KotlinIntegration { + override val order: Byte = -127 + override fun enabledForService(model: Model, settings: KotlinSettings): Boolean = + settings.service == ShapeId.from("com.amazonaws.ec2#AmazonEC2") + + override fun preprocessModel(model: Model, settings: KotlinSettings): Model { + val updates = mutableListOf() + for (struct in model.structureShapes) { + for (member in struct.allMembers.values) { + updates.add(member.toBuilder().addTrait(ClientOptionalTrait()).build()) + } + } + return ModelTransformer.create().replaceShapes(model, updates) + } +} diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/polly/PollyPresigner.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/polly/PollyPresigner.kt index 00b7412f5..678a87e88 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/polly/PollyPresigner.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/polly/PollyPresigner.kt @@ -43,7 +43,7 @@ class PollyPresigner : KotlinIntegration { writer.write("unsignedRequest.method = #T.GET", RuntimeTypes.Http.HttpMethod) writer.withBlock("unsignedRequest.url.#T {", "}", RuntimeTypes.Core.Net.parameters) { val bindings = resolver.requestBindings(operation) - HttpStringValuesMapSerializer(ctx.model, ctx.symbolProvider, bindings, resolver, defaultTimestampFormat).render(writer) + HttpStringValuesMapSerializer(ctx.model, ctx.symbolProvider, ctx.settings, bindings, resolver, defaultTimestampFormat).render(writer) } // Remove the headers that were created by the default HTTP operation serializer diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/s3/S3Generator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/s3/S3Generator.kt index e14d0bc78..b71427ed4 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/s3/S3Generator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/s3/S3Generator.kt @@ -82,7 +82,7 @@ class S3Generator : RestXml() { .dedent() .withBlock("} catch (ex: Exception) {", "}") { withBlock("""throw #T("Failed to parse response as '${ctx.protocol.name}' error", ex).also {""", "}", exceptionBaseSymbol) { - write("#T(it, wrappedResponse, null)", setS3ErrorMetadata) + write("#T(it, wrappedCall.response, null)", setS3ErrorMetadata) } } .write("") diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/core/AwsHttpBindingProtocolGenerator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/core/AwsHttpBindingProtocolGenerator.kt index ac5b0e80e..0d428e7d1 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/core/AwsHttpBindingProtocolGenerator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/core/AwsHttpBindingProtocolGenerator.kt @@ -145,7 +145,7 @@ abstract class AwsHttpBindingProtocolGenerator : HttpBindingProtocolGenerator() .dedent() .withBlock("} catch (ex: Exception) {", "}") { withBlock("""throw #T("Failed to parse response as '${ctx.protocol.name}' error", ex).also {""", "}", exceptionBaseSymbol) { - write("#T(it, wrappedResponse, null)", RuntimeTypes.AwsProtocolCore.setAseErrorMetadata) + write("#T(it, wrappedCall.response, null)", RuntimeTypes.AwsProtocolCore.setAseErrorMetadata) } } .write("") diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration b/codegen/smithy-aws-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration index 7286d1ee3..1199832f4 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +++ b/codegen/smithy-aws-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration @@ -12,7 +12,6 @@ aws.sdk.kotlin.codegen.customization.apigateway.ApiGatewayAddAcceptHeader aws.sdk.kotlin.codegen.customization.glacier.GlacierAddVersionHeader aws.sdk.kotlin.codegen.customization.glacier.GlacierAccountIdDefault aws.sdk.kotlin.codegen.customization.polly.PollyPresigner -aws.sdk.kotlin.codegen.customization.BoxServices aws.sdk.kotlin.codegen.customization.glacier.GlacierBodyChecksum aws.sdk.kotlin.codegen.customization.machinelearning.MachineLearningEndpointCustomization aws.sdk.kotlin.codegen.customization.BackfillOptionalAuth @@ -30,4 +29,5 @@ aws.sdk.kotlin.codegen.customization.s3control.ClientConfigIntegration aws.sdk.kotlin.codegen.protocols.endpoints.BindAwsEndpointBuiltins aws.sdk.kotlin.codegen.customization.s3.HostPrefixRequestRouteFilter aws.sdk.kotlin.codegen.customization.s3.UnwrappedXmlOutputIntegration -aws.sdk.kotlin.codegen.customization.ClockSkew \ No newline at end of file +aws.sdk.kotlin.codegen.customization.ClockSkew +aws.sdk.kotlin.codegen.customization.ec2.EC2MakePrimitivesOptional diff --git a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/customization/BoxServicesTest.kt b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/customization/BoxServicesTest.kt deleted file mode 100644 index 43a1b1898..000000000 --- a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/customization/BoxServicesTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package aws.sdk.kotlin.codegen.customization - -import software.amazon.smithy.kotlin.codegen.model.expectShape -import software.amazon.smithy.kotlin.codegen.model.hasTrait -import software.amazon.smithy.kotlin.codegen.model.isNumberShape -import software.amazon.smithy.kotlin.codegen.test.newTestContext -import software.amazon.smithy.kotlin.codegen.test.prependNamespaceAndService -import software.amazon.smithy.kotlin.codegen.test.toSmithyModel -import software.amazon.smithy.model.shapes.StructureShape -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class BoxServicesTest { - @Test - fun testPrimitiveShapesAreBoxed() { - val model = """ - operation Foo { - input: Primitives - } - - structure Primitives { - int: PrimitiveInteger, - bool: PrimitiveBoolean, - long: PrimitiveLong, - double: PrimitiveDouble, - boxedAlready: BoxedField, - notBoxed: NotBoxedField, - other: Other - } - - @box - integer BoxedField - - structure Other {} - - integer NotBoxedField - """.prependNamespaceAndService(version = "1", operations = listOf("Foo")).toSmithyModel() - - val ctx = model.newTestContext() - val transformed = BoxServices().preprocessModel(model, ctx.generationCtx.settings) - - // get the synthetic input which is the one that will be transformed - val struct = transformed.expectShape("smithy.kotlin.synthetic.test#FooRequest") - struct.members().forEach { - val target = transformed.expectShape(it.target) - if (target.isBooleanShape || target.isNumberShape) { - assertTrue(it.hasTrait<@Suppress("DEPRECATION") software.amazon.smithy.model.traits.BoxTrait>()) - } else { - assertFalse(target.hasTrait<@Suppress("DEPRECATION") software.amazon.smithy.model.traits.BoxTrait>()) - assertFalse(it.hasTrait<@Suppress("DEPRECATION") software.amazon.smithy.model.traits.BoxTrait>()) - } - } - } -} diff --git a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/customization/ec2/EC2MakePrimitivesOptionalTest.kt b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/customization/ec2/EC2MakePrimitivesOptionalTest.kt new file mode 100644 index 000000000..d880549f0 --- /dev/null +++ b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/customization/ec2/EC2MakePrimitivesOptionalTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.codegen.customization.ec2 + +import org.junit.jupiter.api.Test +import software.amazon.smithy.kotlin.codegen.model.expectShape +import software.amazon.smithy.kotlin.codegen.model.isNullable +import software.amazon.smithy.kotlin.codegen.test.newTestContext +import software.amazon.smithy.kotlin.codegen.test.prependNamespaceAndService +import software.amazon.smithy.kotlin.codegen.test.toSmithyModel +import software.amazon.smithy.model.shapes.StructureShape +import kotlin.test.assertTrue + +class EC2MakePrimitivesOptionalTest { + @Test + fun testNullability() { + val model = """ + operation Foo { + input: FooInput + } + + structure FooInput { + @required + @default(0) + int: PrimitiveInteger, + @default(false) + bool: PrimitiveBoolean, + other: Other, + @default(1) + defaultInt: Integer + @required + requiredString: String + } + + structure Other {} + + """.prependNamespaceAndService(version = "2", operations = listOf("Foo")).toSmithyModel() + + val ctx = model.newTestContext() + val transformed = EC2MakePrimitivesOptional().preprocessModel(model, ctx.generationCtx.settings) + + // get the synthetic input which is the one that will be transformed + val struct = transformed.expectShape("smithy.kotlin.synthetic.test#FooRequest") + struct.members().forEach { + val memberSymbol = ctx.generationCtx.symbolProvider.toSymbol(it) + assertTrue(memberSymbol.isNullable) + } + } +}