Skip to content

Commit 743fd39

Browse files
authored
fix: boxing of services (#361)
1 parent 5b40a9d commit 743fd39

File tree

4 files changed

+194
-1
lines changed

4 files changed

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

codegen/smithy-aws-swift-codegen/src/test/kotlin/software/amazon/smithy/aws/swift/codegen/TestContextGenerator.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import software.amazon.smithy.model.Model
1212
import software.amazon.smithy.model.node.Node
1313
import software.amazon.smithy.model.node.ObjectNode
1414
import software.amazon.smithy.model.shapes.ShapeId
15+
import software.amazon.smithy.model.validation.ValidatedResultException
1516
import software.amazon.smithy.swift.codegen.SwiftCodegenPlugin
1617
import software.amazon.smithy.swift.codegen.SwiftDelegator
1718
import software.amazon.smithy.swift.codegen.SwiftSettings
@@ -115,3 +116,22 @@ fun String.shouldSyntacticSanityCheck() {
115116
Assertions.assertEquals(openBraces, closedBraces, "unmatched open/closed braces:\n$this")
116117
Assertions.assertEquals(openParens, closedParens, "unmatched open/close parens:\n$this")
117118
}
119+
120+
/**
121+
* Load and initialize a model from a String
122+
*/
123+
fun String.toSmithyModel(sourceLocation: String? = null, serviceShapeId: String? = null): Model {
124+
val processed = if (this.trimStart().startsWith("\$version")) this else "\$version: \"1.0\"\n$this"
125+
val model = try {
126+
Model.assembler()
127+
.discoverModels()
128+
.addUnparsedModel(sourceLocation ?: "test.smithy", processed)
129+
.assemble()
130+
.unwrap()
131+
} catch (e: ValidatedResultException) {
132+
System.err.println("Model failed to parse:")
133+
System.err.println(this)
134+
throw e
135+
}
136+
return model
137+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package software.amazon.smithy.aws.swift.codegen.customizations
2+
3+
import org.junit.jupiter.api.Assertions.assertTrue
4+
import org.junit.jupiter.api.Test
5+
import software.amazon.smithy.aws.swift.codegen.awsjson.AwsJson1_0_ProtocolGenerator
6+
import software.amazon.smithy.aws.swift.codegen.customization.BoxServices
7+
import software.amazon.smithy.aws.swift.codegen.newTestContext
8+
import software.amazon.smithy.aws.swift.codegen.toSmithyModel
9+
import software.amazon.smithy.model.shapes.StructureShape
10+
import software.amazon.smithy.model.traits.BoxTrait
11+
import software.amazon.smithy.swift.codegen.model.AddOperationShapes
12+
import software.amazon.smithy.swift.codegen.model.expectShape
13+
import software.amazon.smithy.swift.codegen.model.hasTrait
14+
15+
class BoxServicesTest {
16+
@Test
17+
fun testPrimitiveShapesAreBoxed() {
18+
val smithy = """
19+
namespace com.test
20+
service Example {
21+
version: "1.0.0",
22+
operations: [Foo]
23+
}
24+
25+
operation Foo {
26+
input: Primitives
27+
}
28+
29+
structure Primitives {
30+
int: PrimitiveInteger,
31+
bool: PrimitiveBoolean,
32+
long: PrimitiveLong,
33+
double: PrimitiveDouble,
34+
boxedAlready: BoxedField,
35+
notPrimitive: NotPrimitiveField,
36+
other: Other
37+
}
38+
39+
@box
40+
integer BoxedField
41+
42+
structure Other {}
43+
44+
integer NotPrimitiveField
45+
"""
46+
val model = smithy.toSmithyModel()
47+
val ctx = model.newTestContext("com.test#Example", AwsJson1_0_ProtocolGenerator()).ctx
48+
val operationTransform = AddOperationShapes.execute(model, ctx.service, ctx.settings.moduleName)
49+
val transformed = BoxServices().preprocessModel(operationTransform, ctx.settings)
50+
51+
// get the synthetic input which is the one that will be transformed
52+
val struct = transformed.expectShape<StructureShape>("smithy.swift.synthetic#FooInput")
53+
val intMember = struct.getMember("int")
54+
val boolMember = struct.getMember("bool")
55+
val longMember = struct.getMember("long")
56+
val notPrimitiveMember = struct.getMember("notPrimitive")
57+
assertTrue(intMember.get().hasTrait<BoxTrait>())
58+
assertTrue(boolMember.get().hasTrait<BoxTrait>())
59+
assertTrue(longMember.get().hasTrait<BoxTrait>())
60+
assertTrue(notPrimitiveMember.get().hasTrait<BoxTrait>())
61+
}
62+
}

0 commit comments

Comments
 (0)