diff --git a/bin/configs/typescript-fetch-additional-properties-in-multipart-issue.yaml b/bin/configs/typescript-fetch-additional-properties-in-multipart-issue.yaml new file mode 100644 index 000000000000..3c4a45f10e07 --- /dev/null +++ b/bin/configs/typescript-fetch-additional-properties-in-multipart-issue.yaml @@ -0,0 +1,7 @@ +generatorName: typescript-fetch +outputDir: samples/client/others/typescript-fetch/additional-properties-in-multipart-issue +inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-fetch/additional-properties-in-multipart-issue.yaml +templateDir: modules/openapi-generator/src/main/resources/typescript-fetch +additionalProperties: + enumPropertyNaming: "original" + enumUnknownDefaultCase: true diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache index 5c99230c73f0..f4cfdbb9e6d5 100644 --- a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache @@ -268,7 +268,12 @@ export class {{classname}} extends runtime.BaseAPI { {{/isEnumRef}} {{^isEnumRef}} {{^withoutRuntimeChecks}} + {{^isContainer}} formParams.append('{{baseName}}', new Blob([JSON.stringify({{{dataType}}}ToJSON(requestParameters['{{paramName}}']))], { type: "application/json", })); + {{/isContainer}} + {{#isContainer}} + formParams.append('{{baseName}}', new Blob([JSON.stringify(requestParameters['{{paramName}}'])], { type: "application/json", })); + {{/isContainer}} {{/withoutRuntimeChecks}}{{#withoutRuntimeChecks}} formParams.append('{{baseName}}', new Blob([JSON.stringify(requestParameters['{{paramName}}'])], { type: "application/json", })); {{/withoutRuntimeChecks}} diff --git a/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/additional-properties-in-multipart-issue.yaml b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/additional-properties-in-multipart-issue.yaml new file mode 100644 index 000000000000..3a6650e0aaff --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/additional-properties-in-multipart-issue.yaml @@ -0,0 +1,56 @@ +openapi: 3.0.3 +info: + title: Minimal + description: Api to reproduce bug + version: 0.5.1-SNAPSHOT.0 +tags: + - name: test +servers: + - url: http://localhost:8080 +paths: + "/api/v1/file": + post: + tags: + - file + operationId: createFile + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: "#/components/schemas/FileUploadRequest" + encoding: + documentBytes: + contentType: "*/*" + properties: + contentType: application/json + responses: + "201": + description: File created successfully +components: + schemas: + FileUploadRequest: + type: object + properties: + documentBytes: + type: string + format: binary + documentType: + type: string + structured: + $ref: '#/components/schemas/StructuredType' + properties: + $ref: '#/components/schemas/TypeMap' + required: + - documentBytes + - documentType + - properties + TypeMap: + type: object + additionalProperties: + type: string + StructuredType: + type: object + properties: + someString: + type: string \ No newline at end of file diff --git a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/.openapi-generator-ignore b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/.openapi-generator-ignore @@ -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 diff --git a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/.openapi-generator/FILES b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/.openapi-generator/FILES new file mode 100644 index 000000000000..39d4362c18f1 --- /dev/null +++ b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/.openapi-generator/FILES @@ -0,0 +1,8 @@ +apis/FileApi.ts +apis/index.ts +docs/FileApi.md +docs/StructuredType.md +index.ts +models/StructuredType.ts +models/index.ts +runtime.ts diff --git a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/.openapi-generator/VERSION b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/.openapi-generator/VERSION new file mode 100644 index 000000000000..909dcd0eca63 --- /dev/null +++ b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.19.0-SNAPSHOT diff --git a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/apis/FileApi.ts b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/apis/FileApi.ts new file mode 100644 index 000000000000..284ebbe5e311 --- /dev/null +++ b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/apis/FileApi.ts @@ -0,0 +1,117 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Minimal + * Api to reproduce bug + * + * The version of the OpenAPI document: 0.5.1-SNAPSHOT.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'; +import type { + StructuredType, +} from '../models/index'; +import { + StructuredTypeFromJSON, + StructuredTypeToJSON, +} from '../models/index'; + +export interface CreateFileRequest { + documentBytes: Blob; + documentType: string; + properties: { [key: string]: string; }; + structured?: StructuredType; +} + +/** + * + */ +export class FileApi extends runtime.BaseAPI { + + /** + */ + async createFileRaw(requestParameters: CreateFileRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['documentBytes'] == null) { + throw new runtime.RequiredError( + 'documentBytes', + 'Required parameter "documentBytes" was null or undefined when calling createFile().' + ); + } + + if (requestParameters['documentType'] == null) { + throw new runtime.RequiredError( + 'documentType', + 'Required parameter "documentType" was null or undefined when calling createFile().' + ); + } + + if (requestParameters['properties'] == null) { + throw new runtime.RequiredError( + 'properties', + 'Required parameter "properties" was null or undefined when calling createFile().' + ); + } + + 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['documentBytes'] != null) { + formParams.append('documentBytes', requestParameters['documentBytes'] as any); + } + + if (requestParameters['documentType'] != null) { + formParams.append('documentType', requestParameters['documentType'] as any); + } + + if (requestParameters['structured'] != null) { + formParams.append('structured', new Blob([JSON.stringify(StructuredTypeToJSON(requestParameters['structured']))], { type: "application/json", })); + } + + if (requestParameters['properties'] != null) { + formParams.append('properties', new Blob([JSON.stringify(requestParameters['properties'])], { type: "application/json", })); + } + + + let urlPath = `/api/v1/file`; + + const response = await this.request({ + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: formParams, + }, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + */ + async createFile(requestParameters: CreateFileRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.createFileRaw(requestParameters, initOverrides); + } + +} diff --git a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/apis/index.ts b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/apis/index.ts new file mode 100644 index 000000000000..ee5d66fbae8b --- /dev/null +++ b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/apis/index.ts @@ -0,0 +1,3 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './FileApi'; diff --git a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/docs/FileApi.md b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/docs/FileApi.md new file mode 100644 index 000000000000..142a1034e212 --- /dev/null +++ b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/docs/FileApi.md @@ -0,0 +1,83 @@ +# FileApi + +All URIs are relative to *http://localhost:8080* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**createFile**](FileApi.md#createfile) | **POST** /api/v1/file | | + + + +## createFile + +> createFile(documentBytes, documentType, properties, structured) + + + +### Example + +```ts +import { + Configuration, + FileApi, +} from ''; +import type { CreateFileRequest } from ''; + +async function example() { + console.log("🚀 Testing SDK..."); + const api = new FileApi(); + + const body = { + // Blob + documentBytes: BINARY_DATA_HERE, + // string + documentType: documentType_example, + // { [key: string]: string; } + properties: ..., + // StructuredType (optional) + structured: ..., + } satisfies CreateFileRequest; + + try { + const data = await api.createFile(body); + console.log(data); + } catch (error) { + console.error(error); + } +} + +// Run the test +example().catch(console.error); +``` + +### Parameters + + +| Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **documentBytes** | `Blob` | | [Defaults to `undefined`] | +| **documentType** | `string` | | [Defaults to `undefined`] | +| **properties** | `{ [key: string]: string; }` | | | +| **structured** | [StructuredType](StructuredType.md) | | [Optional] [Defaults to `undefined`] | + +### Return type + +`void` (Empty response body) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: `multipart/form-data` +- **Accept**: Not defined + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **201** | File created successfully | - | + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + diff --git a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/docs/StructuredType.md b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/docs/StructuredType.md new file mode 100644 index 000000000000..db3e993f70bb --- /dev/null +++ b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/docs/StructuredType.md @@ -0,0 +1,34 @@ + +# StructuredType + + +## Properties + +Name | Type +------------ | ------------- +`someString` | string + +## Example + +```typescript +import type { StructuredType } from '' + +// TODO: Update the object below with actual values +const example = { + "someString": null, +} satisfies StructuredType + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as StructuredType +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/index.ts b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/index.ts new file mode 100644 index 000000000000..bebe8bbbe206 --- /dev/null +++ b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/index.ts @@ -0,0 +1,5 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './runtime'; +export * from './apis/index'; +export * from './models/index'; diff --git a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/models/StructuredType.ts b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/models/StructuredType.ts new file mode 100644 index 000000000000..2f207427995f --- /dev/null +++ b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/models/StructuredType.ts @@ -0,0 +1,65 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Minimal + * Api to reproduce bug + * + * The version of the OpenAPI document: 0.5.1-SNAPSHOT.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 { mapValues } from '../runtime'; +/** + * + * @export + * @interface StructuredType + */ +export interface StructuredType { + /** + * + * @type {string} + * @memberof StructuredType + */ + someString?: string; +} + +/** + * Check if a given object implements the StructuredType interface. + */ +export function instanceOfStructuredType(value: object): value is StructuredType { + return true; +} + +export function StructuredTypeFromJSON(json: any): StructuredType { + return StructuredTypeFromJSONTyped(json, false); +} + +export function StructuredTypeFromJSONTyped(json: any, ignoreDiscriminator: boolean): StructuredType { + if (json == null) { + return json; + } + return { + + 'someString': json['someString'] == null ? undefined : json['someString'], + }; +} + +export function StructuredTypeToJSON(json: any): StructuredType { + return StructuredTypeToJSONTyped(json, false); +} + +export function StructuredTypeToJSONTyped(value?: StructuredType | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'someString': value['someString'], + }; +} + diff --git a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/models/index.ts b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/models/index.ts new file mode 100644 index 000000000000..869a0d09abca --- /dev/null +++ b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/models/index.ts @@ -0,0 +1,3 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './StructuredType'; diff --git a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/runtime.ts b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/runtime.ts new file mode 100644 index 000000000000..492083e7c372 --- /dev/null +++ b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/runtime.ts @@ -0,0 +1,432 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Minimal + * Api to reproduce bug + * + * The version of the OpenAPI document: 0.5.1-SNAPSHOT.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export const BASE_PATH = "http://localhost:8080".replace(/\/+$/, ""); + +export interface ConfigurationParameters { + basePath?: string; // override base path + fetchApi?: FetchAPI; // override for fetch implementation + middleware?: Middleware[]; // middleware to apply before/after fetch requests + queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings + username?: string; // parameter for basic security + password?: string; // parameter for basic security + apiKey?: string | Promise | ((name: string) => string | Promise); // parameter for apiKey security + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security + headers?: HTTPHeaders; //header params we want to use on every request + credentials?: RequestCredentials; //value for the credentials param we want to use on each request +} + +export class Configuration { + constructor(private configuration: ConfigurationParameters = {}) {} + + set config(configuration: Configuration) { + this.configuration = configuration; + } + + get basePath(): string { + return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; + } + + get fetchApi(): FetchAPI | undefined { + return this.configuration.fetchApi; + } + + get middleware(): Middleware[] { + return this.configuration.middleware || []; + } + + get queryParamsStringify(): (params: HTTPQuery) => string { + return this.configuration.queryParamsStringify || querystring; + } + + get username(): string | undefined { + return this.configuration.username; + } + + get password(): string | undefined { + return this.configuration.password; + } + + get apiKey(): ((name: string) => string | Promise) | undefined { + const apiKey = this.configuration.apiKey; + if (apiKey) { + return typeof apiKey === 'function' ? apiKey : () => apiKey; + } + return undefined; + } + + get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { + const accessToken = this.configuration.accessToken; + if (accessToken) { + return typeof accessToken === 'function' ? accessToken : async () => accessToken; + } + return undefined; + } + + get headers(): HTTPHeaders | undefined { + return this.configuration.headers; + } + + get credentials(): RequestCredentials | undefined { + return this.configuration.credentials; + } +} + +export const DefaultConfig = new Configuration(); + +/** + * This is the base class for all generated API classes. + */ +export class BaseAPI { + + private static readonly jsonRegex = new RegExp('^(:?application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$', 'i'); + private middleware: Middleware[]; + + constructor(protected configuration = DefaultConfig) { + this.middleware = configuration.middleware; + } + + withMiddleware(this: T, ...middlewares: Middleware[]) { + const next = this.clone(); + next.middleware = next.middleware.concat(...middlewares); + return next; + } + + withPreMiddleware(this: T, ...preMiddlewares: Array) { + const middlewares = preMiddlewares.map((pre) => ({ pre })); + return this.withMiddleware(...middlewares); + } + + withPostMiddleware(this: T, ...postMiddlewares: Array) { + const middlewares = postMiddlewares.map((post) => ({ post })); + return this.withMiddleware(...middlewares); + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + protected isJsonMime(mime: string | null | undefined): boolean { + if (!mime) { + return false; + } + return BaseAPI.jsonRegex.test(mime); + } + + protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { + const { url, init } = await this.createFetchParams(context, initOverrides); + const response = await this.fetchApi(url, init); + if (response && (response.status >= 200 && response.status < 300)) { + return response; + } + throw new ResponseError(response, 'Response returned an error code'); + } + + private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { + let url = this.configuration.basePath + context.path; + if (context.query !== undefined && Object.keys(context.query).length !== 0) { + // only add the querystring to the URL if there are query parameters. + // this is done to avoid urls ending with a "?" character which buggy webservers + // do not handle correctly sometimes. + url += '?' + this.configuration.queryParamsStringify(context.query); + } + + const headers = Object.assign({}, this.configuration.headers, context.headers); + Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); + + const initOverrideFn = + typeof initOverrides === "function" + ? initOverrides + : async () => initOverrides; + + const initParams = { + method: context.method, + headers, + body: context.body, + credentials: this.configuration.credentials, + }; + + const overriddenInit: RequestInit = { + ...initParams, + ...(await initOverrideFn({ + init: initParams, + context, + })) + }; + + let body: any; + if (isFormData(overriddenInit.body) + || (overriddenInit.body instanceof URLSearchParams) + || isBlob(overriddenInit.body)) { + body = overriddenInit.body; + } else if (this.isJsonMime(headers['Content-Type'])) { + body = JSON.stringify(overriddenInit.body); + } else { + body = overriddenInit.body; + } + + const init: RequestInit = { + ...overriddenInit, + body + }; + + return { url, init }; + } + + private fetchApi = async (url: string, init: RequestInit) => { + let fetchParams = { url, init }; + for (const middleware of this.middleware) { + if (middleware.pre) { + fetchParams = await middleware.pre({ + fetch: this.fetchApi, + ...fetchParams, + }) || fetchParams; + } + } + let response: Response | undefined = undefined; + try { + response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); + } catch (e) { + for (const middleware of this.middleware) { + if (middleware.onError) { + response = await middleware.onError({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + error: e, + response: response ? response.clone() : undefined, + }) || response; + } + } + if (response === undefined) { + if (e instanceof Error) { + throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); + } else { + throw e; + } + } + } + for (const middleware of this.middleware) { + if (middleware.post) { + response = await middleware.post({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + response: response.clone(), + }) || response; + } + } + return response; + } + + /** + * Create a shallow clone of `this` by constructing a new instance + * and then shallow cloning data members. + */ + private clone(this: T): T { + const constructor = this.constructor as any; + const next = new constructor(this.configuration); + next.middleware = this.middleware.slice(); + return next; + } +}; + +function isBlob(value: any): value is Blob { + return typeof Blob !== 'undefined' && value instanceof Blob; +} + +function isFormData(value: any): value is FormData { + return typeof FormData !== "undefined" && value instanceof FormData; +} + +export class ResponseError extends Error { + override name: "ResponseError" = "ResponseError"; + constructor(public response: Response, msg?: string) { + super(msg); + } +} + +export class FetchError extends Error { + override name: "FetchError" = "FetchError"; + constructor(public cause: Error, msg?: string) { + super(msg); + } +} + +export class RequiredError extends Error { + override name: "RequiredError" = "RequiredError"; + constructor(public field: string, msg?: string) { + super(msg); + } +} + +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; + +export type Json = any; +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; +export type HTTPHeaders = { [key: string]: string }; +export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery }; +export type HTTPBody = Json | FormData | URLSearchParams; +export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody }; +export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original'; + +export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise + +export interface FetchParams { + url: string; + init: RequestInit; +} + +export interface RequestOpts { + path: string; + method: HTTPMethod; + headers: HTTPHeaders; + query?: HTTPQuery; + body?: HTTPBody; +} + +export function querystring(params: HTTPQuery, prefix: string = ''): string { + return Object.keys(params) + .map(key => querystringSingleKey(key, params[key], prefix)) + .filter(part => part.length > 0) + .join('&'); +} + +function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { + const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); + if (value instanceof Array) { + const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) + .join(`&${encodeURIComponent(fullKey)}=`); + return `${encodeURIComponent(fullKey)}=${multiValue}`; + } + if (value instanceof Set) { + const valueAsArray = Array.from(value); + return querystringSingleKey(key, valueAsArray, keyPrefix); + } + if (value instanceof Date) { + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + } + if (value instanceof Object) { + return querystring(value as HTTPQuery, fullKey); + } + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; +} + +export function exists(json: any, key: string) { + const value = json[key]; + return value !== null && value !== undefined; +} + +export function mapValues(data: any, fn: (item: any) => any) { + const result: { [key: string]: any } = {}; + for (const key of Object.keys(data)) { + result[key] = fn(data[key]); + } + return result; +} + +export function canConsumeForm(consumes: Consume[]): boolean { + for (const consume of consumes) { + if ('multipart/form-data' === consume.contentType) { + return true; + } + } + return false; +} + +export interface Consume { + contentType: string; +} + +export interface RequestContext { + fetch: FetchAPI; + url: string; + init: RequestInit; +} + +export interface ResponseContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + response: Response; +} + +export interface ErrorContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + error: unknown; + response?: Response; +} + +export interface Middleware { + pre?(context: RequestContext): Promise; + post?(context: ResponseContext): Promise; + onError?(context: ErrorContext): Promise; +} + +export interface ApiResponse { + raw: Response; + value(): Promise; +} + +export interface ResponseTransformer { + (json: any): T; +} + +export class JSONApiResponse { + constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) {} + + async value(): Promise { + return this.transformer(await this.raw.json()); + } +} + +export class VoidApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return undefined; + } +} + +export class BlobApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return await this.raw.blob(); + }; +} + +export class TextApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return await this.raw.text(); + }; +}