Skip to content

Commit

Permalink
Add intEnum support, tests for enum generation (#605)
Browse files Browse the repository at this point in the history
  • Loading branch information
srchase authored Oct 12, 2022
1 parent fd413c5 commit b9ed0c5
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 3 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
smithyVersion=[1.25.0,1.26.0[
smithyVersion=[1.26.0,1.27.0[
smithyGradleVersion=0.6.0
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -376,6 +377,18 @@ public void generateEnumShape(GenerateEnumDirective<TypeScriptCodegenContext, Ty
});
}

@Override
public void generateIntEnumShape(GenerateIntEnumDirective<TypeScriptCodegenContext, TypeScriptSettings> 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<TypeScriptCodegenContext, TypeScriptSettings> directive) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>For example, given the following Smithy model:</p>
*
* <pre>{@code
* intEnum FaceCard {
* JACK = 1
* QUEEN = 2
* KING = 3
* }
* }</pre>
*
* <p>We will generate the following:
*
* <pre>{@code
* export enum FaceCard {
* JACK = 1,
* QUEEN = 2,
* KING = 3,
* }
* }</pre>
*
* <p>Shapes that refer to this intEnum as a member will use the following
* generated code:
*
* <pre>{@code
* import { FaceCard } from "./FaceCard";
*
* interface MyStructure {
* "facecard": FaceCard | number;
* }
* }</pre>
*/
@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<String, Integer> entry) {
writer.write("$L = $L,", TypeScriptUtils.sanitizePropertyName(entry.getKey()), entry.getValue());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}
Expand All @@ -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, "/")
Expand All @@ -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, "/")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"));
}

}

0 comments on commit b9ed0c5

Please sign in to comment.