From 027a7275e774733b382275b5b5cd24edec3cc3d4 Mon Sep 17 00:00:00 2001 From: klotztech Date: Sat, 28 Aug 2021 14:47:31 +0200 Subject: [PATCH] Add axios client support --- README.md | 2 +- bin/index.js | 2 +- jest.config.js | 2 + package.json | 7 +- src/HttpClient.ts | 1 + src/templates/core/axios/getHeaders.hbs | 34 +++++++++ src/templates/core/axios/getRequestBody.hbs | 9 +++ src/templates/core/axios/getResponseBody.hbs | 16 ++++ .../core/axios/getResponseHeader.hbs | 9 +++ src/templates/core/axios/request.hbs | 74 +++++++++++++++++++ src/templates/core/axios/sendRequest.hbs | 12 +++ src/templates/core/request.hbs | 1 + src/templates/partials/base.hbs | 1 + src/utils/registerHandlebarTemplates.ts | 14 ++++ test/e2e/v2.axios.spec.js | 48 ++++++++++++ test/e2e/v3.axios.spec.js | 61 +++++++++++++++ types/index.d.ts | 1 + 17 files changed, 290 insertions(+), 4 deletions(-) create mode 100644 src/templates/core/axios/getHeaders.hbs create mode 100644 src/templates/core/axios/getRequestBody.hbs create mode 100644 src/templates/core/axios/getResponseBody.hbs create mode 100644 src/templates/core/axios/getResponseHeader.hbs create mode 100644 src/templates/core/axios/request.hbs create mode 100644 src/templates/core/axios/sendRequest.hbs create mode 100644 test/e2e/v2.axios.spec.js create mode 100644 test/e2e/v3.axios.spec.js diff --git a/README.md b/README.md index eacdf7069..b2d55ee22 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ $ openapi --help -V, --version output the version number -i, --input OpenAPI specification, can be a path, url or string content (required) -o, --output Output directory (required) - -c, --client HTTP client to generate [fetch, xhr, node] (default: "fetch") + -c, --client HTTP client to generate [fetch, axios, xhr, node] (default: "fetch") --useOptions Use options instead of arguments --useUnionTypes Use union types instead of enums --exportCore Write core files to disk (default: true) diff --git a/bin/index.js b/bin/index.js index 4419b9964..f5865a4ab 100755 --- a/bin/index.js +++ b/bin/index.js @@ -12,7 +12,7 @@ const params = program .version(pkg.version) .requiredOption('-i, --input ', 'OpenAPI specification, can be a path, url or string content (required)') .requiredOption('-o, --output ', 'Output directory (required)') - .option('-c, --client ', 'HTTP client to generate [fetch, xhr, node]', 'fetch') + .option('-c, --client ', 'HTTP client to generate [fetch, axios, xhr, node]', 'fetch') .option('--useOptions', 'Use options instead of arguments') .option('--useUnionTypes', 'Use union types instead of enums') .option('--exportCore ', 'Write core files to disk', true) diff --git a/jest.config.js b/jest.config.js index 427bbb70a..aa44f5b69 100644 --- a/jest.config.js +++ b/jest.config.js @@ -19,10 +19,12 @@ module.exports = { testEnvironment: 'node', testMatch: [ '/test/e2e/v2.fetch.spec.js', + '/test/e2e/v2.axios.spec.js', '/test/e2e/v2.xhr.spec.js', '/test/e2e/v2.node.spec.js', '/test/e2e/v2.babel.spec.js', '/test/e2e/v3.fetch.spec.js', + '/test/e2e/v3.axios.spec.js', '/test/e2e/v3.xhr.spec.js', '/test/e2e/v3.node.spec.js', '/test/e2e/v3.babel.spec.js', diff --git a/package.json b/package.json index 9b56ef5e4..52cbb6fdb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openapi-typescript-codegen", - "version": "0.9.3", + "version": "0.9.4", "description": "Library that generates Typescript clients based on the OpenAPI specification.", "author": "Ferdi Koomen", "homepage": "https://github.com/ferdikoomen/openapi-typescript-codegen", @@ -23,7 +23,8 @@ "json", "fetch", "xhr", - "node" + "node", + "axios" ], "maintainers": [ { @@ -76,6 +77,7 @@ "@babel/preset-typescript": "7.14.5", "@rollup/plugin-commonjs": "20.0.0", "@rollup/plugin-node-resolve": "13.0.4", + "@types/axios": "0.14.0", "@types/express": "4.17.13", "@types/jest": "26.0.24", "@types/js-yaml": "4.0.2", @@ -84,6 +86,7 @@ "@types/qs": "6.9.7", "@typescript-eslint/eslint-plugin": "4.28.5", "@typescript-eslint/parser": "4.28.5", + "axios": "0.21.1", "codecov": "3.8.3", "eslint": "7.32.0", "eslint-config-prettier": "8.3.0", diff --git a/src/HttpClient.ts b/src/HttpClient.ts index 49c119e20..1c9fc0572 100644 --- a/src/HttpClient.ts +++ b/src/HttpClient.ts @@ -1,5 +1,6 @@ export enum HttpClient { FETCH = 'fetch', + AXIOS = 'axios', XHR = 'xhr', NODE = 'node', } diff --git a/src/templates/core/axios/getHeaders.hbs b/src/templates/core/axios/getHeaders.hbs new file mode 100644 index 000000000..3d1a62c7e --- /dev/null +++ b/src/templates/core/axios/getHeaders.hbs @@ -0,0 +1,34 @@ +async function getHeaders(options: ApiRequestOptions): Promise { + const token = await resolve(options, OpenAPI.TOKEN); + const username = await resolve(options, OpenAPI.USERNAME); + const password = await resolve(options, OpenAPI.PASSWORD); + const defaultHeaders = await resolve(options, OpenAPI.HEADERS); + + const headers = new Headers({ + Accept: 'application/json', + ...defaultHeaders, + ...options.headers, + }); + + if (isStringWithValue(token)) { + headers.append('Authorization', `Bearer ${token}`); + } + + if (isStringWithValue(username) && isStringWithValue(password)) { + const credentials = btoa(`${username}:${password}`); + headers.append('Authorization', `Basic ${credentials}`); + } + + if (options.body) { + if (options.mediaType) { + headers.append('Content-Type', options.mediaType); + } else if (isBlob(options.body)) { + headers.append('Content-Type', options.body.type || 'application/octet-stream'); + } else if (isString(options.body)) { + headers.append('Content-Type', 'text/plain'); + } else { + headers.append('Content-Type', 'application/json'); + } + } + return headers; +} diff --git a/src/templates/core/axios/getRequestBody.hbs b/src/templates/core/axios/getRequestBody.hbs new file mode 100644 index 000000000..c77c33869 --- /dev/null +++ b/src/templates/core/axios/getRequestBody.hbs @@ -0,0 +1,9 @@ +function getRequestBody(options: ApiRequestOptions): BodyInit | undefined { + if (options.formData) { + return getFormData(options.formData); + } + if (options.body) { + return options.body; + } + return undefined; +} diff --git a/src/templates/core/axios/getResponseBody.hbs b/src/templates/core/axios/getResponseBody.hbs new file mode 100644 index 000000000..729fec92f --- /dev/null +++ b/src/templates/core/axios/getResponseBody.hbs @@ -0,0 +1,16 @@ +async function getResponseBody(response: AxiosResponse): Promise { + 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 null; +} diff --git a/src/templates/core/axios/getResponseHeader.hbs b/src/templates/core/axios/getResponseHeader.hbs new file mode 100644 index 000000000..c8864ef0f --- /dev/null +++ b/src/templates/core/axios/getResponseHeader.hbs @@ -0,0 +1,9 @@ +function getResponseHeader(response: AxiosResponse, responseHeader?: string): string | null { + if (responseHeader) { + const content = response.headers.get(responseHeader); + if (isString(content)) { + return content; + } + } + return null; +} diff --git a/src/templates/core/axios/request.hbs b/src/templates/core/axios/request.hbs new file mode 100644 index 000000000..43e8b8bd5 --- /dev/null +++ b/src/templates/core/axios/request.hbs @@ -0,0 +1,74 @@ +{{>header}} + +import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'; + +import { ApiError } from './ApiError'; +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; +import { OpenAPI } from './OpenAPI'; + +{{>functions/isDefined}} + + +{{>functions/isString}} + + +{{>functions/isStringWithValue}} + + +{{>functions/isBlob}} + + +{{>functions/isSuccess}} + + +{{>functions/getQueryString}} + + +{{>functions/getUrl}} + + +{{>functions/getFormData}} + + +{{>functions/resolve}} + + +{{>axios/getHeaders}} + + +{{>axios/getRequestBody}} + + +{{>axios/sendRequest}} + + +{{>axios/getResponseHeader}} + + +{{>functions/catchErrors}} + + +/** + * Request using axios client + * @param options The request options from the the service + * @returns ApiResult + * @throws ApiError + */ +export async function request(options: ApiRequestOptions): Promise { + const url = getUrl(options); + const response = await sendRequest(options, url); + const responseBody = response.data; + const responseHeader = getResponseHeader(response, options.responseHeader); + + const result: ApiResult = { + url, + ok: isSuccess(response.status), + status: response.status, + statusText: response.statusText, + body: responseHeader || responseBody, + }; + + catchErrors(options, result); + return result; +} diff --git a/src/templates/core/axios/sendRequest.hbs b/src/templates/core/axios/sendRequest.hbs new file mode 100644 index 000000000..31268eaa4 --- /dev/null +++ b/src/templates/core/axios/sendRequest.hbs @@ -0,0 +1,12 @@ +async function sendRequest(options: ApiRequestOptions, url: string): Promise> { + const request: AxiosRequestConfig = { + url, + method: options.method, + headers: await getHeaders(options), + data: getRequestBody(options), + }; + if (OpenAPI.WITH_CREDENTIALS) { + request.withCredentials = true; + } + return await axios(request); +} diff --git a/src/templates/core/request.hbs b/src/templates/core/request.hbs index 84c94313e..ca935ae00 100644 --- a/src/templates/core/request.hbs +++ b/src/templates/core/request.hbs @@ -1,3 +1,4 @@ {{~#equals @root.httpClient 'fetch'}}{{>fetch/request}}{{/equals~}} {{~#equals @root.httpClient 'xhr'}}{{>xhr/request}}{{/equals~}} {{~#equals @root.httpClient 'node'}}{{>node/request}}{{/equals~}} +{{~#equals @root.httpClient 'axios'}}{{>axios/request}}{{/equals~}} diff --git a/src/templates/partials/base.hbs b/src/templates/partials/base.hbs index 467724163..48eb4bf9b 100644 --- a/src/templates/partials/base.hbs +++ b/src/templates/partials/base.hbs @@ -1,5 +1,6 @@ {{~#equals base 'File'~}} {{~#equals @root.httpClient 'fetch'}}Blob{{/equals~}} +{{~#equals @root.httpClient 'axios'}}Blob{{/equals~}} {{~#equals @root.httpClient 'xhr'}}Blob{{/equals~}} {{~#equals @root.httpClient 'node'}}Buffer | ArrayBuffer | ArrayBufferView{{/equals~}} {{~else~}} diff --git a/src/utils/registerHandlebarTemplates.ts b/src/utils/registerHandlebarTemplates.ts index 347b2b98b..21b183eec 100644 --- a/src/utils/registerHandlebarTemplates.ts +++ b/src/utils/registerHandlebarTemplates.ts @@ -4,6 +4,12 @@ import { HttpClient } from '../HttpClient'; import templateCoreApiError from '../templates/core/ApiError.hbs'; import templateCoreApiRequestOptions from '../templates/core/ApiRequestOptions.hbs'; import templateCoreApiResult from '../templates/core/ApiResult.hbs'; +import axiosGetHeaders from '../templates/core/axios/getHeaders.hbs'; +import axiosGetRequestBody from '../templates/core/axios/getRequestBody.hbs'; +import axiosGetResponseBody from '../templates/core/axios/getResponseBody.hbs'; +import axiosGetResponseHeader from '../templates/core/axios/getResponseHeader.hbs'; +import axiosRequest from '../templates/core/axios/request.hbs'; +import axiosSendRequest from '../templates/core/axios/sendRequest.hbs'; import fetchGetHeaders from '../templates/core/fetch/getHeaders.hbs'; import fetchGetRequestBody from '../templates/core/fetch/getRequestBody.hbs'; import fetchGetResponseBody from '../templates/core/fetch/getResponseBody.hbs'; @@ -158,6 +164,14 @@ export function registerHandlebarTemplates(root: { httpClient: HttpClient; useOp Handlebars.registerPartial('fetch/sendRequest', Handlebars.template(fetchSendRequest)); Handlebars.registerPartial('fetch/request', Handlebars.template(fetchRequest)); + // Specific files for the axios client implementation + Handlebars.registerPartial('axios/getHeaders', Handlebars.template(axiosGetHeaders)); + Handlebars.registerPartial('axios/getRequestBody', Handlebars.template(axiosGetRequestBody)); + Handlebars.registerPartial('axios/getResponseBody', Handlebars.template(axiosGetResponseBody)); + Handlebars.registerPartial('axios/getResponseHeader', Handlebars.template(axiosGetResponseHeader)); + Handlebars.registerPartial('axios/sendRequest', Handlebars.template(axiosSendRequest)); + Handlebars.registerPartial('axios/request', Handlebars.template(axiosRequest)); + // Specific files for the xhr client implementation Handlebars.registerPartial('xhr/getHeaders', Handlebars.template(xhrGetHeaders)); Handlebars.registerPartial('xhr/getRequestBody', Handlebars.template(xhrGetRequestBody)); diff --git a/test/e2e/v2.axios.spec.js b/test/e2e/v2.axios.spec.js new file mode 100644 index 000000000..f757f3131 --- /dev/null +++ b/test/e2e/v2.axios.spec.js @@ -0,0 +1,48 @@ +'use strict'; + +const generate = require('./scripts/generate'); +const copy = require('./scripts/copy'); +const compileWithTypescript = require('./scripts/compileWithTypescript'); +const compileWithBabel = require('./scripts/compileWithBabel'); +const server = require('./scripts/server'); +const browser = require('./scripts/browser'); + +describe('v2.axios', () => { + beforeAll(async () => { + await generate('v2/axios', 'v2', 'axios'); + await copy('v2/axios'); + // compileWithTypescript('v2/axios'); + compileWithBabel('v2/axios'); + await server.start('v2/axios'); + await browser.start(); + }, 30000); + + afterAll(async () => { + await server.stop(); + await browser.stop(); + }); + + it('requests token', async () => { + await browser.exposeFunction('tokenRequest', jest.fn().mockResolvedValue('MY_TOKEN')); + const result = await browser.evaluate(async () => { + const { OpenAPI, SimpleService } = window.api; + OpenAPI.TOKEN = window.tokenRequest; + return await SimpleService.getCallWithoutParametersAndResponse(); + }); + expect(result.headers.authorization).toBe('Bearer MY_TOKEN'); + }); + + it('complexService', async () => { + const result = await browser.evaluate(async () => { + const { ComplexService } = window.api; + return await ComplexService.complexTypes({ + first: { + second: { + third: 'Hello World!', + }, + }, + }); + }); + expect(result).toBeDefined(); + }); +}); diff --git a/test/e2e/v3.axios.spec.js b/test/e2e/v3.axios.spec.js new file mode 100644 index 000000000..b00c49f76 --- /dev/null +++ b/test/e2e/v3.axios.spec.js @@ -0,0 +1,61 @@ +'use strict'; + +const generate = require('./scripts/generate'); +const copy = require('./scripts/copy'); +const compileWithTypescript = require('./scripts/compileWithTypescript'); +const compileWithBabel = require('./scripts/compileWithBabel'); +const server = require('./scripts/server'); +const browser = require('./scripts/browser'); + +describe('v3.axios', () => { + beforeAll(async () => { + await generate('v3/axios', 'v3', 'axios'); + await copy('v3/axios'); + // compileWithTypescript('v3/axios'); + compileWithBabel('v3/axios'); + await server.start('v3/axios'); + await browser.start(); + }, 30000); + + afterAll(async () => { + await server.stop(); + await browser.stop(); + }); + + it('requests token', async () => { + await browser.exposeFunction('tokenRequest', jest.fn().mockResolvedValue('MY_TOKEN')); + const result = await browser.evaluate(async () => { + const { OpenAPI, SimpleService } = window.api; + OpenAPI.TOKEN = window.tokenRequest; + OpenAPI.USERNAME = undefined; + OpenAPI.PASSWORD = undefined; + return await SimpleService.getCallWithoutParametersAndResponse(); + }); + expect(result.headers.authorization).toBe('Bearer MY_TOKEN'); + }); + + it('uses credentials', async () => { + const result = await browser.evaluate(async () => { + const { OpenAPI, SimpleService } = window.api; + OpenAPI.TOKEN = undefined; + OpenAPI.USERNAME = 'username'; + OpenAPI.PASSWORD = 'password'; + return await SimpleService.getCallWithoutParametersAndResponse(); + }); + expect(result.headers.authorization).toBe('Basic dXNlcm5hbWU6cGFzc3dvcmQ='); + }); + + it('complexService', async () => { + const result = await browser.evaluate(async () => { + const { ComplexService } = window.api; + return await ComplexService.complexTypes({ + first: { + second: { + third: 'Hello World!', + }, + }, + }); + }); + expect(result).toBeDefined(); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index 7bd587e93..1bd749999 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,5 +1,6 @@ export declare enum HttpClient { FETCH = 'fetch', + AXIOS = 'axios', XHR = 'xhr', NODE = 'node', }