-
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(codegen): improve nullability of generated types #968
Conversation
612a292
to
fa93151
Compare
fdad0eb
to
c4513c2
Compare
/** | ||
* Always serialize default values even if they are set to the default | ||
*/ | ||
ALWAYS("always"), | ||
|
||
/** | ||
* Only serialize default values when they differ from the default given in the model. | ||
*/ | ||
WHEN_DIFFERENT("whenDifferent"), | ||
; |
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: These KDocs are confusing to me because they use the term "default values" to (I believe) indicate "values set on fields which have the @default
trait". Perhaps they would be clearer as:
- Always serialize values even when they are set to the default given in the model
- Only serialize values when they differ from the default given in the 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.
or just add one word: "modeled default"
*/ | ||
WHEN_DIFFERENT("whenDifferent"), | ||
; | ||
override fun toString(): String = value |
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.
Question: Why override this? Does some code rely on the output of toString
?
private fun checkModefromValue(value: String): CheckMode { | ||
val camelCaseToMode = CheckMode.values().associateBy { it.toString().toCamelCase() } | ||
return requireNotNull(camelCaseToMode[value]) { "$value is not a valid CheckMode, expected one of ${camelCaseToMode.keys}" } | ||
} |
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.
Correctness: This is a case-sensitive comparison. Do we want that?
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.
Do we not?
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.
I think we do not. Build files will be written mostly by humans and so I think we should be forgiving about capitalization.
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.
I disagree. It results in an error so it's not like a human isn't going to find it and I'd rather be explicit.
fun fromValue(value: String): DefaultValueSerializationMode = | ||
values().find { | ||
it.value == value | ||
} ?: throw IllegalArgumentException("$value is not a valid DefaultValueSerializationMode, expected one of ${values().map { it.value }}") |
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.
Correctness: This is a case-sensitive comparison. Do we want that?
|
||
fun fromNode(node: Optional<ObjectNode>): ApiSettings = node.map { | ||
val visibility = Visibility.fromValue(node.get().getStringMemberOrDefault(VISIBILITY, "public")) | ||
ApiSettings(visibility) | ||
val checkMode = checkModefromValue(node.get().getStringMemberOrDefault(NULLABILITY_CHECK_MODE, "clientCareful")) |
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.
Style: This seems odd because it specifies as the default a string constant which isn't verified at compile time. This might be safer as getting the nullable string member value, ?.let
passing that to the deserializer function, and then null-coalescing to CheckMode.CLIENT_CAREFUL
.
// FIXME - we only generate an endpoint provider type if we have a protocol generator defined | ||
if (context.protocolGenerator != null) { |
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.
Question: Why did this change?
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.
So that we don't have to add a hand written endpoint provider in things like waiter-tests
, nullability-tests
, and paginator-tests
. We shouldn't be generating code that doesn't compile by default and we should likely be generating those definitions without the protocol generator.
|
||
writer.write("input.#L?.let { field(#L, it, #L) }", memberName, memberShape.descriptorName(), tsFormat) | ||
val fn = serializerFn.format(memberShape, "input.$memberName") | ||
writer.write("${defaultCheck}${fn}$postfix") |
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.
Nit: Braces unnecessary.
@@ -510,4 +510,74 @@ internal class SmokeTestOperationDeserializer: HttpDeserialize<SmokeTestResponse | |||
""" | |||
contents.shouldContainOnlyOnceWithDiff(expected) | |||
} | |||
|
|||
@Test | |||
fun itValidatesRequiredAndNonBlankURIBindings() { |
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.
Nit: URI
→ Uri
doLast { | ||
} |
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.
Question: What does this do?
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.
🤔 , nothing not sure where it came from.
private const val SmithyVersion = "1.0" | ||
private const val SmithyVersion = "2.0" |
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.
Question: Where is this used?
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.
Good question, I believe it's used in the test utils somewhere as the model version.
/** | ||
* Always serialize default values even if they are set to the default | ||
*/ | ||
ALWAYS("always"), | ||
|
||
/** | ||
* Only serialize default values when they differ from the default given in the model. | ||
*/ | ||
WHEN_DIFFERENT("whenDifferent"), | ||
; |
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.
or just add one word: "modeled default"
val prefix = if (shape.isError && memberName == "message") { | ||
val targetShape = model.expectShape(memberShape.target) | ||
if (!targetShape.isStringShape) { | ||
throw CodegenException("Message is a reserved name for exception types and cannot be used for any other property") |
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.
Should the exception refer to lowercase "message"?
|
||
plugins { | ||
kotlin("jvm") | ||
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed |
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.
It's fixed in Gradle 8.1, maybe we should add that detail?
61d90fe
to
1b7920f
Compare
…to only serializing when value differs from runtime
1b7920f
to
fccdb86
Compare
Kudos, SonarCloud Quality Gate passed! 0 Bugs No Coverage information |
Issue #
n/a
Description of changes
Previously we set nullability information on symbols coming out of
KotlinSymbolProvider
based on the underlying typeand whether a default value was provided. This meant things like
List
,String
,Enum
, etc were always considered nullable. This was based on IDL v1 semantics w.r.t `Primitive*" shapes being the only shapes that were non-nullable (since they had an implicit default).This PR removes all notion of how we considered nullability previously to instead rely entirely on the
NullableIndex
as the source of truth. The new mental model is that converting any old shape (in particular explicit
member.target
)is not going to give you nullability information. Only member shapes will. This is because nullability is local to how
a target shape is used in the context of some container (e.g. structure) shape.
For operation inputs customers will now see a runtime exception for any
@required
member without a@default
set.This shouldn't affect anyone with a working application assuming services actually validated these fields as required.
For operation outputs any missing field will be corrected using client error correction introduced in #958 such that all non-nullable members of the output struct end up with a value instead of throwing an exception.
CLIENT_CAREFUL
.We now generate structures with non-null members anywhere
NullableIndex
indicates a members is non-nullable. Previously this would have been onlyPrimitive*
shapes. We also update when we respect@default
trait and only set a default in the builder (on the symbol) ifNullableIndex
indicates that it's non-nullableT
@required
. The default settings is the same but can be updated to "always" which would serialize defaults even if they aren't different.httpLabel
required checks introduced in fix: require values for HTTP query- and queryParams-bound parameters #697 to the operation serializer.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.