-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support Smithy default
trait
#857
Changes from 13 commits
0f155c2
51f8dbb
1aff46c
4aebfe7
528a21d
24f6c67
292dc53
1284dd2
de93d87
fc2ff86
10cb7b0
345b411
09719fe
2f84ed7
6b5e7e4
cf9d353
e66c579
42ad105
ccff67e
84f69c7
f419475
eb084e9
781ac4f
ea1fbc1
3a638e1
939ce26
4a3afb5
8a51691
defa0ce
aee25a0
42bbd66
367d8d5
80e2ce8
f8ccaf9
844b970
137a013
1988148
c0ca0ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
|
@@ -11,6 +11,7 @@ import software.amazon.smithy.kotlin.codegen.model.* | |
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 | ||
|
@@ -79,10 +80,14 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli | |
|
||
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() | ||
private fun numberShape(shape: Shape, typeName: String, defaultValue: String = "0"): Symbol { | ||
val symbol = createSymbolBuilder(shape, typeName, namespace = "kotlin").build() | ||
return if (!symbol.properties.containsKey(SymbolProperty.DEFAULT_VALUE_KEY)) { | ||
symbol.toBuilder().defaultValue(defaultValue).build() | ||
} else { | ||
symbol | ||
} | ||
} | ||
|
||
override fun bigIntegerShape(shape: BigIntegerShape?): Symbol = createBigSymbol(shape, "BigInteger") | ||
|
||
|
@@ -172,11 +177,22 @@ 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)) { | ||
var targetSymbol = if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT)) { | ||
toSymbol(targetShape).toBuilder().boxed().build() | ||
} else { | ||
toSymbol(targetShape) | ||
} | ||
|
||
targetSymbol = shape.getTrait<DefaultTrait>()?.let { | ||
val builder = targetSymbol.toBuilder() | ||
val defaultValue = it.getDefaultValue(targetShape) | ||
builder.defaultValue(defaultValue) | ||
if (defaultValue != "null") { | ||
builder.unboxed() | ||
} | ||
builder.build() | ||
} ?: targetSymbol | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Looks like we box when the nullable index says to but then unbox if the default isn't null. Is that right? What happens if these two things conflict? |
||
|
||
// 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,6 +213,21 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli | |
} | ||
} | ||
|
||
private fun DefaultTrait.getDefaultValue(targetShape: Shape): String = | ||
if (toNode().toString() == "null" || (targetShape is BlobShape && toNode().toString() == "")) { | ||
"null" | ||
} else if (toNode().isNumberNode) { | ||
getDefaultValueForNumber(targetShape, toNode().toString()) | ||
} else if (toNode().isArrayNode) { | ||
"listOf()" | ||
} else if (toNode().isObjectNode) { | ||
"mapOf()" | ||
} else if (toNode().isStringNode) { | ||
"\"${toNode()}\"" | ||
} else { | ||
toNode().toString() | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style: Prefer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||
|
||
override fun timestampShape(shape: TimestampShape?): Symbol { | ||
val dependency = KotlinDependency.CORE | ||
return createSymbolBuilder(shape, "Instant", boxed = true) | ||
|
@@ -253,6 +284,13 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli | |
return builder | ||
} | ||
|
||
private fun getDefaultValueForNumber(shape: Shape, value: String) = when (shape) { | ||
is LongShape -> "${value}L" | ||
is FloatShape -> if (value.matches("[0-9]*\\.[0-9]+".toRegex())) "${value}f" else "$value.0f" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Is the decimal check necessary? The |
||
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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,6 +109,11 @@ fun Symbol.defaultValue(defaultBoxed: String? = "null"): String? { | |
*/ | ||
fun Symbol.Builder.boxed(): Symbol.Builder = apply { putProperty(SymbolProperty.BOXED_KEY, true) } | ||
|
||
/** | ||
* Mark a symbol as not being boxed | ||
*/ | ||
fun Symbol.Builder.unboxed(): Symbol.Builder = apply { removeProperty(SymbolProperty.BOXED_KEY) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Should we be keeping the notion of boxed/unboxed in our codebase at this point? It seems like the nullability index should be preferred at this point, should we rename this or change the way we represent symbol nullability? |
||
|
||
/** | ||
* Set the default value used when formatting the symbol | ||
*/ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -98,6 +98,246 @@ class SymbolProviderTest { | |
else -> typeName | ||
} | ||
|
||
@Test | ||
fun `can read default trait from member`() { | ||
val modeledDefault = "5" | ||
val expectedDefault = "5L" | ||
|
||
val model = """ | ||
structure MyStruct { | ||
@default($modeledDefault) | ||
foo: MyFoo | ||
} | ||
|
||
long MyFoo | ||
""".prependNamespaceAndService(version = "2").toSmithyModel() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Should |
||
|
||
val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) | ||
val member = model.expectShape<MemberShape>("com.test#MyStruct\$foo") | ||
val memberSymbol = provider.toSymbol(member) | ||
assertEquals("kotlin", memberSymbol.namespace) | ||
assertEquals(expectedDefault, memberSymbol.defaultValue()) | ||
} | ||
|
||
@Test | ||
fun `can read default trait from target`() { | ||
val modeledDefault = "2500" | ||
val expectedDefault = "2500L" | ||
|
||
val model = """ | ||
structure MyStruct { | ||
@default($modeledDefault) | ||
foo: MyFoo | ||
} | ||
|
||
@default($modeledDefault) | ||
long MyFoo | ||
""".prependNamespaceAndService(version = "2").toSmithyModel() | ||
|
||
val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) | ||
val member = model.expectShape<MemberShape>("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 overriddenDefault = "null" | ||
|
||
val model = """ | ||
structure MyStruct { | ||
@default($overriddenDefault) | ||
foo: RootLevelShape | ||
} | ||
|
||
@default($modeledDefault) | ||
long RootLevelShape | ||
""".prependNamespaceAndService(version = "2").toSmithyModel() | ||
|
||
val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) | ||
val member = model.expectShape<MemberShape>("com.test#MyStruct\$foo") | ||
val memberSymbol = provider.toSymbol(member) | ||
assertEquals("kotlin", memberSymbol.namespace) | ||
assertEquals(overriddenDefault, memberSymbol.defaultValue()) | ||
} | ||
|
||
@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(version = "2").toSmithyModel() | ||
|
||
val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) | ||
val member = model.expectShape<MemberShape>("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(version = "2").toSmithyModel() | ||
|
||
val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) | ||
val member = model.expectShape<MemberShape>("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(version = "2").toSmithyModel() | ||
|
||
val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) | ||
val member = model.expectShape<MemberShape>("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(version = "2").toSmithyModel() | ||
|
||
val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) | ||
val member = model.expectShape<MemberShape>("com.test#MyStruct\$foo") | ||
val memberSymbol = provider.toSymbol(member) | ||
assertEquals("2", memberSymbol.defaultValue()) | ||
} | ||
|
||
@ParameterizedTest(name = "{index} ==> ''can default document with {0} type''") | ||
@CsvSource( | ||
"null,null,null", | ||
"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(version = "2").toSmithyModel() | ||
|
||
val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) | ||
val member = model.expectShape<MemberShape>("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(version = "2").toSmithyModel() | ||
|
||
val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) | ||
val member = model.expectShape<MemberShape>("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(version = "2").toSmithyModel() | ||
|
||
val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) | ||
val member = model.expectShape<MemberShape>("com.test#MyStruct\$foo") | ||
val memberSymbol = provider.toSymbol(member) | ||
assertEquals("mapOf()", memberSymbol.defaultValue()) | ||
} | ||
|
||
@Test | ||
fun `can read box trait from member`() { | ||
val model = """ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: The
var
reference here is unnecessary. You're already dealing with builders is several places so I suggest just keeping it as a builder until you've applied all the customizations necessary: