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

Migrate to EnumShape #1932

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
package software.amazon.smithy.rust.codegen.client.smithy

import software.amazon.smithy.build.PluginContext
import software.amazon.smithy.codegen.core.CodegenException
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.knowledge.NullableIndex
import software.amazon.smithy.model.neighbor.Walker
import software.amazon.smithy.model.shapes.EnumShape
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.shapes.ShapeVisitor
Expand Down Expand Up @@ -37,7 +39,6 @@ import software.amazon.smithy.rust.codegen.core.smithy.transformers.EventStreamN
import software.amazon.smithy.rust.codegen.core.smithy.transformers.OperationNormalizer
import software.amazon.smithy.rust.codegen.core.smithy.transformers.RecursiveShapeBoxer
import software.amazon.smithy.rust.codegen.core.util.CommandFailed
import software.amazon.smithy.rust.codegen.core.util.getTrait
import software.amazon.smithy.rust.codegen.core.util.hasTrait
import software.amazon.smithy.rust.codegen.core.util.letIf
import software.amazon.smithy.rust.codegen.core.util.runCommand
Expand Down Expand Up @@ -94,6 +95,8 @@ class CodegenVisitor(
*/
internal fun baselineTransform(model: Model) =
model
// Convert string shapes with an @enum trait to an enum shape
.let { ModelTransformer.create().changeStringEnumsToEnumShapes(model, true) }
82marbag marked this conversation as resolved.
Show resolved Hide resolved
// Flattens mixins out of the model and removes them from the model
.let { ModelTransformer.create().flattenAndRemoveMixins(it) }
// Add errors attached at the service level to the models
Expand Down Expand Up @@ -194,13 +197,24 @@ class CodegenVisitor(
/**
* String Shape Visitor
*
* Although raw strings require no code generation, enums are actually `EnumTrait` applied to string shapes.
* Unnamed @enum shapes are not supported. If they could not be converted to EnumShape, this will fail.
*/
override fun stringShape(shape: StringShape) {
shape.getTrait<EnumTrait>()?.also { enum ->
rustCrate.useShapeWriter(shape) {
EnumGenerator(model, symbolProvider, this, shape, enum).render()
}
if (shape.hasTrait<EnumTrait>()) {
throw CodegenException(
"Code generation has failed because this unnamed @enum could not be converted to an enum shape: $shape." +
"For more info, look above at the logs from awslabs/smithy.",
)
}
super.stringShape(shape)
}

/**
* Enum Shape Visitor
*/
override fun enumShape(shape: EnumShape) {
rustCrate.useShapeWriter(shape) {
EnumGenerator(model, symbolProvider, this, shape).render()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package software.amazon.smithy.rust.codegen.client.smithy
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.traits.EnumTrait
import software.amazon.smithy.rust.codegen.client.smithy.customizations.ClientCustomizations
import software.amazon.smithy.rust.codegen.client.smithy.customize.CombinedCodegenDecorator
import software.amazon.smithy.rust.codegen.client.smithy.customize.NoOpEventStreamSigningDecorator
Expand Down Expand Up @@ -58,4 +59,73 @@ class CodegenVisitorTest {
val baselineModel = visitor.baselineTransform(model)
baselineModel.getShapesWithTrait(ShapeId.from("smithy.api#mixin")).isEmpty() shouldBe true
}

@Test
fun `baseline transform verify string enum converted to EnumShape`() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd add a negative test too that asserts we throw an exception when an unnamed enum could not be converted to an enum shape.

val model = """
namespace com.example
use aws.protocols#restJson1
@restJson1
service Example {
operations: [ BasicOperation ]
}
operation BasicOperation {
input: Shape
}
structure Shape {
enum: BasicEnum
}
@enum([
{
value: "a0",
},
{
value: "a1",
}
])
string BasicEnum
""".asSmithyModel(smithyVersion = "2.0")
82marbag marked this conversation as resolved.
Show resolved Hide resolved
val (ctx, _) = generatePluginContext(model)
val codegenDecorator =
CombinedCodegenDecorator.fromClasspath(
ctx,
ClientCustomizations(),
)
val visitor = CodegenVisitor(ctx, codegenDecorator)
val baselineModel = visitor.baselineTransform(model)
baselineModel.getShapesWithTrait(EnumTrait.ID).isEmpty() shouldBe true
}

@Test
fun `baseline transform verify bad string enum throws if not converted to EnumShape`() {
val model = """
namespace com.example
use aws.protocols#restJson1
@restJson1
service Example {
operations: [ BasicOperation ]
}
operation BasicOperation {
input: Shape
}
structure Shape {
enum: BasicEnum
}
@enum([
{
value: "$ a/0",
},
])
string BasicEnum
""".asSmithyModel(smithyVersion = "2.0")
val (ctx, _) = generatePluginContext(model)
val codegenDecorator =
CombinedCodegenDecorator.fromClasspath(
ctx,
ClientCustomizations(),
)
val visitor = CodegenVisitor(ctx, codegenDecorator)
val baselineModel = visitor.baselineTransform(model)
baselineModel.getShapesWithTrait(EnumTrait.ID).isEmpty() shouldBe false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package software.amazon.smithy.rust.codegen.client.smithy.generators

import org.junit.jupiter.api.Test
import software.amazon.smithy.model.node.Node
import software.amazon.smithy.model.shapes.StringShape
import software.amazon.smithy.model.shapes.EnumShape
import software.amazon.smithy.rust.codegen.client.testutil.testCodegenContext
import software.amazon.smithy.rust.codegen.core.rustlang.RustModule
import software.amazon.smithy.rust.codegen.core.rustlang.rust
Expand All @@ -18,19 +18,12 @@ import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest
import software.amazon.smithy.rust.codegen.core.testutil.unitTest
import software.amazon.smithy.rust.codegen.core.util.dq
import software.amazon.smithy.rust.codegen.core.util.expectTrait
import software.amazon.smithy.rust.codegen.core.util.lookup

internal class ClientInstantiatorTest {
private val model = """
namespace com.test

@enum([
{ value: "t2.nano" },
{ value: "t2.micro" },
])
string UnnamedEnum

@enum([
{
value: "t2.nano",
Expand All @@ -49,13 +42,13 @@ internal class ClientInstantiatorTest {

@Test
fun `generate named enums`() {
val shape = model.lookup<StringShape>("com.test#NamedEnum")
val shape = model.lookup<EnumShape>("com.test#NamedEnum")
val sut = clientInstantiator(codegenContext)
val data = Node.parse("t2.nano".dq())

val project = TestWorkspace.testProject()
project.withModule(RustModule.Model) {
EnumGenerator(model, symbolProvider, this, shape, shape.expectTrait()).render()
EnumGenerator(model, symbolProvider, this, shape).render()
unitTest("generate_named_enums") {
withBlock("let result = ", ";") {
sut.render(this, shape, data)
Expand All @@ -65,23 +58,4 @@ internal class ClientInstantiatorTest {
}
project.compileAndTest()
}

@Test
fun `generate unnamed enums`() {
val shape = model.lookup<StringShape>("com.test#UnnamedEnum")
val sut = clientInstantiator(codegenContext)
val data = Node.parse("t2.nano".dq())

val project = TestWorkspace.testProject()
project.withModule(RustModule.Model) {
EnumGenerator(model, symbolProvider, this, shape, shape.expectTrait()).render()
unitTest("generate_unnamed_enums") {
withBlock("let result = ", ";") {
sut.render(this, shape, data)
}
rust("""assert_eq!(result, UnnamedEnum("t2.nano".to_owned()));""")
}
}
project.compileAndTest()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,17 @@ package software.amazon.smithy.rust.codegen.core.smithy
import software.amazon.smithy.codegen.core.CodegenException
import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.EnumShape
import software.amazon.smithy.model.shapes.MemberShape
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.shapes.StringShape
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.shapes.UnionShape
import software.amazon.smithy.model.traits.EnumDefinition
import software.amazon.smithy.model.traits.EnumTrait
import software.amazon.smithy.model.traits.StreamingTrait
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata
import software.amazon.smithy.rust.codegen.core.rustlang.Visibility
import software.amazon.smithy.rust.codegen.core.util.hasTrait

/**
* Default delegator to enable easily decorating another symbol provider.
Expand Down Expand Up @@ -54,9 +53,8 @@ abstract class SymbolMetadataProvider(private val base: RustSymbolProvider) : Wr
is MemberShape -> memberMeta(shape)
is StructureShape -> structureMeta(shape)
is UnionShape -> unionMeta(shape)
is StringShape -> if (shape.hasTrait<EnumTrait>()) {
enumMeta(shape)
} else null
is EnumShape -> enumMeta(shape)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can now make enumMeta accept EnumShape instead of StringShape.

is StringShape -> null

else -> null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import software.amazon.smithy.model.shapes.BooleanShape
import software.amazon.smithy.model.shapes.ByteShape
import software.amazon.smithy.model.shapes.DocumentShape
import software.amazon.smithy.model.shapes.DoubleShape
import software.amazon.smithy.model.shapes.EnumShape
import software.amazon.smithy.model.shapes.FloatShape
import software.amazon.smithy.model.shapes.IntegerShape
import software.amazon.smithy.model.shapes.ListShape
Expand All @@ -36,7 +37,6 @@ import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.shapes.TimestampShape
import software.amazon.smithy.model.shapes.UnionShape
import software.amazon.smithy.model.traits.EnumDefinition
import software.amazon.smithy.model.traits.EnumTrait
import software.amazon.smithy.model.traits.ErrorTrait
import software.amazon.smithy.rust.codegen.core.rustlang.RustModule
import software.amazon.smithy.rust.codegen.core.rustlang.RustType
Expand Down Expand Up @@ -255,14 +255,11 @@ open class SymbolVisitor(
override fun longShape(shape: LongShape): Symbol = simpleShape(shape)
override fun floatShape(shape: FloatShape): Symbol = simpleShape(shape)
override fun doubleShape(shape: DoubleShape): Symbol = simpleShape(shape)
override fun stringShape(shape: StringShape): Symbol {
return if (shape.hasTrait<EnumTrait>()) {
val rustType = RustType.Opaque(shape.contextName(serviceShape).toPascalCase())
symbolBuilder(shape, rustType).locatedIn(ModelsModule).build()
} else {
simpleShape(shape)
}
override fun enumShape(shape: EnumShape): Symbol {
val rustType = RustType.Opaque(shape.contextName(serviceShape).toPascalCase())
return symbolBuilder(shape, rustType).locatedIn(ModelsModule).build()
}
override fun stringShape(shape: StringShape): Symbol = simpleShape(shape)

override fun listShape(shape: ListShape): Symbol {
val inner = this.toSymbol(shape.member)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ package software.amazon.smithy.rust.codegen.core.smithy.generators

import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.StringShape
import software.amazon.smithy.model.shapes.EnumShape
import software.amazon.smithy.model.traits.DeprecatedTrait
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
import software.amazon.smithy.rust.codegen.core.rustlang.documentShape
import software.amazon.smithy.rust.codegen.core.rustlang.escape
import software.amazon.smithy.rust.codegen.core.rustlang.rust
import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock
Expand All @@ -29,6 +28,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.expectRustMetadata
import software.amazon.smithy.rust.codegen.core.util.doubleQuote
import software.amazon.smithy.rust.codegen.core.util.dq
import software.amazon.smithy.rust.codegen.core.util.getTrait
import software.amazon.smithy.rust.codegen.core.util.hasTrait
import software.amazon.smithy.rust.codegen.core.util.orNull

/** Model that wraps [EnumDefinition] to calculate and cache values required to generate the Rust enum source. */
Expand Down Expand Up @@ -86,14 +86,26 @@ open class EnumGenerator(
private val model: Model,
private val symbolProvider: RustSymbolProvider,
private val writer: RustWriter,
protected val shape: StringShape,
protected val enumTrait: EnumTrait,
protected val shape: EnumShape,
) {
protected val symbol: Symbol = symbolProvider.toSymbol(shape)
protected val enumName: String = symbol.name
protected val meta = symbol.expectRustMetadata()
protected val sortedMembers: List<EnumMemberModel> =
enumTrait.values.sortedBy { it.value }.map { EnumMemberModel(it, symbolProvider) }
shape.allMembers.entries.sortedBy { it.key }.map {
val builder = EnumDefinition.builder()
.name(it.key)
.value(shape.enumValues[it.key])
.deprecated(it.value.hasTrait<DeprecatedTrait>())
.tags(it.value.tags)
if (it.value.hasTrait<DocumentationTrait>()) {
builder.documentation(it.value.expectTrait(DocumentationTrait::class.java).value)
}
EnumMemberModel(
builder.build(),
symbolProvider,
)
}
protected open var target: CodegenTarget = CodegenTarget.CLIENT

companion object {
Expand All @@ -108,50 +120,19 @@ open class EnumGenerator(
}

open fun render() {
if (enumTrait.hasNames()) {
// pub enum Blah { V1, V2, .. }
renderEnum()
writer.insertTrailingNewline()
// impl From<str> for Blah { ... }
renderFromForStr()
// impl FromStr for Blah { ... }
renderFromStr()
writer.insertTrailingNewline()
// impl Blah { pub fn as_str(&self) -> &str
implBlock()
writer.rustBlock("impl AsRef<str> for $enumName") {
rustBlock("fn as_ref(&self) -> &str") {
rust("self.as_str()")
}
}
} else {
renderUnnamedEnum()
82marbag marked this conversation as resolved.
Show resolved Hide resolved
}
}

private fun renderUnnamedEnum() {
writer.documentShape(shape, model)
writer.deprecatedShape(shape)
meta.render(writer)
writer.write("struct $enumName(String);")
writer.rustBlock("impl $enumName") {
docs("Returns the `&str` value of the enum member.")
rustBlock("pub fn as_str(&self) -> &str") {
rust("&self.0")
}

docs("Returns all the `&str` representations of the enum members.")
rustBlock("pub const fn $Values() -> &'static [&'static str]") {
withBlock("&[", "]") {
val memberList = sortedMembers.joinToString(", ") { it.value.dq() }
rust(memberList)
}
}
}

writer.rustBlock("impl <T> #T<T> for $enumName where T: #T<str>", RuntimeType.From, RuntimeType.AsRef) {
rustBlock("fn from(s: T) -> Self") {
rust("$enumName(s.as_ref().to_owned())")
// pub enum Blah { V1, V2, .. }
renderEnum()
writer.insertTrailingNewline()
// impl From<str> for Blah { ... }
renderFromForStr()
// impl FromStr for Blah { ... }
renderFromStr()
writer.insertTrailingNewline()
// impl Blah { pub fn as_str(&self) -> &str
implBlock()
writer.rustBlock("impl AsRef<str> for $enumName") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that we're touching this, we can rewrite it to make use of a single rust() call.

rustBlock("fn as_ref(&self) -> &str") {
rust("self.as_str()")
}
}
}
Expand Down
Loading