diff --git a/generated/core/ApiError.ts b/generated/core/ApiError.ts new file mode 100644 index 000000000..99d792996 --- /dev/null +++ b/generated/core/ApiError.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; + +export class ApiError extends Error { + public readonly url: string; + public readonly status: number; + public readonly statusText: string; + public readonly body: any; + public readonly request: ApiRequestOptions; + + constructor(request: ApiRequestOptions, response: ApiResult, message: string) { + super(message); + + this.name = 'ApiError'; + this.url = response.url; + this.status = response.status; + this.statusText = response.statusText; + this.body = response.body; + this.request = request; + } +} diff --git a/generated/core/ApiRequestOptions.ts b/generated/core/ApiRequestOptions.ts new file mode 100644 index 000000000..c7b77538c --- /dev/null +++ b/generated/core/ApiRequestOptions.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiRequestOptions = { + readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; + readonly url: string; + readonly path?: Record; + readonly cookies?: Record; + readonly headers?: Record; + readonly query?: Record; + readonly formData?: Record; + readonly body?: any; + readonly mediaType?: string; + readonly responseHeader?: string; + readonly errors?: Record; +}; diff --git a/generated/core/ApiResult.ts b/generated/core/ApiResult.ts new file mode 100644 index 000000000..b095dc770 --- /dev/null +++ b/generated/core/ApiResult.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiResult = { + readonly url: string; + readonly ok: boolean; + readonly status: number; + readonly statusText: string; + readonly body: any; +}; diff --git a/generated/core/CancelablePromise.ts b/generated/core/CancelablePromise.ts new file mode 100644 index 000000000..26ad30391 --- /dev/null +++ b/generated/core/CancelablePromise.ts @@ -0,0 +1,128 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export class CancelError extends Error { + + constructor(message: string) { + super(message); + this.name = 'CancelError'; + } + + public get isCancelled(): boolean { + return true; + } +} + +export interface OnCancel { + readonly isResolved: boolean; + readonly isRejected: boolean; + readonly isCancelled: boolean; + + (cancelHandler: () => void): void; +} + +export class CancelablePromise implements Promise { + readonly [Symbol.toStringTag]!: string; + + private _isResolved: boolean; + private _isRejected: boolean; + private _isCancelled: boolean; + private readonly _cancelHandlers: (() => void)[]; + private readonly _promise: Promise; + private _resolve?: (value: T | PromiseLike) => void; + private _reject?: (reason?: any) => void; + + constructor( + executor: ( + resolve: (value: T | PromiseLike) => void, + reject: (reason?: any) => void, + onCancel: OnCancel + ) => void + ) { + this._isResolved = false; + this._isRejected = false; + this._isCancelled = false; + this._cancelHandlers = []; + this._promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + + const onResolve = (value: T | PromiseLike): void => { + if (this._isResolved || this._isRejected || this._isCancelled) { + return; + } + this._isResolved = true; + this._resolve?.(value); + }; + + const onReject = (reason?: any): void => { + if (this._isResolved || this._isRejected || this._isCancelled) { + return; + } + this._isRejected = true; + this._reject?.(reason); + }; + + const onCancel = (cancelHandler: () => void): void => { + if (this._isResolved || this._isRejected || this._isCancelled) { + return; + } + this._cancelHandlers.push(cancelHandler); + }; + + Object.defineProperty(onCancel, 'isResolved', { + get: (): boolean => this._isResolved, + }); + + Object.defineProperty(onCancel, 'isRejected', { + get: (): boolean => this._isRejected, + }); + + Object.defineProperty(onCancel, 'isCancelled', { + get: (): boolean => this._isCancelled, + }); + + return executor(onResolve, onReject, onCancel as OnCancel); + }); + } + + public then( + onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onRejected?: ((reason: any) => TResult2 | PromiseLike) | null + ): Promise { + return this._promise.then(onFulfilled, onRejected); + } + + public catch( + onRejected?: ((reason: any) => TResult | PromiseLike) | null + ): Promise { + return this._promise.catch(onRejected); + } + + public finally(onFinally?: (() => void) | null): Promise { + return this._promise.finally(onFinally); + } + + public cancel(): void { + if (this._isResolved || this._isRejected || this._isCancelled) { + return; + } + this._isCancelled = true; + if (this._cancelHandlers.length) { + try { + for (const cancelHandler of this._cancelHandlers) { + cancelHandler(); + } + } catch (error) { + console.warn('Cancellation threw an error', error); + return; + } + } + this._cancelHandlers.length = 0; + this._reject?.(new CancelError('Request aborted')); + } + + public get isCancelled(): boolean { + return this._isCancelled; + } +} diff --git a/generated/core/OpenAPI.ts b/generated/core/OpenAPI.ts new file mode 100644 index 000000000..080059c98 --- /dev/null +++ b/generated/core/OpenAPI.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from './ApiRequestOptions'; + +type Resolver = (options: ApiRequestOptions) => Promise; +type Headers = Record; + +export type OpenAPIConfig = { + BASE: string; + VERSION: string; + WITH_CREDENTIALS: boolean; + CREDENTIALS: 'include' | 'omit' | 'same-origin'; + TOKEN?: string | Resolver; + USERNAME?: string | Resolver; + PASSWORD?: string | Resolver; + HEADERS?: Headers | Resolver; + ENCODE_PATH?: (path: string) => string; +}; + +export const OpenAPI: OpenAPIConfig = { + BASE: '', + VERSION: '1.0.0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'include', + TOKEN: undefined, + USERNAME: undefined, + PASSWORD: undefined, + HEADERS: undefined, + ENCODE_PATH: undefined, +}; diff --git a/generated/core/request.ts b/generated/core/request.ts new file mode 100644 index 000000000..058c033f3 --- /dev/null +++ b/generated/core/request.ts @@ -0,0 +1,306 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import { ApiError } from './ApiError'; +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; +import { CancelablePromise } from './CancelablePromise'; +import type { OnCancel } from './CancelablePromise'; +import type { OpenAPIConfig } from './OpenAPI'; + +const isDefined = (value: T | null | undefined): value is Exclude => { + return value !== undefined && value !== null; +}; + +const isString = (value: any): value is string => { + return typeof value === 'string'; +}; + +const isStringWithValue = (value: any): value is string => { + return isString(value) && value !== ''; +}; + +const isBlob = (value: any): value is Blob => { + return ( + typeof value === 'object' && + typeof value.type === 'string' && + typeof value.stream === 'function' && + typeof value.arrayBuffer === 'function' && + typeof value.constructor === 'function' && + typeof value.constructor.name === 'string' && + /^(Blob|File)$/.test(value.constructor.name) && + /^(Blob|File)$/.test(value[Symbol.toStringTag]) + ); +}; + +const isFormData = (value: any): value is FormData => { + return value instanceof FormData; +}; + +const base64 = (str: string): string => { + try { + return btoa(str); + } catch (err) { + // @ts-ignore + return Buffer.from(str).toString('base64'); + } +}; + +const getQueryString = (params: Record): string => { + const qs: string[] = []; + + const append = (key: string, value: any) => { + qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + }; + + const process = (key: string, value: any) => { + if (isDefined(value)) { + if (Array.isArray(value)) { + value.forEach(v => { + process(key, v); + }); + } else if (typeof value === 'object') { + Object.entries(value).forEach(([k, v]) => { + process(`${key}[${k}]`, v); + }); + } else { + append(key, value); + } + } + }; + + Object.entries(params).forEach(([key, value]) => { + process(key, value); + }); + + if (qs.length > 0) { + return `?${qs.join('&')}`; + } + + return ''; +}; + +const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { + const encoder = config.ENCODE_PATH || encodeURI; + + const path = options.url + .replace('{api-version}', config.VERSION) + .replace(/{(.*?)}/g, (substring: string, group: string) => { + if (options.path?.hasOwnProperty(group)) { + return encoder(String(options.path[group])); + } + return substring; + }); + + const url = `${config.BASE}${path}`; + if (options.query) { + return `${url}${getQueryString(options.query)}`; + } + return url; +}; + +const getFormData = (options: ApiRequestOptions): FormData | undefined => { + if (options.formData) { + const formData = new FormData(); + + const process = (key: string, value: any) => { + if (isString(value) || isBlob(value)) { + formData.append(key, value); + } else { + formData.append(key, JSON.stringify(value)); + } + }; + + Object.entries(options.formData) + .filter(([_, value]) => isDefined(value)) + .forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach(v => process(key, v)); + } else { + process(key, value); + } + }); + + return formData; + } + return undefined; +}; + +type Resolver = (options: ApiRequestOptions) => Promise; + +const resolve = async (options: ApiRequestOptions, resolver?: T | Resolver): Promise => { + if (typeof resolver === 'function') { + return (resolver as Resolver)(options); + } + return resolver; +}; + +const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise => { + const token = await resolve(options, config.TOKEN); + const username = await resolve(options, config.USERNAME); + const password = await resolve(options, config.PASSWORD); + const additionalHeaders = await resolve(options, config.HEADERS); + + const headers = Object.entries({ + Accept: 'application/json', + ...additionalHeaders, + ...options.headers, + }) + .filter(([_, value]) => isDefined(value)) + .reduce((headers, [key, value]) => ({ + ...headers, + [key]: String(value), + }), {} as Record); + + if (isStringWithValue(token)) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (isStringWithValue(username) && isStringWithValue(password)) { + const credentials = base64(`${username}:${password}`); + headers['Authorization'] = `Basic ${credentials}`; + } + + if (options.body) { + if (options.mediaType) { + headers['Content-Type'] = options.mediaType; + } else if (isBlob(options.body)) { + headers['Content-Type'] = options.body.type || 'application/octet-stream'; + } else if (isString(options.body)) { + headers['Content-Type'] = 'text/plain'; + } else if (!isFormData(options.body)) { + headers['Content-Type'] = 'application/json'; + } + } + + return new Headers(headers); +}; + +const getRequestBody = (options: ApiRequestOptions): any => { + if (options.body !== undefined) { + if (options.mediaType?.includes('/json')) { + return JSON.stringify(options.body) + } else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) { + return options.body; + } else { + return JSON.stringify(options.body); + } + } + return undefined; +}; + +export const sendRequest = async ( + config: OpenAPIConfig, + options: ApiRequestOptions, + url: string, + body: any, + formData: FormData | undefined, + headers: Headers, + onCancel: OnCancel +): Promise => { + const controller = new AbortController(); + + const request: RequestInit = { + headers, + body: body ?? formData, + method: options.method, + signal: controller.signal, + }; + + if (config.WITH_CREDENTIALS) { + request.credentials = config.CREDENTIALS; + } + + onCancel(() => controller.abort()); + + return await fetch(url, request); +}; + +const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => { + if (responseHeader) { + const content = response.headers.get(responseHeader); + if (isString(content)) { + return content; + } + } + return undefined; +}; + +const getResponseBody = async (response: Response): Promise => { + if (response.status !== 204) { + try { + const contentType = response.headers.get('Content-Type'); + if (contentType) { + const isJSON = contentType.toLowerCase().startsWith('application/json'); + if (isJSON) { + return await response.json(); + } else { + return await response.text(); + } + } + } catch (error) { + console.error(error); + } + } + return undefined; +}; + +const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { + const errors: Record = { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 500: 'Internal Server Error', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + ...options.errors, + } + + const error = errors[result.status]; + if (error) { + throw new ApiError(options, result, error); + } + + if (!result.ok) { + throw new ApiError(options, result, 'Generic Error'); + } +}; + +/** + * Request method + * @param config The OpenAPI configuration object + * @param options The request options from the service + * @returns CancelablePromise + * @throws ApiError + */ +export const request = (config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise => { + return new CancelablePromise(async (resolve, reject, onCancel) => { + try { + const url = getUrl(config, options); + const formData = getFormData(options); + const body = getRequestBody(options); + const headers = await getHeaders(config, options); + + if (!onCancel.isCancelled) { + const response = await sendRequest(config, options, url, body, formData, headers, onCancel); + const responseBody = await getResponseBody(response); + const responseHeader = getResponseHeader(response, options.responseHeader); + + const result: ApiResult = { + url, + ok: response.ok, + status: response.status, + statusText: response.statusText, + body: responseHeader ?? responseBody, + }; + + catchErrorCodes(options, result); + + resolve(result.body); + } + } catch (error) { + reject(error); + } + }); +}; diff --git a/generated/index.ts b/generated/index.ts new file mode 100644 index 000000000..ab9b2f4fe --- /dev/null +++ b/generated/index.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export { ApiError } from './core/ApiError'; +export { CancelablePromise, CancelError } from './core/CancelablePromise'; +export { OpenAPI } from './core/OpenAPI'; +export type { OpenAPIConfig } from './core/OpenAPI'; + +export type { Post } from './models/Post'; +export type { PostsList } from './models/PostsList'; + +export { PostsService } from './services/PostsService'; diff --git a/generated/models/Post.ts b/generated/models/Post.ts new file mode 100644 index 000000000..cc2182181 --- /dev/null +++ b/generated/models/Post.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type Post = { + id: number; + userId: number; + title: string; + completed: string; +}; + diff --git a/generated/models/PostsList.ts b/generated/models/PostsList.ts new file mode 100644 index 000000000..5688218cd --- /dev/null +++ b/generated/models/PostsList.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Post } from './Post'; + +export type PostsList = Array; diff --git a/generated/services/PostsClientService.ts b/generated/services/PostsClientService.ts new file mode 100644 index 000000000..b65628fea --- /dev/null +++ b/generated/services/PostsClientService.ts @@ -0,0 +1,46 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Post } from '../models/Post'; +import type { PostsList } from '../models/PostsList'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class PostsService { + + /** + * Returns all posts + * @returns PostsList Successful response + * @throws ApiError + */ + public static getPosts(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/posts', + }); + } + + /** + * Returns a post by id + * @param id The user id. + * @returns Post Successful response + * @throws ApiError + */ + public static getPost( + id: number, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/posts/{id}', + path: { + 'id': id, + }, + errors: { + 404: `Post not found`, + }, + }); + } + +} diff --git a/generated/services/PostsService.ts b/generated/services/PostsService.ts new file mode 100644 index 000000000..b65628fea --- /dev/null +++ b/generated/services/PostsService.ts @@ -0,0 +1,46 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Post } from '../models/Post'; +import type { PostsList } from '../models/PostsList'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class PostsService { + + /** + * Returns all posts + * @returns PostsList Successful response + * @throws ApiError + */ + public static getPosts(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/posts', + }); + } + + /** + * Returns a post by id + * @param id The user id. + * @returns Post Successful response + * @throws ApiError + */ + public static getPost( + id: number, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/posts/{id}', + path: { + 'id': id, + }, + errors: { + 404: `Post not found`, + }, + }); + } + +} diff --git a/package.json b/package.json index cf2ffb47a..9701c92f4 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ "eslint": "eslint .", "eslint:fix": "eslint . --fix", "prepublishOnly": "npm run clean && npm run release", - "codecov": "codecov --token=66c30c23-8954-4892-bef9-fbaed0a2e42b" + "codecov": "codecov --token=66c30c23-8954-4892-bef9-fbaed0a2e42b", + "sample": "node ./sample.js" }, "dependencies": { "camelcase": "^6.3.0", diff --git a/sample.js b/sample.js new file mode 100644 index 000000000..7e8415ae6 --- /dev/null +++ b/sample.js @@ -0,0 +1,6 @@ +const OpenAPI = require('./dist/index'); + +OpenAPI.generate({ + input: 'https://raw.githubusercontent.com/stefanwille/openapi-generator-typescript-example/master/json-placeholder-api.yaml', + output: './generated', +}); diff --git a/src/templates/exportClientService.hbs b/src/templates/exportClientService.hbs new file mode 100644 index 000000000..da97559c1 --- /dev/null +++ b/src/templates/exportClientService.hbs @@ -0,0 +1,102 @@ +{{>header}} + +{{#if imports}} +{{#each imports}} +import type { {{{this}}} } from '../models/{{{this}}}'; +{{/each}} +{{/if}} + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class {{{name}}}{{{@root.postfix}}} { + + {{#each operations}} + /** + {{#if deprecated}} + * @deprecated + {{/if}} + {{#if summary}} + * {{{escapeComment summary}}} + {{/if}} + {{#if description}} + * {{{escapeComment description}}} + {{/if}} + {{#unless @root.useOptions}} + {{#if parameters}} + {{#each parameters}} + * @param {{{name}}} {{#if description}}{{{escapeComment description}}}{{/if}} + {{/each}} + {{/if}} + {{/unless}} + {{#each results}} + * @returns {{{type}}} {{#if description}}{{{escapeComment description}}}{{/if}} + {{/each}} + * @throws ApiError + */ + public static {{{name}}}({{>parameters}}): CancelablePromise<{{>result}}> { + return __request(OpenAPI, { + method: '{{{method}}}', + url: '{{{path}}}', + {{#if parametersPath}} + path: { + {{#each parametersPath}} + '{{{prop}}}': {{{name}}}, + {{/each}} + }, + {{/if}} + {{#if parametersCookie}} + cookies: { + {{#each parametersCookie}} + '{{{prop}}}': {{{name}}}, + {{/each}} + }, + {{/if}} + {{#if parametersHeader}} + headers: { + {{#each parametersHeader}} + '{{{prop}}}': {{{name}}}, + {{/each}} + }, + {{/if}} + {{#if parametersQuery}} + query: { + {{#each parametersQuery}} + '{{{prop}}}': {{{name}}}, + {{/each}} + }, + {{/if}} + {{#if parametersForm}} + formData: { + {{#each parametersForm}} + '{{{prop}}}': {{{name}}}, + {{/each}} + }, + {{/if}} + {{#if parametersBody}} + {{#equals parametersBody.in 'formData'}} + formData: {{{parametersBody.name}}}, + {{/equals}} + {{#equals parametersBody.in 'body'}} + body: {{{parametersBody.name}}}, + {{/equals}} + {{#if parametersBody.mediaType}} + mediaType: '{{{parametersBody.mediaType}}}', + {{/if}} + {{/if}} + {{#if responseHeader}} + responseHeader: '{{{responseHeader}}}', + {{/if}} + {{#if errors}} + errors: { + {{#each errors}} + {{{code}}}: `{{{escapeDescription description}}}`, + {{/each}} + }, + {{/if}} + }); + } + + {{/each}} +} diff --git a/src/utils/registerHandlebarTemplates.spec.ts b/src/utils/registerHandlebarTemplates.spec.ts index 5e1302384..53503a587 100644 --- a/src/utils/registerHandlebarTemplates.spec.ts +++ b/src/utils/registerHandlebarTemplates.spec.ts @@ -12,6 +12,7 @@ describe('registerHandlebarTemplates', () => { expect(templates.exports.model).toBeDefined(); expect(templates.exports.schema).toBeDefined(); expect(templates.exports.service).toBeDefined(); + expect(templates.exports.clientService).toBeDefined(); expect(templates.core.settings).toBeDefined(); expect(templates.core.apiError).toBeDefined(); expect(templates.core.apiRequestOptions).toBeDefined(); diff --git a/src/utils/registerHandlebarTemplates.ts b/src/utils/registerHandlebarTemplates.ts index bf77cbdc1..78cd6cf75 100644 --- a/src/utils/registerHandlebarTemplates.ts +++ b/src/utils/registerHandlebarTemplates.ts @@ -52,6 +52,7 @@ import xhrGetResponseBody from '../templates/core/xhr/getResponseBody.hbs'; import xhrGetResponseHeader from '../templates/core/xhr/getResponseHeader.hbs'; import xhrRequest from '../templates/core/xhr/request.hbs'; import xhrSendRequest from '../templates/core/xhr/sendRequest.hbs'; +import templateExportClientService from '../templates/exportClientService.hbs'; import templateExportModel from '../templates/exportModel.hbs'; import templateExportSchema from '../templates/exportSchema.hbs'; import templateExportService from '../templates/exportService.hbs'; @@ -92,6 +93,7 @@ export interface Templates { model: Handlebars.TemplateDelegate; schema: Handlebars.TemplateDelegate; service: Handlebars.TemplateDelegate; + clientService: Handlebars.TemplateDelegate; }; core: { settings: Handlebars.TemplateDelegate; @@ -124,6 +126,7 @@ export const registerHandlebarTemplates = (root: { model: Handlebars.template(templateExportModel), schema: Handlebars.template(templateExportSchema), service: Handlebars.template(templateExportService), + clientService: Handlebars.template(templateExportClientService), }, core: { settings: Handlebars.template(templateCoreSettings), diff --git a/src/utils/writeClient.spec.ts b/src/utils/writeClient.spec.ts index 3c06a95a5..c833200d3 100644 --- a/src/utils/writeClient.spec.ts +++ b/src/utils/writeClient.spec.ts @@ -23,6 +23,7 @@ describe('writeClient', () => { model: () => 'model', schema: () => 'schema', service: () => 'service', + clientService: () => 'clientService', }, core: { settings: () => 'settings', diff --git a/src/utils/writeClient.ts b/src/utils/writeClient.ts index a0ffc1821..0fd5e8254 100644 --- a/src/utils/writeClient.ts +++ b/src/utils/writeClient.ts @@ -13,6 +13,7 @@ import { writeClientIndex } from './writeClientIndex'; import { writeClientModels } from './writeClientModels'; import { writeClientSchemas } from './writeClientSchemas'; import { writeClientServices } from './writeClientServices'; +import { writeUserClientServices } from './writeUserClientServices'; /** * Write our OpenAPI client, using the given templates at the given output @@ -78,6 +79,17 @@ export const writeClient = async ( postfix, clientName ); + await writeUserClientServices( + client.services, + templates, + outputPathServices, + httpClient, + useUnionTypes, + useOptions, + indent, + postfix, + clientName + ); } if (exportSchemas) { diff --git a/src/utils/writeClientClass.spec.ts b/src/utils/writeClientClass.spec.ts index 102f2eb57..56c45596f 100644 --- a/src/utils/writeClientClass.spec.ts +++ b/src/utils/writeClientClass.spec.ts @@ -23,6 +23,7 @@ describe('writeClientClass', () => { model: () => 'model', schema: () => 'schema', service: () => 'service', + clientService: () => 'clientService', }, core: { settings: () => 'settings', diff --git a/src/utils/writeClientCore.spec.ts b/src/utils/writeClientCore.spec.ts index 36990054e..88385bfcd 100644 --- a/src/utils/writeClientCore.spec.ts +++ b/src/utils/writeClientCore.spec.ts @@ -25,6 +25,7 @@ describe('writeClientCore', () => { model: () => 'model', schema: () => 'schema', service: () => 'service', + clientService: () => 'clientService', }, core: { settings: () => 'settings', diff --git a/src/utils/writeClientIndex.spec.ts b/src/utils/writeClientIndex.spec.ts index 6284dfd2e..138360c5a 100644 --- a/src/utils/writeClientIndex.spec.ts +++ b/src/utils/writeClientIndex.spec.ts @@ -21,6 +21,7 @@ describe('writeClientIndex', () => { model: () => 'model', schema: () => 'schema', service: () => 'service', + clientService: () => 'clientService', }, core: { settings: () => 'settings', diff --git a/src/utils/writeClientModels.spec.ts b/src/utils/writeClientModels.spec.ts index e147c8e73..7583cdd8f 100644 --- a/src/utils/writeClientModels.spec.ts +++ b/src/utils/writeClientModels.spec.ts @@ -38,6 +38,7 @@ describe('writeClientModels', () => { model: () => 'model', schema: () => 'schema', service: () => 'service', + clientService: () => 'clientService', }, core: { settings: () => 'settings', diff --git a/src/utils/writeClientSchemas.spec.ts b/src/utils/writeClientSchemas.spec.ts index f71286232..199a51354 100644 --- a/src/utils/writeClientSchemas.spec.ts +++ b/src/utils/writeClientSchemas.spec.ts @@ -38,6 +38,7 @@ describe('writeClientSchemas', () => { model: () => 'model', schema: () => 'schema', service: () => 'service', + clientService: () => 'clientService', }, core: { settings: () => 'settings', diff --git a/src/utils/writeClientServices.spec.ts b/src/utils/writeClientServices.spec.ts index b7ebbfe6c..57a87e779 100644 --- a/src/utils/writeClientServices.spec.ts +++ b/src/utils/writeClientServices.spec.ts @@ -26,6 +26,7 @@ describe('writeClientServices', () => { model: () => 'model', schema: () => 'schema', service: () => 'service', + clientService: () => 'clientService', }, core: { settings: () => 'settings', diff --git a/src/utils/writeUserClientServices.spec.ts b/src/utils/writeUserClientServices.spec.ts new file mode 100644 index 000000000..c652ab9b7 --- /dev/null +++ b/src/utils/writeUserClientServices.spec.ts @@ -0,0 +1,56 @@ +import { EOL } from 'os'; + +import type { Service } from '../client/interfaces/Service'; +import { HttpClient } from '../HttpClient'; +import { Indent } from '../Indent'; +import { writeFile } from './fileSystem'; +import type { Templates } from './registerHandlebarTemplates'; +import { writeUserClientServices } from './writeUserClientServices'; + +jest.mock('./fileSystem'); + +describe('writeUserClientServices', () => { + it('should write to filesystem', async () => { + const services: Service[] = [ + { + name: 'User', + operations: [], + imports: [], + }, + ]; + + const templates: Templates = { + index: () => 'index', + client: () => 'client', + exports: { + model: () => 'model', + schema: () => 'schema', + service: () => 'service', + clientService: () => 'clientService', + }, + core: { + settings: () => 'settings', + apiError: () => 'apiError', + apiRequestOptions: () => 'apiRequestOptions', + apiResult: () => 'apiResult', + cancelablePromise: () => 'cancelablePromise', + request: () => 'request', + baseHttpRequest: () => 'baseHttpRequest', + httpRequest: () => 'httpRequest', + }, + }; + + await writeUserClientServices( + services, + templates, + '/', + HttpClient.FETCH, + false, + false, + Indent.SPACE_4, + 'Service' + ); + + expect(writeFile).toBeCalledWith('/UserClientService.ts', `clientService${EOL}`); + }); +}); diff --git a/src/utils/writeUserClientServices.ts b/src/utils/writeUserClientServices.ts new file mode 100644 index 000000000..d803bb445 --- /dev/null +++ b/src/utils/writeUserClientServices.ts @@ -0,0 +1,47 @@ +import { resolve } from 'path'; + +import type { Service } from '../client/interfaces/Service'; +import type { HttpClient } from '../HttpClient'; +import type { Indent } from '../Indent'; +import { writeFile } from './fileSystem'; +import { formatCode as f } from './formatCode'; +import { formatIndentation as i } from './formatIndentation'; +import { isDefined } from './isDefined'; +import type { Templates } from './registerHandlebarTemplates'; + +/** + * Generate Services using the Handlebar template and write to disk. + * @param services Array of Services to write + * @param templates The loaded handlebar templates + * @param outputPath Directory to write the generated files to + * @param httpClient The selected httpClient (fetch, xhr, node or axios) + * @param useUnionTypes Use union types instead of enums + * @param useOptions Use options or arguments functions + * @param indent Indentation options (4, 2 or tab) + * @param postfix Service name postfix + * @param clientName Custom client class name + */ +export const writeUserClientServices = async ( + services: Service[], + templates: Templates, + outputPath: string, + httpClient: HttpClient, + useUnionTypes: boolean, + useOptions: boolean, + indent: Indent, + postfix: string, + clientName?: string +): Promise => { + for (const service of services) { + const file = resolve(outputPath, `${service.name}Client${postfix}.ts`); + const templateResult = templates.exports.clientService({ + ...service, + httpClient, + useUnionTypes, + useOptions, + postfix, + exportClient: isDefined(clientName), + }); + await writeFile(file, i(f(templateResult), indent)); + } +};