Merge branch 'master' into feature/cancelable

# Conflicts:
#	README.md
#	package.json
#	src/utils/registerHandlebarTemplates.ts
#	test/__snapshots__/index.spec.js.snap
#	test/custom/request.ts
#	test/e2e/v3.fetch.spec.js
#	test/e2e/v3.node.spec.js
#	yarn.lock
This commit is contained in:
Ferdi Koomen 2021-10-18 22:19:36 +02:00
commit d7c153ff55
99 changed files with 2794 additions and 3630 deletions

View File

@ -1,31 +1,31 @@
version: 2
jobs:
build:
working_directory: ~/repo
docker:
- image: circleci/node:latest-browsers
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
- v1-dependencies-
- run:
name: Install dependencies
command: yarn install
- save_cache:
key: v1-dependencies-{{ checksum "package.json" }}
paths:
- node_modules
- run:
name: Build library
command: yarn run release
- run:
name: Run unit tests
command: yarn run test:coverage
- run:
name: Run e2e tests
command: yarn run test:e2e
- run:
name: Submit to Codecov
command: yarn run codecov
build:
working_directory: ~/repo
docker:
- image: circleci/node:latest-browsers
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
- v1-dependencies-
- run:
name: Install dependencies
command: yarn install
- save_cache:
key: v1-dependencies-{{ checksum "package.json" }}
paths:
- node_modules
- run:
name: Build library
command: yarn run release
- run:
name: Run unit tests
command: yarn run test:coverage
- run:
name: Run e2e tests
command: yarn run test:e2e
- run:
name: Submit to Codecov
command: yarn run codecov

11
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: monthly
time: "04:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: "@types/node-fetch"
- dependency-name: "node-fetch"

View File

@ -1,7 +0,0 @@
language: node_js
node_js:
- node
script:
- npm run release
- npm run test:coverage
- npm run codecov

View File

@ -27,5 +27,5 @@ https://help.github.com/articles/using-pull-requests
5. Ensure the code is formatted by running: `yarn run eslint:fix`
6. Commit your changes using a descriptive commit message
After your Pull Request is created, it will automatically be build using Circle CI
and Travis. When the build is successful then the Pull Request is ready for review.
After your Pull Request is created, it will automatically be build using Circle CI.
When the build is successful then the Pull Request is ready for review.

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 Ferdi Koomen
Copyright (c) 2021 Ferdi Koomen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -2,12 +2,11 @@
[![NPM][npm-image]][npm-url]
[![License][license-image]][license-url]
[![Build Status][travis-image]][travis-url]
[![Dependency Status][deps-image]][deps-url]
[![Coverage][coverage-image]][coverage-url]
[![Quality][quality-image]][quality-url]
[![Code Climate][climate-image]][climate-url]
[![Downloads][downloads-image]][downloads-url]
[![Build][build-image]][build-url]
> Node.js library that generates Typescript clients based on the OpenAPI specification.
@ -15,7 +14,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
@ -41,13 +40,15 @@ $ 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)
--exportServices <value> Write services to disk (default: true)
--exportModels <value> Write models to disk (default: true)
--exportSchemas <value> Write schemas to disk (default: false)
--request <value> Path to custom request file
-h, --help display help for command
Examples
$ openapi --input ./spec.json
@ -404,12 +405,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
@ -420,14 +421,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
===
@ -453,6 +446,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:
@ -460,7 +456,7 @@ 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` dependencies:
in order to compile and run this client, you might need to install the `node-fetch@2.x` dependencies:
```
npm install @types/node-fetch --save-dev
@ -483,10 +479,6 @@ in your `tsconfig.json` file.
[npm-image]: https://img.shields.io/npm/v/openapi-typescript-codegen.svg
[license-url]: LICENSE
[license-image]: http://img.shields.io/npm/l/openapi-typescript-codegen.svg
[travis-url]: https://travis-ci.org/ferdikoomen/openapi-typescript-codegen
[travis-image]: https://img.shields.io/travis/ferdikoomen/openapi-typescript-codegen.svg
[deps-url]: https://david-dm.org/ferdikoomen/openapi-typescript-codegen
[deps-image]: https://img.shields.io/david/ferdikoomen/openapi-typescript-codegen.svg
[coverage-url]: https://codecov.io/gh/ferdikoomen/openapi-typescript-codegen
[coverage-image]: https://img.shields.io/codecov/c/github/ferdikoomen/openapi-typescript-codegen.svg
[quality-url]: https://lgtm.com/projects/g/ferdikoomen/openapi-typescript-codegen
@ -495,3 +487,5 @@ in your `tsconfig.json` file.
[climate-image]: https://img.shields.io/codeclimate/maintainability/ferdikoomen/openapi-typescript-codegen.svg
[downloads-url]: http://npm-stat.com/charts.html?package=openapi-typescript-codegen
[downloads-image]: http://img.shields.io/npm/dm/openapi-typescript-codegen.svg
[build-url]: https://circleci.com/gh/ferdikoomen/openapi-typescript-codegen/tree/master
[build-image]: https://circleci.com/gh/ferdikoomen/openapi-typescript-codegen/tree/master.svg?style=svg

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

