Skip to content

Commit 87dd1c2

Browse files
authored
feat: support Smithy default trait (#857)
1 parent 1aac44b commit 87dd1c2

File tree

19 files changed

+397
-95
lines changed

19 files changed

+397
-95
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "d3ec877c-68c6-4c21-881f-8767d1f87e1c",
3+
"type": "feature",
4+
"description": "Support Smithy default trait",
5+
"issues": [
6+
"https://github.com/awslabs/smithy-kotlin/issues/718"
7+
]
8+
}

codegen/smithy-kotlin-codegen-testutils/src/main/kotlin/software/amazon/smithy/kotlin/codegen/test/ModelTestUtils.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ import java.net.URL
2828

2929
/**
3030
* Unless necessary to deviate for test reasons, the following literals should be used in test models:
31-
* smithy version: "1"
32-
* model version: "1.0.0"
31+
* smithy version: "2"
32+
* model version: "2.0.0"
3333
* namespace: TestDefault.NAMESPACE
3434
* service name: "Test"
3535
*/
3636
object TestModelDefault {
37-
const val SMITHY_IDL_VERSION = "1"
38-
const val MODEL_VERSION = "1.0.0"
37+
const val SMITHY_IDL_VERSION = "2"
38+
const val MODEL_VERSION = "2.0.0"
3939
const val NAMESPACE = "com.test"
4040
const val SERVICE_NAME = "Test"
4141
const val SDK_ID = "Test"

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import software.amazon.smithy.codegen.core.*
88
import software.amazon.smithy.kotlin.codegen.KotlinSettings
99
import software.amazon.smithy.kotlin.codegen.lang.kotlinReservedWords
1010
import software.amazon.smithy.kotlin.codegen.model.*
11+
import software.amazon.smithy.kotlin.codegen.utils.dq
1112
import software.amazon.smithy.model.Model
1213
import software.amazon.smithy.model.knowledge.NullableIndex
1314
import software.amazon.smithy.model.shapes.*
15+
import software.amazon.smithy.model.traits.DefaultTrait
1416
import software.amazon.smithy.model.traits.SparseTrait
1517
import software.amazon.smithy.model.traits.StreamingTrait
1618
import java.util.logging.Logger
@@ -75,31 +77,29 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli
7577

7678
override fun longShape(shape: LongShape): Symbol = numberShape(shape, "Long", "0L")
7779

78-
override fun floatShape(shape: FloatShape): Symbol = numberShape(shape, "Float", "0.0f")
80+
override fun floatShape(shape: FloatShape): Symbol = numberShape(shape, "Float", "0f")
7981

8082
override fun doubleShape(shape: DoubleShape): Symbol = numberShape(shape, "Double", "0.0")
8183

8284
private fun numberShape(shape: Shape, typeName: String, defaultValue: String = "0"): Symbol =
83-
createSymbolBuilder(shape, typeName, namespace = "kotlin")
84-
.defaultValue(defaultValue)
85-
.build()
85+
createSymbolBuilder(shape, typeName, namespace = "kotlin").defaultValue(defaultValue).build()
8686

8787
override fun bigIntegerShape(shape: BigIntegerShape?): Symbol = createBigSymbol(shape, "BigInteger")
8888

8989
override fun bigDecimalShape(shape: BigDecimalShape?): Symbol = createBigSymbol(shape, "BigDecimal")
9090

9191
private fun createBigSymbol(shape: Shape?, symbolName: String): Symbol =
92-
createSymbolBuilder(shape, symbolName, namespace = "java.math", boxed = true).build()
92+
createSymbolBuilder(shape, symbolName, namespace = "java.math", nullable = true).build()
9393

9494
override fun stringShape(shape: StringShape): Symbol = if (shape.isEnum) {
9595
createEnumSymbol(shape)
9696
} else {
97-
createSymbolBuilder(shape, "String", boxed = true, namespace = "kotlin").build()
97+
createSymbolBuilder(shape, "String", nullable = true, namespace = "kotlin").build()
9898
}
9999

100100
private fun createEnumSymbol(shape: Shape): Symbol {
101101
val namespace = "$rootNamespace.model"
102-
return createSymbolBuilder(shape, shape.defaultName(service), namespace, boxed = true)
102+
return createSymbolBuilder(shape, shape.defaultName(service), namespace, nullable = true)
103103
.definitionFile("${shape.defaultName(service)}.kt")
104104
.build()
105105
}
@@ -110,7 +110,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli
110110
override fun structureShape(shape: StructureShape): Symbol {
111111
val name = shape.defaultName(service)
112112
val namespace = "$rootNamespace.model"
113-
val builder = createSymbolBuilder(shape, name, namespace, boxed = true)
113+
val builder = createSymbolBuilder(shape, name, namespace, nullable = true)
114114
.definitionFile("$name.kt")
115115

116116
// add a reference to each member symbol
@@ -149,7 +149,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli
149149
override fun listShape(shape: ListShape): Symbol {
150150
val reference = toSymbol(shape.member)
151151
val valueType = if (shape.hasTrait<SparseTrait>()) "${reference.name}?" else reference.name
152-
return createSymbolBuilder(shape, "List<$valueType>", boxed = true)
152+
return createSymbolBuilder(shape, "List<$valueType>", nullable = true)
153153
.addReferences(reference)
154154
.putProperty(SymbolProperty.MUTABLE_COLLECTION_FUNCTION, "mutableListOf<$valueType>")
155155
.putProperty(SymbolProperty.IMMUTABLE_COLLECTION_FUNCTION, "listOf<$valueType>")
@@ -160,7 +160,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli
160160
val reference = toSymbol(shape.value)
161161
val valueType = if (shape.hasTrait<SparseTrait>()) "${reference.name}?" else reference.name
162162

163-
return createSymbolBuilder(shape, "Map<String, $valueType>", boxed = true)
163+
return createSymbolBuilder(shape, "Map<String, $valueType>", nullable = true)
164164
.addReferences(reference)
165165
.putProperty(SymbolProperty.MUTABLE_COLLECTION_FUNCTION, "mutableMapOf<String, $valueType>")
166166
.putProperty(SymbolProperty.IMMUTABLE_COLLECTION_FUNCTION, "mapOf<String, $valueType>")
@@ -172,11 +172,17 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli
172172
val targetShape =
173173
model.getShape(shape.target).orElseThrow { CodegenException("Shape not found: ${shape.target}") }
174174

175-
val targetSymbol = if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT_ZERO_VALUE_V1_NO_INPUT)) {
176-
toSymbol(targetShape).toBuilder().boxed().build()
177-
} else {
178-
toSymbol(targetShape)
179-
}
175+
val targetSymbol = toSymbol(targetShape)
176+
.toBuilder()
177+
.apply {
178+
if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT_ZERO_VALUE_V1_NO_INPUT)) nullable()
179+
180+
shape.getTrait<DefaultTrait>()?.let {
181+
defaultValue(it.getDefaultValue(targetShape), DefaultValueType.MODELED)
182+
}
183+
}
184+
.build()
185+
180186
// figure out if we are referencing an event stream or not.
181187
// NOTE: unlike blob streams we actually re-use the target (union) shape which is why we can't do this
182188
// when visiting a unionShape() like we can for blobShape()
@@ -197,9 +203,18 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli
197203
}
198204
}
199205

