Skip to content

Commit d6095a0

Browse files
authored
[typescript] Align multipart file array handling (#24133)
* Align TypeScript multipart file array handling * Add TypeScript fetch multipart file array sample * Document TypeScript binary form array helper * Fix TypeScript fetch multipart docs example * Limit TypeScript fetch multipart docs examples
1 parent c1f9e2a commit d6095a0

19 files changed

Lines changed: 840 additions & 4 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
generatorName: typescript-fetch
2+
outputDir: samples/client/others/typescript-fetch/multipart-file-array
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-fetch/multipart-file-array.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/typescript-fetch

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,23 @@ protected void addImport(CodegenModel m, String type) {
10531053
}
10541054
}
10551055

1056+
/**
1057+
* Returns true for multipart form arrays whose array or item schema is binary.
1058+
*
1059+
* @param parameter Codegen parameter
1060+
*/
1061+
protected static boolean isBinaryFormArray(CodegenParameter parameter) {
1062+
if (!parameter.isFormParam || !parameter.isArray) {
1063+
return false;
1064+
}
1065+
if ("binary".equals(parameter.dataFormat)) {
1066+
return true;
1067+
}
1068+
1069+
CodegenProperty items = parameter.items;
1070+
return items != null && (items.isFile || items.isBinary || "binary".equals(items.dataFormat));
1071+
}
1072+
10561073
/**
10571074
* Override to fix the inner enum naming issue for maps/arrays of enums.
10581075
* <p>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,8 @@ private void updateOperationParameterForEnum(OperationsMap operations) {
230230
@Override
231231
public void postProcessParameter(CodegenParameter parameter) {
232232
super.postProcessParameter(parameter);
233-
if (parameter.isFormParam && parameter.isArray && "binary".equals(parameter.dataFormat)) {
233+
if (isBinaryFormArray(parameter)) {
234+
parameter.isFile = true;
234235
parameter.isCollectionFormatMulti = true;
235236
}
236237
}

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
9292
private static final String X_ENTITY_ID = "x-entityId";
9393
private static final String X_OPERATION_RETURN_PASSTHROUGH = "x-operationReturnPassthrough";
9494
private static final String X_KEEP_AS_JS_OBJECT = "x-keepAsJSObject";
95+
private static final String X_TYPESCRIPT_FETCH_API_EXAMPLE = "x-typescriptFetchApiExample";
96+
private static final String BLOB_API_EXAMPLE = "new Blob(['example file content'], { type: 'application/octet-stream' })";
9597

9698
protected boolean sagasAndRecords = false;
9799
@Getter @Setter
@@ -416,11 +418,46 @@ public ModelsMap postProcessModels(ModelsMap objs) {
416418
@Override
417419
public void postProcessParameter(CodegenParameter parameter) {
418420
super.postProcessParameter(parameter);
419-
if (parameter.isFormParam && parameter.isArray && "binary".equals(parameter.dataFormat)) {
421+
if (isBinaryFormArray(parameter)) {
422+
parameter.isFile = true;
420423
parameter.isCollectionFormatMulti = true;
421424
}
422425
}
423426

427+
private void addMultipartFileArrayApiExampleValues(OperationsMap operations) {
428+
for (CodegenOperation operation : operations.getOperations().getOperation()) {
429+
if (operation.allParams == null || operation.allParams.stream().noneMatch(TypeScriptFetchClientCodegen::isBinaryFormArray)) {
430+
continue;
431+
}
432+
433+
for (CodegenParameter parameter : operation.allParams) {
434+
setApiExampleValue(parameter);
435+
}
436+
}
437+
}
438+
439+
private void setApiExampleValue(CodegenParameter parameter) {
440+
String example = toApiExampleValue(parameter);
441+
if (example != null) {
442+
parameter.vendorExtensions.put(X_TYPESCRIPT_FETCH_API_EXAMPLE, example);
443+
}
444+
}
445+
446+
private String toApiExampleValue(CodegenParameter parameter) {
447+
if (isBinaryFormArray(parameter)) {
448+
return "[" + BLOB_API_EXAMPLE + "]";
449+
} else if (parameter.isFile || parameter.isBinary) {
450+
return BLOB_API_EXAMPLE;
451+
} else if (parameter.isString) {
452+
String example = parameter.example;
453+
if (example == null) {
454+
example = parameter.paramName + "_example";
455+
}
456+
return "'" + escapeText(example) + "'";
457+
}
458+
return null;
459+
}
460+
424461
@Override
425462
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
426463
List<ExtendedCodegenModel> allModels = new ArrayList<>();
@@ -746,6 +783,7 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L
746783
}
747784
this.addOperationObjectResponseInformation(operations);
748785
this.addOperationPrefixParameterInterfacesInformation(operations);
786+
this.addMultipartFileArrayApiExampleValues(operations);
749787

750788
return operations;
751789
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ private boolean isLanguageGenericType(String type) {
201201
public void postProcessParameter(CodegenParameter parameter) {
202202
super.postProcessParameter(parameter);
203203
parameter.dataType = applyLocalTypeMapping(parameter.dataType);
204-
if (parameter.isFormParam && parameter.isArray && "binary".equals(parameter.dataFormat)) {
204+
if (isBinaryFormArray(parameter)) {
205+
parameter.isFile = true;
205206
parameter.isCollectionFormatMulti = true;
206207
}
207208
}

modules/openapi-generator/src/main/resources/typescript-fetch/api_example.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ async function example() {
2727
const body = {
2828
{{#allParams}}
2929
// {{{dataType}}}{{#description}} | {{{description}}}{{/description}}{{^required}} (optional){{/required}}
30-
{{paramName}}: {{{example}}}{{^example}}...{{/example}},
30+
{{paramName}}: {{#vendorExtensions.x-typescriptFetchApiExample}}{{{.}}}{{/vendorExtensions.x-typescriptFetchApiExample}}{{^vendorExtensions.x-typescriptFetchApiExample}}{{{example}}}{{^example}}...{{/example}}{{/vendorExtensions.x-typescriptFetchApiExample}},
3131
{{/allParams}}
3232
} satisfies {{operationIdCamelCase}}Request;
3333

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public final class TypeScriptGroups {
2222
public static final String TYPESCRIPT_AURELIA = "typescript-aurelia";
2323
public static final String TYPESCRIPT_AXIOS = "typescript-axios";
2424
public static final String TYPESCRIPT_FETCH = "typescript-fetch";
25+
public static final String TYPESCRIPT_INVERSIFY = "typescript-inversify";
2526
public static final String TYPESCRIPT_ANGULAR = "typescript-angular";
2627
public static final String TYPESCRIPT_NESTJS = "typescript-nestjs";
2728
public static final String TYPESCRIPT_NODE = "typescript-node";

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,29 @@ public void testDeprecatedArrayAttribute() throws Exception {
179179
TestUtils.assertFileContains(file, "'nicknames'?: Array<string>");
180180
}
181181

182+
@Test(description = "Verify multipart file arrays use repeated form fields")
183+
public void testMultipartFileArrayUsesRepeatedFormFields() throws Exception {
184+
final File output = Files.createTempDirectory("typescript_axios_multipart_file_array_").toFile();
185+
output.deleteOnExit();
186+
187+
final CodegenConfigurator configurator = new CodegenConfigurator()
188+
.setGeneratorName("typescript-axios")
189+
.setInputSpec("src/test/resources/3_0/form-multipart-binary-array.yaml")
190+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
191+
192+
final ClientOptInput clientOptInput = configurator.toClientOptInput();
193+
final DefaultGenerator generator = new DefaultGenerator();
194+
final List<File> files = generator.opts(clientOptInput).generate();
195+
files.forEach(File::deleteOnExit);
196+
197+
Path api = Paths.get(output + "/api.ts");
198+
TestUtils.assertFileExists(api);
199+
TestUtils.assertFileContains(api, "files?: Array<File>");
200+
TestUtils.assertFileContains(api, "files.forEach((element) => {");
201+
TestUtils.assertFileContains(api, "localVarFormParams.append('files', element as any);");
202+
TestUtils.assertFileNotContains(api, "files.join(COLLECTION_FORMATS.csv)");
203+
}
204+
182205
@Test
183206
public void generatesTrailingCommasInAsConstEnumObjects() throws Exception {
184207
final File output = Files.createTempDirectory("typescript_axios_trailing_commas_").toFile();

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,32 @@ public void testOneOfModelsImportNonPrimitiveTypes() throws IOException {
453453
TestUtils.assertFileContains(testResponse, "import type { OptionThree } from './OptionThree'");
454454
}
455455

456+
@Test(description = "Verify multipart file arrays use FormData with repeated file fields")
457+
public void testMultipartFileArrayUsesFormData() throws IOException {
458+
File output = generate(
459+
Collections.emptyMap(),
460+
"src/test/resources/3_0/typescript-fetch/multipart-file-array.yaml"
461+
);
462+
463+
Path api = Paths.get(output + "/apis/DefaultApi.ts");
464+
TestUtils.assertFileExists(api);
465+
TestUtils.assertFileContains(api, "files: Array<Blob>;");
466+
TestUtils.assertFileContains(api, "metadata?: string;");
467+
TestUtils.assertFileContains(api, "// use FormData to transmit files using content-type \"multipart/form-data\"");
468+
TestUtils.assertFileContains(api, "useForm = canConsumeForm;");
469+
TestUtils.assertFileContains(api, "formParams = new FormData();");
470+
TestUtils.assertFileContains(api, "requestParameters['files'].forEach((element) => {");
471+
TestUtils.assertFileContains(api, "formParams.append('files', element as any);");
472+
TestUtils.assertFileNotContains(api, "requestParameters['files']!.join(runtime.COLLECTION_FORMATS[\"csv\"])");
473+
474+
Path apiDocs = Paths.get(output + "/docs/DefaultApi.md");
475+
TestUtils.assertFileExists(apiDocs);
476+
TestUtils.assertFileContains(apiDocs, "files: [new Blob(['example file content'], { type: 'application/octet-stream' })],");
477+
TestUtils.assertFileContains(apiDocs, "metadata: 'metadata_example',");
478+
TestUtils.assertFileNotContains(apiDocs, "files: /path/to/file.txt");
479+
TestUtils.assertFileNotContains(apiDocs, "metadata: metadata_example");
480+
}
481+
456482
@Test(description = "Verify instanceOf checks discriminator value for single-value enums")
457483
public void testInstanceOfChecksDiscriminatorValue() throws IOException {
458484
File output = generate(Collections.emptyMap(), "src/test/resources/3_0/typescript-fetch/oneOf.yaml");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.openapitools.codegen.typescript.inversify;
2+
3+
import org.openapitools.codegen.ClientOptInput;
4+
import org.openapitools.codegen.DefaultGenerator;
5+
import org.openapitools.codegen.TestUtils;
6+
import org.openapitools.codegen.config.CodegenConfigurator;
7+
import org.openapitools.codegen.typescript.TypeScriptGroups;
8+
import org.testng.annotations.Test;
9+
10+
import java.io.File;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.nio.file.Paths;
14+
import java.util.List;
15+
16+
@Test(groups = {TypeScriptGroups.TYPESCRIPT, TypeScriptGroups.TYPESCRIPT_INVERSIFY})
17+
public class TypeScriptInversifyClientCodegenTest {
18+
19+
@Test(description = "Verify multipart file arrays use repeated form fields")
20+
public void testMultipartFileArrayUsesRepeatedFormFields() throws Exception {
21+
final File output = Files.createTempDirectory("typescript_inversify_multipart_file_array_").toFile();
22+
output.deleteOnExit();
23+
24+
final CodegenConfigurator configurator = new CodegenConfigurator()
25+
.setGeneratorName("typescript-inversify")
26+
.setInputSpec("src/test/resources/3_0/form-multipart-binary-array.yaml")
27+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
28+
29+
final ClientOptInput clientOptInput = configurator.toClientOptInput();
30+
final DefaultGenerator generator = new DefaultGenerator();
31+
final List<File> files = generator.opts(clientOptInput).generate();
32+
files.forEach(File::deleteOnExit);
33+
34+
Path api = Paths.get(output + "/api/multipart.service.ts");
35+
TestUtils.assertFileExists(api);
36+
TestUtils.assertFileContains(api, "files?: Array<Blob>");
37+
TestUtils.assertFileContains(api, "files.forEach((element) => {");
38+
TestUtils.assertFileContains(api, "formData.append('files', <any>element);");
39+
TestUtils.assertFileNotContains(api, "files.join(COLLECTION_FORMATS['csv'])");
40+
}
41+
}

0 commit comments

Comments
 (0)