- Added axios client

- Added function to escape path and stopped replacement of ":" character in path
This commit is contained in:
Ferdi Koomen 2021-10-08 23:34:49 +02:00
parent de3a44ae1b
commit 5b3348d6f9
49 changed files with 833 additions and 865 deletions

View File

@ -6,3 +6,6 @@ updates:
interval: weekly
time: "04:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: "@types/node-fetch"
- dependency-name: "node-fetch"

View File

@ -15,7 +15,7 @@
- Frontend ❤️ OpenAPI, but we do not want to use JAVA codegen in our builds
- Quick, lightweight, robust and framework agnostic 🚀
- Supports generation of TypeScript clients
- Supports generations of fetch and XHR http clients
- Supports generations of fetch, XHR and Axios http clients
- Supports OpenAPI specification v2.0 and v3.0
- Supports JSON and YAML files for input
- Supports generation through CLI, Node.js and NPX
@ -40,7 +40,7 @@ $ openapi --help
-V, --version output the version number
-i, --input <value> OpenAPI specification, can be a path, url or string content (required)
-o, --output <value> Output directory (required)
-c, --client <value> HTTP client to generate [fetch, xhr, node] (default: "fetch")
-c, --client <value> HTTP client to generate [fetch, xhr, axios, node] (default: "fetch")
--useOptions Use options instead of arguments
--useUnionTypes Use union types instead of enums
--exportCore <value> Write core files to disk (default: true)
@ -403,12 +403,12 @@ as part of your types to ensure everything is able to be TypeScript generated.
External references may be:
* *relative references* - references to other files at the same location e.g.
`{ $ref: 'schemas/customer.yml' }`
`{ $ref: 'schemas/customer.yml' }`
* *remote references* - fully qualified references to another remote location
e.g. `{ $ref: 'https://myexampledomain.com/schemas/customer_schema.yml' }`
e.g. `{ $ref: 'https://myexampledomain.com/schemas/customer_schema.yml' }`
For remote references, both files (when the file is on the current filesystem)
and http(s) URLs are supported.
For remote references, both files (when the file is on the current filesystem)
and http(s) URLs are supported.
External references may also contain internal paths in the external schema (e.g.
`schemas/collection.yml#/definitions/schemas/Customer`) and back-references to
@ -419,14 +419,6 @@ At start-up, an OpenAPI or Swagger file with external references will be "bundle
so that all external references and back-references will be resolved (but local
references preserved).
### Compare to other generators
Depending on which swagger generator you use, you will see different output.
For instance: Different ways of generating models, services, level of quality,
HTTP client, etc. I've compiled a list with the results per area and how they
compare against the openapi-typescript-codegen.
[Click here to see the comparison](https://htmlpreview.github.io/?https://github.com/ferdikoomen/openapi-typescript-codegen/blob/master/samples/index.html)
FAQ
===
@ -452,6 +444,9 @@ module.exports = {
### Node.js support
> Since version 3.x [`node-fetch`](https://www.npmjs.com/package/node-fetch) switched to ESM only, breaking many
> CommonJS based toolchains (like Jest). Right now we do not support this new version!
By default, this library will generate a client that is compatible with the (browser based) [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API),
however this client will not work inside the Node.js environment. If you want to generate a Node.js compatible client then
you can specify `--client node` in the openapi call:
@ -459,11 +454,12 @@ you can specify `--client node` in the openapi call:
`openapi --input ./spec.json --output ./dist --client node`
This will generate a client that uses [`node-fetch`](https://www.npmjs.com/package/node-fetch) internally. However,
in order to compile and run this client, you will need to install the `node-fetch@2.x` dependencies:
in order to compile and run this client, you might need to install the `node-fetch@2.x` dependencies:
```
npm install node-fetch --save-dev
npm install form-data --save-dev
npm install @types/node-fetch@2.x --save-dev
npm install node-fetch@2.x --save-dev
npm install form-data@4.x --save-dev
```
In order to compile the project and resolve the imports, you will need to enable the `allowSyntheticDefaultImports`

View File

@ -12,7 +12,7 @@ const params = program
.version(pkg.version)
.requiredOption('-i, --input <value>', 'OpenAPI specification, can be a path, url or string content (required)')
.requiredOption('-o, --output <value>', 'Output directory (required)')
.option('-c, --client <value>', 'HTTP client to generate [fetch, xhr, node]', 'fetch')
.option('-c, --client <value>', 'HTTP client to generate [fetch, xhr, node, axios]', 'fetch')
.option('--useOptions', 'Use options instead of arguments')
.option('--useUnionTypes', 'Use union types instead of enums')
.option('--exportCore <value>', 'Write core files to disk', true)

View File

@ -20,10 +20,12 @@ module.exports = {
testMatch: [
'<rootDir>/test/e2e/v2.fetch.spec.js',
'<rootDir>/test/e2e/v2.xhr.spec.js',
'<rootDir>/test/e2e/v2.axios.spec.js',
'<rootDir>/test/e2e/v2.node.spec.js',
'<rootDir>/test/e2e/v2.babel.spec.js',
'<rootDir>/test/e2e/v3.fetch.spec.js',
'<rootDir>/test/e2e/v3.xhr.spec.js',
'<rootDir>/test/e2e/v3.axios.spec.js',
'<rootDir>/test/e2e/v3.node.spec.js',
'<rootDir>/test/e2e/v3.babel.spec.js',
],

View File

@ -32,7 +32,6 @@
}
],
"main": "dist/index.js",
"module": "dist/index.js",
"types": "types/index.d.ts",
"bin": {
"openapi": "bin/index.js"
@ -61,7 +60,8 @@
"codecov": "codecov --token=66c30c23-8954-4892-bef9-fbaed0a2e42b"
},
"dependencies": {
"@types/node-fetch": "^3.0.3",
"@types/node-fetch": "^2.5.12",
"axios": "^0.22.0",
"camelcase": "^6.2.0",
"commander": "^8.0.0",
"form-data": "^4.0.0",
@ -69,23 +69,24 @@
"js-yaml": "^4.0.0",
"json-schema-ref-parser": "^9.0.7",
"mkdirp": "^1.0.4",
"node-fetch": "^3.0.0",
"node-fetch": "^2.6.5",
"rimraf": "^3.0.2"
},
"devDependencies": {
"@babel/cli": "7.15.7",
"@babel/core": "7.15.5",
"@babel/preset-env": "7.15.6",
"@babel/core": "7.15.8",
"@babel/preset-env": "7.15.8",
"@babel/preset-typescript": "7.15.0",
"@rollup/plugin-commonjs": "21.0.0",
"@rollup/plugin-node-resolve": "13.0.5",
"@types/express": "4.17.13",
"@types/glob": "7.1.4",
"@types/jest": "27.0.2",
"@types/js-yaml": "4.0.3",
"@types/node": "16.10.3",
"@types/qs": "6.9.7",
"@typescript-eslint/eslint-plugin": "4.33.0",
"@typescript-eslint/parser": "4.32.0",
"@typescript-eslint/parser": "4.33.0",
"codecov": "3.8.3",
"eslint": "7.32.0",
"eslint-config-prettier": "8.3.0",

View File

@ -1,274 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="google" value="notranslate">
<meta http-equiv="Content-Language" content="en_EN">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" crossorigin="anonymous">
<title>Compared to other generators</title>
<style>
details summary {
outline: none;
}
</style>
</head>
<body>
<div class="container">
<section class="page-header">
<h1>Compared to other generators</h1>
<p class="lead">
Depending on which swagger generator you use, you will see different output. For instance:
Different ways of generating models, services, level of quality, HTTP client, etc.
I've compiled a list below with the results per area and how they compare
against the <strong>openapi-typescript-codegen.</strong>
</p>
</section>
<h5>I've used the standard petshop examples from OpenAPI:</h5>
<ul>
<li><a href="https://petstore3.swagger.io/api/v3/openapi.json" target="_blank">https://petstore3.swagger.io/api/v3/openapi.json</a></li>
<li><a href="https://petstore.swagger.io/v2/swagger.json" target="_blank">https://petstore.swagger.io/v2/swagger.json</a></li>
</ul>
<hr/>
<h5>And used the following generators with their default options:</h5>
<ul>
<li>typescript-aurelia</li>
<li>typescript-angular</li>
<li>typescript-inversify</li>
<li>typescript-angular</li>
<li>typescript-fetch</li>
<li>typescript-jquery</li>
<li>typescript-node</li>
</ul>
<hr/>
<table class="table">
<thead>
<tr>
<th width="30%"></th>
<th width="10%">openapi-typescript-codegen</th>
<th width="10%">aurelia</th>
<th width="10%">inversify</th>
<th width="10%">angular</th>
<th width="10%">fetch</th>
<th width="10%">jquery</th>
<th width="10%">node</th>
</tr>
</thead>
<tbody>
<tr>
<th>Supports OpenApi v2 specification</th>
<td class="success" data-type="openapi-typescript-codegen"></td>
<td class="success" data-type="aurelia"></td>
<td class="success" data-type="inversify"></td>
<td class="success" data-type="angular"></td>
<td class="success" data-type="fetch"></td>
<td class="success" data-type="jquery"></td>
<td class="success" data-type="node"></td>
</tr>
<tr>
<th>Supports OpenApi v3 specification</th>
<td class="success" data-type="openapi-typescript-codegen"></td>
<td class="danger" data-type="aurelia"></td>
<td class="danger" data-type="inversify"></td>
<td class="success" data-type="angular"></td>
<td class="success" data-type="fetch"></td>
<td class="danger" data-type="jquery"></td>
<td class="danger" data-type="node"></td>
</tr>
<tr>
<th>Supports authentication</th>
<td class="success" data-type="openapi-typescript-codegen"></td>
<td class="danger" data-type="aurelia"></td>
<td class="success" data-type="inversify"></td>
<td class="success" data-type="angular"></td>
<td class="success" data-type="fetch"></td>
<td class="success" data-type="jquery"></td>
<td class="success" data-type="node"></td>
</tr>
<tr>
<th>Strongly typed models</th>
<td class="success" data-type="openapi-typescript-codegen"></td>
<td class="success" data-type="aurelia"></td>
<td class="success" data-type="inversify"></td>
<td class="success" data-type="angular"></td>
<td class="success" data-type="fetch"></td>
<td class="success" data-type="jquery"></td>
<td class="success" data-type="node"></td>
</tr>
<tr>
<th>Strongly typed enums</th>
<td class="success" data-type="openapi-typescript-codegen"></td>
<td class="success" data-type="aurelia"></td>
<td class="success" data-type="inversify"></td>
<td class="success" data-type="angular"></td>
<td class="success" data-type="fetch"></td>
<td class="success" data-type="jquery"></td>
<td class="success" data-type="node"></td>
</tr>
<tr>
<th>Models and services exported as individual files</th>
<td class="success" data-type="openapi-typescript-codegen"></td>
<td class="danger" data-type="aurelia"></td>
<td class="success" data-type="inversify"></td>
<td class="success" data-type="angular"></td>
<td class="danger" data-type="fetch"></td>
<td class="success" data-type="jquery"></td>
<td class="danger" data-type="node"></td>
</tr>
<tr>
<th>Index file that exports all services and models</th>
<td class="success" data-type="openapi-typescript-codegen"></td>
<td class="success" data-type="aurelia"></td>
<td class="danger" data-type="inversify"></td>
<td class="success" data-type="angular"></td>
<td class="success" data-type="fetch"></td>
<td class="success" data-type="jquery"></td>
<td class="danger" data-type="node"></td>
</tr>
<tr>
<th>Service returns typed result</th>
<td class="success" data-type="openapi-typescript-codegen"></td>
<td class="success" data-type="aurelia"></td>
<td class="success" data-type="inversify"></td>
<td class="success" data-type="angular"></td>
<td class="danger" data-type="fetch"></td>
<td class="success" data-type="jquery"></td>
<td class="success" data-type="node"></td>
</tr>
<tr>
<th>Service supports sending and receiving binary content</th>
<td class="success" data-type="openapi-typescript-codegen"></td>
<td class="success" data-type="aurelia"></td>
<td class="success" data-type="inversify"></td>
<td class="warning" data-type="angular">
<span>⚠️</span>
<details>
<summary>Details</summary>
<p>V3 version sends data as <code>application/octet-stream</code> instead of <code>application/x-www-form-urlencoded</code></p>
</details>
</td>
<td class="warning" data-type="fetch">
<span>⚠️</span>
<details>
<summary>Details</summary>
<p>V3 version sends data as <code>application/octet-stream</code> instead of <code>application/x-www-form-urlencoded</code></p>
</details>
</td>
<td class="success" data-type="jquery"></td>
<td class="success" data-type="node"></td>
</tr>
<tr>
<th>Models and services contain inline documentation</th>
<td class="success" data-type="openapi-typescript-codegen"></td>
<td class="success" data-type="aurelia"></td>
<td class="success" data-type="inversify"></td>
<td class="success" data-type="angular"></td>
<td class="success" data-type="fetch"></td>
<td class="success" data-type="jquery"></td>
<td class="success" data-type="node"></td>
</tr>
<tr>
<th>Framework agnostic</th>
<td class="success" data-type="openapi-typescript-codegen"></td>
<td class="danger" data-type="aurelia"></td>
<td class="danger" data-type="inversify"></td>
<td class="danger" data-type="angular"></td>
<td class="warning" data-type="node">
<span>⚠️</span>
<details>
<summary>Details</summary>
<p>Requires portable-fetch</p>
</details>
</td>
<td class="danger" data-type="jquery"></td>
<td class="warning" data-type="node">
<span>⚠️</span>
<details>
<summary>Details</summary>
<p>Requires bluebird</p>
</details>
</td>
</tr>
<tr>
<th>Compiles in strict mode without issues</th>
<td class="success" data-type="openapi-typescript-codegen"></td>
<td class="danger" data-type="aurelia">
<span></span>
<details>
<summary>Details</summary>
<p>Errors when compiling: <code>PetApi.ts:147:30 - error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'</code></p>
</details>
</td>
<td class="danger" data-type="inversify">
<span></span>
<details>
<summary>Details</summary>
<p>Errors when compiling: <code>pet.service.ts:312:159 - error TS2304: Cannot find name 'body'</code></p>
</details>
</td>
<td class="danger" data-type="angular">
<span></span>
<details>
<summary>Details</summary>
<p>Errors when compiling: <code>pet.service.ts:528:26 - error TS1345: An expression of type 'void' cannot be tested for truthiness</code></p>
</details>
</td>
<td class="danger" data-type="fetch">
<span></span>
<details>
<summary>Details</summary>
<p>Errors when compiling: <code>api.ts:2276:67 - error TS2300: Duplicate identifier 'username'</code></p>
</details>
</td>
<td class="danger" data-type="jquery">
<span></span>
<details>
<summary>Details</summary>
<p>Errors when compiling: <code>PetApi.ts:25:12 - error TS2322: Type 'null' is not assignable to type 'JQueryAjaxSettings | undefined'</code></p>
</details>
</td>
<td class="danger" data-type="node">
<span></span>
<details>
<summary>Details</summary>
<p>Errors when compiling: <code>api.ts:1631:45 - error TS2694: Namespace '"http"' has no exported member 'ClientResponse'</code></p>
</details>
</td>
</tr>
<tr>
<th>Generated size (typescript)</th>
<td data-type="openapi-typescript-codegen"><span class="badge">30KB</span></td>
<td data-type="aurelia"><span class="badge">29KB</span></td>
<td data-type="inversify"><span class="badge">37KB</span></td>
<td data-type="angular"><span class="badge">63KB</span></td>
<td data-type="fetch"><span class="badge">85KB</span></td>
<td data-type="jquery"><span class="badge">57KB</span></td>
<td data-type="node"><span class="badge">65KB</span></td>
</tr>
<tr>
<th>Build size (javascript, not minimized)</th>
<td data-type="openapi-typescript-codegen"><span class="badge">17KB</span></td>
<td data-type="aurelia"><span class="badge">16KB</span></td>
<td data-type="inversify"><span class="badge">22KB</span></td>
<td data-type="angular"><span class="badge">36KB</span></td>
<td data-type="fetch"><span class="badge">48KB</span></td>
<td data-type="jquery"><span class="badge">37KB</span></td>
<td data-type="node"><span class="badge">53KB</span></td>
</tr>
<tr>
<th>Generation time</th>
<td data-type="openapi-typescript-codegen"><span class="badge">0.2s</span></td>
<td data-type="aurelia"><span class="badge">0.7s</span></td>
<td data-type="inversify"><span class="badge">0.7s</span></td>
<td data-type="angular"><span class="badge">1.4s</span></td>
<td data-type="fetch"><span class="badge">1.1s</span></td>
<td data-type="jquery"><span class="badge">0.7s</span></td>
<td data-type="node"><span class="badge">0.7s</span></td>
</tr>
</tbody>
</table>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" crossorigin="anonymous"></script>
</div>
</body>
</html>

View File

@ -2,4 +2,5 @@ export enum HttpClient {
FETCH = 'fetch',
XHR = 'xhr',
NODE = 'node',
AXIOS = 'axios',
}

View File

@ -30,7 +30,7 @@ export type Options = {
* service layer, etc.
* @param input The relative location of the OpenAPI spec
* @param output The relative location of the output directory
* @param httpClient The selected httpClient (fetch or XHR)
* @param httpClient The selected httpClient (fetch, xhr, node or axios)
* @param useOptions Use options or arguments functions
* @param useUnionTypes Use union types instead of enums
* @param exportCore: Generate core client classes

View File

@ -7,6 +7,7 @@ describe('getModelTemplate', () => {
base: 'Link',
template: 'Model',
imports: ['Model'],
isNullable: false,
});
expect(template).toEqual('<T>');
});
@ -17,6 +18,7 @@ describe('getModelTemplate', () => {
base: 'string',
template: null,
imports: [],
isNullable: false,
});
expect(template).toEqual('');
});

View File

@ -7,6 +7,7 @@ describe('getModelTemplate', () => {
base: 'Link',
template: 'Model',
imports: ['Model'],
isNullable: false,
});
expect(template).toEqual('<T>');
});
@ -17,6 +18,7 @@ describe('getModelTemplate', () => {
base: 'string',
template: null,
imports: [],
isNullable: false,
});
expect(template).toEqual('');
});

