Skip to content
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

Rework enum for forward-compatibility #1945

Merged
merged 28 commits into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cd61a8e
Make enum forward-compatible
Nov 2, 2022
120be37
Add unit test to ensure enums are forward-compatible
Nov 3, 2022
57a5147
Generate rustdoc for enum's forward-compatibility
Nov 4, 2022
fa86f2d
Make snippet in rustdoc text
Nov 4, 2022
6511cf7
Suppress missing doc lint for UnknownVariantValue
Nov 4, 2022
975fa1d
Update codegen-core/src/main/kotlin/software/amazon/smithy/rust/codeg…
ysaito1001 Nov 8, 2022
99a645f
Generate UnknownVariantValue via forInlineFun
Nov 8, 2022
8edd3c8
Merge branch 'ysaito/rework-enum-for-forward-compatibility' of https:…
Nov 8, 2022
83a15e0
Replace "target == CodegenTarget.CLIENT" with a helper
Nov 8, 2022
712c983
Update EnumGeneratorTests to use TestWorkspace
Nov 8, 2022
92daa79
Merge branch 'main' into ysaito/rework-enum-for-forward-compatibility
Nov 8, 2022
73bb0de
Make sure to use the passed-in variable for shapeId
Nov 8, 2022
2055e04
Address https://github.com/awslabs/smithy-rs/pull/1945\#discussion_r1…
Nov 8, 2022
38d371b
Update CHANGELOG.next.toml
Nov 8, 2022
6e8cbe6
Update CHANGELOG.next.toml
Nov 9, 2022
7e14280
Avoid potential name collisions by UnknownVariantValue
Nov 9, 2022
6fd1c57
Move re-exports from lib.rs to types.rs
ysaito1001 Nov 11, 2022
d107741
Merge branch 'main' into ysaito/rework-enum-for-forward-compatibility
ysaito1001 Nov 11, 2022
55f8de7
Add docs on UnknownVariantValue
ysaito1001 Nov 11, 2022
04e1a22
Update CHANGELOG.next.toml
ysaito1001 Nov 11, 2022
5972c11
Update the module documentation for types
ysaito1001 Nov 14, 2022
7d4d43a
Add extensions to run code block depending on the target
ysaito1001 Nov 14, 2022
daa611c
Merge branch 'main' into ysaito/rework-enum-for-forward-compatibility
ysaito1001 Nov 14, 2022
c56ad07
Update codegen-core/src/main/kotlin/software/amazon/smithy/rust/codeg…
ysaito1001 Nov 14, 2022
7b719e1
Move extensions into CodegenTarget as methods
ysaito1001 Nov 14, 2022
127644c
Merge branch 'main' into ysaito/rework-enum-for-forward-compatibility
ysaito1001 Nov 16, 2022
5e36aa5
Merge branch 'main' into ysaito/rework-enum-for-forward-compatibility
ysaito1001 Nov 16, 2022
1fafa49
Merge branch 'main' into ysaito/rework-enum-for-forward-compatibility
ysaito1001 Nov 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.next.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,8 @@ references = ["smithy-rs#1935"]
meta = { "breaking" = true, "tada" = false, "bug" = false }
author = "jdisanti"

[[smithy-rs]]
ysaito1001 marked this conversation as resolved.
Show resolved Hide resolved
message = "Generate enums that guide the users to write match expressions in a forward-compatible way."
ysaito1001 marked this conversation as resolved.
Show resolved Hide resolved
references = ["smithy-rs#1945"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client"}
author = "ysaito1001"
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ abstract class SymbolMetadataProvider(private val base: RustSymbolProvider) : Wr
is StringShape -> if (shape.hasTrait<EnumTrait>()) {
enumMeta(shape)
} else null

else -> null
}
return baseSymbol.toBuilder().meta(meta).build()
Expand Down Expand Up @@ -100,11 +101,13 @@ class BaseSymbolMetadataProvider(
)
}
}

container.isUnionShape ||
container.isListShape ||
container.isSetShape ||
container.isMapShape
-> RustMetadata(visibility = Visibility.PUBLIC)

