Skip to content

Commit 5c92a6c

Browse files
committed
test parity for typescript-axios-slim
1 parent e62e6d0 commit 5c92a6c

2 files changed

Lines changed: 724 additions & 0 deletions

File tree

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
package org.openapitools.codegen.typescript.axios;
2+
3+
import org.openapitools.codegen.ClientOptInput;
4+
import org.openapitools.codegen.DefaultGenerator;
5+
import org.openapitools.codegen.config.CodegenConfigurator;
6+
import org.openapitools.codegen.typescript.TypeScriptGroups;
7+
import org.testng.annotations.Test;
8+
9+
import java.io.File;
10+
import java.io.IOException;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.util.ArrayList;
14+
import java.util.Arrays;
15+
import java.util.Collections;
16+
import java.util.LinkedHashSet;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.Set;
20+
import java.util.TreeMap;
21+
import java.util.TreeSet;
22+
import java.util.function.Consumer;
23+
import java.util.regex.Matcher;
24+
import java.util.regex.Pattern;
25+
26+
import static org.testng.Assert.assertEquals;
27+
import static org.testng.Assert.assertFalse;
28+
import static org.testng.Assert.assertTrue;
29+
30+
@Test(groups = {TypeScriptGroups.TYPESCRIPT, TypeScriptGroups.TYPESCRIPT_AXIOS})
31+
public class TypeScriptAxiosSlimParityTest {
32+
private static final String EDGE_CASE_SPEC = "src/test/resources/3_0/typescript-axios-slim/identity-edge-cases.yaml";
33+
34+
private static final Pattern REQUEST_INTERFACE_PATTERN = Pattern.compile("export interface (\\w+Request) \\{([\\s\\S]*?)\\n\\}");
35+
private static final Pattern API_INTERFACE_PATTERN = Pattern.compile("export interface (\\w+Interface) \\{([\\s\\S]*?)\\n\\}");
36+
private static final Pattern API_INTERFACE_METHOD_PATTERN = Pattern.compile("(\\w+)\\(([^)]*)\\):\\s*AxiosPromise<([^;]+)>;");
37+
private static final Pattern ENUM_DECL_PATTERN = Pattern.compile("export enum (\\w+) \\{([\\s\\S]*?)\\n\\}");
38+
private static final Pattern CONST_ENUM_DECL_PATTERN = Pattern.compile("export const (\\w+) = \\{([\\s\\S]*?)\\} as const;");
39+
40+
private static final List<String> API_DIR_CANDIDATES = Arrays.asList("api", "apis");
41+
private static final List<String> MODEL_DIR_CANDIDATES = Arrays.asList("model", "models");
42+
43+
private static final Set<String> EXPECTED_EDGE_METHODS = new TreeSet<>(Arrays.asList(
44+
"getUserByCompany",
45+
"createUserByCompany",
46+
"deleteUserByCompany",
47+
"aliasLookup",
48+
"listReports",
49+
"submitForm",
50+
"uploadEvidence",
51+
"healthCheck",
52+
"searchUsers",
53+
"getUnionPayload",
54+
"getWithQueryApiKey"
55+
));
56+
57+
private static final Consumer<CodegenConfigurator> NO_CUSTOMIZER = cfg -> {
58+
};
59+
60+
@Test(description = "identity: comprehensive edge-case spec across option matrix")
61+
public void shouldKeepIdentityForComprehensiveEdgeCaseSpecAcrossOptionMatrix() throws Exception {
62+
List<OptionScenario> scenarios = Arrays.asList(
63+
new OptionScenario("default", NO_CUSTOMIZER),
64+
new OptionScenario("string-enums", cfg -> cfg.addAdditionalProperty("stringEnums", true)),
65+
new OptionScenario("node-imports", cfg -> cfg.addAdditionalProperty("withNodeImports", true)),
66+
new OptionScenario("aws-v4-signature", cfg -> cfg.addAdditionalProperty("withAWSV4Signature", true)),
67+
new OptionScenario("separate-models-and-api", cfg -> cfg
68+
.addAdditionalProperty("withSeparateModelsAndApi", true)
69+
.addAdditionalProperty("apiPackage", "api")
70+
.addAdditionalProperty("modelPackage", "model")),
71+
new OptionScenario("import-js-extension", cfg -> cfg.addAdditionalProperty("importFileExtension", ".js")),
72+
new OptionScenario("square-bracket-form-arrays", cfg -> cfg.addAdditionalProperty("useSquareBracketsInArrayNames", true))
73+
);
74+
75+
for (OptionScenario scenario : scenarios) {
76+
IdentitySurface axiosSurface = generateIdentity("typescript-axios", EDGE_CASE_SPEC, scenario.customizer);
77+
IdentitySurface slimSurface = generateIdentity("typescript-axios-slim", EDGE_CASE_SPEC, scenario.customizer);
78+
79+
assertIdentitySurfaceEquals(scenario.name, axiosSurface, slimSurface);
80+
assertTrue(axiosSurface.allMethodNames().containsAll(EXPECTED_EDGE_METHODS),
81+
"Scenario " + scenario.name + " did not generate the expected operation set");
82+
}
83+
}
84+
85+
@Test(description = "identity: known regression fixtures (req/res surface)")
86+
public void shouldKeepIdentityAcrossKnownRegressionFixtures() throws Exception {
87+
List<SpecScenario> scenarios = Arrays.asList(
88+
new SpecScenario("petstore", "src/test/resources/3_0/petstore.yaml", NO_CUSTOMIZER),
89+
new SpecScenario("nullable-required", "src/test/resources/3_0/petstore-with-nullable-required.yaml", NO_CUSTOMIZER),
90+
new SpecScenario("multiple-2xx", "src/test/resources/3_0/petstore-multiple-2xx-responses.yaml", NO_CUSTOMIZER),
91+
new SpecScenario("query-form", "src/test/resources/3_0/query-param-form.yaml", NO_CUSTOMIZER),
92+
new SpecScenario("query-deep-object", "src/test/resources/3_0/query-param-deep-object.yaml", NO_CUSTOMIZER),
93+
new SpecScenario("deepobject", "src/test/resources/3_0/deepobject.yaml", NO_CUSTOMIZER),
94+
new SpecScenario("parameter-name-mapping", "src/test/resources/3_0/name-parameter-mappings.yaml", NO_CUSTOMIZER),
95+
new SpecScenario("shared-parameters-3_1", "src/test/resources/3_1/common-parameters.yaml", NO_CUSTOMIZER),
96+
new SpecScenario("multipart-enum-3_1", "src/test/resources/3_1/enum-in-multipart.yaml", NO_CUSTOMIZER),
97+
new SpecScenario("map-array-inner-enum", "src/test/resources/3_0/issue_19393_map_of_inner_enum.yaml", NO_CUSTOMIZER),
98+
new SpecScenario("generic-type-mapping", "src/test/resources/3_1/issue_21317.yaml", cfg -> cfg
99+
.addTypeMapping("UserSummary", "Pick<User, \"email\">")
100+
.addTypeMapping("object", "Record<string,unknown>"))
101+
);
102+
103+
for (SpecScenario scenario : scenarios) {
104+
IdentitySurface axiosSurface = generateIdentity("typescript-axios", scenario.specPath, scenario.customizer);
105+
IdentitySurface slimSurface = generateIdentity("typescript-axios-slim", scenario.specPath, scenario.customizer);
106+
assertIdentitySurfaceEquals(scenario.name, axiosSurface, slimSurface);
107+
}
108+
}
109+
110+
@Test(description = "identity: mapped generic response signature is preserved")
111+
public void shouldPreserveMappedGenericResponseSignature() throws Exception {
112+
Consumer<CodegenConfigurator> mappedTypeCustomizer = cfg -> cfg
113+
.addTypeMapping("UserSummary", "Pick<User, \"email\">")
114+
.addTypeMapping("object", "Record<string,unknown>");
115+
116+
IdentitySurface axiosSurface = generateIdentity("typescript-axios", "src/test/resources/3_1/issue_21317.yaml", mappedTypeCustomizer);
117+
IdentitySurface slimSurface = generateIdentity("typescript-axios-slim", "src/test/resources/3_1/issue_21317.yaml", mappedTypeCustomizer);
118+
119+
assertIdentitySurfaceEquals("generic-type-mapping-signature", axiosSurface, slimSurface);
120+
}
121+
122+
private IdentitySurface generateIdentity(String generatorName, String specPath, Consumer<CodegenConfigurator> customizer) throws Exception {
123+
File output = Files.createTempDirectory("typescript_axios_identity_").toFile().getCanonicalFile();
124+
output.deleteOnExit();
125+
126+
CodegenConfigurator configurator = new CodegenConfigurator()
127+
.setGeneratorName(generatorName)
128+
.setInputSpec(specPath)
129+
.setOutputDir(output.getAbsolutePath())
130+
.addAdditionalProperty("withInterfaces", true)
131+
.addAdditionalProperty("useSingleRequestParameter", true);
132+
customizer.accept(configurator);
133+
134+
ClientOptInput clientOptInput = configurator.toClientOptInput();
135+
new DefaultGenerator().opts(clientOptInput).generate();
136+
137+
return extractIdentity(output.toPath());
138+
}
139+
140+
private IdentitySurface extractIdentity(Path outputDir) throws IOException {
141+
IdentitySurface surface = new IdentitySurface();
142+
143+
List<Path> apiFiles = collectApiFiles(outputDir);
144+
assertFalse(apiFiles.isEmpty(), "No API source files were generated under " + outputDir);
145+
146+
for (Path apiFile : apiFiles) {
147+
String content = Files.readString(apiFile);
148+
String relativePath = normalizePath(outputDir.relativize(apiFile));
149+
surface.apiFiles.put(relativePath, normalize(content));
150+
151+
extractRequestInterfaces(surface.requestInterfaces, content);
152+
extractApiInterfaceMethods(surface.apiInterfaceMethods, content);
153+
extractOperationEnums(surface.operationEnums, content);
154+
}
155+
156+
List<Path> modelFiles = collectModelFiles(outputDir);
157+
for (Path modelFile : modelFiles) {
158+
String relativePath = normalizePath(outputDir.relativize(modelFile));
159+
surface.modelFiles.put(relativePath, normalize(Files.readString(modelFile)));
160+
}
161+
162+
assertTrue(!surface.requestInterfaces.isEmpty() || !surface.apiInterfaceMethods.isEmpty(),
163+
"No comparable request/response surface was extracted from generated sources under " + outputDir);
164+
165+
return surface;
166+
}
167+
168+
private List<Path> collectApiFiles(Path outputDir) throws IOException {
169+
LinkedHashSet<Path> files = new LinkedHashSet<>();
170+
Path rootApi = outputDir.resolve("api.ts");
171+
if (Files.exists(rootApi)) {
172+
files.add(rootApi);
173+
}
174+
175+
for (String dirName : API_DIR_CANDIDATES) {
176+
Path dir = outputDir.resolve(dirName);
177+
if (Files.isDirectory(dir)) {
178+
Files.walk(dir)
179+
.filter(Files::isRegularFile)
180+
.filter(path -> path.getFileName().toString().endsWith(".ts"))
181+
.forEach(files::add);
182+
}
183+
}
184+
185+
List<Path> sorted = new ArrayList<>(files);
186+
Collections.sort(sorted);
187+
return sorted;
188+
}
189+
190+
private List<Path> collectModelFiles(Path outputDir) throws IOException {
191+
LinkedHashSet<Path> files = new LinkedHashSet<>();
192+
for (String dirName : MODEL_DIR_CANDIDATES) {
193+
Path dir = outputDir.resolve(dirName);
194+
if (Files.isDirectory(dir)) {
195+
Files.walk(dir)
196+
.filter(Files::isRegularFile)
197+
.filter(path -> path.getFileName().toString().endsWith(".ts"))
198+
.forEach(files::add);
199+
}
200+
}
201+
202+
List<Path> sorted = new ArrayList<>(files);
203+
Collections.sort(sorted);
204+
return sorted;
205+
}
206+
207+
private void extractRequestInterfaces(Map<String, String> target, String apiSource) {
208+
Matcher matcher = REQUEST_INTERFACE_PATTERN.matcher(apiSource);
209+
while (matcher.find()) {
210+
target.put(matcher.group(1), normalize(matcher.group(2)));
211+
}
212+
}
213+
214+
private void extractApiInterfaceMethods(Map<String, Set<String>> target, String apiSource) {
215+
Matcher interfaceMatcher = API_INTERFACE_PATTERN.matcher(apiSource);
216+
while (interfaceMatcher.find()) {
217+
String interfaceName = interfaceMatcher.group(1);
218+
String interfaceBody = interfaceMatcher.group(2);
219+
Matcher methodMatcher = API_INTERFACE_METHOD_PATTERN.matcher(interfaceBody);
220+
while (methodMatcher.find()) {
221+
String methodName = methodMatcher.group(1);
222+
String params = normalize(methodMatcher.group(2));
223+
String returnType = normalize(methodMatcher.group(3));
224+
target.computeIfAbsent(interfaceName, ignored -> new TreeSet<>())
225+
.add(methodName + "(" + params + "):" + returnType);
226+
}
227+
}
228+
}
229+
230+
private void extractOperationEnums(Map<String, String> target, String apiSource) {
231+
Matcher enumMatcher = ENUM_DECL_PATTERN.matcher(apiSource);
232+
while (enumMatcher.find()) {
233+
target.put("enum:" + enumMatcher.group(1), normalize(enumMatcher.group(2)));
234+
}
235+
236+
Matcher constEnumMatcher = CONST_ENUM_DECL_PATTERN.matcher(apiSource);
237+
while (constEnumMatcher.find()) {
238+
target.put("const:" + constEnumMatcher.group(1), normalize(constEnumMatcher.group(2)));
239+
}
240+
}
241+
242+
private void assertIdentitySurfaceEquals(String scenarioName, IdentitySurface expectedAxios, IdentitySurface actualSlim) {
243+
assertEquals(actualSlim.requestInterfaces, expectedAxios.requestInterfaces,
244+
scenarioName + ": request interface identity mismatch");
245+
assertEquals(actualSlim.apiInterfaceMethods, expectedAxios.apiInterfaceMethods,
246+
scenarioName + ": API interface method identity mismatch");
247+
assertEquals(actualSlim.operationEnums, expectedAxios.operationEnums,
248+
scenarioName + ": enum identity mismatch");
249+
assertEquals(actualSlim.modelFiles, expectedAxios.modelFiles,
250+
scenarioName + ": model file identity mismatch");
251+
}
252+
253+
private static String normalize(String content) {
254+
return content
255+
.replace("\r\n", "\n")
256+
.replace('\r', '\n')
257+
.replaceAll("\\s+", " ")
258+
.trim();
259+
}
260+
261+
private static String normalizePath(Path path) {
262+
return path.toString().replace('\\', '/');
263+
}
264+
265+
private static final class IdentitySurface {
266+
private final Map<String, String> requestInterfaces = new TreeMap<>();
267+
private final Map<String, Set<String>> apiInterfaceMethods = new TreeMap<>();
268+
private final Map<String, String> operationEnums = new TreeMap<>();
269+
private final Map<String, String> modelFiles = new TreeMap<>();
270+
private final Map<String, String> apiFiles = new TreeMap<>();
271+
272+
private Set<String> allMethodNames() {
273+
Set<String> names = new TreeSet<>();
274+
275+
for (Set<String> signatures : apiInterfaceMethods.values()) {
276+
for (String signature : signatures) {
277+
names.add(methodNameFromSignature(signature));
278+
}
279+
}
280+
return names;
281+
}
282+
283+
private String methodNameFromSignature(String signature) {
284+
int index = signature.indexOf('(');
285+
return index >= 0 ? signature.substring(0, index) : signature;
286+
}
287+
}
288+
289+
private static final class OptionScenario {
290+
private final String name;
291+
private final Consumer<CodegenConfigurator> customizer;
292+
293+
private OptionScenario(String name, Consumer<CodegenConfigurator> customizer) {
294+
this.name = name;
295+
this.customizer = customizer;
296+
}
297+
}
298+
299+
private static final class SpecScenario {
300+
private final String name;
301+
private final String specPath;
302+
private final Consumer<CodegenConfigurator> customizer;
303+
304+
private SpecScenario(String name, String specPath, Consumer<CodegenConfigurator> customizer) {
305+
this.name = name;
306+
this.specPath = specPath;
307+
this.customizer = customizer;
308+
}
309+
}
310+
}

0 commit comments

Comments
 (0)