View File

@ -13,6 +13,7 @@ type Config = {
USERNAME?: string | Resolver<string>;
PASSWORD?: string | Resolver<string>;
HEADERS?: Headers | Resolver<Headers>;
ENCODE_PATH?: (path: string) => string;
}
export const OpenAPI: Config = {
@ -23,4 +24,5 @@ export const OpenAPI: Config = {
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};

View File

@ -0,0 +1,29 @@
async function getHeaders(options: ApiRequestOptions, formData?: FormData): Promise<Record<string, string>> {
const token = await resolve(options, OpenAPI.TOKEN);
const username = await resolve(options, OpenAPI.USERNAME);
const password = await resolve(options, OpenAPI.PASSWORD);
const additionalHeaders = await resolve(options, OpenAPI.HEADERS);
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
...formData?.getHeaders()
})
.filter(([key, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: value,
}), {} as Record<string, string>);
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
return headers;
}

View File

@ -0,0 +1,6 @@
function getResponseBody(response: AxiosResponse<any>): any {
if (response.status !== 204) {
return response.data;
}
return null;
}

View File

@ -0,0 +1,9 @@
function getResponseHeader(response: AxiosResponse<any>, responseHeader?: string): string | null {
if (responseHeader) {
const content = response.headers[responseHeader];
if (isString(content)) {
return content;
}
}
return null;
}

