diff --git a/.changes/d3ec877c-68c6-4c21-881f-8767d1f87e1c.json b/.changes/d3ec877c-68c6-4c21-881f-8767d1f87e1c.json new file mode 100644 index 000000000..31e9224eb --- /dev/null +++ b/.changes/d3ec877c-68c6-4c21-881f-8767d1f87e1c.json @@ -0,0 +1,8 @@ +{ + "id": "d3ec877c-68c6-4c21-881f-8767d1f87e1c", + "type": "feature", + "description": "Support Smithy default trait", + "issues": [ + "https://github.com/awslabs/smithy-kotlin/issues/718" + ] +} \ No newline at end of file diff --git a/codegen/smithy-kotlin-codegen-testutils/src/main/kotlin/software/amazon/smithy/kotlin/codegen/test/ModelTestUtils.kt b/codegen/smithy-kotlin-codegen-testutils/src/main/kotlin/software/amazon/smithy/kotlin/codegen/test/ModelTestUtils.kt index 40c856165..79ff942b8 100644 --- a/codegen/smithy-kotlin-codegen-testutils/src/main/kotlin/software/amazon/smithy/kotlin/codegen/test/ModelTestUtils.kt +++ b/codegen/smithy-kotlin-codegen-testutils/src/main/kotlin/software/amazon/smithy/kotlin/codegen/test/ModelTestUtils.kt @@ -28,14 +28,14 @@ import java.net.URL /** * Unless necessary to deviate for test reasons, the following literals should be used in test models: - * smithy version: "1" - * model version: "1.0.0" + * smithy version: "2" + * model version: "2.0.0" * namespace: TestDefault.NAMESPACE * service name: "Test" */ object TestModelDefault { - const val SMITHY_IDL_VERSION = "1" - const val MODEL_VERSION = "1.0.0" + const val SMITHY_IDL_VERSION = "2" + const val MODEL_VERSION = "2.0.0" const val NAMESPACE = "com.test" const val SERVICE_NAME = "Test" const val SDK_ID = "Test" diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt index fb12507e2..cd6ac83f2 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt @@ -8,9 +8,11 @@ import software.amazon.smithy.codegen.core.* import software.amazon.smithy.kotlin.codegen.KotlinSettings import software.amazon.smithy.kotlin.codegen.lang.kotlinReservedWords import software.amazon.smithy.kotlin.codegen.model.* +import software.amazon.smithy.kotlin.codegen.utils.dq import software.amazon.smithy.model.Model import software.amazon.smithy.model.knowledge.NullableIndex import software.amazon.smithy.model.shapes.* +import software.amazon.smithy.model.traits.DefaultTrait import software.amazon.smithy.model.traits.SparseTrait import software.amazon.smithy.model.traits.StreamingTrait import java.util.logging.Logger @@ -75,31 +77,29 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli override fun longShape(shape: LongShape): Symbol = numberShape(shape, "Long", "0L") - override fun floatShape(shape: FloatShape): Symbol = numberShape(shape, "Float", "0.0f") + override fun floatShape(shape: FloatShape): Symbol = numberShape(shape, "Float", "0f") override fun doubleShape(shape: DoubleShape): Symbol = numberShape(shape, "Double", "0.0") private fun numberShape(shape: Shape, typeName: String, defaultValue: String = "0"): Symbol = - createSymbolBuilder(shape, typeName, namespace = "kotlin") - .defaultValue(defaultValue) - .build() + createSymbolBuilder(shape, typeName, namespace = "kotlin").defaultValue(defaultValue).build() override fun bigIntegerShape(shape: BigIntegerShape?): Symbol = createBigSymbol(shape, "BigInteger") override fun bigDecimalShape(shape: BigDecimalShape?): Symbol = createBigSymbol(shape, "BigDecimal") private fun createBigSymbol(shape: Shape?, symbolName: String): Symbol = - createSymbolBuilder(shape, symbolName, namespace = "java.math", boxed = true).build() + createSymbolBuilder(shape, symbolName, namespace = "java.math", nullable = true).build() override fun stringShape(shape: StringShape): Symbol = if (shape.isEnum) { createEnumSymbol(shape) } else { - createSymbolBuilder(shape, "String", boxed = true, namespace = "kotlin").build() + createSymbolBuilder(shape, "String", nullable = true, namespace = "kotlin").build() } private fun createEnumSymbol(shape: Shape): Symbol { val namespace = "$rootNamespace.model" - return createSymbolBuilder(shape, shape.defaultName(service), namespace, boxed = true) + return createSymbolBuilder(shape, shape.defaultName(service), namespace, nullable = true) .definitionFile("${shape.defaultName(service)}.kt") .build() } @@ -110,7 +110,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli override fun structureShape(shape: StructureShape): Symbol { val name = shape.defaultName(service) val namespace = "$rootNamespace.model" - val builder = createSymbolBuilder(shape, name, namespace, boxed = true) + val builder = createSymbolBuilder(shape, name, namespace, nullable = true) .definitionFile("$name.kt") // add a reference to each member symbol @@ -149,7 +149,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli override fun listShape(shape: ListShape): Symbol { val reference = toSymbol(shape.member) val valueType = if (shape.hasTrait()) "${reference.name}?" else reference.name - return createSymbolBuilder(shape, "List<$valueType>", boxed = true) + return createSymbolBuilder(shape, "List<$valueType>", nullable = true) .addReferences(reference) .putProperty(SymbolProperty.MUTABLE_COLLECTION_FUNCTION, "mutableListOf<$valueType>") .putProperty(SymbolProperty.IMMUTABLE_COLLECTION_FUNCTION, "listOf<$valueType>") @@ -160,7 +160,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli val reference = toSymbol(shape.value) val valueType = if (shape.hasTrait()) "${reference.name}?" else reference.name - return createSymbolBuilder(shape, "Map", boxed = true) + return createSymbolBuilder(shape, "Map", nullable = true) .addReferences(reference) .putProperty(SymbolProperty.MUTABLE_COLLECTION_FUNCTION, "mutableMapOf") .putProperty(SymbolProperty.IMMUTABLE_COLLECTION_FUNCTION, "mapOf") @@ -172,11 +172,17 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli val targetShape = model.getShape(shape.target).orElseThrow { CodegenException("Shape not found: ${shape.target}") } - val targetSymbol = if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT_ZERO_VALUE_V1_NO_INPUT)) { - toSymbol(targetShape).toBuilder().boxed().build() - } else { - toSymbol(targetShape) - } + val targetSymbol = toSymbol(targetShape) + .toBuilder() + .apply { + if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT_ZERO_VALUE_V1_NO_INPUT)) nullable() + + shape.getTrait()?.let { + defaultValue(it.getDefaultValue(targetShape), DefaultValueType.MODELED) + } + } + .build() + // figure out if we are referencing an event stream or not. // NOTE: unlike blob streams we actually re-use the target (union) shape which is why we can't do this // when visiting a unionShape() like we can for blobShape() @@ -197,9 +203,18 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli } } + private fun DefaultTrait.getDefaultValue(targetShape: Shape): String? = when { + toNode().toString() == "null" || targetShape is BlobShape && toNode().toString() == "" -> null + toNode().isNumberNode -> getDefaultValueForNumber(targetShape, toNode().toString()) + toNode().isArrayNode -> "listOf()" + toNode().isObjectNode -> "mapOf()" + toNode().isStringNode -> toNode().toString().dq() + else -> toNode().toString() + } + override fun timestampShape(shape: TimestampShape?): Symbol { val dependency = KotlinDependency.CORE - return createSymbolBuilder(shape, "Instant", boxed = true) + return createSymbolBuilder(shape, "Instant", nullable = true) .namespace("${dependency.namespace}.time", ".") .addDependency(dependency) .build() @@ -208,7 +223,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli override fun blobShape(shape: BlobShape): Symbol = if (shape.hasTrait()) { RuntimeTypes.Core.Content.ByteStream.asNullable() } else { - createSymbolBuilder(shape, "ByteArray", boxed = true, namespace = "kotlin").build() + createSymbolBuilder(shape, "ByteArray", nullable = true, namespace = "kotlin").build() } override fun documentShape(shape: DocumentShape?): Symbol = @@ -217,7 +232,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli override fun unionShape(shape: UnionShape): Symbol { val name = shape.defaultName(service) val namespace = "$rootNamespace.model" - val builder = createSymbolBuilder(shape, name, namespace, boxed = true) + val builder = createSymbolBuilder(shape, name, namespace, nullable = true) .definitionFile("$name.kt") // add a reference to each member symbol @@ -243,16 +258,23 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli /** * Creates a symbol builder for the shape with the given type name in the root namespace. */ - private fun createSymbolBuilder(shape: Shape?, typeName: String, boxed: Boolean = false): Symbol.Builder { + private fun createSymbolBuilder(shape: Shape?, typeName: String, nullable: Boolean = false): Symbol.Builder { val builder = Symbol.builder() .putProperty(SymbolProperty.SHAPE_KEY, shape) .name(typeName) - if (boxed) { - builder.boxed() + if (nullable) { + builder.nullable() } return builder } + private fun getDefaultValueForNumber(shape: Shape, value: String) = when (shape) { + is LongShape -> "${value}L" + is FloatShape -> "${value}f" + is DoubleShape -> if (value.matches("[0-9]*\\.[0-9]+".toRegex())) value else "$value.0" + else -> value + } + /** * Creates a symbol builder for the shape with the given type name in a child namespace relative * 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 shape: Shape?, typeName: String, namespace: String, - boxed: Boolean = false, - ): Symbol.Builder = createSymbolBuilder(shape, typeName, boxed).namespace(namespace, ".") + nullable: Boolean = false, + ): Symbol.Builder = createSymbolBuilder(shape, typeName, nullable).namespace(namespace, ".") } // Add a reference and it's children diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt index a808eb87e..884b1cdec 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt @@ -320,7 +320,7 @@ class KotlinPropertyFormatter( is Symbol -> { writer.addImport(type) var formatted = if (fullyQualifiedNames) type.fullName else type.name - if (includeNullability && type.isBoxed) { + if (includeNullability && type.isNullable) { formatted += "?" } diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/SymbolBuilder.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/SymbolBuilder.kt index b587190ba..30a5b1910 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/SymbolBuilder.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/SymbolBuilder.kt @@ -78,7 +78,7 @@ open class SymbolBuilder { fun build(): Symbol { builder.name(name) if (nullable) { - builder.boxed() + builder.nullable() } builder.putProperty(SymbolProperty.IS_EXTENSION, isExtension) if (objectRef != null) { diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/SymbolExt.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/SymbolExt.kt index 88b8dadbe..ec80cfeeb 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/SymbolExt.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/SymbolExt.kt @@ -18,8 +18,11 @@ object SymbolProperty { // The key that holds the default value for a type (symbol) as a string const val DEFAULT_VALUE_KEY: String = "defaultValue" - // Boolean property indicating this symbol should be boxed - const val BOXED_KEY: String = "boxed" + // The key that holds the type of default value + const val DEFAULT_VALUE_TYPE_KEY: String = "defaultValueType" + + // Boolean property indicating this symbol is nullable + const val NULLABLE_KEY: String = "nullable" // the original shape the symbol was created from const val SHAPE_KEY: String = "shape" @@ -47,10 +50,10 @@ object SymbolProperty { } /** - * Test if a symbol is boxed + * Test if a symbol is nullable */ -val Symbol.isBoxed: Boolean - get() = getProperty(SymbolProperty.BOXED_KEY).map { +val Symbol.isNullable: Boolean + get() = getProperty(SymbolProperty.NULLABLE_KEY).map { when (it) { is Boolean -> it else -> false @@ -58,10 +61,10 @@ val Symbol.isBoxed: Boolean }.orElse(false) /** - * Test if a symbol is not boxed + * Test if a symbol is not nullable */ -val Symbol.isNotBoxed: Boolean - get() = !isBoxed +val Symbol.isNotNullable: Boolean + get() = !isNullable enum class PropertyTypeMutability { /** @@ -82,6 +85,21 @@ enum class PropertyTypeMutability { } } +enum class DefaultValueType { + /** + * A default value which has been inferred, such as 0f for floats and false for booleans + */ + INFERRED, + + /** + * A default value which has been modeled using Smithy's default trait. + */ + MODELED, +} + +val Symbol.defaultValueType: DefaultValueType? + get() = getProperty(SymbolProperty.DEFAULT_VALUE_TYPE_KEY, DefaultValueType::class.java).getOrNull() + /** * Get the property type mutability of this symbol if set. */ @@ -92,27 +110,30 @@ val Symbol.propertyTypeMutability: PropertyTypeMutability? /** * Gets the default value for the symbol if present, else null - * @param defaultBoxed the string to pass back for boxed values + * @param defaultNullable the string to pass back for nullable values */ -fun Symbol.defaultValue(defaultBoxed: String? = "null"): String? { - // boxed types should always be defaulted to null - if (isBoxed) { - return defaultBoxed - } - +fun Symbol.defaultValue(defaultNullable: String? = "null"): String? { val default = getProperty(SymbolProperty.DEFAULT_VALUE_KEY, String::class.java) - return if (default.isPresent) default.get() else null + + // nullable types should default to null if there is no modeled default + if (isNullable && (!default.isPresent || defaultValueType == DefaultValueType.INFERRED)) { + return defaultNullable + } + return default.getOrNull() } /** - * Mark a symbol as being boxed (nullable) i.e. `T?` + * Mark a symbol as being nullable (i.e. `T?`) */ -fun Symbol.Builder.boxed(): Symbol.Builder = apply { putProperty(SymbolProperty.BOXED_KEY, true) } +fun Symbol.Builder.nullable(): Symbol.Builder = apply { putProperty(SymbolProperty.NULLABLE_KEY, true) } /** * Set the default value used when formatting the symbol */ -fun Symbol.Builder.defaultValue(value: String): Symbol.Builder = apply { putProperty(SymbolProperty.DEFAULT_VALUE_KEY, value) } +fun Symbol.Builder.defaultValue(value: String?, type: DefaultValueType = DefaultValueType.INFERRED): Symbol.Builder = apply { + putProperty(SymbolProperty.DEFAULT_VALUE_KEY, value) + putProperty(SymbolProperty.DEFAULT_VALUE_TYPE_KEY, type) +} /** * Convenience function for specifying kotlin namespace @@ -177,7 +198,7 @@ val Symbol.shape: Shape? /** * Get the nullable version of a symbol */ -fun Symbol.asNullable(): Symbol = toBuilder().boxed().build() +fun Symbol.asNullable(): Symbol = toBuilder().nullable().build() /** * Check whether a symbol represents an extension diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt index 8302a4ba4..85d54f13f 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt @@ -170,7 +170,7 @@ class StructureGenerator( // Return the appropriate hashCode fragment based on ShapeID of member target. private fun selectHashFunctionForShape(member: MemberShape): String { val targetShape = model.expectShape(member.target) - val isNullable = memberNameSymbolIndex[member]!!.second.isBoxed + val isNullable = memberNameSymbolIndex[member]!!.second.isNullable return when (targetShape.type) { ShapeType.INTEGER -> when (isNullable) { diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/UnionGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/UnionGenerator.kt index 89898ee7e..a186e6595 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/UnionGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/UnionGenerator.kt @@ -10,7 +10,7 @@ import software.amazon.smithy.kotlin.codegen.core.* import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes import software.amazon.smithy.kotlin.codegen.model.filterEventStreamErrors import software.amazon.smithy.kotlin.codegen.model.hasTrait -import software.amazon.smithy.kotlin.codegen.model.isBoxed +import software.amazon.smithy.kotlin.codegen.model.isNullable import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.* import software.amazon.smithy.model.traits.SensitiveTrait @@ -126,12 +126,12 @@ class UnionGenerator( return when (targetShape.type) { ShapeType.INTEGER -> - when (targetSymbol.isBoxed) { + when (targetSymbol.isNullable) { true -> " ?: 0" else -> "" } ShapeType.BYTE -> - when (targetSymbol.isBoxed) { + when (targetSymbol.isNullable) { true -> ".toInt() ?: 0" else -> ".toInt()" } @@ -144,7 +144,7 @@ class UnionGenerator( ".contentHashCode()" } else -> - when (targetSymbol.isBoxed) { + when (targetSymbol.isNullable) { true -> ".hashCode() ?: 0" else -> ".hashCode()" } diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGenerator.kt index 98a099d26..5668e22e5 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGenerator.kt @@ -703,7 +703,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { val headerName = hdrBinding.locationName val targetSymbol = ctx.symbolProvider.toSymbol(hdrBinding.member) - val defaultValuePostfix = if (targetSymbol.isNotBoxed && targetSymbol.defaultValue() != null) { + val defaultValuePostfix = if (targetSymbol.isNotNullable && targetSymbol.defaultValue() != null) { " ?: ${targetSymbol.defaultValue()}" } else { "" diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt index 0cb05af7b..949ee383c 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt @@ -8,7 +8,6 @@ package software.amazon.smithy.kotlin.codegen.rendering.protocol import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.kotlin.codegen.core.KotlinWriter import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes -import software.amazon.smithy.kotlin.codegen.core.addImport import software.amazon.smithy.kotlin.codegen.model.* import software.amazon.smithy.kotlin.codegen.rendering.serde.formatInstant import software.amazon.smithy.model.Model @@ -81,7 +80,7 @@ class HttpStringValuesMapSerializer( val targetSymbol = symbolProvider.toSymbol(member) val defaultValue = targetSymbol.defaultValue() - if ((memberTarget.isNumberShape || memberTarget.isBooleanShape) && targetSymbol.isNotBoxed && defaultValue != null) { + if ((memberTarget.isNumberShape || memberTarget.isBooleanShape) && targetSymbol.isNotNullable && defaultValue != null) { // unboxed primitive with a default value if (member.hasTrait()) { // always serialize a required member even if it's the default diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt index f321628b3..212c27135 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt @@ -561,7 +561,7 @@ open class SerializeStructGenerator( val defaultValue = targetSymbol.defaultValue() val memberName = ctx.symbolProvider.toMemberName(memberShape) - if ((targetShape.isNumberShape || targetShape.isBooleanShape) && targetSymbol.isNotBoxed && defaultValue != null) { + if ((targetShape.isNumberShape || targetShape.isBooleanShape) && targetSymbol.isNotNullable && defaultValue != null) { // unboxed primitive with a default value val ident = "input.$memberName" val check = when (memberShape.hasTrait()) { diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/ConfigProperty.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/ConfigProperty.kt index 9d956e25d..d40dc7a35 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/ConfigProperty.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/ConfigProperty.kt @@ -7,9 +7,9 @@ package software.amazon.smithy.kotlin.codegen.rendering.util import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.kotlin.codegen.core.* import software.amazon.smithy.kotlin.codegen.model.asNullable -import software.amazon.smithy.kotlin.codegen.model.boxed import software.amazon.smithy.kotlin.codegen.model.buildSymbol import software.amazon.smithy.kotlin.codegen.model.defaultValue +import software.amazon.smithy.kotlin.codegen.model.nullable typealias CustomPropertyRenderer = (ConfigProperty, KotlinWriter) -> Unit @@ -268,7 +268,7 @@ private fun builtInSymbol(symbolName: String, defaultValue: String?): Symbol { if (defaultValue != null) { builder.defaultValue(defaultValue) } else { - builder.boxed() + builder.nullable() } return builder.build() } diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt index 09cca5b15..2d2d6ccae 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt @@ -13,7 +13,7 @@ import software.amazon.smithy.kotlin.codegen.KotlinCodegenPlugin import software.amazon.smithy.kotlin.codegen.core.KotlinDependency.Companion.CORE import software.amazon.smithy.kotlin.codegen.model.defaultValue import software.amazon.smithy.kotlin.codegen.model.expectShape -import software.amazon.smithy.kotlin.codegen.model.isBoxed +import software.amazon.smithy.kotlin.codegen.model.isNullable import software.amazon.smithy.kotlin.codegen.model.traits.SYNTHETIC_NAMESPACE import software.amazon.smithy.kotlin.codegen.test.* import software.amazon.smithy.model.shapes.* @@ -68,25 +68,32 @@ class SymbolProviderTest { "Byte, null, true", "PrimitiveByte, 0, false", "Float, null, true", - "PrimitiveFloat, 0.0f, false", + "PrimitiveFloat, 0f, false", "Double, null, true", "PrimitiveDouble, 0.0, false", "Boolean, null, true", "PrimitiveBoolean, false, false", ) - fun `creates primitives`(primitiveType: String, expectedDefault: String, boxed: Boolean) { + fun `creates primitives`(primitiveType: String, expectedDefault: String, nullable: Boolean) { + // IDLv2.0 requires modeling a default value on primitives + val defaultTrait = when { + primitiveType == "PrimitiveBoolean" -> "@default(false)" + primitiveType.startsWith("Primitive") -> "@default(0)" + else -> "" + } + val model = """ structure MyStruct { + $defaultTrait quux: $primitiveType, } """.prependNamespaceAndService(namespace = "foo.bar").toSmithyModel() - val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model, rootNamespace = "foo.bar") val member = model.expectShape("foo.bar#MyStruct\$quux") val memberSymbol = provider.toSymbol(member) assertEquals("kotlin", memberSymbol.namespace) assertEquals(expectedDefault, memberSymbol.defaultValue()) - assertEquals(boxed, memberSymbol.isBoxed) + assertEquals(nullable, memberSymbol.isNullable) val expectedName = translateTypeName(primitiveType.removePrefix("Primitive")) assertEquals(expectedName, memberSymbol.name) @@ -99,12 +106,16 @@ class SymbolProviderTest { } @Test - fun `can read box trait from member`() { + fun `can read default trait from member`() { + val modeledDefault = "5" + val expectedDefault = "5L" + val model = """ structure MyStruct { - @box + @default($modeledDefault) foo: MyFoo } + long MyFoo """.prependNamespaceAndService().toSmithyModel() @@ -112,17 +123,22 @@ class SymbolProviderTest { val member = model.expectShape("com.test#MyStruct\$foo") val memberSymbol = provider.toSymbol(member) assertEquals("kotlin", memberSymbol.namespace) - assertEquals("null", memberSymbol.defaultValue()) - assertTrue(memberSymbol.isBoxed) + assertEquals(expectedDefault, memberSymbol.defaultValue()) + assertTrue(memberSymbol.isNullable) } @Test - fun `can read box trait from target`() { + fun `can read default trait from target`() { + val modeledDefault = "2500" + val expectedDefault = "2500L" + val model = """ structure MyStruct { + @default($modeledDefault) foo: MyFoo } - @box + + @default($modeledDefault) long MyFoo """.prependNamespaceAndService().toSmithyModel() @@ -130,8 +146,243 @@ class SymbolProviderTest { val member = model.expectShape("com.test#MyStruct\$foo") val memberSymbol = provider.toSymbol(member) assertEquals("kotlin", memberSymbol.namespace) + assertEquals(expectedDefault, memberSymbol.defaultValue()) + } + + @Test + fun `can override default trait from root-level shape`() { + val modeledDefault = "2500" + + val model = """ + structure MyStruct { + @default(null) + foo: RootLevelShape + } + + @default($modeledDefault) + long RootLevelShape + """.prependNamespaceAndService().toSmithyModel() + + val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) + val member = model.expectShape("com.test#MyStruct\$foo") + val memberSymbol = provider.toSymbol(member) + assertEquals("kotlin", memberSymbol.namespace) + assertEquals("null", memberSymbol.defaultValue()) + assertTrue(memberSymbol.isNullable) + } + + @ParameterizedTest(name = "{index} ==> ''can default simple {0} type''") + @CsvSource( + "long,100,100L", + "integer,5,5", + "short,32767,32767", + "float,3.14159,3.14159f", + "double,2.71828,2.71828", + "byte,10,10", + "string,\"hello\",\"hello\"", + "blob,\"abcdefg\",\"abcdefg\"", + "boolean,true,true", + "bigInteger,5,5", + "bigDecimal,9.0123456789,9.0123456789", + "timestamp,1684869901,1684869901", + ) + fun `can default simple types`(typeName: String, modeledDefault: String, expectedDefault: String) { + val model = """ + structure MyStruct { + @default($modeledDefault) + foo: Shape + } + + @default($modeledDefault) + $typeName Shape + """.prependNamespaceAndService().toSmithyModel() + + val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) + val member = model.expectShape("com.test#MyStruct\$foo") + val memberSymbol = provider.toSymbol(member) + assertEquals(expectedDefault, memberSymbol.defaultValue()) + } + + @Test + fun `can default empty string`() { + val model = """ + structure MyStruct { + @default("") + foo: myString + } + string myString + """.prependNamespaceAndService().toSmithyModel() + + val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) + val member = model.expectShape("com.test#MyStruct\$foo") + val memberSymbol = provider.toSymbol(member) + assertEquals("\"\"", memberSymbol.defaultValue()) + } + + @Test + fun `can default enum type`() { + val model = """ + structure MyStruct { + @default("club") + foo: Suit + } + + enum Suit { + @enumValue("diamond") + DIAMOND + + @enumValue("club") + CLUB + + @enumValue("heart") + HEART + + @enumValue("spade") + SPADE + } + """.prependNamespaceAndService().toSmithyModel() + + val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) + val member = model.expectShape("com.test#MyStruct\$foo") + val memberSymbol = provider.toSymbol(member) + assertEquals("\"club\"", memberSymbol.defaultValue()) + } + + @Test + fun `can default int enum type`() { + val model = """ + structure MyStruct { + @default(2) + foo: Season + } + + intEnum Season { + SPRING = 1 + SUMMER = 2 + FALL = 3 + WINTER = 4 + } + """.prependNamespaceAndService().toSmithyModel() + + val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) + val member = model.expectShape("com.test#MyStruct\$foo") + val memberSymbol = provider.toSymbol(member) + assertEquals("2", memberSymbol.defaultValue()) + } + + @ParameterizedTest(name = "{index} ==> ''can default document with {0} type''") + @CsvSource( + "boolean,true,true", + "boolean,false,false", + "string,\"hello\",\"hello\"", + "long,100,100", + "integer,5,5", + "short,32767,32767", + "float,3.14159,3.14159", + "double,2.71828,2.71828", + "byte,10,10", + "list,[],listOf()", + "map,{},mapOf()", + ) + @Suppress("UNUSED_PARAMETER") // using the first parameter in the test name, but compiler doesn't acknowledge that + fun `can default document type`(typeName: String, modeledDefault: String, expectedDefault: String) { + val model = """ + structure MyStruct { + @default($modeledDefault) + foo: MyDocument + } + + document MyDocument + """.prependNamespaceAndService().toSmithyModel() + + val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) + val member = model.expectShape("com.test#MyStruct\$foo") + val memberSymbol = provider.toSymbol(member) + assertEquals(expectedDefault, memberSymbol.defaultValue()) + } + + @Test + fun `can default list type`() { + val model = """ + structure MyStruct { + @default([]) + foo: MyStringList + } + + list MyStringList { + member: MyString + } + + string MyString + """.prependNamespaceAndService().toSmithyModel() + + val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) + val member = model.expectShape("com.test#MyStruct\$foo") + val memberSymbol = provider.toSymbol(member) + assertEquals("listOf()", memberSymbol.defaultValue()) + } + + @Test + fun `can default map type`() { + val model = """ + structure MyStruct { + @default({}) + foo: MyStringToIntegerMap + } + + map MyStringToIntegerMap { + key: MyString + value: MyInteger + } + + string MyString + integer MyInteger + """.prependNamespaceAndService().toSmithyModel() + + val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) + val member = model.expectShape("com.test#MyStruct\$foo") + val memberSymbol = provider.toSymbol(member) + assertEquals("mapOf()", memberSymbol.defaultValue()) + } + + @Test + fun `@clientOptional`() { + val model = """ + structure MyStruct { + @required + @clientOptional + quux: QuuxType + } + + string QuuxType + """.prependNamespaceAndService().toSmithyModel() + + val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) + val member = model.expectShape("com.test#MyStruct\$quux") + val memberSymbol = provider.toSymbol(member) + assertEquals("kotlin", memberSymbol.namespace) + assertTrue(memberSymbol.isNullable) + assertEquals("null", memberSymbol.defaultValue()) + } + + @Test + fun `@input`() { + val model = """ + @input + structure MyStruct { + @required + quux: QuuxType + } + + long QuuxType + """.prependNamespaceAndService().toSmithyModel() + + val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) + val member = model.expectShape("com.test#MyStruct\$quux") + val memberSymbol = provider.toSymbol(member) + assertEquals("kotlin", memberSymbol.namespace) + assertTrue(memberSymbol.isNullable) assertEquals("null", memberSymbol.defaultValue()) - assertTrue(memberSymbol.isBoxed) } @Test @@ -147,7 +398,7 @@ class SymbolProviderTest { val memberSymbol = provider.toSymbol(member) assertEquals("kotlin", memberSymbol.namespace) assertEquals("null", memberSymbol.defaultValue()) - assertEquals(true, memberSymbol.isBoxed) + assertTrue(memberSymbol.isNullable) assertEquals("ByteArray", memberSymbol.name) } @@ -155,6 +406,7 @@ class SymbolProviderTest { fun `creates streaming blobs`() { val model = """ structure MyStruct { + @required quux: BodyStream, } @@ -168,7 +420,7 @@ class SymbolProviderTest { assertEquals("$RUNTIME_ROOT_NS.content", memberSymbol.namespace) assertEquals("null", memberSymbol.defaultValue()) - assertEquals(true, memberSymbol.isBoxed) + assertEquals(true, memberSymbol.isNullable) assertEquals("ByteStream", memberSymbol.name) val dependency = memberSymbol.dependencies[0].expectProperty("dependency") as KotlinDependency assertEquals(CORE.artifact, dependency.artifact) @@ -193,7 +445,7 @@ class SymbolProviderTest { val listSymbol = provider.toSymbol(model.expectShape("foo.bar#Records")) assertEquals("List", listSymbol.name) - assertEquals(true, listSymbol.isBoxed) + assertEquals(true, listSymbol.isNullable) assertEquals("null", listSymbol.defaultValue()) // collections should contain a reference to the member type @@ -202,7 +454,7 @@ class SymbolProviderTest { val sparseListSymbol = provider.toSymbol(model.expectShape("foo.bar#SparseRecords")) assertEquals("List", sparseListSymbol.name) - assertEquals(true, sparseListSymbol.isBoxed) + assertEquals(true, sparseListSymbol.isNullable) assertEquals("null", sparseListSymbol.defaultValue()) // collections should contain a reference to the member type @@ -225,7 +477,7 @@ class SymbolProviderTest { val listSymbol = provider.toSymbol(listShape) assertEquals("List", listSymbol.name) - assertEquals(true, listSymbol.isBoxed) + assertEquals(true, listSymbol.isNullable) assertEquals("null", listSymbol.defaultValue()) // collections should contain a reference to the member type @@ -254,7 +506,7 @@ class SymbolProviderTest { val mapSymbol = provider.toSymbol(model.expectShape("${TestModelDefault.NAMESPACE}#MyMap")) assertEquals("Map", mapSymbol.name) - assertEquals(true, mapSymbol.isBoxed) + assertEquals(true, mapSymbol.isNullable) assertEquals("null", mapSymbol.defaultValue()) // collections should contain a reference to the member type @@ -263,7 +515,7 @@ class SymbolProviderTest { val sparseMapSymbol = provider.toSymbol(model.expectShape("${TestModelDefault.NAMESPACE}#MySparseMap")) assertEquals("Map", sparseMapSymbol.name) - assertEquals(true, sparseMapSymbol.isBoxed) + assertEquals(true, sparseMapSymbol.isNullable) assertEquals("null", sparseMapSymbol.defaultValue()) // collections should contain a reference to the member type @@ -285,7 +537,7 @@ class SymbolProviderTest { val bigSymbol = provider.toSymbol(member) assertEquals("java.math", bigSymbol.namespace) assertEquals("null", bigSymbol.defaultValue()) - assertEquals(true, bigSymbol.isBoxed) + assertEquals(true, bigSymbol.isNullable) assertEquals(type, bigSymbol.name) } @@ -309,7 +561,7 @@ class SymbolProviderTest { assertEquals("foo.bar.model", symbol.namespace) assertEquals("null", symbol.defaultValue()) - assertEquals(true, symbol.isBoxed) + assertEquals(true, symbol.isNullable) assertEquals("Baz", symbol.name) assertEquals("Baz.kt", symbol.definitionFile) } @@ -321,7 +573,7 @@ class SymbolProviderTest { FOO = 1 BAR = 2 } - """.prependNamespaceAndService(version = "2", namespace = "foo.bar").toSmithyModel() + """.prependNamespaceAndService(namespace = "foo.bar").toSmithyModel() val provider = KotlinCodegenPlugin.createSymbolProvider(model, rootNamespace = "foo.bar") val shape = model.expectShape("foo.bar#Baz") @@ -329,7 +581,7 @@ class SymbolProviderTest { assertEquals("foo.bar.model", symbol.namespace) assertEquals("null", symbol.defaultValue()) - assertEquals(true, symbol.isBoxed) + assertEquals(true, symbol.isNullable) assertEquals("Baz", symbol.name) assertEquals("Baz.kt", symbol.definitionFile) } @@ -350,7 +602,7 @@ class SymbolProviderTest { assertEquals("com.test.model", symbol.namespace) assertEquals("null", symbol.defaultValue()) - assertEquals(true, symbol.isBoxed) + assertEquals(true, symbol.isNullable) assertEquals("MyUnion", symbol.name) assertEquals("MyUnion.kt", symbol.definitionFile) } @@ -369,7 +621,7 @@ class SymbolProviderTest { assertEquals("foo.bar.model", structSymbol.namespace) assertEquals("MyStruct", structSymbol.name) assertEquals("null", structSymbol.defaultValue()) - assertEquals(true, structSymbol.isBoxed) + assertEquals(true, structSymbol.isNullable) assertEquals("MyStruct.kt", structSymbol.definitionFile) assertEquals(1, structSymbol.references.size) } @@ -385,7 +637,7 @@ class SymbolProviderTest { val documentSymbol = provider.toSymbol(documentShape) assertEquals("Document", documentSymbol.name) assertEquals("null", documentSymbol.defaultValue()) - assertEquals(true, documentSymbol.isBoxed) + assertEquals(true, documentSymbol.isNullable) assertEquals(RuntimeTypes.Core.Content.Document.namespace, documentSymbol.namespace) assertEquals(1, documentSymbol.dependencies.size) } @@ -424,7 +676,7 @@ class SymbolProviderTest { assertEquals("$RUNTIME_ROOT_NS.time", timestampSymbol.namespace) assertEquals("Instant", timestampSymbol.name) assertEquals("null", timestampSymbol.defaultValue()) - assertEquals(true, timestampSymbol.isBoxed) + assertEquals(true, timestampSymbol.isNullable) assertEquals(1, timestampSymbol.dependencies.size) } @@ -449,7 +701,7 @@ class SymbolProviderTest { assertEquals("foo.bar.model", structSymbol.namespace) assertEquals("MyStruct1", structSymbol.name) assertEquals("null", structSymbol.defaultValue()) - assertEquals(true, structSymbol.isBoxed) + assertEquals(true, structSymbol.isNullable) assertEquals("MyStruct1.kt", structSymbol.definitionFile) assertEquals(2, structSymbol.references.size) } @@ -488,7 +740,7 @@ class SymbolProviderTest { assertEquals("", symbol.namespace) assertEquals("null", symbol.defaultValue()) - assertEquals(true, symbol.isBoxed) + assertEquals(true, symbol.isNullable) assertEquals("Flow", symbol.name) assertEquals("com.test.model.Events", symbol.references[0].symbol.fullName) diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/EnumGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/EnumGeneratorTest.kt index f7000761a..52027513b 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/EnumGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/EnumGeneratorTest.kt @@ -187,7 +187,7 @@ public sealed class Baz { @documentation("Documentation for this value") T2_MICRO = 1 } - """.prependNamespaceAndService(version = "2", namespace = "test") + """.prependNamespaceAndService(namespace = "test") .toSmithyModel() val provider = KotlinCodegenPlugin.createSymbolProvider(model, rootNamespace = "test") diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGeneratorTest.kt index cd06b84b5..7f16414ea 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGeneratorTest.kt @@ -34,6 +34,7 @@ class StructureGeneratorTest { foo: String, object: String, @documentation("This *is* documentation about the member.") + @default(0) bar: PrimitiveInteger, baz: Integer, Quux: Qux, @@ -437,6 +438,7 @@ class StructureGeneratorTest { structure MyStruct { foo: Blob, + @required bar: BlobStream } diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt index 4814606ef..0609f5ac3 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt @@ -413,7 +413,7 @@ internal class SmokeTestOperationDeserializer: HttpDeserialize