Skip to content

Commit

Permalink
feat: support Smithy default trait (#857)
Browse files Browse the repository at this point in the history
  • Loading branch information
lauzadis authored Jun 16, 2023
1 parent 1aac44b commit 87dd1c2
Show file tree
Hide file tree
Showing 19 changed files with 397 additions and 95 deletions.
8 changes: 8 additions & 0 deletions .changes/d3ec877c-68c6-4c21-881f-8767d1f87e1c.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand All @@ -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
Expand Down Expand Up @@ -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<SparseTrait>()) "${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>")
Expand All @@ -160,7 +160,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli
val reference = toSymbol(shape.value)
val valueType = if (shape.hasTrait<SparseTrait>()) "${reference.name}?" else reference.name

return createSymbolBuilder(shape, "Map<String, $valueType>", boxed = true)
return createSymbolBuilder(shape, "Map<String, $valueType>", nullable = true)
.addReferences(reference)
.putProperty(SymbolProperty.MUTABLE_COLLECTION_FUNCTION, "mutableMapOf<String, $valueType>")
.putProperty(SymbolProperty.IMMUTABLE_COLLECTION_FUNCTION, "mapOf<String, $valueType>")
Expand All @@ -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<DefaultTrait>()?.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()
Expand All @@ -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()
Expand All @@ -208,7 +223,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli
override fun blobShape(shape: BlobShape): Symbol = if (shape.hasTrait<StreamingTrait>()) {
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 =
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 += "?"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -47,21 +50,21 @@ 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
}
}.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 {
/**
Expand All @@ -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.
*/
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()"
}
Expand All @@ -144,7 +144,7 @@ class UnionGenerator(
".contentHashCode()"
}
else ->
when (targetSymbol.isBoxed) {
when (targetSymbol.isNullable) {
true -> ".hashCode() ?: 0"
else -> ".hashCode()"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
""
Expand Down
Loading

0 comments on commit 87dd1c2

Please sign in to comment.