Skip to content

Commit 5975308

Browse files
authored
fix!: box primitive shapes for services that require defaults sent (#287)
1 parent a7d79b5 commit 5975308

File tree

3 files changed

+165
-1
lines changed

3 files changed

+165
-1
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
package aws.sdk.kotlin.codegen.customization
7+
8+
import software.amazon.smithy.codegen.core.CodegenException
9+
import software.amazon.smithy.kotlin.codegen.KotlinSettings
10+
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
11+
import software.amazon.smithy.kotlin.codegen.model.isNumberShape
12+
import software.amazon.smithy.model.Model
13+
import software.amazon.smithy.model.neighbor.Walker
14+
import software.amazon.smithy.model.shapes.*
15+
import software.amazon.smithy.model.traits.BoxTrait
16+
import software.amazon.smithy.model.transform.ModelTransformer
17+
import software.amazon.smithy.utils.ToSmithyBuilder
18+
19+
/**
20+
* Integration that pre-processes the model to box all unboxed primitives.
21+
*
22+
* See: https://github.com/awslabs/aws-sdk-kotlin/issues/261
23+
*
24+
* EC2 incorrectly models primitive shapes as unboxed when they actually
25+
* need to be boxed for the API to work properly (e.g. sending default values). The
26+
* rest of these services are at risk of similar behavior because they aren't true coral services
27+
*/
28+
class BoxServices : KotlinIntegration {
29+
override val order: Byte = -127
30+
31+
private val serviceIds = listOf(
32+
"com.amazonaws.ec2#AmazonEC2",
33+
"com.amazonaws.nimble#nimble",
34+
"com.amazonaws.amplifybackend#AmplifyBackend",
35+
"com.amazonaws.apigatewaymanagementapi#ApiGatewayManagementApi",
36+
"com.amazonaws.apigatewayv2#ApiGatewayV2",
37+
"com.amazonaws.dataexchange#DataExchange",
38+
"com.amazonaws.greengrass#Greengrass",
39+
"com.amazonaws.iot1clickprojects#AWSIoT1ClickProjects",
40+
"com.amazonaws.kafka#Kafka",
41+
"com.amazonaws.macie2#Macie2",
42+
"com.amazonaws.mediaconnect#MediaConnect",
43+
"com.amazonaws.mediaconvert#MediaConvert",
44+
"com.amazonaws.medialive#MediaLive",
45+
"com.amazonaws.mediapackage#MediaPackage",
46+
"com.amazonaws.mediapackagevod#MediaPackageVod",
47+
"com.amazonaws.mediatailor#MediaTailor",
48+
"com.amazonaws.pinpoint#Pinpoint",
49+
"com.amazonaws.pinpointsmsvoice#PinpointSMSVoice",
50+
"com.amazonaws.serverlessapplicationrepository#ServerlessApplicationRepository",
51+
"com.amazonaws.mq#mq",
52+
"com.amazonaws.schemas#schemas",
53+
).map(ShapeId::from)
54+
55+
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean =
56+
serviceIds.any { it == settings.service }
57+
58+
override fun preprocessModel(model: Model, settings: KotlinSettings): Model {
59+
val serviceClosure = Walker(model).walkShapes(model.expectShape(settings.service))
60+
61+
return ModelTransformer.create().mapShapes(model) {
62+
if (it in serviceClosure && !it.id.namespace.startsWith("smithy.api")) {
63+
boxPrimitives(model, it)
64+
} else {
65+
it
66+
}
67+
}
68+
}
69+
70+
private fun boxPrimitives(model: Model, shape: Shape): Shape {
71+
val target = when (shape) {
72+
is MemberShape -> model.expectShape(shape.target)
73+
else -> shape
74+
}
75+
76+
return when {
77+
shape is MemberShape && target.isPrimitiveShape -> box(shape)
78+
shape is NumberShape -> boxNumber(shape)
79+
shape is BooleanShape -> box(shape)
80+
else -> shape
81+
}
82+
}
83+
84+
private val Shape.isPrimitiveShape: Boolean
85+
get() = isBooleanShape || isNumberShape
86+
87+
private fun <T> box(shape: T): Shape where T : Shape, T : ToSmithyBuilder<T> {
88+
return (shape.toBuilder() as AbstractShapeBuilder<*, T>).addTrait(BoxTrait()).build()
89+
}
90+
91+
private fun boxNumber(shape: NumberShape): Shape = when (shape) {
92+
is ByteShape -> box(shape)
93+
is IntegerShape -> box(shape)
94+
is LongShape -> box(shape)
95+
is ShortShape -> box(shape)
96+
is FloatShape -> box(shape)
97+
is DoubleShape -> box(shape)
98+
is BigDecimalShape -> box(shape)
99+
is BigIntegerShape -> box(shape)
100+
else -> throw CodegenException("unhandled numeric shape: $shape")
101+
}
102+
}

codegen/smithy-aws-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ aws.sdk.kotlin.codegen.customization.PresignableModelIntegration
1010
aws.sdk.kotlin.codegen.PresignerGenerator
1111
aws.sdk.kotlin.codegen.customization.apigateway.ApiGatewayAddAcceptHeader
1212
aws.sdk.kotlin.codegen.customization.glacier.GlacierAddVersionHeader
13-
aws.sdk.kotlin.codegen.customization.polly.PollyPresigner
13+
aws.sdk.kotlin.codegen.customization.polly.PollyPresigner
14+
aws.sdk.kotlin.codegen.customization.BoxServices
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
package aws.sdk.kotlin.codegen.customization
7+
8+
import org.junit.jupiter.api.Test
9+
import software.amazon.smithy.kotlin.codegen.model.expectShape
10+
import software.amazon.smithy.kotlin.codegen.model.hasTrait
11+
import software.amazon.smithy.kotlin.codegen.model.isNumberShape
12+
import software.amazon.smithy.kotlin.codegen.test.newTestContext
13+
import software.amazon.smithy.kotlin.codegen.test.prependNamespaceAndService
14+
import software.amazon.smithy.kotlin.codegen.test.toSmithyModel
15+
import software.amazon.smithy.model.shapes.StructureShape
16+
import software.amazon.smithy.model.traits.BoxTrait
17+
import kotlin.test.assertFalse
18+
import kotlin.test.assertTrue
19+
20+
class BoxServicesTest {
21+
@Test
22+
fun testPrimitiveShapesAreBoxed() {
23+
val model = """
24+
operation Foo {
25+
input: Primitives
26+
}
27+
28+
structure Primitives {
29+
int: PrimitiveInteger,
30+
bool: PrimitiveBoolean,
31+
long: PrimitiveLong,
32+
double: PrimitiveDouble,
33+
boxedAlready: BoxedField,
34+
notBoxed: NotBoxedField,
35+
other: Other
36+
}
37+
38+
@box
39+
integer BoxedField
40+
41+
structure Other {}
42+
43+
integer NotBoxedField
44+
""".prependNamespaceAndService(operations = listOf("Foo")).toSmithyModel()
45+
46+
val ctx = model.newTestContext()
47+
val transformed = BoxServices().preprocessModel(model, ctx.generationCtx.settings)
48+
49+
// get the synthetic input which is the one that will be transformed
50+
val struct = transformed.expectShape<StructureShape>("smithy.kotlin.synthetic.test#FooRequest")
51+
struct.members().forEach {
52+
val target = transformed.expectShape(it.target)
53+
if (target.isBooleanShape || target.isNumberShape) {
54+
assertTrue(it.hasTrait<BoxTrait>())
55+
} else {
56+
assertFalse(target.hasTrait<BoxTrait>())
57+
assertFalse(it.hasTrait<BoxTrait>())
58+
}
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)