Skip to content

Commit 9adc829

Browse files
committed
Typescript utility types now taken into account when generating. Addresses #21317
1 parent a231ab6 commit 9adc829

File tree

9 files changed

+231
-47
lines changed

9 files changed

+231
-47
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,13 @@ public AbstractTypeScriptClientCodegen() {
304304
// Typescript reserved words
305305
"abstract", "await", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "debugger", "default", "delete", "do", "double", "else", "enum", "export", "extends", "false", "final", "finally", "float", "for", "function", "goto", "if", "implements", "import", "in", "instanceof", "int", "interface", "let", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", "super", "switch", "synchronized", "this", "throw", "transient", "true", "try", "typeof", "var", "void", "volatile", "while", "with", "yield"));
306306

307+
defaultIncludes = new HashSet<>(Arrays.asList(
308+
//Utility types
309+
"Awaited","Partial","Required","Readonly","Record","Pick","Omit","Exclude","Extract","NonNullable",
310+
"Parameters","ConstructorParameters","ReturnType","InstanceType","NoInfer","ThisParameterType",
311+
"OmitThisParameter","ThisType","Uppercase","Lowercase","Capitalize","Uncapitalize"
312+
));
313+
307314
languageSpecificPrimitives = new HashSet<>(Arrays.asList(
308315
"string",
309316
"String",
@@ -323,7 +330,11 @@ public AbstractTypeScriptClientCodegen() {
323330
"Error",
324331
"Map",
325332
"object",
326-
"Set"
333+
"Set",
334+
//Utility types
335+
"Awaited","Partial","Required","Readonly","Record","Pick","Omit","Exclude","Extract","NonNullable",
336+
"Parameters","ConstructorParameters","ReturnType","InstanceType","NoInfer","ThisParameterType",
337+
"OmitThisParameter","ThisType","Uppercase","Lowercase","Capitalize","Uncapitalize"
327338
));
328339

329340
languageGenericTypes = new HashSet<>(Collections.singletonList(
@@ -805,8 +816,15 @@ public String getSchemaType(Schema p) {
805816
return openAPIType;
806817
} else if (typeMapping.containsKey(openAPIType)) {
807818
type = typeMapping.get(openAPIType);
819+
String typeWithoutGeneric = null;
820+
int genericIndex = type.indexOf("<");
821+
if(genericIndex != -1) {
822+
typeWithoutGeneric = type.substring(0, genericIndex);
823+
}
808824
if (languageSpecificPrimitives.contains(type)) {
809825
return type;
826+
} else if(typeWithoutGeneric != null && languageSpecificPrimitives.contains(typeWithoutGeneric)) {
827+
return type;
810828
}
811829
} else {
812830
type = openAPIType;
@@ -1157,6 +1175,13 @@ protected List<String> getTypesFromSchemas(List<Schema> schemas) {
11571175
}).distinct().collect(Collectors.toList());
11581176
}
11591177

1178+
@Override
1179+
protected boolean needToImport(String type) {
1180+
int genericIndex = type.indexOf("<");
1181+
String typeWithoutGeneric = genericIndex != -1 ? type.substring(0, genericIndex) : type;
1182+
return super.needToImport(typeWithoutGeneric);
1183+
}
1184+
11601185
@Override
11611186
public GeneratorLanguage generatorLanguage() {
11621187
return GeneratorLanguage.TYPESCRIPT;

modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/SharedTypeScriptTest.java

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.openapitools.codegen.antlr4.TypeScriptParserBaseListener;
1616
import org.openapitools.codegen.config.CodegenConfigurator;
1717
import org.openapitools.codegen.languages.TypeScriptAxiosClientCodegen;
18+
import org.openapitools.codegen.languages.TypeScriptFetchClientCodegen;
1819
import org.openapitools.codegen.typescript.assertions.TypescriptFileAssert;
1920
import org.testng.Assert;
2021
import org.testng.annotations.Test;
@@ -126,28 +127,67 @@ public void givenTypeMappingContainsGenericAndMappedTypeIsUtilityTypeThenTypeIsN
126127
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
127128
output.deleteOnExit();
128129

130+
File mainOutput = new File(output, "main");
129131

130132
Generator generator = new DefaultGenerator();
131-
CodegenConfigurator configurator = new CodegenConfigurator()
133+
CodegenConfigurator runtimeChecksConfigurator = new CodegenConfigurator()
132134
.setInputSpec("src/test/resources/3_1/issue_21317.yaml")
133135
.setGeneratorName("typescript-fetch")
134136
.addTypeMapping("object", "Record<string,unknown>")
135-
.setOutputDir(output.getAbsolutePath());
136-
ClientOptInput clientOptInput = configurator.toClientOptInput();
137+
.setOutputDir(mainOutput.getAbsolutePath());
138+
ClientOptInput clientOptInput = runtimeChecksConfigurator.toClientOptInput();
137139
generator.opts(clientOptInput)
138140
.generate();
139-
//TODO Delete
140-
System.out.println("Generator Settings: " + clientOptInput.getGeneratorSettings());
141-
String outputPath = output.getAbsolutePath();
142-
File testModel = new File(outputPath, "/models/User.ts");
143-
String fileContent = Files.readString(testModel.toPath());
144-
//TODO Delete
145-
System.out.println(fileContent);
141+
String mainPath = mainOutput.getAbsolutePath();
142+
File userModel = new File(mainPath, "/models/User.ts");
143+
String userModelContent = Files.readString(userModel.toPath());
146144

147-
TypescriptFileAssert.assertThat(fileContent)
148-
.isValid()
145+
TypescriptFileAssert.assertThat(userModelContent)
149146
.importsNotContain("Record")
150147
.assertInterface("User")
151-
.propertyAssert("metadata").isGeneric();
148+
.propertyAssert("metadata").propertyTypeAssert().assertHasGeneric();
149+
150+
File noRuntimeOutput = new File(output, "noruntime");
151+
152+
CodegenConfigurator noRuntimeConfigurator = new CodegenConfigurator()
153+
.setInputSpec("src/test/resources/3_1/issue_21317.yaml")
154+
.setGeneratorName("typescript-fetch")
155+
.addTypeMapping("object", "Record<string, unknown>")
156+
.addTypeMapping("UserSummary", "Pick<User, \"email\">")
157+
.addAdditionalProperty(TypeScriptFetchClientCodegen.WITHOUT_RUNTIME_CHECKS, true)
158+
.setOutputDir(noRuntimeOutput.getAbsolutePath());
159+
ClientOptInput clientOptInput2 = noRuntimeConfigurator.toClientOptInput();
160+
generator.opts(clientOptInput2)
161+
.generate();
162+
String noRuntimePath = noRuntimeOutput.getAbsolutePath();
163+
File apiFile = new File(noRuntimePath, "/apis/DefaultApi.ts");
164+
String apiFileContent = Files.readString(apiFile.toPath());
165+
166+
//TODO Delete
167+
System.out.println("User File: " + userModelContent);
168+
System.out.println("Api File: " + apiFileContent);
169+
TypescriptFileAssert.assertThat(apiFileContent)
170+
.importsNotContain("UserSummary")
171+
.assertClass("DefaultApi")
172+
.methodDeclarationAssert("usersSummaryGet").returnTypeAssert().assertEquals("Promise<Pick<User,\"email\">>");
173+
174+
175+
File axiosOutputPath = new File(output, "axios");
176+
177+
runtimeChecksConfigurator.setGeneratorName("typescript-axios")
178+
.addTypeMapping("UserSummary", "Pick<User, \"email\">")
179+
.setOutputDir(axiosOutputPath.getAbsolutePath());
180+
generator.opts(runtimeChecksConfigurator.toClientOptInput())
181+
.generate();
182+
183+
File axiosApiFile = new File(axiosOutputPath, "/api.ts");
184+
String axiosApiFileContent = Files.readString(axiosApiFile.toPath());
185+
186+
//TODO Delete
187+
System.out.println("Axios Api File: " + axiosApiFileContent);
188+
//Parser check fails for some reason, resorting to regular check
189+
TestUtils.assertFileContains(axiosApiFile.toPath(), "AxiosPromise<Pick<User, \"email\">>");
190+
TestUtils.assertFileNotContains(axiosApiFile.toPath(), "AxiosPromise<UserSummary>");
152191
}
192+
153193
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.openapitools.codegen.typescript.assertions;
2+
3+
import org.assertj.core.api.AbstractAssert;
4+
import org.assertj.core.api.Assertions;
5+
import org.openapitools.codegen.antlr4.TypeScriptParser;
6+
7+
import java.util.Optional;
8+
9+
public class ClassAssert extends AbstractAssert<ClassAssert, TypeScriptParser.ClassDeclarationContext> {
10+
11+
private final TypescriptFileAssert typescriptFileAssert;
12+
public ClassAssert(TypescriptFileAssert typescriptFileAssert, TypeScriptParser.ClassDeclarationContext classDeclarationContext) {
13+
super(classDeclarationContext, ClassAssert.class);
14+
this.typescriptFileAssert = typescriptFileAssert;
15+
}
16+
17+
public MethodDeclarationAssert methodDeclarationAssert(String methodName) {
18+
Optional<TypeScriptParser.MethodDeclarationExpressionContext> methodDeclarationExpressionContext = actual.classTail().classElement()
19+
.stream().filter((ce) -> {
20+
return ce.propertyMemberDeclaration() instanceof TypeScriptParser.MethodDeclarationExpressionContext &&
21+
((TypeScriptParser.MethodDeclarationExpressionContext) ce.propertyMemberDeclaration()).propertyName().getText().equals(methodName);
22+
}).map((ce) -> (TypeScriptParser.MethodDeclarationExpressionContext) ce.propertyMemberDeclaration()).findFirst();
23+
Assertions.assertThat(methodDeclarationExpressionContext)
24+
.withFailMessage("%s is not present or is not a method", methodName)
25+
.isPresent();
26+
return new MethodDeclarationAssert(typescriptFileAssert, methodDeclarationExpressionContext.get());
27+
}
28+
29+
public TypescriptFileAssert toFileAssert() {
30+
return typescriptFileAssert;
31+
}
32+
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/assertions/InterfaceAssert.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@ public PropertyAssert propertyAssert(String propertyName) {
2424
.isPresent();
2525
return new PropertyAssert(typescriptFileAssert, propertySignaturContext.get());
2626
}
27+
28+
public TypescriptFileAssert toFileAssert() {
29+
return typescriptFileAssert;
30+
}
2731
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.openapitools.codegen.typescript.assertions;
2+
3+
import org.assertj.core.api.AbstractAssert;
4+
import org.assertj.core.api.Assertions;
5+
import org.openapitools.codegen.antlr4.TypeScriptParser;
6+
7+
public class MethodDeclarationAssert extends AbstractAssert<MethodDeclarationAssert, TypeScriptParser.MethodDeclarationExpressionContext> {
8+
9+
private final TypescriptFileAssert typescriptFileAssert;
10+
public MethodDeclarationAssert(TypescriptFileAssert typescriptFileAssert, TypeScriptParser.MethodDeclarationExpressionContext methodDeclarationExpressionContext) {
11+
super(methodDeclarationExpressionContext, MethodDeclarationAssert.class);
12+
this.typescriptFileAssert = typescriptFileAssert;
13+
}
14+
15+
public TypeAssert returnTypeAssert() {
16+
TypeScriptParser.TypeAnnotationContext returnType = actual.callSignature().typeAnnotation();
17+
Assertions.assertThat(returnType)
18+
.withFailMessage("%s does not have a return type", actual.propertyName())
19+
.isNotNull();
20+
return new TypeAssert(typescriptFileAssert, returnType.type_());
21+
}
22+
23+
24+
public TypescriptFileAssert toFileAssert() {
25+
return typescriptFileAssert;
26+
}
27+
28+
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/assertions/PropertyAssert.java

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,11 @@ public PropertyAssert(TypescriptFileAssert typescriptFileAssert, TypeScriptParse
1414
this.typescriptFileAssert = typescriptFileAssert;
1515
}
1616

17-
public PropertyAssert isGeneric() {
18-
Condition<TypeScriptParser.PropertySignaturContext> hasType = new Condition<>() {
19-
@Override
20-
public boolean matches(TypeScriptParser.PropertySignaturContext value) {
21-
return value.typeAnnotation() != null;
22-
}
23-
};
24-
Condition<TypeScriptParser.TypeAnnotationContext> typeGeneric = new Condition<>() {
25-
@Override
26-
public boolean matches(TypeScriptParser.TypeAnnotationContext value) {
27-
return value.type_().typeGeneric() != null;
28-
}
29-
};
30-
Assertions.assertThat(actual)
31-
.withFailMessage("Type is not specified")
32-
.satisfies(hasType);
17+
public TypeAssert propertyTypeAssert() {
3318
Assertions.assertThat(actual.typeAnnotation())
34-
.withFailMessage("Type is not generic")
35-
.satisfies(typeGeneric);
36-
return this;
19+
.withFailMessage("%s does not have type", actual.propertyName().getText())
20+
.isNotNull();
21+
return new TypeAssert(typescriptFileAssert, actual.typeAnnotation().type_());
3722
}
3823

3924
public TypescriptFileAssert toFileAssert() {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.openapitools.codegen.typescript.assertions;
2+
3+
import org.assertj.core.api.AbstractAssert;
4+
import org.assertj.core.api.Assertions;
5+
import org.assertj.core.api.Condition;
6+
import org.openapitools.codegen.antlr4.TypeScriptParser;
7+
8+
public class TypeAssert extends AbstractAssert<TypeAssert, TypeScriptParser.Type_Context> {
9+
private final TypescriptFileAssert typescriptFileAssert;
10+
11+
public TypeAssert(TypescriptFileAssert typescriptFileAssert, TypeScriptParser.Type_Context typeContext) {
12+
super(typeContext, TypeAssert.class);
13+
this.typescriptFileAssert = typescriptFileAssert;
14+
}
15+
16+
17+
18+
public TypeAssert assertHasGeneric() {
19+
Condition<TypeScriptParser.Type_Context> typeHasGeneric = new Condition<>() {
20+
@Override
21+
public boolean matches(TypeScriptParser.Type_Context value) {
22+
return (value.unionOrIntersectionOrPrimaryType() instanceof TypeScriptParser.PrimaryContext) &&
23+
((TypeScriptParser.PrimaryContext) value.unionOrIntersectionOrPrimaryType()).primaryType() instanceof TypeScriptParser.ReferencePrimTypeContext &&
24+
((TypeScriptParser.ReferencePrimTypeContext) ((TypeScriptParser.PrimaryContext) value.unionOrIntersectionOrPrimaryType()).primaryType()).typeReference().typeGeneric() != null;
25+
}
26+
};
27+
Assertions.assertThat(actual)
28+
.withFailMessage("%s does not have generic")
29+
.satisfies(typeHasGeneric);
30+
return this;
31+
}
32+
33+
public TypeAssert assertEquals(String type) {
34+
Condition<TypeScriptParser.Type_Context> typeHasGeneric = new Condition<>() {
35+
@Override
36+
public boolean matches(TypeScriptParser.Type_Context value) {
37+
return value.getText().equals(type);
38+
}
39+
};
40+
Assertions.assertThat(actual)
41+
.withFailMessage("Expected %s, got %s", type, actual.getText())
42+
.satisfies(typeHasGeneric);
43+
return this;
44+
}
45+
46+
public TypescriptFileAssert toFileAssert() {
47+
return typescriptFileAssert;
48+
}
49+
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/assertions/TypescriptFileAssert.java

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ public class TypescriptFileAssert extends AbstractAssert<TypescriptFileAssert, T
1616

1717
private CustomErrorListener customErrorListener;
1818

19-
// public TypescriptFileAssert(TypeScriptParser.ProgramContext programContext) {
20-
// super(programContext, TypescriptFileAssert.class);
21-
// }
22-
2319
private TypescriptFileAssert(TypeScriptParser.ProgramContext programContext, CustomErrorListener customErrorListener) {
2420
super(programContext, TypescriptFileAssert.class);
2521
this.customErrorListener = customErrorListener;
22+
int syntaxErrorCount = customErrorListener.syntaxErrorCount;
23+
Assertions.assertThat(syntaxErrorCount)
24+
.isEqualTo(0)
25+
.withFailMessage("%d syntax errors", syntaxErrorCount);
2626
}
2727
public static TypescriptFileAssert assertThat(String source) {
2828
CustomErrorListener customErrorListener = new CustomErrorListener();
@@ -35,14 +35,6 @@ public static TypescriptFileAssert assertThat(String source) {
3535
return new TypescriptFileAssert(programContext, customErrorListener);
3636
}
3737

38-
public TypescriptFileAssert isValid() {
39-
int syntaxErrorCount = customErrorListener.syntaxErrorCount;
40-
Assertions.assertThat(syntaxErrorCount)
41-
.isEqualTo(0)
42-
.withFailMessage("%d syntax errors", syntaxErrorCount);
43-
return this;
44-
}
45-
4638
public InterfaceAssert assertInterface(String interfaceName) {
4739
Optional<TypeScriptParser.InterfaceDeclarationContext> interfaceDeclarationContext = actual
4840
.sourceElements().sourceElement().stream().filter((se) -> {
@@ -54,6 +46,18 @@ public InterfaceAssert assertInterface(String interfaceName) {
5446
.isPresent();
5547
return new InterfaceAssert(this, interfaceDeclarationContext.get());
5648
}
49+
public ClassAssert assertClass(String className) {
50+
Optional<TypeScriptParser.ClassDeclarationContext> classDeclarationContext = actual
51+
.sourceElements().sourceElement().stream().filter((se) -> {
52+
return se.statement().classDeclaration() != null &&
53+
se.statement().classDeclaration().identifier().getText().equals(className);
54+
}).map((se) -> se.statement().classDeclaration()).findFirst();
55+
Assertions.assertThat(classDeclarationContext)
56+
.withFailMessage("%s is not present or is not a class", className)
57+
.isPresent();
58+
return new ClassAssert(this, classDeclarationContext.get());
59+
}
60+
5761

5862
/**
5963
* Placeholder for more detailed import-related assertions

modules/openapi-generator/src/test/resources/3_1/issue_21317.yaml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# Significantly modified from the original
12
openapi: 3.1.0
23
info:
34
title: Sample API
@@ -22,7 +23,17 @@ paths:
2223
application/json:
2324
schema:
2425
$ref: '#/components/schemas/User'
25-
26+
/users/summary:
27+
get:
28+
summary: Returns a list of users.
29+
description: Optional extended description in CommonMark or HTML.
30+
responses:
31+
"200": # status code
32+
description: A JSON array of user names
33+
content:
34+
application/json:
35+
schema:
36+
$ref: '#/components/schemas/UserSummary'
2637
components:
2738
schemas:
2839
User:
@@ -40,4 +51,10 @@ components:
4051
format: email
4152
description: The user email address.
4253
metadata:
43-
type: object
54+
type: object
55+
UserSummary:
56+
type: object
57+
properties:
58+
name:
59+
type: string
60+
description: The user name.

0 commit comments

Comments
 (0)