Skip to content

Commit b9c39ed

Browse files
committed
Add intEnum support, tests for enum generation (smithy-lang#605)
1 parent 5c9808a commit b9c39ed

File tree

7 files changed

+226
-3
lines changed

7 files changed

+226
-3
lines changed

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
smithyVersion=[1.25.0,1.26.0[
1+
smithyVersion=[1.26.0,1.27.0[
22
smithyGradleVersion=0.6.0

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java

+13
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import software.amazon.smithy.codegen.core.directed.DirectedCodegen;
3838
import software.amazon.smithy.codegen.core.directed.GenerateEnumDirective;
3939
import software.amazon.smithy.codegen.core.directed.GenerateErrorDirective;
40+
import software.amazon.smithy.codegen.core.directed.GenerateIntEnumDirective;
4041
import software.amazon.smithy.codegen.core.directed.GenerateServiceDirective;
4142
import software.amazon.smithy.codegen.core.directed.GenerateStructureDirective;
4243
import software.amazon.smithy.codegen.core.directed.GenerateUnionDirective;
@@ -376,6 +377,18 @@ public void generateEnumShape(GenerateEnumDirective<TypeScriptCodegenContext, Ty
376377
});
377378
}
378379

380+
@Override
381+
public void generateIntEnumShape(GenerateIntEnumDirective<TypeScriptCodegenContext, TypeScriptSettings> directive) {
382+
directive.context().writerDelegator().useShapeWriter(directive.shape(), writer -> {
383+
IntEnumGenerator generator = new IntEnumGenerator(
384+
directive.shape().asIntEnumShape().get(),
385+
directive.symbolProvider().toSymbol(directive.shape()),
386+
writer
387+
);
388+
generator.run();
389+
});
390+
}
391+
379392
@Override
380393
public void customizeBeforeIntegrations(
381394
CustomizeDirective<TypeScriptCodegenContext, TypeScriptSettings> directive) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.smithy.typescript.codegen;
17+
18+
import java.util.Comparator;
19+
import java.util.Map;
20+
import software.amazon.smithy.codegen.core.Symbol;
21+
import software.amazon.smithy.model.shapes.IntEnumShape;
22+
import software.amazon.smithy.utils.SmithyInternalApi;
23+
24+
/**
25+
* Generates an appropriate TypeScript type from a Smithy intEnum shape.
26+
*
27+
* <p>For example, given the following Smithy model:</p>
28+
*
29+
* <pre>{@code
30+
* intEnum FaceCard {
31+
* JACK = 1
32+
* QUEEN = 2
33+
* KING = 3
34+
* }
35+
* }</pre>
36+
*
37+
* <p>We will generate the following:
38+
*
39+
* <pre>{@code
40+
* export enum FaceCard {
41+
* JACK = 1,
42+
* QUEEN = 2,
43+
* KING = 3,
44+
* }
45+
* }</pre>
46+
*
47+
* <p>Shapes that refer to this intEnum as a member will use the following
48+
* generated code:
49+
*
50+
* <pre>{@code
51+
* import { FaceCard } from "./FaceCard";
52+
*
53+
* interface MyStructure {
54+
* "facecard": FaceCard | number;
55+
* }
56+
* }</pre>
57+
*/
58+
@SmithyInternalApi
59+
final class IntEnumGenerator implements Runnable {
60+
61+
private final Symbol symbol;
62+
private final IntEnumShape shape;
63+
private final TypeScriptWriter writer;
64+
65+
IntEnumGenerator(IntEnumShape shape, Symbol symbol, TypeScriptWriter writer) {
66+
this.shape = shape;
67+
this.symbol = symbol;
68+
this.writer = writer;
69+
}
70+
71+
@Override
72+
public void run() {
73+
generateIntEnum();
74+
}
75+
76+
private void generateIntEnum() {
77+
writer.openBlock("export enum $L {", "}", symbol.getName(), () -> {
78+
// Sort by the values to ensure a stable order and sane diffs.
79+
shape.getEnumValues().entrySet()
80+
.stream()
81+
.sorted(Comparator.comparing(e -> e.getValue()))
82+
.forEach(this::writeIntEnumEntry);
83+
});
84+
}
85+
86+
private void writeIntEnumEntry(Map.Entry<String, Integer> entry) {
87+
writer.write("$L = $L,", TypeScriptUtils.sanitizePropertyName(entry.getKey()), entry.getValue());
88+
}
89+
}

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/SymbolVisitor.java

+24
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import software.amazon.smithy.model.shapes.DocumentShape;
4545
import software.amazon.smithy.model.shapes.DoubleShape;
4646
import software.amazon.smithy.model.shapes.FloatShape;
47+
import software.amazon.smithy.model.shapes.IntEnumShape;
4748
import software.amazon.smithy.model.shapes.IntegerShape;
4849
import software.amazon.smithy.model.shapes.ListShape;
4950
import software.amazon.smithy.model.shapes.LongShape;
@@ -291,6 +292,11 @@ public Symbol stringShape(StringShape shape) {
291292
return createSymbolBuilder(shape, "string").build();
292293
}
293294

295+
@Override
296+
public Symbol intEnumShape(IntEnumShape shape) {
297+
return createObjectSymbolBuilder(shape).build();
298+
}
299+
294300
private Symbol createEnumSymbol(StringShape shape, EnumTrait enumTrait) {
295301
return createObjectSymbolBuilder(shape)
296302
.putProperty(EnumTrait.class.getName(), enumTrait)
@@ -338,6 +344,10 @@ public Symbol memberShape(MemberShape shape) {
338344
.orElseThrow(() -> new CodegenException("Shape not found: " + shape.getTarget()));
339345
Symbol targetSymbol = toSymbol(targetShape);
340346

347+
if (targetShape.isIntEnumShape()) {
348+
return createMemberSymbolWithIntEnumTarget(targetSymbol);
349+
}
350+
341351
if (targetSymbol.getProperties().containsKey(EnumTrait.class.getName())) {
342352
return createMemberSymbolWithEnumTarget(targetSymbol);
343353
}
@@ -352,6 +362,9 @@ public Symbol memberShape(MemberShape shape) {
352362
return targetSymbol;
353363
}
354364

365+
// Enums are considered "open", meaning it is a backward compatible change to add new
366+
// members. Adding the `string` variant allows for previously generated clients to be
367+
// able to handle unknown enum values that may be added in the future.
355368
private Symbol createMemberSymbolWithEnumTarget(Symbol targetSymbol) {
356369
return targetSymbol.toBuilder()
357370
.namespace(null, "/")
@@ -360,6 +373,17 @@ private Symbol createMemberSymbolWithEnumTarget(Symbol targetSymbol) {
360373
.build();
361374
}
362375

376+
// IntEnums are considered "open", meaning it is a backward compatible change to add new
377+
// members. Adding the `number` variant allows for previously generated clients to be
378+
// able to handle unknown int enum values that may be added in the future.
379+
private Symbol createMemberSymbolWithIntEnumTarget(Symbol targetSymbol) {
380+
return targetSymbol.toBuilder()
381+
.namespace(null, "/")
382+
.name(targetSymbol.getName() + " | number")
383+
.addReference(targetSymbol)
384+
.build();
385+
}
386+
363387
private Symbol createMemberSymbolWithEventStream(Symbol targetSymbol) {
364388
return targetSymbol.toBuilder()
365389
.namespace(null, "/")

smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/EnumGeneratorTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static org.hamcrest.MatcherAssert.assertThat;
44
import static org.hamcrest.Matchers.containsString;
5+
import static org.hamcrest.Matchers.stringContainsInOrder;
56

67
import org.junit.jupiter.api.Test;
78
import software.amazon.smithy.codegen.core.Symbol;
@@ -33,8 +34,7 @@ public void generatesNamedEnums() {
3334
new EnumGenerator(shape, symbol, writer).run();
3435

3536
assertThat(writer.toString(), containsString("export enum Baz {"));
36-
assertThat(writer.toString(), containsString("FOO = \"FOO\""));
37-
assertThat(writer.toString(), containsString("BAR = \"BAR\","));
37+
assertThat(writer.toString(), stringContainsInOrder("BAR = \"BAR\",", "FOO = \"FOO\""));
3838
}
3939

4040
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package software.amazon.smithy.typescript.codegen;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.containsString;
5+
import static org.hamcrest.Matchers.stringContainsInOrder;
6+
7+
import org.junit.jupiter.api.Test;
8+
import software.amazon.smithy.codegen.core.Symbol;
9+
import software.amazon.smithy.model.Model;
10+
import software.amazon.smithy.model.node.Node;
11+
import software.amazon.smithy.model.shapes.IntEnumShape;
12+
13+
public class IntEnumGeneratorTest {
14+
@Test
15+
public void generatesIntEnums() {
16+
IntEnumShape shape = IntEnumShape.builder()
17+
.id("com.foo#Foo")
18+
.addMember("BAR", 5)
19+
.addMember("BAZ", 2)
20+
.build();
21+
TypeScriptWriter writer = new TypeScriptWriter("foo");
22+
Model model = Model.assembler()
23+
.addShape(shape)
24+
.addImport(getClass().getResource("simple-service.smithy"))
25+
.assemble()
26+
.unwrap();
27+
TypeScriptSettings settings = TypeScriptSettings.from(model, Node.objectNodeBuilder()
28+
.withMember("package", Node.from("example"))
29+
.withMember("packageVersion", Node.from("1.0.0"))
30+
.build());
31+
Symbol symbol = new SymbolVisitor(model, settings).toSymbol(shape);
32+
new IntEnumGenerator(shape, symbol, writer).run();
33+
34+
assertThat(writer.toString(), containsString("export enum Foo {"));
35+
assertThat(writer.toString(), stringContainsInOrder("BAZ = 2,", "BAR = 5,"));
36+
}
37+
}

smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/SymbolProviderTest.java

+60
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111
import software.amazon.smithy.codegen.core.SymbolProvider;
1212
import software.amazon.smithy.model.Model;
1313
import software.amazon.smithy.model.node.Node;
14+
import software.amazon.smithy.model.shapes.IntEnumShape;
1415
import software.amazon.smithy.model.shapes.ListShape;
1516
import software.amazon.smithy.model.shapes.MemberShape;
1617
import software.amazon.smithy.model.shapes.Shape;
1718
import software.amazon.smithy.model.shapes.ShapeId;
1819
import software.amazon.smithy.model.shapes.StringShape;
1920
import software.amazon.smithy.model.shapes.StructureShape;
21+
import software.amazon.smithy.model.traits.EnumDefinition;
22+
import software.amazon.smithy.model.traits.EnumTrait;
2023
import software.amazon.smithy.model.traits.MediaTypeTrait;
2124

2225
public class SymbolProviderTest {
@@ -189,4 +192,61 @@ public void usesLazyJsonStringForJsonMediaType() {
189192

190193
assertThat(memberSymbol.getName(), equalTo("__LazyJsonString | string"));
191194
}
195+
196+
@Test
197+
public void addsUnknownStringEnumVariant() {
198+
EnumTrait trait = EnumTrait.builder()
199+
.addEnum(EnumDefinition.builder().value("FOO").name("FOO").build())
200+
.addEnum(EnumDefinition.builder().value("BAR").name("BAR").build())
201+
.build();
202+
StringShape stringShape = StringShape.builder().id("foo.bar#enumString").addTrait(trait).build();
203+
MemberShape member = MemberShape.builder().id("foo.bar#test$a").target(stringShape).build();
204+
StructureShape struct = StructureShape.builder()
205+
.id("foo.bar#test")
206+
.addMember(member)
207+
.build();
208+
Model model = Model.assembler()
209+
.addImport(getClass().getResource("simple-service.smithy"))
210+
.addShapes(struct, member, stringShape)
211+
.assemble()
212+
.unwrap();
213+
TypeScriptSettings settings = TypeScriptSettings.from(model, Node.objectNodeBuilder()
214+
.withMember("package", Node.from("example"))
215+
.withMember("packageVersion", Node.from("1.0.0"))
216+
.build());
217+
218+
SymbolProvider provider = new SymbolVisitor(model, settings);
219+
Symbol memberSymbol = provider.toSymbol(member);
220+
221+
assertThat(memberSymbol.getName(), equalTo("EnumString | string"));
222+
}
223+
224+
@Test
225+
public void addsUnknownNumberIntEnumVariant() {
226+
IntEnumShape shape = IntEnumShape.builder()
227+
.id("com.foo#Foo")
228+
.addMember("BAR", 2)
229+
.addMember("BAZ", 5)
230+
.build();
231+
MemberShape member = MemberShape.builder().id("foo.bar#test$a").target(shape).build();
232+
StructureShape struct = StructureShape.builder()
233+
.id("foo.bar#test")
234+
.addMember(member)
235+
.build();
236+
Model model = Model.assembler()
237+
.addImport(getClass().getResource("simple-service.smithy"))
238+
.addShapes(struct, member, shape)
239+
.assemble()
240+
.unwrap();
241+
TypeScriptSettings settings = TypeScriptSettings.from(model, Node.objectNodeBuilder()
242+
.withMember("package", Node.from("example"))
243+
.withMember("packageVersion", Node.from("1.0.0"))
244+
.build());
245+
246+
SymbolProvider provider = new SymbolVisitor(model, settings);
247+
Symbol memberSymbol = provider.toSymbol(member);
248+
249+
assertThat(memberSymbol.getName(), equalTo("Foo | number"));
250+
}
251+
192252
}

0 commit comments

Comments
 (0)