else -> TODO("Unrecognized container type: $container")
}
}
Expand All @@ -120,9 +123,10 @@ class BaseSymbolMetadataProvider(
override fun enumMeta(stringShape: StringShape): RustMetadata {
return containerDefault.withDerives(
RuntimeType.std.member("hash::Hash"),
).withDerives( // enums can be eq because they can only contain strings
).withDerives(
// enums can be eq because they can only contain ints and strings
RuntimeType.std.member("cmp::Eq"),
// enums can be Ord because they can only contain strings
// enums can be Ord because they can only contain ints and strings
RuntimeType.std.member("cmp::PartialOrd"),
RuntimeType.std.member("cmp::Ord"),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import software.amazon.smithy.model.traits.DocumentationTrait
import software.amazon.smithy.model.traits.EnumDefinition
import software.amazon.smithy.model.traits.EnumTrait
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
import software.amazon.smithy.rust.codegen.core.rustlang.RustModule
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.core.rustlang.deprecatedShape
import software.amazon.smithy.rust.codegen.core.rustlang.docs
Expand Down Expand Up @@ -99,6 +100,9 @@ open class EnumGenerator(
/** Name of the generated unknown enum member name for enums with named members. */
const val UnknownVariant = "Unknown"

/** Name of the opaque struct that is inner data for the generated [UnknownVariant]. */
const val UnknownVariantValue = "UnknownVariantValue"

/** Name of the function on the enum impl to get a vec of value names */
const val Values = "values"
}
Expand Down Expand Up @@ -153,6 +157,10 @@ open class EnumGenerator(
}

private fun renderEnum() {
if (target.renderUnknownVariant()) {
writer.renderForwardCompatibilityNote(enumName, sortedMembers, UnknownVariant, UnknownVariantValue)
}

val renamedWarning =
sortedMembers.mapNotNull { it.name() }.filter { it.renamedFrom != null }.joinToString("\n") {
val previousName = it.renamedFrom!!
Expand All @@ -167,9 +175,9 @@ open class EnumGenerator(
meta.render(writer)
writer.rustBlock("enum $enumName") {
sortedMembers.forEach { member -> member.render(writer) }
if (target == CodegenTarget.CLIENT) {
docs("$UnknownVariant contains new variants that have been added since this code was generated.")
rust("$UnknownVariant(String)")
if (target.renderUnknownVariant()) {
docs("`$UnknownVariant` contains new variants that have been added since this code was generated.")
rust("$UnknownVariant(#T)", unknownVariantValue())
}
}
}
Expand All @@ -182,8 +190,8 @@ open class EnumGenerator(
sortedMembers.forEach { member ->
rust("""$enumName::${member.derivedName()} => ${member.value.dq()},""")
}
if (target == CodegenTarget.CLIENT) {
rust("$enumName::$UnknownVariant(s) => s.as_ref()")
if (target.renderUnknownVariant()) {
rust("$enumName::$UnknownVariant(value) => value.as_str()")
}
}
}
Expand All @@ -198,14 +206,28 @@ open class EnumGenerator(
}
}

private fun unknownVariantValue(): RuntimeType {
return RuntimeType.forInlineFun(UnknownVariantValue, RustModule.Model) {
ysaito1001 marked this conversation as resolved.
Show resolved Hide resolved
rust("##[allow(missing_docs)]")
ysaito1001 marked this conversation as resolved.
Show resolved Hide resolved
meta.render(this)
rust("struct $UnknownVariantValue(String);")
rustBlock("impl $UnknownVariantValue") {
// The generated as_str is not pub as we need to prevent users from calling it on this opaque struct.
rustBlock("fn as_str(&self) -> &str") {
rust("&self.0")
}
}
}
}

protected open fun renderFromForStr() {
writer.rustBlock("impl #T<&str> for $enumName", RuntimeType.From) {
rustBlock("fn from(s: &str) -> Self") {
rustBlock("match s") {
sortedMembers.forEach { member ->
rust("""${member.value.dq()} => $enumName::${member.derivedName()},""")
}
rust("other => $enumName::$UnknownVariant(other.to_owned())")
rust("other => $enumName::$UnknownVariant(#T(other.to_owned()))", unknownVariantValue())
}
}
}
Expand All @@ -225,3 +247,61 @@ open class EnumGenerator(
)
}
}

/**
* Generate the rustdoc describing how to write a match expression against a generated enum in a
* forward-compatible way.
*/
private fun RustWriter.renderForwardCompatibilityNote(
ysaito1001 marked this conversation as resolved.
Show resolved Hide resolved
enumName: String, sortedMembers: List<EnumMemberModel>,
unknownVariant: String, unknownVariantValue: String,
) {
docs(
"""
When writing a match expression against `$enumName`, it is important to ensure
your code is forward-compatible. That is, if a match arm handles a case for a
feature that is supported by the service but has not been represented as an enum
variant in a current version of SDK, your code should continue to work when you
upgrade SDK to a future version in which the enum does include a variant for that
feature.
""".trimIndent(),
)
docs("")
docs("Here is an example of how you can make a match expression forward-compatible:")
docs("")
docs("```text")
rust("/// ## let ${enumName.lowercase()} = unimplemented!();")
rust("/// match ${enumName.lowercase()} {")
sortedMembers.mapNotNull { it.name() }.forEach { member ->
rust("/// $enumName::${member.name} => { /* ... */ },")
}
rust("""/// other @ _ if other.as_str() == "NewFeature" => { /* handles a case for `NewFeature` */ },""")
rust("/// _ => { /* ... */ },")
rust("/// }")
docs("```")
docs(
"""
The above code demonstrates that when `${enumName.lowercase()}` represents
`NewFeature`, the execution path will lead to the second last match arm,
even though the enum does not contain a variant `$enumName::NewFeature`
in the current version of SDK. The reason is that the variable `other`,
created by the `@` operator, is bound to
`$enumName::$unknownVariant($unknownVariantValue("NewFeature".to_owned()))`
and calling `as_str` on it yields `"NewFeature"`.
This match expression is forward-compatible when executed with a newer
version of SDK where the variant `$enumName::NewFeature` is defined.
Specifically, when `${enumName.lowercase()}` represents `NewFeature`,
the execution path will hit the second last match arm as before by virtue of
calling `as_str` on `$enumName::NewFeature` also yielding `"NewFeature"`.
""".trimIndent(),
)
docs("")
docs(
"""
Explicitly matching on the `$unknownVariant` variant should
be avoided for two reasons:
- The inner data `$unknownVariantValue` is opaque, and no further information can be extracted.
- It might inadvertently shadow other intended match arms.
""".trimIndent(),
)
}
Loading