@ -1,6 +1,6 @@
{
"name": "openapi-typescript-codegen",
"version": "0.10.0",
"version": "0.11.0",
"description": "Library that generates Typescript clients based on the OpenAPI specification.",
"author": "Ferdi Koomen",
"homepage": "https://github.com/ferdikoomen/openapi-typescript-codegen",
@ -32,7 +32,6 @@
}
],
"main": "dist/index.js",
"module": "dist/index.js",
"types": "types/index.d.ts",
"bin": {
"openapi": "bin/index.js"
@ -61,47 +60,48 @@
"codecov": "codecov --token=66c30c23-8954-4892-bef9-fbaed0a2e42b"
},
"dependencies": {
"@types/node-fetch": "^2.5.12",
"abort-controller": "^3.0.0",
"axios": "^0.23.0",
"camelcase": "^6.2.0",
"commander": "^7.0.0",
"commander": "^8.0.0",
"form-data": "^4.0.0",
"handlebars": "^4.7.6",
"js-yaml": "^4.0.0",
"json-schema-ref-parser": "^9.0.7",
"mkdirp": "^1.0.4",
"node-fetch": "^2.6.5",
"rimraf": "^3.0.2"
},
"devDependencies": {
"@babel/cli": "7.13.10",
"@babel/core": "7.13.10",
"@babel/preset-env": "7.13.10",
"@babel/preset-typescript": "7.13.0",
"@rollup/plugin-commonjs": "17.1.0",
"@rollup/plugin-node-resolve": "11.2.0",
"@types/express": "4.17.11",
"@types/jest": "26.0.20",
"@types/js-yaml": "4.0.0",
"@types/node": "14.14.32",
"@types/node-fetch": "2.5.8",
"@types/qs": "6.9.6",
"@typescript-eslint/eslint-plugin": "4.17.0",
"@typescript-eslint/parser": "4.17.0",
"abort-controller": "3.0.0",
"codecov": "3.8.1",
"eslint": "7.21.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-prettier": "3.3.1",
"@babel/cli": "7.15.7",
"@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/node": "16.11.1",
"@types/qs": "6.9.7",
"@typescript-eslint/eslint-plugin": "5.1.0",
"@typescript-eslint/parser": "5.1.0",
"codecov": "3.8.3",
"eslint": "8.0.1",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-simple-import-sort": "7.0.0",
"express": "4.17.1",
"form-data": "4.0.0",
"glob": "7.1.6",
"jest": "26.6.3",
"jest-cli": "26.6.3",
"node-fetch": "2.6.1",
"prettier": "2.2.1",
"puppeteer": "8.0.0",
"qs": "6.9.6",
"rollup": "2.41.0",
"glob": "7.2.0",
"jest": "27.3.0",
"jest-cli": "27.3.0",
"prettier": "2.4.1",
"puppeteer": "10.4.0",
"qs": "6.10.1",
"rollup": "2.58.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-typescript2": "0.30.0",
"typescript": "4.2.3"
"tslib": "2.3.1",
"typescript": "4.4.4"
}
}

View File

