diff --git a/bin/configs/typescript-fetch-self-import-issue.yaml b/bin/configs/typescript-fetch-self-import-issue.yaml new file mode 100644 index 000000000000..d4312e932919 --- /dev/null +++ b/bin/configs/typescript-fetch-self-import-issue.yaml @@ -0,0 +1,7 @@ +generatorName: typescript-fetch +outputDir: samples/client/others/typescript-fetch/self-import-issue +inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-fetch/self-import-issue.yaml +templateDir: modules/openapi-generator/src/main/resources/typescript-fetch +additionalProperties: + typescriptThreePlus: "true" + legacyDiscriminatorBehavior: "false" diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java index 73ce991ffaf2..dcbf5db9cf16 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java @@ -41,6 +41,7 @@ import java.util.*; import java.util.stream.Collectors; +import static java.util.Objects.nonNull; import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER; import static org.openapitools.codegen.utils.StringUtils.*; @@ -387,8 +388,32 @@ public Map postProcessAllModels(Map objs) for (ModelsMap entry : result.values()) { for (ModelMap model : entry.getModels()) { ExtendedCodegenModel codegenModel = (ExtendedCodegenModel) model.getModel(); - model.put("hasImports", codegenModel.imports.size() > 0); + boolean importsPresent = !codegenModel.imports.isEmpty(); + + // When legacyDiscriminatorBehaviour = false, DefaultCodegen will add the mapped models of the + // discriminator to codegenModel.imports, causing us to duplicate the import if we don't remove them + CodegenDiscriminator discriminator = codegenModel.discriminator; + boolean mappedDiscriminatorModelsPresent = nonNull(discriminator) + && nonNull(discriminator.getMappedModels()); + if (importsPresent && mappedDiscriminatorModelsPresent) { + Set mappedDiscriminatorModelNames = discriminator.getMappedModels() + .stream() + .map(CodegenDiscriminator.MappedModel::getModelName) + .collect(Collectors.toSet()); + Set filteredImports = codegenModel.imports + .stream() + .filter(modelImport -> + !mappedDiscriminatorModelNames.contains(modelImport)) + .collect(Collectors.toSet()); + + codegenModel.imports.clear(); + codegenModel.imports.addAll(filteredImports); + } + + model.put("hasImports", importsPresent); model.put("tsImports", toTsImports(codegenModel, parseImports(codegenModel))); + + allModels.add(codegenModel); if (codegenModel.isEntity) { entityModelClassnames.add(codegenModel.classname); diff --git a/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/self-import-issue.yaml b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/self-import-issue.yaml new file mode 100644 index 000000000000..0a2116cc581b --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/self-import-issue.yaml @@ -0,0 +1,34 @@ +openapi: "3.0.1" +info: + title: Example + version: "1" +paths: {} +components: + schemas: + BranchDto: + type: object + properties: + name: + type: string + AbstractUserDto: + type: object + properties: + username: + type: string + branch: + "$ref": "#/components/schemas/BranchDto" + type: + type: string + discriminator: + propertyName: type + mapping: + internal-authenticated: "#/components/schemas/InternalAuthenticatedUserDto" + remote-authenticated: "#/components/schemas/RemoteAuthenticatedUserDto" + InternalAuthenticatedUserDto: + type: object + allOf: + - "$ref": "#/components/schemas/AbstractUserDto" + RemoteAuthenticatedUserDto: + type: object + allOf: + - "$ref": "#/components/schemas/AbstractUserDto" diff --git a/samples/client/others/typescript-fetch/self-import-issue/.openapi-generator-ignore b/samples/client/others/typescript-fetch/self-import-issue/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/client/others/typescript-fetch/self-import-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/self-import-issue/.openapi-generator/FILES b/samples/client/others/typescript-fetch/self-import-issue/.openapi-generator/FILES new file mode 100644 index 000000000000..e75080e4b5f4 --- /dev/null +++ b/samples/client/others/typescript-fetch/self-import-issue/.openapi-generator/FILES @@ -0,0 +1,7 @@ +index.ts +models/AbstractUserDto.ts +models/BranchDto.ts +models/InternalAuthenticatedUserDto.ts +models/RemoteAuthenticatedUserDto.ts +models/index.ts +runtime.ts diff --git a/samples/client/others/typescript-fetch/self-import-issue/.openapi-generator/VERSION b/samples/client/others/typescript-fetch/self-import-issue/.openapi-generator/VERSION new file mode 100644 index 000000000000..6116b14d2c59 --- /dev/null +++ b/samples/client/others/typescript-fetch/self-import-issue/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.8.0-SNAPSHOT diff --git a/samples/client/others/typescript-fetch/self-import-issue/index.ts b/samples/client/others/typescript-fetch/self-import-issue/index.ts new file mode 100644 index 000000000000..ff7dac6ec151 --- /dev/null +++ b/samples/client/others/typescript-fetch/self-import-issue/index.ts @@ -0,0 +1,4 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './runtime'; +export * from './models/index'; diff --git a/samples/client/others/typescript-fetch/self-import-issue/models/AbstractUserDto.ts b/samples/client/others/typescript-fetch/self-import-issue/models/AbstractUserDto.ts new file mode 100644 index 000000000000..0b133d0c066c --- /dev/null +++ b/samples/client/others/typescript-fetch/self-import-issue/models/AbstractUserDto.ts @@ -0,0 +1,93 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Example + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1 + * + * + * 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'; +import type { BranchDto } from './BranchDto'; +import { + BranchDtoFromJSON, + BranchDtoFromJSONTyped, + BranchDtoToJSON, +} from './BranchDto'; + +import { InternalAuthenticatedUserDtoFromJSONTyped } from './InternalAuthenticatedUserDto'; +import { RemoteAuthenticatedUserDtoFromJSONTyped } from './RemoteAuthenticatedUserDto'; +/** + * + * @export + * @interface AbstractUserDto + */ +export interface AbstractUserDto { + /** + * + * @type {string} + * @memberof AbstractUserDto + */ + username?: string; + /** + * + * @type {BranchDto} + * @memberof AbstractUserDto + */ + branch?: BranchDto; + /** + * + * @type {string} + * @memberof AbstractUserDto + */ + type?: string; +} + +/** + * Check if a given object implements the AbstractUserDto interface. + */ +export function instanceOfAbstractUserDto(value: object): value is AbstractUserDto { + return true; +} + +export function AbstractUserDtoFromJSON(json: any): AbstractUserDto { + return AbstractUserDtoFromJSONTyped(json, false); +} + +export function AbstractUserDtoFromJSONTyped(json: any, ignoreDiscriminator: boolean): AbstractUserDto { + if (json == null) { + return json; + } + if (!ignoreDiscriminator) { + if (json['type'] === 'internal-authenticated') { + return InternalAuthenticatedUserDtoFromJSONTyped(json, true); + } + if (json['type'] === 'remote-authenticated') { + return RemoteAuthenticatedUserDtoFromJSONTyped(json, true); + } + } + return { + + 'username': json['username'] == null ? undefined : json['username'], + 'branch': json['branch'] == null ? undefined : BranchDtoFromJSON(json['branch']), + 'type': json['type'] == null ? undefined : json['type'], + }; +} + +export function AbstractUserDtoToJSON(value?: AbstractUserDto | null): any { + if (value == null) { + return value; + } + return { + + 'username': value['username'], + 'branch': BranchDtoToJSON(value['branch']), + 'type': value['type'], + }; +} + diff --git a/samples/client/others/typescript-fetch/self-import-issue/models/BranchDto.ts b/samples/client/others/typescript-fetch/self-import-issue/models/BranchDto.ts new file mode 100644 index 000000000000..916a5431fdb9 --- /dev/null +++ b/samples/client/others/typescript-fetch/self-import-issue/models/BranchDto.ts @@ -0,0 +1,60 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Example + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1 + * + * + * 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 BranchDto + */ +export interface BranchDto { + /** + * + * @type {string} + * @memberof BranchDto + */ + name?: string; +} + +/** + * Check if a given object implements the BranchDto interface. + */ +export function instanceOfBranchDto(value: object): value is BranchDto { + return true; +} + +export function BranchDtoFromJSON(json: any): BranchDto { + return BranchDtoFromJSONTyped(json, false); +} + +export function BranchDtoFromJSONTyped(json: any, ignoreDiscriminator: boolean): BranchDto { + if (json == null) { + return json; + } + return { + + 'name': json['name'] == null ? undefined : json['name'], + }; +} + +export function BranchDtoToJSON(value?: BranchDto | null): any { + if (value == null) { + return value; + } + return { + + 'name': value['name'], + }; +} + diff --git a/samples/client/others/typescript-fetch/self-import-issue/models/InternalAuthenticatedUserDto.ts b/samples/client/others/typescript-fetch/self-import-issue/models/InternalAuthenticatedUserDto.ts new file mode 100644 index 000000000000..9d4c67613353 --- /dev/null +++ b/samples/client/others/typescript-fetch/self-import-issue/models/InternalAuthenticatedUserDto.ts @@ -0,0 +1,55 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Example + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1 + * + * + * 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'; +import type { BranchDto } from './BranchDto'; +import { + BranchDtoFromJSON, + BranchDtoFromJSONTyped, + BranchDtoToJSON, +} from './BranchDto'; +import type { AbstractUserDto } from './AbstractUserDto'; +import { + AbstractUserDtoFromJSON, + AbstractUserDtoFromJSONTyped, + AbstractUserDtoToJSON, +} from './AbstractUserDto'; + +/** + * + * @export + * @interface InternalAuthenticatedUserDto + */ +export interface InternalAuthenticatedUserDto extends AbstractUserDto { +} + +/** + * Check if a given object implements the InternalAuthenticatedUserDto interface. + */ +export function instanceOfInternalAuthenticatedUserDto(value: object): value is InternalAuthenticatedUserDto { + return true; +} + +export function InternalAuthenticatedUserDtoFromJSON(json: any): InternalAuthenticatedUserDto { + return InternalAuthenticatedUserDtoFromJSONTyped(json, false); +} + +export function InternalAuthenticatedUserDtoFromJSONTyped(json: any, ignoreDiscriminator: boolean): InternalAuthenticatedUserDto { + return json; +} + +export function InternalAuthenticatedUserDtoToJSON(value?: InternalAuthenticatedUserDto | null): any { + return value; +} + diff --git a/samples/client/others/typescript-fetch/self-import-issue/models/RemoteAuthenticatedUserDto.ts b/samples/client/others/typescript-fetch/self-import-issue/models/RemoteAuthenticatedUserDto.ts new file mode 100644 index 000000000000..041a56577027 --- /dev/null +++ b/samples/client/others/typescript-fetch/self-import-issue/models/RemoteAuthenticatedUserDto.ts @@ -0,0 +1,55 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Example + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1 + * + * + * 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'; +import type { BranchDto } from './BranchDto'; +import { + BranchDtoFromJSON, + BranchDtoFromJSONTyped, + BranchDtoToJSON, +} from './BranchDto'; +import type { AbstractUserDto } from './AbstractUserDto'; +import { + AbstractUserDtoFromJSON, + AbstractUserDtoFromJSONTyped, + AbstractUserDtoToJSON, +} from './AbstractUserDto'; + +/** + * + * @export + * @interface RemoteAuthenticatedUserDto + */ +export interface RemoteAuthenticatedUserDto extends AbstractUserDto { +} + +/** + * Check if a given object implements the RemoteAuthenticatedUserDto interface. + */ +export function instanceOfRemoteAuthenticatedUserDto(value: object): value is RemoteAuthenticatedUserDto { + return true; +} + +export function RemoteAuthenticatedUserDtoFromJSON(json: any): RemoteAuthenticatedUserDto { + return RemoteAuthenticatedUserDtoFromJSONTyped(json, false); +} + +export function RemoteAuthenticatedUserDtoFromJSONTyped(json: any, ignoreDiscriminator: boolean): RemoteAuthenticatedUserDto { + return json; +} + +export function RemoteAuthenticatedUserDtoToJSON(value?: RemoteAuthenticatedUserDto | null): any { + return value; +} + diff --git a/samples/client/others/typescript-fetch/self-import-issue/models/index.ts b/samples/client/others/typescript-fetch/self-import-issue/models/index.ts new file mode 100644 index 000000000000..4eb72f75acc2 --- /dev/null +++ b/samples/client/others/typescript-fetch/self-import-issue/models/index.ts @@ -0,0 +1,6 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './AbstractUserDto'; +export * from './BranchDto'; +export * from './InternalAuthenticatedUserDto'; +export * from './RemoteAuthenticatedUserDto'; diff --git a/samples/client/others/typescript-fetch/self-import-issue/runtime.ts b/samples/client/others/typescript-fetch/self-import-issue/runtime.ts new file mode 100644 index 000000000000..7dab7bfd087e --- /dev/null +++ b/samples/client/others/typescript-fetch/self-import-issue/runtime.ts @@ -0,0 +1,426 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Example + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1 + * + * + * 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".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 mapValues(data: any, fn: (item: any) => any) { + return Object.keys(data).reduce( + (acc, key) => ({ ...acc, [key]: fn(data[key]) }), + {} + ); +} + +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(); + }; +}