View File

@ -0,0 +1,75 @@
{{>header}}
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import FormData from 'form-data';
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/isSuccess}}
{{>functions/base64}}
{{>functions/getQueryString}}
{{>functions/getUrl}}
{{>functions/getFormData}}
{{>functions/resolve}}
{{>axios/getHeaders}}
{{>axios/sendRequest}}
{{>axios/getResponseHeader}}
{{>axios/getResponseBody}}
{{>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<ApiResult> {
const url = getUrl(options);
const response = await sendRequest(options, url);
const responseBody = getResponseBody(response);
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;
}

View File

@ -0,0 +1,11 @@
async function sendRequest(options: ApiRequestOptions, url: string): Promise<AxiosResponse<any>> {
const formData = options.formData && getFormData(options.formData);
const data = formData || options.body;
const config: AxiosRequestConfig = {
url,
data,
method: options.method,
headers: await getHeaders(options, formData),
};
return await axios.request(config);
}

View File

@ -22,7 +22,7 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = btoa(`${username}:${password}`);
const credentials = base64(`${username}:${password}`);
headers.append('Authorization', `Basic ${credentials}`);
}

View File

@ -17,6 +17,9 @@ import { OpenAPI } from './OpenAPI';
{{>functions/isBlob}}
{{>functions/base64}}
{{>functions/getQueryString}}

View File

@ -0,0 +1,7 @@
function base64(str: string): string {
try {
return btoa(str);
} catch (err) {
return Buffer.from(str).toString('base64');
}
}

View File

@ -1,7 +1,6 @@
function getUrl(options: ApiRequestOptions): string {
const path = options.path.replace(/[:]/g, '_');
const path = OpenAPI.ENCODE_PATH ? OpenAPI.ENCODE_PATH(options.path) : options.path;
const url = `${OpenAPI.BASE}${path}`;
if (options.query) {
return `${url}${getQueryString(options.query)}`;
}

View File

@ -22,14 +22,14 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = Buffer.from(`${username}:${password}`).toString('base64');
const credentials = base64(`${username}:${password}`);
headers.append('Authorization', `Basic ${credentials}`);
}
if (options.body) {
if (options.mediaType) {
headers.append('Content-Type', options.mediaType);
} else if (isBlob(options.body) || isBinary(options.body)) {
} else if (isBinary(options.body)) {
headers.append('Content-Type', 'application/octet-stream');
} else if (isString(options.body)) {
headers.append('Content-Type', 'text/plain');

View File

@ -24,6 +24,9 @@ import { OpenAPI } from './OpenAPI';
{{>functions/isBinary}}
{{>functions/base64}}
{{>functions/getQueryString}}

View File

@ -1,3 +1,4 @@
{{~#equals @root.httpClient 'fetch'}}{{>fetch/request}}{{/equals~}}
{{~#equals @root.httpClient 'xhr'}}{{>xhr/request}}{{/equals~}}
{{~#equals @root.httpClient 'axios'}}{{>axios/request}}{{/equals~}}
{{~#equals @root.httpClient 'node'}}{{>node/request}}{{/equals~}}

View File

@ -22,7 +22,7 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = btoa(`${username}:${password}`);
const credentials = base64(`${username}:${password}`);
headers.append('Authorization', `Basic ${credentials}`);
}

View File

@ -20,6 +20,9 @@ import { OpenAPI } from './OpenAPI';
{{>functions/isSuccess}}
{{>functions/base64}}
{{>functions/getQueryString}}

View File

@ -1,7 +1,8 @@
{{~#equals base 'File'~}}
{{~#equals @root.httpClient 'fetch'}}Blob{{/equals~}}
{{~#equals @root.httpClient 'xhr'}}Blob{{/equals~}}
{{~#equals @root.httpClient 'node'}}Blob | Buffer | ArrayBuffer | ArrayBufferView{{/equals~}}
{{~#equals @root.httpClient 'axios'}}Blob{{/equals~}}
{{~#equals @root.httpClient 'node'}}Buffer | ArrayBuffer | ArrayBufferView{{/equals~}}
{{~else~}}
{{{base}}}
{{~/equals~}}

View File

@ -4,12 +4,18 @@ 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 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';
import fetchGetResponseHeader from '../templates/core/fetch/getResponseHeader.hbs';
import fetchRequest from '../templates/core/fetch/request.hbs';
import fetchSendRequest from '../templates/core/fetch/sendRequest.hbs';
import functionBase64 from '../templates/core/functions/base64.hbs';
import functionCatchErrors from '../templates/core/functions/catchErrors.hbs';
import functionGetFormData from '../templates/core/functions/getFormData.hbs';
import functionGetQueryString from '../templates/core/functions/getQueryString.hbs';
@ -148,6 +154,7 @@ export function registerHandlebarTemplates(root: { httpClient: HttpClient; useOp
Handlebars.registerPartial('functions/isString', Handlebars.template(functionIsString));
Handlebars.registerPartial('functions/isStringWithValue', Handlebars.template(functionIsStringWithValue));
Handlebars.registerPartial('functions/isSuccess', Handlebars.template(functionIsSuccess));
Handlebars.registerPartial('functions/base64', Handlebars.template(functionBase64));
Handlebars.registerPartial('functions/resolve', Handlebars.template(functionResolve));
// Specific files for the fetch client implementation
@ -174,5 +181,12 @@ export function registerHandlebarTemplates(root: { httpClient: HttpClient; useOp
Handlebars.registerPartial('node/sendRequest', Handlebars.template(nodeSendRequest));
Handlebars.registerPartial('node/request', Handlebars.template(nodeRequest));
// Specific files for the axios client implementation
Handlebars.registerPartial('axios/getHeaders', Handlebars.template(axiosGetHeaders));
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));
return templates;
}

View File

@ -16,7 +16,7 @@ import { writeClientServices } from './writeClientServices';
* @param client Client object with all the models, services, etc.
* @param templates Templates wrapper with all loaded Handlebars templates
* @param output The relative location of the output directory
* @param httpClient The selected httpClient (fetch, xhr or node)
* @param httpClient The selected httpClient (fetch, xhr, node or axios)
* @param useOptions Use options or arguments functions
* @param useUnionTypes Use union types instead of enums
* @param exportCore: Generate core client classes

View File

@ -10,7 +10,7 @@ import { Templates } from './registerHandlebarTemplates';
* @param client Client object, containing, models, schemas and services
* @param templates The loaded handlebar templates
* @param outputPath Directory to write the generated files to
* @param httpClient The selected httpClient (fetch, xhr or node)
* @param httpClient The selected httpClient (fetch, xhr, node or axios)
* @param request: Path to custom request file
*/
export async function writeClientCore(client: Client, templates: Templates, outputPath: string, httpClient: HttpClient, request?: string): Promise<void> {

View File

@ -11,7 +11,7 @@ import { Templates } from './registerHandlebarTemplates';
* @param models Array of Models to write
* @param templates The loaded handlebar templates
* @param outputPath Directory to write the generated files to
* @param httpClient The selected httpClient (fetch, xhr or node)
* @param httpClient The selected httpClient (fetch, xhr, node or axios)
* @param useUnionTypes Use union types instead of enums
*/
export async function writeClientModels(models: Model[], templates: Templates, outputPath: string, httpClient: HttpClient, useUnionTypes: boolean): Promise<void> {

View File

@ -11,7 +11,7 @@ import { Templates } from './registerHandlebarTemplates';
* @param models Array of Models to write
* @param templates The loaded handlebar templates
* @param outputPath Directory to write the generated files to
* @param httpClient The selected httpClient (fetch, xhr or node)
* @param httpClient The selected httpClient (fetch, xhr, node or axios)
* @param useUnionTypes Use union types instead of enums
*/
export async function writeClientSchemas(models: Model[], templates: Templates, outputPath: string, httpClient: HttpClient, useUnionTypes: boolean): Promise<void> {

View File

@ -13,7 +13,7 @@ const VERSION_TEMPLATE_STRING = 'OpenAPI.VERSION';
* @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 or node)
* @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
*/

View File

@ -71,6 +71,7 @@ type Config = {
USERNAME?: string | Resolver<string>;
PASSWORD?: string | Resolver<string>;
HEADERS?: Headers | Resolver<Headers>;
ENCODE_PATH?: (path: string) => string;
}
export const OpenAPI: Config = {
@ -81,6 +82,7 @@ export const OpenAPI: Config = {
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};"
`;
@ -109,6 +111,14 @@ function isBlob(value: any): value is Blob {
return value instanceof Blob;
}
function base64(str: string): string {
try {
return btoa(str);
} catch (err) {
return Buffer.from(str).toString('base64');
}
}
function getQueryString(params: Record<string, any>): string {
const qs: string[] = [];
Object.keys(params).forEach(key => {
@ -130,9 +140,8 @@ function getQueryString(params: Record<string, any>): string {
}
function getUrl(options: ApiRequestOptions): string {
const path = options.path.replace(/[:]/g, '_');
const path = OpenAPI.ENCODE_PATH ? OpenAPI.ENCODE_PATH(options.path) : options.path;
const url = \`\${OpenAPI.BASE}\${path}\`;
if (options.query) {
return \`\${url}\${getQueryString(options.query)}\`;
}
@ -183,7 +192,7 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = btoa(\`\${username}:\${password}\`);
const credentials = base64(\`\${username}:\${password}\`);
headers.append('Authorization', \`Basic \${credentials}\`);
}
@ -2433,6 +2442,7 @@ type Config = {
USERNAME?: string | Resolver<string>;
PASSWORD?: string | Resolver<string>;
HEADERS?: Headers | Resolver<Headers>;
ENCODE_PATH?: (path: string) => string;
}
export const OpenAPI: Config = {
@ -2443,6 +2453,7 @@ export const OpenAPI: Config = {
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};"
`;
@ -2471,6 +2482,14 @@ function isBlob(value: any): value is Blob {
return value instanceof Blob;
}
function base64(str: string): string {
try {
return btoa(str);
} catch (err) {
return Buffer.from(str).toString('base64');
}
}
function getQueryString(params: Record<string, any>): string {
const qs: string[] = [];
Object.keys(params).forEach(key => {
@ -2492,9 +2511,8 @@ function getQueryString(params: Record<string, any>): string {
}
function getUrl(options: ApiRequestOptions): string {
const path = options.path.replace(/[:]/g, '_');
const path = OpenAPI.ENCODE_PATH ? OpenAPI.ENCODE_PATH(options.path) : options.path;
const url = \`\${OpenAPI.BASE}\${path}\`;
if (options.query) {
return \`\${url}\${getQueryString(options.query)}\`;
}
@ -2545,7 +2563,7 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = btoa(\`\${username}:\${password}\`);
const credentials = base64(\`\${username}:\${password}\`);
headers.append('Authorization', \`Basic \${credentials}\`);
}

View File

@ -6,7 +6,6 @@ import type { ApiResult } from './ApiResult';
import { OpenAPI } from './OpenAPI';
export async function request(options: ApiRequestOptions): Promise<ApiResult> {
const url = `${OpenAPI.BASE}${options.path}`;
// Do your request...
@ -17,7 +16,7 @@ export async function request(options: ApiRequestOptions): Promise<ApiResult> {
status: 200,
statusText: 'dummy',
body: {
...options
...options,
},
};
}

View File

@ -3,7 +3,7 @@
const puppeteer = require('puppeteer');
let browser;
let page
let page;
async function start() {
// This starts the a new puppeteer browser (Chrome)

View File

@ -22,7 +22,7 @@ function compileWithTypescript(dir) {
strictNullChecks: true,
strictFunctionTypes: true,
allowSyntheticDefaultImports: true,
skipLibCheck: true
skipLibCheck: true,
},
include: ['./index.ts'],
};

View File

@ -3,7 +3,7 @@
const express = require('express');
let app;
let server
let server;
async function start(dir) {
return new Promise(resolve => {

38
test/e2e/v2.axios.spec.js Normal file
View File

@ -0,0 +1,38 @@
'use strict';
const generate = require('./scripts/generate');
const compileWithTypescript = require('./scripts/compileWithTypescript');
const server = require('./scripts/server');
describe('v2.node', () => {
beforeAll(async () => {
await generate('v2/axios', 'v2', 'axios');
compileWithTypescript('v2/axios');
await server.start('v2/axios');
}, 30000);
afterAll(async () => {
await server.stop();
});
it('requests token', async () => {
const { OpenAPI, SimpleService } = require('./generated/v2/axios/index.js');
const tokenRequest = jest.fn().mockResolvedValue('MY_TOKEN');
OpenAPI.TOKEN = tokenRequest;
const result = await SimpleService.getCallWithoutParametersAndResponse();
expect(tokenRequest.mock.calls.length).toBe(1);
expect(result.headers.authorization).toBe('Bearer MY_TOKEN');
});
it('complexService', async () => {
const { ComplexService } = require('./generated/v2/axios/index.js');
const result = await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!',
},
},
});
expect(result).toBeDefined();
});
});

View File

@ -7,7 +7,6 @@ const server = require('./scripts/server');
const browser = require('./scripts/browser');
describe('v2.fetch', () => {
beforeAll(async () => {
await generate('v2/babel', 'v2', 'fetch', true, true);
await copy('v2/babel');
@ -37,9 +36,9 @@ describe('v2.fetch', () => {
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
});
expect(result).toBeDefined();

View File

@ -7,7 +7,6 @@ const server = require('./scripts/server');
const browser = require('./scripts/browser');
describe('v2.fetch', () => {
beforeAll(async () => {
await generate('v2/fetch', 'v2', 'fetch');
await copy('v2/fetch');
@ -37,9 +36,9 @@ describe('v2.fetch', () => {
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
});
expect(result).toBeDefined();

View File

@ -30,9 +30,9 @@ describe('v2.node', () => {
const result = await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
expect(result).toBeDefined();
});

View File

@ -7,7 +7,6 @@ const server = require('./scripts/server');
const browser = require('./scripts/browser');
describe('v2.xhr', () => {
beforeAll(async () => {
await generate('v2/xhr', 'v2', 'xhr');
await copy('v2/xhr');
@ -37,9 +36,9 @@ describe('v2.xhr', () => {
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
});
expect(result).toBeDefined();

64
test/e2e/v3.axios.spec.js Normal file
View File

@ -0,0 +1,64 @@
'use strict';
const generate = require('./scripts/generate');
const compileWithTypescript = require('./scripts/compileWithTypescript');
const server = require('./scripts/server');
describe('v3.node', () => {
beforeAll(async () => {
await generate('v3/axios', 'v3', 'axios');
compileWithTypescript('v3/axios');
await server.start('v3/axios');
}, 30000);
afterAll(async () => {
await server.stop();
});
it('requests token', async () => {
const { OpenAPI, SimpleService } = require('./generated/v3/axios/index.js');
const tokenRequest = jest.fn().mockResolvedValue('MY_TOKEN');
OpenAPI.TOKEN = tokenRequest;
OpenAPI.USERNAME = undefined;
OpenAPI.PASSWORD = undefined;
const result = await SimpleService.getCallWithoutParametersAndResponse();
expect(tokenRequest.mock.calls.length).toBe(1);
expect(result.headers.authorization).toBe('Bearer MY_TOKEN');
});
it('uses credentials', async () => {
const { OpenAPI, SimpleService } = require('./generated/v3/axios/index.js');
OpenAPI.TOKEN = undefined;
OpenAPI.USERNAME = 'username';
OpenAPI.PASSWORD = 'password';
const result = await SimpleService.getCallWithoutParametersAndResponse();
expect(result.headers.authorization).toBe('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
});
it('complexService', async () => {
const { ComplexService } = require('./generated/v3/axios/index.js');
const result = await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!',
},
},
});
expect(result).toBeDefined();
});
it('formData', async () => {
const { ParametersService } = require('./generated/v3/axios/index.js');
const result = await ParametersService.callWithParameters(
'valueHeader',
'valueQuery',
'valueForm',
'valueCookie',
'valuePath',
{
prop: 'valueBody',
},
);
expect(result).toBeDefined();
});
});

View File

@ -7,7 +7,6 @@ const server = require('./scripts/server');
const browser = require('./scripts/browser');
describe('v3.fetch', () => {
beforeAll(async () => {
await generate('v3/babel', 'v3', 'fetch', true, true);
await copy('v3/babel');
@ -50,9 +49,9 @@ describe('v3.fetch', () => {
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
});
expect(result).toBeDefined();

View File

@ -7,7 +7,6 @@ const server = require('./scripts/server');
const browser = require('./scripts/browser');
describe('v3.fetch', () => {
beforeAll(async () => {
await generate('v3/fetch', 'v3', 'fetch');
await copy('v3/fetch');
@ -50,9 +49,9 @@ describe('v3.fetch', () => {
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
});
expect(result).toBeDefined();
@ -61,18 +60,10 @@ describe('v3.fetch', () => {
it('formData', async () => {
const result = await browser.evaluate(async () => {
const { ParametersService } = window.api;
return await ParametersService.callWithParameters(
'valueHeader',
'valueQuery',
'valueForm',
'valueCookie',
'valuePath',
{
prop: 'valueBody'
}
);
return await ParametersService.callWithParameters('valueHeader', 'valueQuery', 'valueForm', 'valueCookie', 'valuePath', {
prop: 'valueBody',
});
});
expect(result).toBeDefined();
});
});

View File

@ -41,9 +41,9 @@ describe('v3.node', () => {
const result = await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
expect(result).toBeDefined();
});
@ -57,8 +57,8 @@ describe('v3.node', () => {
'valueCookie',
'valuePath',
{
prop: 'valueBody'
}
prop: 'valueBody',
},
);
expect(result).toBeDefined();
});

View File

@ -7,7 +7,6 @@ const server = require('./scripts/server');
const browser = require('./scripts/browser');
describe('v3.xhr', () => {
beforeAll(async () => {
await generate('v3/xhr', 'v3', 'xhr');
await copy('v3/xhr');
@ -50,9 +49,9 @@ describe('v3.xhr', () => {
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
});
expect(result).toBeDefined();

1
types/index.d.ts vendored
View File

@ -2,6 +2,7 @@ export declare enum HttpClient {
FETCH = 'fetch',
XHR = 'xhr',
NODE = 'node',
AXIOS = 'axios',
}
export type Options = {

966
yarn.lock

File diff suppressed because it is too large Load Diff