@ -57,7 +57,7 @@ const getPlugins = () => {
return plugins;
}
return [...plugins, terser()];
}
};
module.exports = {
input: './src/index.ts',

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

@ -3,4 +3,5 @@ import type { Model } from './Model';
export interface OperationParameter extends Model {
in: 'path' | 'query' | 'header' | 'formData' | 'body' | 'cookie';
prop: string;
mediaType: string | null;
}

View File

@ -3,4 +3,5 @@ export interface Type {
base: string;
template: string | null;
imports: string[];
isNullable: boolean;
}

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

@ -3,7 +3,7 @@ import type { Enum } from '../../../client/interfaces/Enum';
export function getEnumFromDescription(description: string): Enum[] {
// Check if we can find this special format string:
// None=0,Something=1,AnotherThing=2
if (/^(\w+=[0-9]+,?)+$/g.test(description)) {
if (/^(\w+=[0-9]+)/g.test(description)) {
const matches = description.match(/(\w+=[0-9]+,?)/g);
if (matches) {
// Grab the values from the description

View File

@ -22,7 +22,7 @@ export function getModel(openApi: OpenApi, definition: OpenApiSchema, isDefiniti
isDefinition,
isReadOnly: definition.readOnly === true,
isNullable: definition['x-nullable'] === true,
isRequired: definition.default !== undefined,
isRequired: false,
format: definition.format,
maximum: definition.maximum,
exclusiveMaximum: definition.exclusiveMaximum,

View File

@ -31,6 +31,11 @@ export function getModelComposition(openApi: OpenApi, definition: OpenApiSchema,
});
if (definition.properties) {
const properties = getModelProperties(openApi, definition, getModel);
properties.forEach(property => {
composition.imports.push(...property.imports);
composition.enums.push(...property.enums);
});
composition.properties.push({
name: 'properties',
export: 'interface',
@ -46,7 +51,7 @@ export function getModelComposition(openApi: OpenApi, definition: OpenApiSchema,
imports: [],
enum: [],
enums: [],
properties: [...getModelProperties(openApi, definition, getModel)],
properties,
});
}
return composition;

View File

@ -15,7 +15,7 @@ export function getModelProperties(openApi: OpenApi, definition: OpenApiSchema,
for (const propertyName in definition.properties) {
if (definition.properties.hasOwnProperty(propertyName)) {
const property = definition.properties[propertyName];
const propertyRequired = definition.required?.includes(propertyName) || property.default !== undefined;
const propertyRequired = !!definition.required?.includes(propertyName);
if (property.$ref) {
const model = getType(property.$ref);
models.push({

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

@ -42,6 +42,7 @@ export function getOperationParameter(openApi: OpenApi, parameter: OpenApiParame
enum: [],
enums: [],
properties: [],
mediaType: null,
};
if (parameter.$ref) {

View File

@ -1,6 +1,7 @@
import camelCase from 'camelcase';
const reservedWords = /^(arguments|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|eval|export|extends|false|finally|for|function|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)$/g;
const reservedWords =
/^(arguments|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|eval|export|extends|false|finally|for|function|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)$/g;
/**
* Replaces any invalid characters from a parameter name.

View File

@ -1,6 +1,9 @@
import type { OpenApi } from '../interfaces/OpenApi';
import type { OpenApiReference } from '../interfaces/OpenApiReference';
const ESCAPED_REF_SLASH = /~1/g;
const ESCAPED_REF_TILDE = /~0/g;
export function getRef<T>(openApi: OpenApi, item: T & OpenApiReference): T {
if (item.$ref) {
// Fetch the paths to the definitions, this converts:
@ -13,9 +16,10 @@ export function getRef<T>(openApi: OpenApi, item: T & OpenApiReference): T {
// Try to find the reference by walking down the path,
// if we cannot find it, then we throw an error.
let result: any = openApi;
paths.forEach((path: string): void => {
if (result.hasOwnProperty(path)) {
result = result[path];
paths.forEach(path => {
const decodedPath = decodeURIComponent(path.replace(ESCAPED_REF_SLASH, '/').replace(ESCAPED_REF_TILDE, '~'));
if (result.hasOwnProperty(decodedPath)) {
result = result[decodedPath];
} else {
throw new Error(`Could not find reference: "${item.$ref}"`);
}

View File

@ -17,9 +17,10 @@ export function getType(value?: string, template?: string): Type {
base: 'any',
template: null,
imports: [],
isNullable: false,
};
const valueClean = stripNamespace(value || '');
const valueClean = decodeURIComponent(stripNamespace(value || ''));
if (/\[.*\]$/g.test(valueClean)) {
const matches = valueClean.match(/(.*?)\[(.*)\]$/);

View File

@ -4,6 +4,6 @@ export function sortByRequired(a: OperationParameter, b: OperationParameter): nu
const aNeedsValue = a.isRequired && a.default === undefined;
const bNeedsValue = b.isRequired && b.default === undefined;
if (aNeedsValue && !bNeedsValue) return -1;
if (!aNeedsValue && bNeedsValue) return 1;
if (bNeedsValue && !aNeedsValue) return 1;
return 0;
}

View File

@ -25,7 +25,7 @@ export interface OpenApiSchema extends OpenApiReference, WithEnumExtension {
minProperties?: number;
required?: string[];
enum?: (string | number)[];
type?: string;
type?: string | string[];
allOf?: OpenApiSchema[];
oneOf?: OpenApiSchema[];
anyOf?: OpenApiSchema[];

View File

@ -1,30 +1,24 @@
import { isDefined } from '../../../utils/isDefined';
import type { Dictionary } from '../../../utils/types';
import type { OpenApi } from '../interfaces/OpenApi';
import type { OpenApiMediaType } from '../interfaces/OpenApiMediaType';
import type { OpenApiSchema } from '../interfaces/OpenApiSchema';
export function getContent(openApi: OpenApi, content: Dictionary<OpenApiMediaType>): OpenApiSchema | null {
/* prettier-ignore */
return (
content['application/json-patch+json'] &&
content['application/json-patch+json'].schema
) || (
content['application/json'] &&
content['application/json'].schema
) || (
content['text/json'] &&
content['text/json'].schema
) || (
content['text/plain'] &&
content['text/plain'].schema
) || (
content['multipart/mixed'] &&
content['multipart/mixed'].schema
) || (
content['multipart/related'] &&
content['multipart/related'].schema
) || (
content['multipart/batch'] &&
content['multipart/batch'].schema
) || null;
const basicMediaTypeSchema =
content['application/json-patch+json']?.schema ||
content['application/json']?.schema ||
content['text/json']?.schema ||
content['text/plain']?.schema ||
content['multipart/mixed']?.schema ||
content['multipart/related']?.schema ||
content['multipart/batch']?.schema;
if (basicMediaTypeSchema) {
return basicMediaTypeSchema;
}
const mediaTypes = Object.values(content);
const mediaType = mediaTypes.find(mediaType => isDefined(mediaType.schema));
return mediaType?.schema || null;
}

View File

@ -3,7 +3,7 @@ import type { Enum } from '../../../client/interfaces/Enum';
export function getEnumFromDescription(description: string): Enum[] {
// Check if we can find this special format string:
// None=0,Something=1,AnotherThing=2
if (/^(\w+=[0-9]+,?)+$/g.test(description)) {
if (/^(\w+=[0-9]+)/g.test(description)) {
const matches = description.match(/(\w+=[0-9]+,?)/g);
if (matches) {
// Grab the values from the description

View File

@ -0,0 +1,10 @@
import type { Dictionary } from '../../../utils/types';
import type { OpenApi } from '../interfaces/OpenApi';
import type { OpenApiMediaType } from '../interfaces/OpenApiMediaType';
export function getMediaType(openApi: OpenApi, content: Dictionary<OpenApiMediaType>): string | null {
return (
Object.keys(content).find(key => ['application/json-patch+json', 'application/json', 'text/json', 'text/plain', 'multipart/mixed', 'multipart/related', 'multipart/batch'].includes(key)) ||
null
);
}

View File

@ -23,7 +23,7 @@ export function getModel(openApi: OpenApi, definition: OpenApiSchema, isDefiniti
isDefinition,
isReadOnly: definition.readOnly === true,
isNullable: definition.nullable === true,
isRequired: definition.default !== undefined,
isRequired: false,
format: definition.format,
maximum: definition.maximum,
exclusiveMaximum: definition.exclusiveMaximum,
@ -180,6 +180,7 @@ export function getModel(openApi: OpenApi, definition: OpenApiSchema, isDefiniti
model.type = definitionType.type;
model.base = definitionType.base;
model.template = definitionType.template;
model.isNullable = definitionType.isNullable;
model.imports.push(...definitionType.imports);
model.default = getModelDefault(definition, model);
return model;

View File

@ -31,6 +31,11 @@ export function getModelComposition(openApi: OpenApi, definition: OpenApiSchema,
});
if (definition.properties) {
const properties = getModelProperties(openApi, definition, getModel);
properties.forEach(property => {
composition.imports.push(...property.imports);
composition.enums.push(...property.enums);
});
composition.properties.push({
name: 'properties',
export: 'interface',
@ -46,7 +51,7 @@ export function getModelComposition(openApi: OpenApi, definition: OpenApiSchema,
imports: [],
enum: [],
enums: [],
properties: [...getModelProperties(openApi, definition, getModel)],
properties,
});
}

View File

@ -15,7 +15,7 @@ export function getModelProperties(openApi: OpenApi, definition: OpenApiSchema,
for (const propertyName in definition.properties) {
if (definition.properties.hasOwnProperty(propertyName)) {
const property = definition.properties[propertyName];
const propertyRequired = definition.required?.includes(propertyName) || property.default !== undefined;
const propertyRequired = !!definition.required?.includes(propertyName);
if (property.$ref) {
const model = getType(property.$ref);
models.push({

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

@ -27,6 +27,7 @@ export function getOperationParameter(openApi: OpenApi, parameter: OpenApiParame
enum: [],
enums: [],
properties: [],
mediaType: null,
};
if (parameter.$ref) {

View File

@ -1,6 +1,7 @@
import camelCase from 'camelcase';
const reservedWords = /^(arguments|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|eval|export|extends|false|finally|for|function|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)$/g;
const reservedWords =
/^(arguments|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|eval|export|extends|false|finally|for|function|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)$/g;
/**
* Replaces any invalid characters from a parameter name.

View File

@ -4,6 +4,7 @@ import type { OpenApi } from '../interfaces/OpenApi';
import type { OpenApiRequestBody } from '../interfaces/OpenApiRequestBody';
import { getComment } from './getComment';
import { getContent } from './getContent';
import { getMediaType } from './getMediaType';
import { getModel } from './getModel';
import { getType } from './getType';
@ -27,11 +28,13 @@ export function getOperationRequestBody(openApi: OpenApi, parameter: OpenApiRequ
enum: [],
enums: [],
properties: [],
mediaType: null,
};
if (parameter.content) {
const schema = getContent(openApi, parameter.content);
if (schema) {
requestBody.mediaType = getMediaType(openApi, parameter.content);
if (schema?.$ref) {
const model = getType(schema.$ref);
requestBody.export = 'reference';

View File

@ -34,4 +34,28 @@ describe('getRef', () => {
type: 'integer',
});
});
it('should produce correct result for encoded ref path', () => {
expect(
getRef(
{
openapi: '3.0',
info: {
title: 'dummy',
version: '1.0',
},
paths: {
'/api/user/{id}': {
description: 'This is an Example path',
},
},
},
{
$ref: '#/paths/~1api~1user~1%7Bid%7D',
}
)
).toEqual({
description: 'This is an Example path',
});
});
});

View File

@ -1,6 +1,9 @@
import type { OpenApi } from '../interfaces/OpenApi';
import type { OpenApiReference } from '../interfaces/OpenApiReference';
const ESCAPED_REF_SLASH = /~1/g;
const ESCAPED_REF_TILDE = /~0/g;
export function getRef<T>(openApi: OpenApi, item: T & OpenApiReference): T {
if (item.$ref) {
// Fetch the paths to the definitions, this converts:
@ -13,9 +16,10 @@ export function getRef<T>(openApi: OpenApi, item: T & OpenApiReference): T {
// Try to find the reference by walking down the path,
// if we cannot find it, then we throw an error.
let result: any = openApi;
paths.forEach((path: string): void => {
if (result.hasOwnProperty(path)) {
result = result[path];
paths.forEach(path => {
const decodedPath = decodeURIComponent(path.replace(ESCAPED_REF_SLASH, '/').replace(ESCAPED_REF_TILDE, '~'));
if (result.hasOwnProperty(decodedPath)) {
result = result[decodedPath];
} else {
throw new Error(`Could not find reference: "${item.$ref}"`);
}

View File

@ -7,6 +7,7 @@ describe('getType', () => {
expect(type.base).toEqual('number');
expect(type.template).toEqual(null);
expect(type.imports).toEqual([]);
expect(type.isNullable).toEqual(false);
});
it('should convert string', () => {
@ -15,6 +16,7 @@ describe('getType', () => {
expect(type.base).toEqual('string');
expect(type.template).toEqual(null);
expect(type.imports).toEqual([]);
expect(type.isNullable).toEqual(false);
});
it('should convert string array', () => {
@ -23,6 +25,7 @@ describe('getType', () => {
expect(type.base).toEqual('string');
expect(type.template).toEqual(null);
expect(type.imports).toEqual([]);
expect(type.isNullable).toEqual(false);
});
it('should convert template with primary', () => {
@ -31,6 +34,7 @@ describe('getType', () => {
expect(type.base).toEqual('Link');
expect(type.template).toEqual('string');
expect(type.imports).toEqual(['Link']);
expect(type.isNullable).toEqual(false);
});
it('should convert template with model', () => {
@ -39,6 +43,7 @@ describe('getType', () => {
expect(type.base).toEqual('Link');
expect(type.template).toEqual('Model');
expect(type.imports).toEqual(['Link', 'Model']);
expect(type.isNullable).toEqual(false);
});
it('should have double imports', () => {
@ -47,6 +52,7 @@ describe('getType', () => {
expect(type.base).toEqual('Link');
expect(type.template).toEqual('Link');
expect(type.imports).toEqual(['Link', 'Link']);
expect(type.isNullable).toEqual(false);
});
it('should convert generic', () => {
@ -55,6 +61,7 @@ describe('getType', () => {
expect(type.base).toEqual('T');
expect(type.template).toEqual(null);
expect(type.imports).toEqual([]);
expect(type.isNullable).toEqual(false);
});
it('should support dot', () => {
@ -63,6 +70,7 @@ describe('getType', () => {
expect(type.base).toEqual('model_000');
expect(type.template).toEqual(null);
expect(type.imports).toEqual(['model_000']);
expect(type.isNullable).toEqual(false);
});
it('should support dashes', () => {
@ -71,6 +79,7 @@ describe('getType', () => {
expect(type.base).toEqual('some_special_schema');
expect(type.template).toEqual(null);
expect(type.imports).toEqual(['some_special_schema']);
expect(type.isNullable).toEqual(false);
});
it('should support dollar sign', () => {
@ -79,5 +88,24 @@ describe('getType', () => {
expect(type.base).toEqual('$some_special_schema');
expect(type.template).toEqual(null);
expect(type.imports).toEqual(['$some_special_schema']);
expect(type.isNullable).toEqual(false);
});
it('should support multiple base types', () => {
const type = getType(['string', 'int']);
expect(type.type).toEqual('string | number');
expect(type.base).toEqual('string | number');
expect(type.template).toEqual(null);
expect(type.imports).toEqual([]);
expect(type.isNullable).toEqual(false);
});
it('should support multiple nullable types', () => {
const type = getType(['string', 'null']);
expect(type.type).toEqual('string');
expect(type.base).toEqual('string');
expect(type.template).toEqual(null);
expect(type.imports).toEqual([]);
expect(type.isNullable).toEqual(true);
});
});

View File

@ -5,21 +5,35 @@ import { stripNamespace } from './stripNamespace';
function encode(value: string): string {
return value.replace(/^[^a-zA-Z_$]+/g, '').replace(/[^\w$]+/g, '_');
}
/**
* Parse any string value into a type object.
* @param value String value like "integer" or "Link[Model]".
* @param values String or String[] value like "integer", "Link[Model]" or ["string", "null"]
* @param template Optional template class from parent (needed to process generics)
*/
export function getType(value?: string, template?: string): Type {
export function getType(values?: string | string[], template?: string): Type {
const result: Type = {
type: 'any',
base: 'any',
template: null,
imports: [],
isNullable: false,
};
const valueClean = stripNamespace(value || '');
// Special case for JSON Schema spec (december 2020, page 17),
// that allows type to be an array of primitive types...
if (Array.isArray(values)) {
const type = values
.filter(value => value !== 'null')
.filter(value => hasMappedType(value))
.map(value => getMappedType(value))
.join(' | ');
result.type = type;
result.base = type;
result.isNullable = values.includes('null');
return result;
}
const valueClean = decodeURIComponent(stripNamespace(values || ''));
if (/\[.*\]$/g.test(valueClean)) {
const matches = valueClean.match(/(.*?)\[(.*)\]$/);

View File

@ -4,6 +4,6 @@ export function sortByRequired(a: OperationParameter, b: OperationParameter): nu
const aNeedsValue = a.isRequired && a.default === undefined;
const bNeedsValue = b.isRequired && b.default === undefined;
if (aNeedsValue && !bNeedsValue) return -1;
if (!aNeedsValue && bNeedsValue) return 1;
if (bNeedsValue && !aNeedsValue) return 1;
return 0;
}

View File

@ -8,6 +8,7 @@ export type ApiRequestOptions = {
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
}

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,30 @@
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 formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {}
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
...formHeaders,
})
.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,85 @@
{{>header}}
import { AbortController, AbortSignal } from 'abort-controller';
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 { CancelablePromise } from './CancelablePromise';
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 CancelablePromise<T>
* @throws ApiError
*/
export function request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(options);
const response = await sendRequest(options, url, onCancel);
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);
resolve(result.body);
} catch (error) {
reject(error);
}
});
}

View File

@ -0,0 +1,16 @@
async function sendRequest(options: ApiRequestOptions, url: string, onCancel: (cancelHandler: () => void) => void): Promise<AxiosResponse<any>> {
const controller = new AbortController();
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),
signal: controller.signal,
};
onCancel(() => controller.abort());
return await axios.request(config);
}

View File

@ -2,25 +2,34 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
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 additionalHeaders = await resolve(options, OpenAPI.HEADERS);
const headers = new Headers({
const defaultHeaders = Object.entries({
Accept: 'application/json',
...defaultHeaders,
...additionalHeaders,
...options.headers,
});
})
.filter(([key, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: value,
}), {});
const headers = new Headers(defaultHeaders);
if (isStringWithValue(token)) {
headers.append('Authorization', `Bearer ${token}`);
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = btoa(`${username}:${password}`);
const credentials = base64(`${username}:${password}`);
headers.append('Authorization', `Basic ${credentials}`);
}
if (options.body) {
if (isBlob(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');

View File

@ -4,7 +4,9 @@ function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
}
if (options.body) {
if (isString(options.body) || isBlob(options.body)) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);

View File

@ -1,16 +1,18 @@
async function getResponseBody(response: Response): Promise<any> {
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();
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);
}
} catch (error) {
console.error(error);
}
return null;

View File

@ -18,6 +18,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

@ -2,25 +2,34 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
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 additionalHeaders = await resolve(options, OpenAPI.HEADERS);
const headers = new Headers({
const defaultHeaders = Object.entries({
Accept: 'application/json',
...defaultHeaders,
...additionalHeaders,
...options.headers,
});
})
.filter(([key, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: value,
}), {});
const headers = new Headers(defaultHeaders);
if (isStringWithValue(token)) {
headers.append('Authorization', `Bearer ${token}`);
}
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 (isBinary(options.body)) {
if (options.mediaType) {
headers.append('Content-Type', options.mediaType);
} 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

@ -4,8 +4,10 @@ function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
}
if (options.body) {
if (isString(options.body) || isBinary(options.body)) {
return options.body;
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body) || isBinary(options.body)) {
return options.body as any;
} else {
return JSON.stringify(options.body);
}

View File

@ -1,16 +1,18 @@
async function getResponseBody(response: Response): Promise<any> {
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();
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);
}
} catch (error) {
console.error(error);
}
return null;

View File

@ -20,9 +20,15 @@ import { OpenAPI } from './OpenAPI';
{{>functions/isStringWithValue}}
{{>functions/isBlob}}
{{>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

@ -2,25 +2,34 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
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 additionalHeaders = await resolve(options, OpenAPI.HEADERS);
const headers = new Headers({
const defaultHeaders = Object.entries({
Accept: 'application/json',
...defaultHeaders,
...additionalHeaders,
...options.headers,
});
})
.filter(([key, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: value,
}), {});
const headers = new Headers(defaultHeaders);
if (isStringWithValue(token)) {
headers.append('Authorization', `Bearer ${token}`);
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = btoa(`${username}:${password}`);
const credentials = base64(`${username}:${password}`);
headers.append('Authorization', `Basic ${credentials}`);
}
if (options.body) {
if (isBlob(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');

View File

@ -4,7 +4,9 @@ function getRequestBody(options: ApiRequestOptions): any {
}
if (options.body) {
if (isString(options.body) || isBlob(options.body)) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);

View File

@ -1,16 +1,18 @@
function getResponseBody(xhr: XMLHttpRequest): any {
try {
const contentType = xhr.getResponseHeader('Content-Type');
if (contentType) {
const isJSON = contentType.toLowerCase().startsWith('application/json');
if (isJSON) {
return JSON.parse(xhr.responseText);
} else {
return xhr.responseText;
if (xhr.status !== 204) {
try {
const contentType = xhr.getResponseHeader('Content-Type');
if (contentType) {
const isJSON = contentType.toLowerCase().startsWith('application/json');
if (isJSON) {
return JSON.parse(xhr.responseText);
} else {
return xhr.responseText;
}
}
} catch (error) {
console.error(error);
}
} catch (error) {
console.error(error);
}
return null;

View File

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

View File

@ -16,7 +16,11 @@ import type { {{{this}}} } from './{{{this}}}';
{{else equals export 'all-of'}}
{{>exportComposition}}
{{else equals export 'enum'}}
{{#if @root.useUnionTypes}}
{{>exportType}}
{{else}}
{{>exportEnum}}
{{/if}}
{{else}}
{{>exportType}}
{{/equals}}

View File

@ -70,6 +70,9 @@ export class {{{name}}} {
{{/if}}
{{#if parametersBody}}
body: {{{parametersBody.name}}},
{{#if parametersBody.mediaType}}
mediaType: '{{{parametersBody.mediaType}}}',
{{/if}}
{{/if}}
{{#if responseHeader}}
responseHeader: '{{{responseHeader}}}',

View File

@ -9,10 +9,10 @@ export { OpenAPI } from './core/OpenAPI';
{{#if models}}
{{#each models}}
{{#if enum}}
export { {{{name}}} } from './models/{{{name}}}';
{{else if @root.useUnionTypes}}
{{#if @root.useUnionTypes}}
export type { {{{name}}} } from './models/{{{name}}}';
{{else if enum}}
export { {{{name}}} } from './models/{{{name}}}';
{{else if enums}}
export { {{{name}}} } from './models/{{{name}}}';
{{else}}

View File

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

View File

@ -1 +1 @@
({{#intersection properties parent}}{{this}}{{/intersection}}){{>isNullable}}
{{#intersection properties parent}}{{this}}{{/intersection}}{{>isNullable}}

View File

@ -1 +1 @@
({{#union properties parent}}{{this}}{{/union}}){{>isNullable}}
{{#union properties parent}}{{this}}{{/union}}{{>isNullable}}

View File

@ -1,27 +0,0 @@
import { exists, readFile } from './fileSystem';
import { getOpenApiSpec } from './getOpenApiSpec';
jest.mock('./fileSystem');
const existsMocked = exists as jest.MockedFunction<typeof exists>;
const readFileMocked = readFile as jest.MockedFunction<typeof readFile>;
function mockPromise<T>(value: T): Promise<T> {
return new Promise<T>(resolve => resolve(value));
}
describe('getOpenApiSpec', () => {
it('should read the json file', async () => {
existsMocked.mockReturnValue(mockPromise(true));
readFileMocked.mockReturnValue(mockPromise('{"message": "Hello World!"}'));
const spec = await getOpenApiSpec('spec.json');
expect(spec.message).toEqual('Hello World!');
});
it('should read the yaml file', async () => {
existsMocked.mockReturnValue(mockPromise(true));
readFileMocked.mockReturnValue(mockPromise('message: "Hello World!"'));
const spec = await getOpenApiSpec('spec.yaml');
expect(spec.message).toEqual('Hello World!');
});
});

View File

@ -1,8 +1,4 @@
import { load } from 'js-yaml';
import RefParser from 'json-schema-ref-parser';
import { extname } from 'path';
import { readSpec } from './readSpec';
/**
* Load and parse te open api spec. If the file extension is ".yml" or ".yaml"
@ -11,26 +7,5 @@ import { readSpec } from './readSpec';
* @param input
*/
export async function getOpenApiSpec(input: string): Promise<any> {
const extension = extname(input).toLowerCase();
const content = await readSpec(input);
let rootObject: any;
switch (extension) {
case '.yml':
case '.yaml':
try {
rootObject = load(content);
} catch (e) {
throw new Error(`Could not parse OpenApi YAML: "${input}"`);
}
break;
default:
try {
rootObject = JSON.parse(content);
} catch (e) {
throw new Error(`Could not parse OpenApi JSON: "${input}"`);
}
break;
}
return await RefParser.bundle(rootObject);
return await RefParser.bundle(input);
}

View File

@ -21,13 +21,23 @@ export function registerHandlebarHelpers(root: { httpClient: HttpClient; useOpti
Handlebars.registerHelper('union', function (this: any, properties: Model[], parent: string | undefined, options: Handlebars.HelperOptions) {
const type = Handlebars.partials['type'];
const types = properties.map(property => type({ ...root, ...property, parent }));
return options.fn(types.filter(unique).join(' | '));
const uniqueTypes = types.filter(unique);
let uniqueTypesString = uniqueTypes.join(' | ');
if (uniqueTypes.length > 1) {
uniqueTypesString = `(${uniqueTypesString})`;
}
return options.fn(uniqueTypesString);
});
Handlebars.registerHelper('intersection', function (this: any, properties: Model[], parent: string | undefined, options: Handlebars.HelperOptions) {
const type = Handlebars.partials['type'];
const types = properties.map(property => type({ ...root, ...property, parent }));
return options.fn(types.filter(unique).join(' & '));
const uniqueTypes = types.filter(unique);
let uniqueTypesString = uniqueTypes.join(' & ');
if (uniqueTypes.length > 1) {
uniqueTypesString = `(${uniqueTypesString})`;
}
return options.fn(uniqueTypesString);
});
Handlebars.registerHelper('enumerator', function (this: any, enumerators: Enum[], parent: string | undefined, name: string | undefined, options: Handlebars.HelperOptions) {

View File

@ -4,6 +4,11 @@ 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 templateCancelablePromise from '../templates/core/CancelablePromise.hbs';
import fetchGetHeaders from '../templates/core/fetch/getHeaders.hbs';
import fetchGetRequestBody from '../templates/core/fetch/getRequestBody.hbs';
@ -11,6 +16,7 @@ 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';
@ -151,6 +157,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
@ -177,5 +184,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

@ -35,6 +35,7 @@ export type ApiRequestOptions = {
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
}"
@ -146,6 +147,7 @@ type Config = {
USERNAME?: string | Resolver<string>;
PASSWORD?: string | Resolver<string>;
HEADERS?: Headers | Resolver<Headers>;
ENCODE_PATH?: (path: string) => string;
}
export const OpenAPI: Config = {
@ -156,6 +158,7 @@ export const OpenAPI: Config = {
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};"
`;
@ -185,6 +188,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[] = [];
@ -209,9 +220,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)}\`;
}
@ -245,25 +255,34 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
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 additionalHeaders = await resolve(options, OpenAPI.HEADERS);
const headers = new Headers({
const defaultHeaders = Object.entries({
Accept: 'application/json',
...defaultHeaders,
...additionalHeaders,
...options.headers,
});
})
.filter(([key, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: value,
}), {});
const headers = new Headers(defaultHeaders);
if (isStringWithValue(token)) {
headers.append('Authorization', \`Bearer \${token}\`);
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = btoa(\`\${username}:\${password}\`);
const credentials = base64(\`\${username}:\${password}\`);
headers.append('Authorization', \`Basic \${credentials}\`);
}
if (options.body) {
if (isBlob(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');
@ -280,7 +299,9 @@ function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
}
if (options.body) {
if (isString(options.body) || isBlob(options.body)) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);
@ -321,18 +342,20 @@ function getResponseHeader(response: Response, responseHeader?: string): string
}
async function getResponseBody(response: Response): Promise<any> {
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();
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);
}
} catch (error) {
console.error(error);
}
return null;
@ -696,12 +719,18 @@ export enum EnumWithNumbers {
'_1' = 1,
'_2' = 2,
'_3' = 3,
'_1.1' = 1.1,
'_1.2' = 1.2,
'_1.3' = 1.3,
'_100' = 100,
'_200' = 200,
'_300' = 300,
'_-100' = -100,
'_-200' = -200,
'_-300' = -300,
'_-1.1' = -1.1,
'_-1.2' = -1.2,
'_-1.3' = -1.3,
}"
`;
@ -2172,7 +2201,7 @@ export class ParametersService {
parameterPath: string,
): CancelablePromise<void> {
return __request({
method: 'GET',
method: 'POST',
path: \`/api/v\${OpenAPI.VERSION}/parameters/\${parameterPath}\`,
headers: {
'parameterHeader': parameterHeader,
@ -2209,7 +2238,7 @@ export class ParametersService {
_default?: string,
): CancelablePromise<void> {
return __request({
method: 'GET',
method: 'POST',
path: \`/api/v\${OpenAPI.VERSION}/parameters/\${parameterPath1}/\${parameterPath2}/\${parameterPath3}\`,
headers: {
'parameter.header': parameterHeader,
@ -2465,6 +2494,7 @@ export type ApiRequestOptions = {
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
}"
@ -2576,6 +2606,7 @@ type Config = {
USERNAME?: string | Resolver<string>;
PASSWORD?: string | Resolver<string>;
HEADERS?: Headers | Resolver<Headers>;
ENCODE_PATH?: (path: string) => string;
}
export const OpenAPI: Config = {
@ -2586,6 +2617,7 @@ export const OpenAPI: Config = {
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};"
`;
@ -2615,6 +2647,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[] = [];
@ -2639,9 +2679,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)}\`;
}
@ -2675,25 +2714,34 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
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 additionalHeaders = await resolve(options, OpenAPI.HEADERS);
const headers = new Headers({
const defaultHeaders = Object.entries({
Accept: 'application/json',
...defaultHeaders,
...additionalHeaders,
...options.headers,
});
})
.filter(([key, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: value,
}), {});
const headers = new Headers(defaultHeaders);
if (isStringWithValue(token)) {
headers.append('Authorization', \`Bearer \${token}\`);
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = btoa(\`\${username}:\${password}\`);
const credentials = base64(\`\${username}:\${password}\`);
headers.append('Authorization', \`Basic \${credentials}\`);
}
if (options.body) {
if (isBlob(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');
@ -2710,7 +2758,9 @@ function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
}
if (options.body) {
if (isString(options.body) || isBlob(options.body)) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);
@ -2751,18 +2801,20 @@ function getResponseHeader(response: Response, responseHeader?: string): string
}
async function getResponseBody(response: Response): Promise<any> {
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();
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);
}
} catch (error) {
console.error(error);
}
return null;
@ -3258,12 +3310,18 @@ export enum EnumWithNumbers {
'_1' = 1,
'_2' = 2,
'_3' = 3,
'_1.1' = 1.1,
'_1.2' = 1.2,
'_1.3' = 1.3,
'_100' = 100,
'_200' = 200,
'_300' = 300,
'_-100' = -100,
'_-200' = -200,
'_-300' = -300,
'_-1.1' = -1.1,
'_-1.2' = -1.2,
'_-1.3' = -1.3,
}"
`;
@ -3722,7 +3780,7 @@ exports[`v3 should generate: ./test/generated/v3/models/SimpleStringWithPattern.
/**
* This is a simple string
*/
export type SimpleStringWithPattern = string | null;"
export type SimpleStringWithPattern = string;"
`;
exports[`v3 should generate: ./test/generated/v3/schemas/$ArrayWithArray.ts 1`] = `
@ -4542,7 +4600,6 @@ exports[`v3 should generate: ./test/generated/v3/schemas/$SimpleStringWithPatter
/* eslint-disable */
export const $SimpleStringWithPattern = {
type: 'string',
isNullable: true,
maxLength: 64,
pattern: '^[a-zA-Z0-9_]*$',
};"
@ -4644,7 +4701,7 @@ export class ComplexService {
requestBody?: {
readonly key: string | null,
name: string | null,
enabled: boolean,
enabled?: boolean,
readonly type: 'Monkey' | 'Horse' | 'Bird',
listOfModels?: Array<ModelWithString> | null,
listOfStrings?: Array<string> | null,
@ -4659,6 +4716,7 @@ export class ComplexService {
method: 'PUT',
path: \`/api/v\${OpenAPI.VERSION}/complex/\${id}\`,
body: requestBody,
mediaType: 'application/json-patch+json',
});
}
@ -4935,7 +4993,7 @@ export class ParametersService {
requestBody: ModelWithString | null,
): CancelablePromise<void> {
return __request({
method: 'GET',
method: 'POST',
path: \`/api/v\${OpenAPI.VERSION}/parameters/\${parameterPath}\`,
cookies: {
'parameterCookie': parameterCookie,
@ -4950,6 +5008,7 @@ export class ParametersService {
'parameterForm': parameterForm,
},
body: requestBody,
mediaType: 'application/json',
});
}
@ -4977,7 +5036,7 @@ export class ParametersService {
_default?: string,
): CancelablePromise<void> {
return __request({
method: 'GET',
method: 'POST',
path: \`/api/v\${OpenAPI.VERSION}/parameters/\${parameterPath1}/\${parameterPath2}/\${parameterPath3}\`,
cookies: {
'PARAMETER-COOKIE': parameterCookie,
@ -4993,6 +5052,7 @@ export class ParametersService {
'parameter_form': parameterForm,
},
body: requestBody,
mediaType: 'application/json',
});
}
@ -5012,6 +5072,7 @@ export class ParametersService {
'parameter': parameter,
},
body: requestBody,
mediaType: 'application/json',
});
}
@ -5031,6 +5092,7 @@ export class ParametersService {
'parameter': parameter,
},
body: requestBody,
mediaType: 'application/json',
});
}
@ -5059,6 +5121,7 @@ export class RequestBodyService {
method: 'POST',
path: \`/api/v\${OpenAPI.VERSION}/requestBody/\`,
body: requestBody,
mediaType: 'application/json',
});
}

View File

@ -12,7 +12,15 @@ export function request<T>(options: ApiRequestOptions): CancelablePromise<T> {
try {
// Do your request...
const timeout = setTimeout(() => {
resolve({ ...options });
resolve({
url,
ok: true,
status: 200,
statusText: 'dummy',
body: {
...options,
},
});
}, 500);
// Cancel your request...

View File

@ -10,10 +10,7 @@ async function start() {
// and load the localhost page, this page will load the
// javascript modules (see server.js for more info)
browser = await puppeteer.launch({
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
]
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
page = await browser.newPage();
page.on('console', msg => console.log(msg.text()));

View File

@ -11,15 +11,21 @@ function compileWithBabel(dir) {
const result = babel.transformSync(content, {
filename: file,
presets: [
['@babel/preset-env', {
modules: false,
targets: {
node: true,
[
'@babel/preset-env',
{
modules: false,
targets: {
node: true,
},
},
}],
['@babel/preset-typescript', {
onlyRemoveTypeImports: true,
}],
],
[
'@babel/preset-typescript',
{
onlyRemoveTypeImports: true,
},
],
],
});
const out = file.replace(/\.ts$/, '.js');

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,10 +3,7 @@
const fs = require('fs');
function copy(dir) {
fs.copyFileSync(
'./test/e2e/assets/script.js',
`./test/e2e/generated/${dir}/script.js`,
);
fs.copyFileSync('./test/e2e/assets/script.js', `./test/e2e/generated/${dir}/script.js`);
}
module.exports = copy;

View File

@ -3,7 +3,7 @@
const express = require('express');
let app;
let server
let server;
async function start(dir) {
return new Promise(resolve => {
@ -12,10 +12,13 @@ async function start(dir) {
// Serve the JavaScript files from the specific folder, since we are using browser
// based ES6 modules, this also means that we can just request the js/index.js file
// and all other relative paths are resolved from that file.
app.use('/js', express.static(`./test/e2e/generated/${dir}/`, {
extensions: ['', 'js'],
index: 'index.js',
}));
app.use(
'/js',
express.static(`./test/e2e/generated/${dir}/`, {
extensions: ['', 'js'],
index: 'index.js',
})
);
// When we request the index then we can just return the script loader.
// This file is copied from test/e2e/assets/script.js to the output directory

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

@ -0,0 +1,51 @@
'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();
});
it('can abort the request', async () => {
try {
const { SimpleService } = require('./generated/v2/axios/index.js');
const promise = SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
} catch (e) {
expect(e.message).toContain('canceled');
}
});
});

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');
@ -17,8 +16,8 @@ describe('v2.fetch', () => {
}, 30000);
afterAll(async () => {
await server.stop();
await browser.stop();
await server.stop();
});
it('requests token', async () => {

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');
@ -17,8 +16,8 @@ describe('v2.fetch', () => {
}, 30000);
afterAll(async () => {
await server.stop();
await browser.stop();
await server.stop();
});
it('requests token', async () => {

View File

@ -5,7 +5,6 @@ const compileWithTypescript = require('./scripts/compileWithTypescript');
const server = require('./scripts/server');
describe('v2.node', () => {
beforeAll(async () => {
await generate('v2/node', 'v2', 'node');
compileWithTypescript('v2/node');
@ -18,7 +17,7 @@ describe('v2.node', () => {
it('requests token', async () => {
const { OpenAPI, SimpleService } = require('./generated/v2/node/index.js');
const tokenRequest = jest.fn().mockResolvedValue('MY_TOKEN')
const tokenRequest = jest.fn().mockResolvedValue('MY_TOKEN');
OpenAPI.TOKEN = tokenRequest;
const result = await SimpleService.getCallWithoutParametersAndResponse();
expect(tokenRequest.mock.calls.length).toBe(1);

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');
@ -17,8 +16,8 @@ describe('v2.xhr', () => {
}, 30000);
afterAll(async () => {
await server.stop();
await browser.stop();
await server.stop();
});
it('requests token', async () => {

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

@ -0,0 +1,70 @@
'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();
});
it('can abort the request', async () => {
try {
const { SimpleService } = require('./generated/v3/axios/index.js');
const promise = SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
} catch (e) {
expect(e.message).toContain('canceled');
}
});
});

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');
@ -17,8 +16,8 @@ describe('v3.fetch', () => {
}, 30000);
afterAll(async () => {
await server.stop();
await browser.stop();
await server.stop();
});
it('requests token', async () => {

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');
@ -17,8 +16,8 @@ describe('v3.fetch', () => {
}, 30000);
afterAll(async () => {
await server.stop();
await browser.stop();
await server.stop();
});
it('requests token', async () => {
@ -58,6 +57,16 @@ describe('v3.fetch', () => {
expect(result).toBeDefined();
});
it('formData', async () => {
const result = await browser.evaluate(async () => {
const { ParametersService } = window.api;
return await ParametersService.callWithParameters('valueHeader', 'valueQuery', 'valueForm', 'valueCookie', 'valuePath', {
prop: 'valueBody',
});
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
try {
await browser.evaluate(async () => {

View File

@ -5,7 +5,6 @@ const compileWithTypescript = require('./scripts/compileWithTypescript');
const server = require('./scripts/server');
describe('v3.node', () => {
beforeAll(async () => {
await generate('v3/node', 'v3', 'node');
compileWithTypescript('v3/node');
@ -18,7 +17,7 @@ describe('v3.node', () => {
it('requests token', async () => {
const { OpenAPI, SimpleService } = require('./generated/v3/node/index.js');
const tokenRequest = jest.fn().mockResolvedValue('MY_TOKEN')
const tokenRequest = jest.fn().mockResolvedValue('MY_TOKEN');
OpenAPI.TOKEN = tokenRequest;
OpenAPI.USERNAME = undefined;
OpenAPI.PASSWORD = undefined;
@ -48,6 +47,14 @@ describe('v3.node', () => {
expect(result).toBeDefined();
});
it('formData', async () => {
const { ParametersService } = require('./generated/v3/node/index.js');
const result = await ParametersService.callWithParameters('valueHeader', 'valueQuery', 'valueForm', 'valueCookie', 'valuePath', {
prop: 'valueBody',
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
try {
const { SimpleService } = require('./generated/v3/node/index.js');

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');
@ -17,8 +16,8 @@ describe('v3.xhr', () => {
}, 30000);
afterAll(async () => {
await server.stop();
await browser.stop();
await server.stop();
});
it('requests token', async () => {

View File

@ -55,7 +55,7 @@
}
},
"/api/v{api-version}/parameters/{parameterPath}": {
"get": {
"post": {
"tags": [
"Parameters"
],
@ -106,7 +106,7 @@
}
},
"/api/v{api-version}/parameters/{parameter.path.1}/{parameter-path-2}/{PARAMETER-PATH-3}": {
"get": {
"post": {
"tags": [
"Parameters"
],
@ -816,12 +816,18 @@
1,
2,
3,
1.1,
1.20,
1.300,
100,
200,
300,
-100,
-200,
-300
-300,
-1.1,
-1.20,
-1.300
]
},
"EnumFromDescription": {

View File

@ -55,7 +55,7 @@
}
},
"/api/v{api-version}/parameters/{parameterPath}": {
"get": {
"post": {
"tags": [
"Parameters"
],
@ -137,7 +137,7 @@
}
},
"/api/v{api-version}/parameters/{parameter.path.1}/{parameter-path-2}/{PARAMETER-PATH-3}": {
"get": {
"post": {
"tags": [
"Parameters"
],
@ -1315,12 +1315,18 @@
1,
2,
3,
1.1,
1.20,
1.300,
100,
200,
300,
-100,
-200,
-300
-300,
-1.1,
-1.20,
-1.300
]
},
"EnumFromDescription": {

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 = {

4937
yarn.lock

File diff suppressed because it is too large Load Diff