Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bin/configs/typescript-fetch-multipart-file-array.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
generatorName: typescript-fetch
outputDir: samples/client/others/typescript-fetch/multipart-file-array
inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-fetch/multipart-file-array.yaml
templateDir: modules/openapi-generator/src/main/resources/typescript-fetch
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,23 @@ protected void addImport(CodegenModel m, String type) {
}
}

/**
* Returns true for multipart form arrays whose array or item schema is binary.
*
* @param parameter Codegen parameter
*/
protected static boolean isBinaryFormArray(CodegenParameter parameter) {
if (!parameter.isFormParam || !parameter.isArray) {
return false;
}
if ("binary".equals(parameter.dataFormat)) {
return true;
}

CodegenProperty items = parameter.items;
return items != null && (items.isFile || items.isBinary || "binary".equals(items.dataFormat));
}

/**
* Override to fix the inner enum naming issue for maps/arrays of enums.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ private void updateOperationParameterForEnum(OperationsMap operations) {
@Override
public void postProcessParameter(CodegenParameter parameter) {
super.postProcessParameter(parameter);
if (parameter.isFormParam && parameter.isArray && "binary".equals(parameter.dataFormat)) {
if (isBinaryFormArray(parameter)) {
parameter.isFile = true;
parameter.isCollectionFormatMulti = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
private static final String X_ENTITY_ID = "x-entityId";
private static final String X_OPERATION_RETURN_PASSTHROUGH = "x-operationReturnPassthrough";
private static final String X_KEEP_AS_JS_OBJECT = "x-keepAsJSObject";
private static final String X_TYPESCRIPT_FETCH_API_EXAMPLE = "x-typescriptFetchApiExample";
private static final String BLOB_API_EXAMPLE = "new Blob(['example file content'], { type: 'application/octet-stream' })";

protected boolean sagasAndRecords = false;
@Getter @Setter
Expand Down Expand Up @@ -416,11 +418,46 @@ public ModelsMap postProcessModels(ModelsMap objs) {
@Override
public void postProcessParameter(CodegenParameter parameter) {
super.postProcessParameter(parameter);
if (parameter.isFormParam && parameter.isArray && "binary".equals(parameter.dataFormat)) {
if (isBinaryFormArray(parameter)) {
parameter.isFile = true;
parameter.isCollectionFormatMulti = true;
}
}

private void addMultipartFileArrayApiExampleValues(OperationsMap operations) {
for (CodegenOperation operation : operations.getOperations().getOperation()) {
if (operation.allParams == null || operation.allParams.stream().noneMatch(TypeScriptFetchClientCodegen::isBinaryFormArray)) {
continue;
}

for (CodegenParameter parameter : operation.allParams) {
setApiExampleValue(parameter);
}
}
}

private void setApiExampleValue(CodegenParameter parameter) {
String example = toApiExampleValue(parameter);
if (example != null) {
parameter.vendorExtensions.put(X_TYPESCRIPT_FETCH_API_EXAMPLE, example);
}
}

private String toApiExampleValue(CodegenParameter parameter) {
if (isBinaryFormArray(parameter)) {
return "[" + BLOB_API_EXAMPLE + "]";
} else if (parameter.isFile || parameter.isBinary) {
return BLOB_API_EXAMPLE;
} else if (parameter.isString) {
String example = parameter.example;
if (example == null) {
example = parameter.paramName + "_example";
}
return "'" + escapeText(example) + "'";
}
return null;
}

@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
List<ExtendedCodegenModel> allModels = new ArrayList<>();
Expand Down Expand Up @@ -746,6 +783,7 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L
}
this.addOperationObjectResponseInformation(operations);
this.addOperationPrefixParameterInterfacesInformation(operations);
this.addMultipartFileArrayApiExampleValues(operations);

return operations;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ private boolean isLanguageGenericType(String type) {
public void postProcessParameter(CodegenParameter parameter) {
super.postProcessParameter(parameter);
parameter.dataType = applyLocalTypeMapping(parameter.dataType);
if (parameter.isFormParam && parameter.isArray && "binary".equals(parameter.dataFormat)) {
if (isBinaryFormArray(parameter)) {
parameter.isFile = true;
parameter.isCollectionFormatMulti = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async function example() {
const body = {
{{#allParams}}
// {{{dataType}}}{{#description}} | {{{description}}}{{/description}}{{^required}} (optional){{/required}}
{{paramName}}: {{{example}}}{{^example}}...{{/example}},
{{paramName}}: {{#vendorExtensions.x-typescriptFetchApiExample}}{{{.}}}{{/vendorExtensions.x-typescriptFetchApiExample}}{{^vendorExtensions.x-typescriptFetchApiExample}}{{{example}}}{{^example}}...{{/example}}{{/vendorExtensions.x-typescriptFetchApiExample}},
{{/allParams}}
} satisfies {{operationIdCamelCase}}Request;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public final class TypeScriptGroups {
public static final String TYPESCRIPT_AURELIA = "typescript-aurelia";
public static final String TYPESCRIPT_AXIOS = "typescript-axios";
public static final String TYPESCRIPT_FETCH = "typescript-fetch";
public static final String TYPESCRIPT_INVERSIFY = "typescript-inversify";
public static final String TYPESCRIPT_ANGULAR = "typescript-angular";
public static final String TYPESCRIPT_NESTJS = "typescript-nestjs";
public static final String TYPESCRIPT_NODE = "typescript-node";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,29 @@ public void testDeprecatedArrayAttribute() throws Exception {
TestUtils.assertFileContains(file, "'nicknames'?: Array<string>");
}

@Test(description = "Verify multipart file arrays use repeated form fields")
public void testMultipartFileArrayUsesRepeatedFormFields() throws Exception {
final File output = Files.createTempDirectory("typescript_axios_multipart_file_array_").toFile();
output.deleteOnExit();

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("typescript-axios")
.setInputSpec("src/test/resources/3_0/form-multipart-binary-array.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

final ClientOptInput clientOptInput = configurator.toClientOptInput();
final DefaultGenerator generator = new DefaultGenerator();
final List<File> files = generator.opts(clientOptInput).generate();
files.forEach(File::deleteOnExit);

Path api = Paths.get(output + "/api.ts");
TestUtils.assertFileExists(api);
TestUtils.assertFileContains(api, "files?: Array<File>");
TestUtils.assertFileContains(api, "files.forEach((element) => {");
TestUtils.assertFileContains(api, "localVarFormParams.append('files', element as any);");
TestUtils.assertFileNotContains(api, "files.join(COLLECTION_FORMATS.csv)");
}

@Test
public void generatesTrailingCommasInAsConstEnumObjects() throws Exception {
final File output = Files.createTempDirectory("typescript_axios_trailing_commas_").toFile();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,32 @@ public void testOneOfModelsImportNonPrimitiveTypes() throws IOException {
TestUtils.assertFileContains(testResponse, "import type { OptionThree } from './OptionThree'");
}

@Test(description = "Verify multipart file arrays use FormData with repeated file fields")
public void testMultipartFileArrayUsesFormData() throws IOException {
File output = generate(
Collections.emptyMap(),
"src/test/resources/3_0/typescript-fetch/multipart-file-array.yaml"
);

Path api = Paths.get(output + "/apis/DefaultApi.ts");
TestUtils.assertFileExists(api);
TestUtils.assertFileContains(api, "files: Array<Blob>;");
TestUtils.assertFileContains(api, "metadata?: string;");
TestUtils.assertFileContains(api, "// use FormData to transmit files using content-type \"multipart/form-data\"");
TestUtils.assertFileContains(api, "useForm = canConsumeForm;");
TestUtils.assertFileContains(api, "formParams = new FormData();");
TestUtils.assertFileContains(api, "requestParameters['files'].forEach((element) => {");
TestUtils.assertFileContains(api, "formParams.append('files', element as any);");
TestUtils.assertFileNotContains(api, "requestParameters['files']!.join(runtime.COLLECTION_FORMATS[\"csv\"])");

Path apiDocs = Paths.get(output + "/docs/DefaultApi.md");
TestUtils.assertFileExists(apiDocs);
TestUtils.assertFileContains(apiDocs, "files: [new Blob(['example file content'], { type: 'application/octet-stream' })],");
TestUtils.assertFileContains(apiDocs, "metadata: 'metadata_example',");
TestUtils.assertFileNotContains(apiDocs, "files: /path/to/file.txt");
TestUtils.assertFileNotContains(apiDocs, "metadata: metadata_example");
}

@Test(description = "Verify instanceOf checks discriminator value for single-value enums")
public void testInstanceOfChecksDiscriminatorValue() throws IOException {
File output = generate(Collections.emptyMap(), "src/test/resources/3_0/typescript-fetch/oneOf.yaml");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.openapitools.codegen.typescript.inversify;

import org.openapitools.codegen.ClientOptInput;
import org.openapitools.codegen.DefaultGenerator;
import org.openapitools.codegen.TestUtils;
import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.typescript.TypeScriptGroups;
import org.testng.annotations.Test;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

@Test(groups = {TypeScriptGroups.TYPESCRIPT, TypeScriptGroups.TYPESCRIPT_INVERSIFY})
public class TypeScriptInversifyClientCodegenTest {

@Test(description = "Verify multipart file arrays use repeated form fields")
public void testMultipartFileArrayUsesRepeatedFormFields() throws Exception {
final File output = Files.createTempDirectory("typescript_inversify_multipart_file_array_").toFile();
output.deleteOnExit();

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("typescript-inversify")
.setInputSpec("src/test/resources/3_0/form-multipart-binary-array.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

final ClientOptInput clientOptInput = configurator.toClientOptInput();
final DefaultGenerator generator = new DefaultGenerator();
final List<File> files = generator.opts(clientOptInput).generate();
files.forEach(File::deleteOnExit);

Path api = Paths.get(output + "/api/multipart.service.ts");
TestUtils.assertFileExists(api);
TestUtils.assertFileContains(api, "files?: Array<Blob>");
TestUtils.assertFileContains(api, "files.forEach((element) => {");
TestUtils.assertFileContains(api, "formData.append('files', <any>element);");
TestUtils.assertFileNotContains(api, "files.join(COLLECTION_FORMATS['csv'])");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
openapi: 3.0.3
info:
title: Multipart File Array
version: 1.0.0
paths:
/upload:
post:
operationId: uploadFiles
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required:
- files
properties:
files:
type: array
items:
type: string
format: binary
metadata:
type: string
responses:
'204':
description: Successful upload
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator

# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.

# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs

# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux

# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux

# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apis/DefaultApi.ts
apis/index.ts
docs/DefaultApi.md
index.ts
runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7.24.0-SNAPSHOT
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* tslint:disable */
/* eslint-disable */
/**
* Multipart File Array
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/

import * as runtime from '../runtime';

export interface UploadFilesRequest {
files: Array<Blob>;
metadata?: string;
}

/**
*
*/
export class DefaultApi extends runtime.BaseAPI {

/**
* Creates request options for uploadFiles without sending the request
*/
async uploadFilesRequestOpts(requestParameters: UploadFilesRequest): Promise<runtime.RequestOpts> {
if (requestParameters['files'] == null) {
throw new runtime.RequiredError(
'files',
'Required parameter "files" was null or undefined when calling uploadFiles().'
);
}

const queryParameters: any = {};

const headerParameters: runtime.HTTPHeaders = {};

const consumes: runtime.Consume[] = [
{ contentType: 'multipart/form-data' },
];
// @ts-ignore: canConsumeForm may be unused
const canConsumeForm = runtime.canConsumeForm(consumes);

let formParams: { append(param: string, value: any): any };
let useForm = false;
// use FormData to transmit files using content-type "multipart/form-data"
useForm = canConsumeForm;
if (useForm) {
formParams = new FormData();
} else {
formParams = new URLSearchParams();
}

if (requestParameters['files'] != null) {
requestParameters['files'].forEach((element) => {
formParams.append('files', element as any);
})
}

if (requestParameters['metadata'] != null) {
formParams.append('metadata', requestParameters['metadata'] as any);
}


let urlPath = `/upload`;

return {
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: formParams,
};
}

/**
*/
async uploadFilesRaw(requestParameters: UploadFilesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
const requestOptions = await this.uploadFilesRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);

return new runtime.VoidApiResponse(response);
}

/**
*/
async uploadFiles(requestParameters: UploadFilesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
await this.uploadFilesRaw(requestParameters, initOverrides);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* tslint:disable */
/* eslint-disable */
export * from './DefaultApi';
Loading
Loading