206+
private fun DefaultTrait.getDefaultValue(targetShape: Shape): String? = when {
207+
toNode().toString() == "null" || targetShape is BlobShape && toNode().toString() == "" -> null
208+
toNode().isNumberNode -> getDefaultValueForNumber(targetShape, toNode().toString())
209+
toNode().isArrayNode -> "listOf()"
210+
toNode().isObjectNode -> "mapOf()"
211+
toNode().isStringNode -> toNode().toString().dq()
212+
else -> toNode().toString()
213+
}
214+
200215
override fun timestampShape(shape: TimestampShape?): Symbol {
201216
val dependency = KotlinDependency.CORE
202-
return createSymbolBuilder(shape, "Instant", boxed = true)
217+
return createSymbolBuilder(shape, "Instant", nullable = true)
203218
.namespace("${dependency.namespace}.time", ".")
204219
.addDependency(dependency)
205220
.build()
@@ -208,7 +223,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli
208223
override fun blobShape(shape: BlobShape): Symbol = if (shape.hasTrait<StreamingTrait>()) {
209224
RuntimeTypes.Core.Content.ByteStream.asNullable()
210225
} else {
211-
createSymbolBuilder(shape, "ByteArray", boxed = true, namespace = "kotlin").build()
226+
createSymbolBuilder(shape, "ByteArray", nullable = true, namespace = "kotlin").build()
212227
}
213228

214229
override fun documentShape(shape: DocumentShape?): Symbol =
@@ -217,7 +232,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli
217232
override fun unionShape(shape: UnionShape): Symbol {
218233
val name = shape.defaultName(service)
219234
val namespace = "$rootNamespace.model"
220-
val builder = createSymbolBuilder(shape, name, namespace, boxed = true)
235+
val builder = createSymbolBuilder(shape, name, namespace, nullable = true)
221236
.definitionFile("$name.kt")
222237

223238
// add a reference to each member symbol
@@ -243,16 +258,23 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli
243258
/**
244259
* Creates a symbol builder for the shape with the given type name in the root namespace.
245260
*/
246-
private fun createSymbolBuilder(shape: Shape?, typeName: String, boxed: Boolean = false): Symbol.Builder {
261+
private fun createSymbolBuilder(shape: Shape?, typeName: String, nullable: Boolean = false): Symbol.Builder {
247262
val builder = Symbol.builder()
248263
.putProperty(SymbolProperty.SHAPE_KEY, shape)
249264
.name(typeName)
250-
if (boxed) {
251-
builder.boxed()
265+
if (nullable) {
266+
builder.nullable()
252267
}
253268
return builder
254269
}
255270

271+
private fun getDefaultValueForNumber(shape: Shape, value: String) = when (shape) {
272+
is LongShape -> "${value}L"
273+
is FloatShape -> "${value}f"
274+
is DoubleShape -> if (value.matches("[0-9]*\\.[0-9]+".toRegex())) value else "$value.0"
275+
else -> value
276+
}
277+
256278
/**
257279
* Creates a symbol builder for the shape with the given type name in a child namespace relative
258280
* to the root namespace e.g. `relativeNamespace = bar` with a root namespace of `foo` would set
@@ -262,8 +284,8 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli
262284
shape: Shape?,
263285
typeName: String,
264286
namespace: String,
265-
boxed: Boolean = false,
266-
): Symbol.Builder = createSymbolBuilder(shape, typeName, boxed).namespace(namespace, ".")
287+
nullable: Boolean = false,
288+
): Symbol.Builder = createSymbolBuilder(shape, typeName, nullable).namespace(namespace, ".")
267289
}
268290

269291
// Add a reference and it's children

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ class KotlinPropertyFormatter(
320320
is Symbol -> {
321321
writer.addImport(type)
322322
var formatted = if (fullyQualifiedNames) type.fullName else type.name
323-
if (includeNullability && type.isBoxed) {
323+
if (includeNullability && type.isNullable) {
324324
formatted += "?"
325325
}
326326

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/SymbolBuilder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ open class SymbolBuilder {
7878
fun build(): Symbol {
7979
builder.name(name)
8080
if (nullable) {
81-
builder.boxed()
81+
builder.nullable()
8282
}
8383
builder.putProperty(SymbolProperty.IS_EXTENSION, isExtension)
8484
if (objectRef != null) {

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/SymbolExt.kt

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ object SymbolProperty {
1818
// The key that holds the default value for a type (symbol) as a string
1919
const val DEFAULT_VALUE_KEY: String = "defaultValue"
2020

21-
// Boolean property indicating this symbol should be boxed
22-
const val BOXED_KEY: String = "boxed"
21+
// The key that holds the type of default value
22+
const val DEFAULT_VALUE_TYPE_KEY: String = "defaultValueType"
23+
24+
// Boolean property indicating this symbol is nullable
25+
const val NULLABLE_KEY: String = "nullable"
2326

2427
// the original shape the symbol was created from
2528
const val SHAPE_KEY: String = "shape"
@@ -47,21 +50,21 @@ object SymbolProperty {
4750
}
4851

4952
/**
50-
* Test if a symbol is boxed
53+
* Test if a symbol is nullable
5154
*/
52-
val Symbol.isBoxed: Boolean
53-
get() = getProperty(SymbolProperty.BOXED_KEY).map {
55+
val Symbol.isNullable: Boolean
56+
get() = getProperty(SymbolProperty.NULLABLE_KEY).map {
5457
when (it) {
5558
is Boolean -> it
5659
else -> false
5760
}
5861
}.orElse(false)
5962

6063
/**
61-
* Test if a symbol is not boxed
64+
* Test if a symbol is not nullable
6265
*/
63-
val Symbol.isNotBoxed: Boolean
64-
get() = !isBoxed
66+
val Symbol.isNotNullable: Boolean
67+
get() = !isNullable
6568

6669
enum class PropertyTypeMutability {
6770
/**
@@ -82,6 +85,21 @@ enum class PropertyTypeMutability {
8285
}
8386
}
8487

88+
enum class DefaultValueType {
89+
/**
90+
* A default value which has been inferred, such as 0f for floats and false for booleans
91+
*/
92+
INFERRED,
93+
94+
/**
95+
* A default value which has been modeled using Smithy's default trait.
96+
*/
97+
MODELED,
98+
}
99+
100+
val Symbol.defaultValueType: DefaultValueType?
101+
get() = getProperty(SymbolProperty.DEFAULT_VALUE_TYPE_KEY, DefaultValueType::class.java).getOrNull()
102+
85103
/**
86104
* Get the property type mutability of this symbol if set.
87105
*/
@@ -92,27 +110,30 @@ val Symbol.propertyTypeMutability: PropertyTypeMutability?
92110

93111
/**
94112
* Gets the default value for the symbol if present, else null
95-
* @param defaultBoxed the string to pass back for boxed values
113+
* @param defaultNullable the string to pass back for nullable values
96114
*/
97-
fun Symbol.defaultValue(defaultBoxed: String? = "null"): String? {
98-
// boxed types should always be defaulted to null
99-
if (isBoxed) {
100-
return defaultBoxed
101-
}
102-
115+
fun Symbol.defaultValue(defaultNullable: String? = "null"): String? {
103116
val default = getProperty(SymbolProperty.DEFAULT_VALUE_KEY, String::class.java)
104-
return if (default.isPresent) default.get() else null
117+
118+
// nullable types should default to null if there is no modeled default
119+
if (isNullable && (!default.isPresent || defaultValueType == DefaultValueType.INFERRED)) {
120+
return defaultNullable
121+
}
122+
return default.getOrNull()
105123
}
106124

107125
/**
108-
* Mark a symbol as being boxed (nullable) i.e. `T?`
126+
* Mark a symbol as being nullable (i.e. `T?`)
109127
*/
110-
fun Symbol.Builder.boxed(): Symbol.Builder = apply { putProperty(SymbolProperty.BOXED_KEY, true) }
128+
fun Symbol.Builder.nullable(): Symbol.Builder = apply { putProperty(SymbolProperty.NULLABLE_KEY, true) }
111129

112130
/**
113131
* Set the default value used when formatting the symbol
114132
*/
115-
fun Symbol.Builder.defaultValue(value: String): Symbol.Builder = apply { putProperty(SymbolProperty.DEFAULT_VALUE_KEY, value) }
133+
fun Symbol.Builder.defaultValue(value: String?, type: DefaultValueType = DefaultValueType.INFERRED): Symbol.Builder = apply {
134+
putProperty(SymbolProperty.DEFAULT_VALUE_KEY, value)
135+
putProperty(SymbolProperty.DEFAULT_VALUE_TYPE_KEY, type)
136+
}
116137

117138
/**
118139
* Convenience function for specifying kotlin namespace
@@ -177,7 +198,7 @@ val Symbol.shape: Shape?
177198
/**
178199
* Get the nullable version of a symbol
179200
*/
180-
fun Symbol.asNullable(): Symbol = toBuilder().boxed().build()
201+
fun Symbol.asNullable(): Symbol = toBuilder().nullable().build()
181202

182203
/**
183204
* Check whether a symbol represents an extension

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ class StructureGenerator(
170170
// Return the appropriate hashCode fragment based on ShapeID of member target.
171171
private fun selectHashFunctionForShape(member: MemberShape): String {
172172
val targetShape = model.expectShape(member.target)
173-
val isNullable = memberNameSymbolIndex[member]!!.second.isBoxed
173+
val isNullable = memberNameSymbolIndex[member]!!.second.isNullable
174174
return when (targetShape.type) {
175175
ShapeType.INTEGER ->
176176
when (isNullable) {

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/UnionGenerator.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import software.amazon.smithy.kotlin.codegen.core.*
1010
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
1111
import software.amazon.smithy.kotlin.codegen.model.filterEventStreamErrors
1212
import software.amazon.smithy.kotlin.codegen.model.hasTrait
13-
import software.amazon.smithy.kotlin.codegen.model.isBoxed
13+
import software.amazon.smithy.kotlin.codegen.model.isNullable
1414
import software.amazon.smithy.model.Model
1515
import software.amazon.smithy.model.shapes.*
1616
import software.amazon.smithy.model.traits.SensitiveTrait
@@ -126,12 +126,12 @@ class UnionGenerator(
126126

127127
return when (targetShape.type) {
128128
ShapeType.INTEGER ->
129-
when (targetSymbol.isBoxed) {
129+
when (targetSymbol.isNullable) {
130130
true -> " ?: 0"
131131
else -> ""
132132
}
133133
ShapeType.BYTE ->
134-
when (targetSymbol.isBoxed) {
134+
when (targetSymbol.isNullable) {
135135
true -> ".toInt() ?: 0"
136136
else -> ".toInt()"
137137
}
@@ -144,7 +144,7 @@ class UnionGenerator(
144144
".contentHashCode()"
145145
}
146146
else ->
147-
when (targetSymbol.isBoxed) {
147+
when (targetSymbol.isNullable) {
148148
true -> ".hashCode() ?: 0"
149149
else -> ".hashCode()"
150150
}

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
703703
val headerName = hdrBinding.locationName
704704

705705
val targetSymbol = ctx.symbolProvider.toSymbol(hdrBinding.member)
706-
val defaultValuePostfix = if (targetSymbol.isNotBoxed && targetSymbol.defaultValue() != null) {
706+
val defaultValuePostfix = if (targetSymbol.isNotNullable && targetSymbol.defaultValue() != null) {
707707
" ?: ${targetSymbol.defaultValue()}"
708708
} else {
709709
""

0 commit comments

Comments
 (0)