From b9ed0c5141c32a02f5eb496e632a8ccea05dc4a5 Mon Sep 17 00:00:00 2001 From: Chase Coalwell <782571+srchase@users.noreply.github.com> Date: Wed, 12 Oct 2022 15:57:13 -0600 Subject: [PATCH] Add intEnum support, tests for enum generation (#605) --- gradle.properties | 2 +- .../codegen/DirectedTypeScriptCodegen.java | 13 +++ .../typescript/codegen/IntEnumGenerator.java | 89 +++++++++++++++++++ .../typescript/codegen/SymbolVisitor.java | 24 +++++ .../typescript/codegen/EnumGeneratorTest.java | 4 +- .../codegen/IntEnumGeneratorTest.java | 37 ++++++++ .../codegen/SymbolProviderTest.java | 60 +++++++++++++ 7 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/IntEnumGenerator.java create mode 100644 smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/IntEnumGeneratorTest.java diff --git a/gradle.properties b/gradle.properties index 4ecbe42ddfc..3dfe2f51a17 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -smithyVersion=[1.25.0,1.26.0[ +smithyVersion=[1.26.0,1.27.0[ smithyGradleVersion=0.6.0 diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java index 53cba654f77..68da5fefca6 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java @@ -37,6 +37,7 @@ import software.amazon.smithy.codegen.core.directed.DirectedCodegen; import software.amazon.smithy.codegen.core.directed.GenerateEnumDirective; import software.amazon.smithy.codegen.core.directed.GenerateErrorDirective; +import software.amazon.smithy.codegen.core.directed.GenerateIntEnumDirective; import software.amazon.smithy.codegen.core.directed.GenerateServiceDirective; import software.amazon.smithy.codegen.core.directed.GenerateStructureDirective; import software.amazon.smithy.codegen.core.directed.GenerateUnionDirective; @@ -376,6 +377,18 @@ public void generateEnumShape(GenerateEnumDirective directive) { + directive.context().writerDelegator().useShapeWriter(directive.shape(), writer -> { + IntEnumGenerator generator = new IntEnumGenerator( + directive.shape().asIntEnumShape().get(), + directive.symbolProvider().toSymbol(directive.shape()), + writer + ); + generator.run(); + }); + } + @Override public void customizeBeforeIntegrations( CustomizeDirective directive) { diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/IntEnumGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/IntEnumGenerator.java new file mode 100644 index 00000000000..19b7bed2e85 --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/IntEnumGenerator.java @@ -0,0 +1,89 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.typescript.codegen; + +import java.util.Comparator; +import java.util.Map; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Generates an appropriate TypeScript type from a Smithy intEnum shape. + * + *

For example, given the following Smithy model:

+ * + *
{@code
+ * intEnum FaceCard {
+ *     JACK = 1
+ *     QUEEN = 2
+ *     KING = 3
+ * }
+ * }
+ * + *

We will generate the following: + * + *

{@code
+ * export enum FaceCard {
+ *   JACK = 1,
+ *   QUEEN = 2,
+ *   KING = 3,
+ * }
+ * }
+ * + *

Shapes that refer to this intEnum as a member will use the following + * generated code: + * + *

{@code
+ * import { FaceCard } from "./FaceCard";
+ *
+ * interface MyStructure {
+ *   "facecard": FaceCard | number;
+ * }
+ * }
+ */ +@SmithyInternalApi +final class IntEnumGenerator implements Runnable { + + private final Symbol symbol; + private final IntEnumShape shape; + private final TypeScriptWriter writer; + + IntEnumGenerator(IntEnumShape shape, Symbol symbol, TypeScriptWriter writer) { + this.shape = shape; + this.symbol = symbol; + this.writer = writer; + } + + @Override + public void run() { + generateIntEnum(); + } + + private void generateIntEnum() { + writer.openBlock("export enum $L {", "}", symbol.getName(), () -> { + // Sort by the values to ensure a stable order and sane diffs. + shape.getEnumValues().entrySet() + .stream() + .sorted(Comparator.comparing(e -> e.getValue())) + .forEach(this::writeIntEnumEntry); + }); + } + + private void writeIntEnumEntry(Map.Entry entry) { + writer.write("$L = $L,", TypeScriptUtils.sanitizePropertyName(entry.getKey()), entry.getValue()); + } +} diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/SymbolVisitor.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/SymbolVisitor.java index 5d744d04e14..707995b7a8c 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/SymbolVisitor.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/SymbolVisitor.java @@ -44,6 +44,7 @@ import software.amazon.smithy.model.shapes.DocumentShape; import software.amazon.smithy.model.shapes.DoubleShape; import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.model.shapes.IntegerShape; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.LongShape; @@ -291,6 +292,11 @@ public Symbol stringShape(StringShape shape) { return createSymbolBuilder(shape, "string").build(); } + @Override + public Symbol intEnumShape(IntEnumShape shape) { + return createObjectSymbolBuilder(shape).build(); + } + private Symbol createEnumSymbol(StringShape shape, EnumTrait enumTrait) { return createObjectSymbolBuilder(shape) .putProperty(EnumTrait.class.getName(), enumTrait) @@ -338,6 +344,10 @@ public Symbol memberShape(MemberShape shape) { .orElseThrow(() -> new CodegenException("Shape not found: " + shape.getTarget())); Symbol targetSymbol = toSymbol(targetShape); + if (targetShape.isIntEnumShape()) { + return createMemberSymbolWithIntEnumTarget(targetSymbol); + } + if (targetSymbol.getProperties().containsKey(EnumTrait.class.getName())) { return createMemberSymbolWithEnumTarget(targetSymbol); } @@ -352,6 +362,9 @@ public Symbol memberShape(MemberShape shape) { return targetSymbol; } + // Enums are considered "open", meaning it is a backward compatible change to add new + // members. Adding the `string` variant allows for previously generated clients to be + // able to handle unknown enum values that may be added in the future. private Symbol createMemberSymbolWithEnumTarget(Symbol targetSymbol) { return targetSymbol.toBuilder() .namespace(null, "/") @@ -360,6 +373,17 @@ private Symbol createMemberSymbolWithEnumTarget(Symbol targetSymbol) { .build(); } + // IntEnums are considered "open", meaning it is a backward compatible change to add new + // members. Adding the `number` variant allows for previously generated clients to be + // able to handle unknown int enum values that may be added in the future. + private Symbol createMemberSymbolWithIntEnumTarget(Symbol targetSymbol) { + return targetSymbol.toBuilder() + .namespace(null, "/") + .name(targetSymbol.getName() + " | number") + .addReference(targetSymbol) + .build(); + } + private Symbol createMemberSymbolWithEventStream(Symbol targetSymbol) { return targetSymbol.toBuilder() .namespace(null, "/") diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/EnumGeneratorTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/EnumGeneratorTest.java index 536325fc724..1039505f94c 100644 --- a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/EnumGeneratorTest.java +++ b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/EnumGeneratorTest.java @@ -2,6 +2,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.stringContainsInOrder; import org.junit.jupiter.api.Test; import software.amazon.smithy.codegen.core.Symbol; @@ -33,8 +34,7 @@ public void generatesNamedEnums() { new EnumGenerator(shape, symbol, writer).run(); assertThat(writer.toString(), containsString("export enum Baz {")); - assertThat(writer.toString(), containsString("FOO = \"FOO\"")); - assertThat(writer.toString(), containsString("BAR = \"BAR\",")); + assertThat(writer.toString(), stringContainsInOrder("BAR = \"BAR\",", "FOO = \"FOO\"")); } @Test diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/IntEnumGeneratorTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/IntEnumGeneratorTest.java new file mode 100644 index 00000000000..7af9f8f63a5 --- /dev/null +++ b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/IntEnumGeneratorTest.java @@ -0,0 +1,37 @@ +package software.amazon.smithy.typescript.codegen; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.stringContainsInOrder; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.IntEnumShape; + +public class IntEnumGeneratorTest { + @Test + public void generatesIntEnums() { + IntEnumShape shape = IntEnumShape.builder() + .id("com.foo#Foo") + .addMember("BAR", 5) + .addMember("BAZ", 2) + .build(); + TypeScriptWriter writer = new TypeScriptWriter("foo"); + Model model = Model.assembler() + .addShape(shape) + .addImport(getClass().getResource("simple-service.smithy")) + .assemble() + .unwrap(); + TypeScriptSettings settings = TypeScriptSettings.from(model, Node.objectNodeBuilder() + .withMember("package", Node.from("example")) + .withMember("packageVersion", Node.from("1.0.0")) + .build()); + Symbol symbol = new SymbolVisitor(model, settings).toSymbol(shape); + new IntEnumGenerator(shape, symbol, writer).run(); + + assertThat(writer.toString(), containsString("export enum Foo {")); + assertThat(writer.toString(), stringContainsInOrder("BAZ = 2,", "BAR = 5,")); + } +} diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/SymbolProviderTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/SymbolProviderTest.java index 6b8cbd8305c..f8555889ce8 100644 --- a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/SymbolProviderTest.java +++ b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/SymbolProviderTest.java @@ -11,12 +11,15 @@ import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.EnumDefinition; +import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.MediaTypeTrait; public class SymbolProviderTest { @@ -189,4 +192,61 @@ public void usesLazyJsonStringForJsonMediaType() { assertThat(memberSymbol.getName(), equalTo("__LazyJsonString | string")); } + + @Test + public void addsUnknownStringEnumVariant() { + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder().value("FOO").name("FOO").build()) + .addEnum(EnumDefinition.builder().value("BAR").name("BAR").build()) + .build(); + StringShape stringShape = StringShape.builder().id("foo.bar#enumString").addTrait(trait).build(); + MemberShape member = MemberShape.builder().id("foo.bar#test$a").target(stringShape).build(); + StructureShape struct = StructureShape.builder() + .id("foo.bar#test") + .addMember(member) + .build(); + Model model = Model.assembler() + .addImport(getClass().getResource("simple-service.smithy")) + .addShapes(struct, member, stringShape) + .assemble() + .unwrap(); + TypeScriptSettings settings = TypeScriptSettings.from(model, Node.objectNodeBuilder() + .withMember("package", Node.from("example")) + .withMember("packageVersion", Node.from("1.0.0")) + .build()); + + SymbolProvider provider = new SymbolVisitor(model, settings); + Symbol memberSymbol = provider.toSymbol(member); + + assertThat(memberSymbol.getName(), equalTo("EnumString | string")); + } + + @Test + public void addsUnknownNumberIntEnumVariant() { + IntEnumShape shape = IntEnumShape.builder() + .id("com.foo#Foo") + .addMember("BAR", 2) + .addMember("BAZ", 5) + .build(); + MemberShape member = MemberShape.builder().id("foo.bar#test$a").target(shape).build(); + StructureShape struct = StructureShape.builder() + .id("foo.bar#test") + .addMember(member) + .build(); + Model model = Model.assembler() + .addImport(getClass().getResource("simple-service.smithy")) + .addShapes(struct, member, shape) + .assemble() + .unwrap(); + TypeScriptSettings settings = TypeScriptSettings.from(model, Node.objectNodeBuilder() + .withMember("package", Node.from("example")) + .withMember("packageVersion", Node.from("1.0.0")) + .build()); + + SymbolProvider provider = new SymbolVisitor(model, settings); + Symbol memberSymbol = provider.toSymbol(member); + + assertThat(memberSymbol.getName(), equalTo("Foo | number")); + } + }