Skip to content

Commit ddd6257

Browse files
committed
Add fixes for symbols and errors
1. Reserved words are now escaped in the symbol visitor to account for escaping reserved words in symbol references (like a list of Record becomes "Array<_Record>". 2. Error structures now normalize the "message" member regardless of the model. This prevents typing issues with super. 3. "message" is no longer generated as a property of SmithyException subtypes. 4. Fixed a bug in how relative module imports were handled and simplified turning module names into file names. Now the module name of shapes that are generated always contains "index" to fix relativizing imports.
1 parent 9ada040 commit ddd6257

File tree

12 files changed

+211
-58
lines changed

12 files changed

+211
-58
lines changed

smithy-typescript-codegen-test/model/main.smithy

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ structure CityCoordinates {
6969
structure NoSuchResource {
7070
/// The type of resource that was not found.
7171
@required
72-
resourceType: String
72+
resourceType: String,
73+
74+
message: String,
7375
}
7476

7577
// The paginated trait indicates that the operation may

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ void execute() {
8282
nonTraits.shapes().sorted().forEach(shape -> shape.accept(this));
8383

8484
// Write each pending writer.
85-
// writers.forEach((filename, writer) -> fileManifest.writeFile(filename, writer.toString()));
8685
writers.writeFiles();
8786

8887
// Write the package.json file, including all symbol dependencies.
@@ -250,24 +249,28 @@ private void renderErrorStructure(StructureShape shape) {
250249
writer.openBlock("export class $L extends $$SmithyException {", symbol.getName());
251250

252251
// Write properties.
252+
// Skip "message" since it is something that SmithyException defines.
253253
StructuredMemberWriter config = new StructuredMemberWriter(
254254
model, symbolProvider, shape.getAllMembers().values());
255255
config.memberPrefix = "readonly ";
256+
config.skipMembers.add("message");
256257
config.writeMembers(writer, shape);
257258

258259
// Write constructor.
259260
writer.openBlock("constructor(args: {");
260261
writer.write("$$service: string;");
261-
if (!shape.getMemberNames().contains("message")) {
262-
writer.write("message?: string;");
263-
}
262+
writer.write("message?: string;");
263+
264264
config.memberPrefix = "";
265265
config.noDocs = true;
266266
config.writeMembers(writer, shape);
267267
writer.closeBlock("}) {");
268268
writer.indent();
269269

270270
writer.openBlock("super({");
271+
// Provide a default value for message in case it was optional in the shape.
272+
// It's required in SmithyException, so provide a default value.
273+
writer.write("message: args.message || \"\",");
271274
writer.write("id: $S,", shape.getId());
272275
writer.write("name: $S,", shape.getId().getName());
273276
writer.write("fault: $S,", errorTrait.getValue());
@@ -276,7 +279,9 @@ private void renderErrorStructure(StructureShape shape) {
276279

277280
for (MemberShape member : shape.getAllMembers().values()) {
278281
String memberName = symbolProvider.toMemberName(member);
279-
writer.write("this.$1L = args.$1L;", memberName);
282+
if (!memberName.equals("message")) {
283+
writer.write("this.$1L = args.$1L;", memberName);
284+
}
280285
}
281286

282287
writer.closeBlock("}"); // constructor

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616
package software.amazon.smithy.typescript.codegen;
1717

1818
import java.util.Collection;
19+
import java.util.HashSet;
20+
import java.util.Set;
1921
import software.amazon.smithy.codegen.core.SymbolProvider;
2022
import software.amazon.smithy.model.Model;
2123
import software.amazon.smithy.model.shapes.MemberShape;
2224
import software.amazon.smithy.model.shapes.Shape;
2325

2426
/**
2527
* Generates objects, interfaces, enums, etc.
28+
*
29+
* TODO: Replace this with a builder for generating classes and interfaces.
2630
*/
2731
final class StructuredMemberWriter {
2832

@@ -31,6 +35,7 @@ final class StructuredMemberWriter {
3135
Collection<MemberShape> members;
3236
String memberPrefix = "";
3337
boolean noDocs;
38+
final Set<String> skipMembers = new HashSet<>();
3439

3540
StructuredMemberWriter(Model model, SymbolProvider symbolProvider, Collection<MemberShape> members) {
3641
this.model = model;
@@ -41,13 +46,18 @@ final class StructuredMemberWriter {
4146
void writeMembers(TypeScriptWriter writer, Shape shape) {
4247
int position = -1;
4348
for (MemberShape member : members) {
49+
if (skipMembers.contains(member.getMemberName())) {
50+
continue;
51+
}
52+
4453
position++;
4554
boolean wroteDocs = !noDocs && writer.writeMemberDocs(model, member);
4655
String memberName = TypeScriptUtils.sanitizePropertyName(symbolProvider.toMemberName(member));
4756
String optionalSuffix = shape.isUnionShape() || !member.isRequired() ? "?" : "";
4857
String typeSuffix = member.isRequired() ? " | undefined" : "";
4958
writer.write("${L}${L}${L}: ${T}${L};", memberPrefix, memberName, optionalSuffix,
5059
symbolProvider.toSymbol(member), typeSuffix);
60+
5161
if (wroteDocs && position < members.size() - 1) {
5262
writer.write("");
5363
}

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

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
import static java.lang.String.format;
1919

2020
import java.util.Locale;
21-
import java.util.StringJoiner;
2221
import java.util.logging.Logger;
2322
import java.util.regex.Pattern;
2423
import software.amazon.smithy.codegen.core.CodegenException;
24+
import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider;
25+
import software.amazon.smithy.codegen.core.ReservedWords;
26+
import software.amazon.smithy.codegen.core.ReservedWordsBuilder;
2527
import software.amazon.smithy.codegen.core.ShapeIdShader;
2628
import software.amazon.smithy.codegen.core.Symbol;
2729
import software.amazon.smithy.codegen.core.SymbolProvider;
@@ -72,11 +74,11 @@ final class SymbolVisitor implements SymbolProvider, ShapeVisitor<Symbol> {
7274
private static final String BIG_JS_VERSION = "^5.2.2";
7375
private static final Pattern SHAPE_ID_NAMESPACE_SPLITTER = Pattern.compile("\\.");
7476
private static final Pattern SHAPE_ID_NAMESPACE_PART_SPLITTER = Pattern.compile("_");
75-
private static final Pattern SLASH_SPLITTER = Pattern.compile("/");
7677

7778
private final Model model;
7879
private final ShapeIdShader shader;
7980
private final String targetNamespace;
81+
private final ReservedWordSymbolProvider.Escaper escaper;
8082

8183
SymbolVisitor(Model model, String rootNamespace, String targetNamespace) {
8284
this.model = model;
@@ -90,13 +92,30 @@ final class SymbolVisitor implements SymbolProvider, ShapeVisitor<Symbol> {
9092
} else {
9193
shader = null;
9294
}
95+
96+
// Load reserved words from a new-line delimited file.
97+
ReservedWords reservedWords = new ReservedWordsBuilder()
98+
.loadWords(TypeScriptCodegenPlugin.class.getResource("reserved-words.txt"))
99+
.build();
100+
101+
escaper = ReservedWordSymbolProvider.builder()
102+
.nameReservedWords(reservedWords)
103+
// Only escape words when the symbol has a definition file to
104+
// prevent escaping intentional references to built-in types.
105+
.escapePredicate((shape, symbol) -> !StringUtils.isEmpty(symbol.getDefinitionFile()))
106+
.buildEscaper();
93107
}
94108

95109
@Override
96110
public Symbol toSymbol(Shape shape) {
97111
Symbol symbol = shape.accept(this);
98112
LOGGER.fine(() -> "Creating symbol from " + shape + ": " + symbol);
99-
return symbol;
113+
return escaper.escapeSymbol(shape, symbol);
114+
}
115+
116+
@Override
117+
public String toMemberName(MemberShape shape) {
118+
return escaper.escapeMemberName(shape.getMemberName());
100119
}
101120

102121
@Override
@@ -210,7 +229,7 @@ public Symbol documentShape(DocumentShape shape) {
210229
public Symbol operationShape(OperationShape shape) {
211230
ShapeId shaded = shadeShapeId(shape);
212231
String commandName = StringUtils.capitalize(shaded.getName()) + "Command";
213-
return createSymbolBuilder(shape, commandName, formatModuleName(shaded)).build();
232+
return createGeneratedSymbolBuilder(shape, commandName, formatModuleName(shaded)).build();
214233
}
215234

216235
@Override
@@ -295,7 +314,7 @@ private ShapeId shadeShapeId(ToShapeId id) {
295314
private Symbol.Builder createObjectSymbolBuilder(Shape shape) {
296315
ShapeId shaded = shadeShapeId(shape);
297316
String name = StringUtils.capitalize(shaded.getName());
298-
return createSymbolBuilder(shape, name, formatModuleName(shaded));
317+
return createGeneratedSymbolBuilder(shape, name, formatModuleName(shaded));
299318
}
300319

301320
private Symbol.Builder createSymbolBuilder(Shape shape, String typeName) {
@@ -306,7 +325,11 @@ private Symbol.Builder createSymbolBuilder(Shape shape, String typeName, String
306325
return Symbol.builder()
307326
.putProperty("shape", shape)
308327
.name(typeName)
309-
.namespace(namespace, "/")
328+
.namespace(namespace, "/");
329+
}
330+
331+
private Symbol.Builder createGeneratedSymbolBuilder(Shape shape, String typeName, String namespace) {
332+
return createSymbolBuilder(shape, typeName, namespace)
310333
.definitionFile(toFilename(shape, typeName, namespace));
311334
}
312335

@@ -325,27 +348,14 @@ private String formatModuleName(ToShapeId id) {
325348
result.append("/");
326349
}
327350

328-
// Remove the trailing "/".
329-
result.deleteCharAt(result.length() - 1);
330-
return result.toString();
351+
return result.append("index").toString();
331352
}
332353

333354
private String toFilename(Shape shape, String name, String namespace) {
334355
if (shape.isServiceShape()) {
335356
return name + "Client.ts";
336357
}
337358

338-
String filename;
339-
if (namespace == null) {
340-
filename = "index.ts";
341-
} else {
342-
StringJoiner joiner = new StringJoiner("/", "", "/index.ts");
343-
for (String part : SLASH_SPLITTER.split(namespace)) {
344-
joiner.add(part);
345-
}
346-
filename = joiner.toString();
347-
}
348-
349-
return format("types/%s", filename).replace("//", "/");
359+
return "types/" + namespace.replace(".", "/") + ".ts";
350360
}
351361
}

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

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,8 @@
1717

1818
import software.amazon.smithy.build.PluginContext;
1919
import software.amazon.smithy.build.SmithyBuildPlugin;
20-
import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider;
21-
import software.amazon.smithy.codegen.core.ReservedWords;
22-
import software.amazon.smithy.codegen.core.ReservedWordsBuilder;
2320
import software.amazon.smithy.codegen.core.SymbolProvider;
2421
import software.amazon.smithy.model.Model;
25-
import software.amazon.smithy.utils.StringUtils;
2622

2723
/**
2824
* Plugin to trigger TypeScript code generation.
@@ -58,19 +54,6 @@ public static SymbolProvider createSymbolProvider(Model model) {
5854
* @return Returns the created provider.
5955
*/
6056
public static SymbolProvider createSymbolProvider(Model model, String rootNamespace, String targetNamespace) {
61-
SymbolVisitor symbolProvider = new SymbolVisitor(model, rootNamespace, targetNamespace);
62-
63-
// Load reserved words from a new-line delimited file.
64-
ReservedWords reservedWords = new ReservedWordsBuilder()
65-
.loadWords(TypeScriptCodegenPlugin.class.getResource("reserved-words.txt"))
66-
.build();
67-
68-
return ReservedWordSymbolProvider.builder()
69-
.nameReservedWords(reservedWords)
70-
.symbolProvider(symbolProvider)
71-
// Only escape words when the symbol has a namespace. This
72-
// prevents escaping intentional references to reserved words.
73-
.escapePredicate((shape, symbol) -> !StringUtils.isEmpty(symbol.getNamespace()))
74-
.build();
57+
return new SymbolVisitor(model, rootNamespace, targetNamespace);
7558
}
7659
}

smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/shapeTypes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ export class SmithyException extends Error implements SmithyStructure {
3030
name: string;
3131
service: string;
3232
fault: "client" | "server";
33-
message?: string;
33+
message?: string | undefined;
3434
}) {
35-
super(args.message);
35+
super(args.message || "");
3636
this.$id = args.id;
3737
this.name = args.name;
3838
this.$service = args.service;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package software.amazon.smithy.typescript.codegen;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.containsString;
5+
6+
import org.junit.jupiter.api.Test;
7+
import software.amazon.smithy.build.MockManifest;
8+
import software.amazon.smithy.build.PluginContext;
9+
import software.amazon.smithy.model.Model;
10+
import software.amazon.smithy.model.node.Node;
11+
12+
public class CodegenVisitorTest {
13+
@Test
14+
public void properlyGeneratesEmptyMessageMemberOfException() {
15+
testErrorStructureCodegen("error-test-empty.smithy");
16+
}
17+
18+
@Test
19+
public void properlyGeneratesOptionalMessageMemberOfException() {
20+
testErrorStructureCodegen("error-test-optional-message.smithy");
21+
}
22+
23+
@Test
24+
public void properlyGeneratesRequiredMessageMemberOfException() {
25+
testErrorStructureCodegen("error-test-required-message.smithy");
26+
}
27+
28+
public void testErrorStructureCodegen(String file) {
29+
Model model = Model.assembler()
30+
.addImport(getClass().getResource(file))
31+
.assemble()
32+
.unwrap();
33+
MockManifest manifest = new MockManifest();
34+
PluginContext context = PluginContext.builder()
35+
.model(model)
36+
.fileManifest(manifest)
37+
.settings(Node.objectNodeBuilder()
38+
.withMember("service", Node.from("smithy.example#Example"))
39+
.withMember("package", Node.from("example"))
40+
.withMember("packageVersion", Node.from("1.0.0"))
41+
.build())
42+
.build();
43+
44+
new TypeScriptCodegenPlugin().execute(context);
45+
String contents = manifest.getFileString("/types/smithy/example/index.ts").get();
46+
47+
assertThat(contents, containsString("export class Err extends $SmithyException {\n"
48+
+ " constructor(args: {\n"
49+
+ " $service: string;\n"
50+
+ " message?: string;\n"
51+
+ " }) {\n"
52+
+ " super({\n"
53+
+ " message: args.message || \"\",\n"
54+
+ " id: \"smithy.example#Err\",\n"
55+
+ " name: \"Err\",\n"
56+
+ " fault: \"client\",\n"
57+
+ " service: args.$service,\n"
58+
+ " });\n"
59+
+ " }\n"
60+
+ "}"));
61+
}
62+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ public void relativizesImports() {
4343
assertThat(result, containsString("import { Baz } from \"./bam\";"));
4444
}
4545

46+
@Test
47+
public void relativizesImportsWithTrailingFilename() {
48+
ImportDeclarations declarations = new ImportDeclarations("foo/bar/index");
49+
declarations.addImport("Baz", "", "./shared/shapeTypes");
50+
String result = declarations.toString();
51+
52+
assertThat(result, containsString("import { Baz } from \"../../../shared/shapeTypes\";"));
53+
}
54+
4655
@Test
4756
public void automaticallyCorrectsBasePath() {
4857
ImportDeclarations declarations = new ImportDeclarations("/foo/bar");

0 commit comments

Comments
 (0)