- Working on babel support (union types)

This commit is contained in:
Ferdi Koomen 2020-09-23 11:02:39 +02:00
parent 12c46cf686
commit 0f10589581
7 changed files with 159 additions and 108 deletions

146
README.md
View File

@ -6,37 +6,68 @@
[![Codecov](https://codecov.io/gh/ferdikoomen/openapi-typescript-codegen/branch/master/graph/badge.svg)](https://codecov.io/gh/ferdikoomen/openapi-typescript-codegen)
[![Quality](https://badgen.net/lgtm/grade/javascript/g/ferdikoomen/openapi-typescript-codegen)](https://lgtm.com/projects/g/ferdikoomen/openapi-typescript-codegen)
> NodeJS library that generates Typescript clients based on the OpenAPI specification.
> Node.js library that generates Typescript clients based on the OpenAPI specification.
#### Why?
- 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 OpenAPI specification v2.0 and v3.0.
- Supports JSON and YAML files for input.
## Why?
- 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 OpenAPI specification v2.0 and v3.0
- Supports JSON and YAML files for input
- Supports generation through CLI, Node.js and NPX
- Supports tsc and @babel/plugin-transform-typescript
## Known issues:
- If you use enums inside your models / definitions then those enums are now
## Babel support:
- If you use enums inside your models / definitions then those enums are by default
inside a namespace with the same name as your model. This is called declaration
merging. However, Babel 7 now support compiling of Typescript and right now they
do not support namespaces.
merging. However, the @babel/plugin-transform-typescript does not support these
namespaces, so if you are using babel in your project please use the `--useUnionTypes`
flag to generate union types instead of traditional enums. More info can be found
here: [Enums vs. Union Types](#enums-vs-union-types).
## Installation
## Install
```
npm install openapi-typescript-codegen --save-dev
```
## Usage
```
$ openapi --help
Usage: openapi [options]
Options:
-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] (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)
Examples
$ openapi --input ./spec.json
$ openapi --input ./spec.json --output ./dist
$ openapi --input ./spec.json --output ./dist --client xhr
```
## Example
**package.json**
```json
{
"scripts": {
"generate": "openapi --input ./api/openapi.json --output ./dist"
"generate": "openapi --input ./spec.json --output ./dist"
}
}
```
@ -44,19 +75,17 @@ npm install openapi-typescript-codegen --save-dev
**Command line**
```
npm install openapi-typescript-codegen -g
openapi --input ./api/openapi.json --output ./dist
npx openapi-typescript-codegen --input ./spec.json --output ./dist
```
**NodeJS API**
**Node.js API**
```javascript
const OpenAPI = require('openapi-typescript-codegen');
OpenAPI.generate({
input: './api/openapi.json',
output: './generated'
input: './spec.json',
output: './dist'
});
```
@ -65,21 +94,22 @@ Or by providing the JSON directly:
```javascript
const OpenAPI = require('openapi-typescript-codegen');
const spec = require('./api/openapi.json');
const spec = require('./spec.json');
OpenAPI.generate({
input: spec,
output: './generated'
output: './dist'
});
```
## Features
### Argument-style vs. Object-style
There's no [named parameter](https://en.wikipedia.org/wiki/Named_parameter) in Javascript or Typescript, because of
### Argument style vs. Object style `--useOptions`
There's no [named parameter](https://en.wikipedia.org/wiki/Named_parameter) in JavaScript or TypeScript, because of
that, we offer the flag `--useOptions` to generate code in two different styles.
Argument-style:
**Argument-style:**
```typescript
function createUser(name: string, password: string, type?: string, address?: string) {
// ...
@ -89,7 +119,7 @@ function createUser(name: string, password: string, type?: string, address?: str
createUser('Jack', '123456', undefined, 'NY US');
```
Object-style:
**Object-style:**
```typescript
function createUser({ name, password, type, address }: {
name: string,
@ -108,10 +138,60 @@ createUser({
});
```
### Enums vs. Union Types `--useUnionTypes`
The OpenAPI spec allows you to define [enums](https://swagger.io/docs/specification/data-models/enums/) inside the
data model. By default, we convert these enums definitions to [TypeScript enums](https://www.typescriptlang.org/docs/handbook/enums.html).
However, these enums are merged inside the namespace of the model, this is unsupported by Babel, [see docs](https://babeljs.io/docs/en/babel-plugin-transform-typescript#impartial-namespace-support).
### Runtime schemas
By default the OpenAPI generator only exports interfaces for your models. These interfaces will help you during
development, but will not be available in javascript during runtime. However, Swagger allows you to define properties
Because we also want to support projects that use Babel [@babel/plugin-transform-typescript](https://babeljs.io/docs/en/babel-plugin-transform-typescript), we offer the flag `--useOptions` to generate
[union types](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#union-types) instead of
the traditional enums. The difference can be seen below:
**Enums:**
```typescript
// Model
export interface Order {
id?: number;
quantity?: number;
status?: Order.status;
}
export namespace Order {
export enum status {
PLACED = 'placed',
APPROVED = 'approved',
DELIVERED = 'delivered',
}
}
// Usage
const order: Order = {
id: 1,
quantity: 40,
status: Order.status.PLACED
}
```
**Union Types:**
```typescript
// Model
export interface Order {
id?: number;
quantity?: number;
status?: 'placed' | 'approved' | 'delivered';
}
// Usage
const order: Order = {
id: 1,
quantity: 40,
status: 'placed'
}
```
### Runtime schemas `--exportSchemas`
By default, the OpenAPI generator only exports interfaces for your models. These interfaces will help you during
development, but will not be available in JavaScript during runtime. However, Swagger allows you to define properties
that can be useful during runtime, for instance: `maxLength` of a string or a `pattern` to match, etc. Let's say
we have the following model:
@ -192,7 +272,7 @@ export const $MyModel = {
```
These runtime object are prefixed with a `$` character and expose all the interesting attributes of a model
and it's properties. We can now use this object to generate the form:
and its properties. We can now use this object to generate the form:
```typescript jsx
import { $MyModel } from './generated';
@ -235,12 +315,12 @@ that can help developers use more meaningful enumerators.
],
"x-enum-varnames": [
"Success",
"Warning"
"Warning",
"Error"
],
"x-enum-descriptions": [
"Used when the status of something is successful",
"Used when the status of something has a warning"
"Used when the status of something has a warning",
"Used when the status of something has an error"
]
}
@ -265,6 +345,7 @@ enum EnumWithStrings {
}
```
### Authorization
The OpenAPI generator supports Bearer Token authorization. In order to enable the sending
of tokens in each request you can set the token using the global OpenAPI configuration:
@ -277,7 +358,6 @@ OpenAPI.TOKEN = 'some-bearer-token';
### 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

View File

@ -7,16 +7,18 @@ const program = require('commander');
const pkg = require('../package.json');
program
.name('openapi')
.usage('[options]')
.version(pkg.version)
.option('-i, --input <value>', 'Path to swagger specification', './spec.json')
.option('-o, --output <value>', 'Output directory', './generated')
.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]', 'fetch')
.option('--useOptions', 'Use options vs arguments style functions')
.option('--useOptions', 'Use options instead of arguments')
.option('--useUnionTypes', 'Use union types instead of enums')
.option('--exportCore <value>', 'Generate core', true)
.option('--exportServices <value>', 'Generate services', true)
.option('--exportModels <value>', 'Generate models', true)
.option('--exportSchemas <value>', 'Generate schemas', false)
.option('--exportCore <value>', 'Write core files to disk', true)
.option('--exportServices <value>', 'Write services to disk', true)
.option('--exportModels <value>', 'Write models to disk', true)
.option('--exportSchemas <value>', 'Write schemas to disk', false)
.parse(process.argv);
const OpenAPI = require(path.resolve(__dirname, '../dist/index.js'));

View File

@ -114,8 +114,9 @@ export function getModel(openApi: OpenApi, definition: OpenApiSchema, isDefiniti
}
}
// TODO: Add correct support for oneOf, anyOf, allOf
// TODO: https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/
// TODO:
// Add correct support for oneOf, anyOf, allOf
// https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/
if (definition.anyOf && definition.anyOf.length && !definition.properties) {
model.export = 'generic';

View File

@ -58,6 +58,7 @@ export function getOperation(openApi: OpenApi, url: string, method: string, op:
operation.parametersBody = parameters.parametersBody;
}
// TODO: form data goes wrong here: https://github.com/ferdikoomen/openapi-typescript-codegen/issues/257§
if (op.requestBody) {
const requestBodyDef = getRef<OpenApiRequestBody>(openApi, op.requestBody);
const requestBody = getOperationRequestBody(openApi, requestBodyDef);

View File

@ -22,35 +22,22 @@ export class ApiError extends Error {
}
}
export namespace ApiError {
export enum Message {
BAD_REQUEST = 'Bad Request',
UNAUTHORIZED = 'Unauthorized',
FORBIDDEN = 'Forbidden',
NOT_FOUND = 'Not Found',
INTERNAL_SERVER_ERROR = 'Internal Server Error',
BAD_GATEWAY = 'Bad Gateway',
SERVICE_UNAVAILABLE = 'Service Unavailable',
GENERIC_ERROR = 'Generic Error',
}
}
/**
* Catch common errors (based on status code).
* @param result
*/
export function catchGenericError(result: Result): void {
switch (result.status) {
case 400: throw new ApiError(result, ApiError.Message.BAD_REQUEST);
case 401: throw new ApiError(result, ApiError.Message.UNAUTHORIZED);
case 403: throw new ApiError(result, ApiError.Message.FORBIDDEN);
case 404: throw new ApiError(result, ApiError.Message.NOT_FOUND);
case 500: throw new ApiError(result, ApiError.Message.INTERNAL_SERVER_ERROR);
case 502: throw new ApiError(result, ApiError.Message.BAD_GATEWAY);
case 503: throw new ApiError(result, ApiError.Message.SERVICE_UNAVAILABLE);
case 400: throw new ApiError(result, 'Bad Request');
case 401: throw new ApiError(result, 'Unauthorized');
case 403: throw new ApiError(result, 'Forbidden');
case 404: throw new ApiError(result, 'Not Found');
case 500: throw new ApiError(result, 'Internal Server Error');
case 502: throw new ApiError(result, 'Bad Gateway');
case 503: throw new ApiError(result, 'Service Unavailable');
}
if (!isSuccess(result.status)) {
throw new ApiError(result, ApiError.Message.GENERIC_ERROR);
throw new ApiError(result, 'Generic Error');
}
}

View File

@ -50,9 +50,11 @@ export async function request(options: Readonly<RequestOptions>): Promise<Result
url += getQueryString(options.query);
}
// Append formData as body
// Append formData as body, this needs to be parsed to key=value pairs
// so the backend can parse this just like a regular HTML form.
if (options.formData) {
request.body = getFormData(options.formData);
headers.append('Content-Type', 'application/x-www-form-urlencoded');
} else if (options.body) {
// If this is blob data, then pass it directly to the body and set content type.

View File

@ -25,36 +25,23 @@ export class ApiError extends Error {
}
}
export namespace ApiError {
export enum Message {
BAD_REQUEST = 'Bad Request',
UNAUTHORIZED = 'Unauthorized',
FORBIDDEN = 'Forbidden',
NOT_FOUND = 'Not Found',
INTERNAL_SERVER_ERROR = 'Internal Server Error',
BAD_GATEWAY = 'Bad Gateway',
SERVICE_UNAVAILABLE = 'Service Unavailable',
GENERIC_ERROR = 'Generic Error',
}
}
/**
* Catch common errors (based on status code).
* @param result
*/
export function catchGenericError(result: Result): void {
switch (result.status) {
case 400: throw new ApiError(result, ApiError.Message.BAD_REQUEST);
case 401: throw new ApiError(result, ApiError.Message.UNAUTHORIZED);
case 403: throw new ApiError(result, ApiError.Message.FORBIDDEN);
case 404: throw new ApiError(result, ApiError.Message.NOT_FOUND);
case 500: throw new ApiError(result, ApiError.Message.INTERNAL_SERVER_ERROR);
case 502: throw new ApiError(result, ApiError.Message.BAD_GATEWAY);
case 503: throw new ApiError(result, ApiError.Message.SERVICE_UNAVAILABLE);
case 400: throw new ApiError(result, 'Bad Request');
case 401: throw new ApiError(result, 'Unauthorized');
case 403: throw new ApiError(result, 'Forbidden');
case 404: throw new ApiError(result, 'Not Found');
case 500: throw new ApiError(result, 'Internal Server Error');
case 502: throw new ApiError(result, 'Bad Gateway');
case 503: throw new ApiError(result, 'Service Unavailable');
}
if (!isSuccess(result.status)) {
throw new ApiError(result, ApiError.Message.GENERIC_ERROR);
throw new ApiError(result, 'Generic Error');
}
}
"
@ -242,9 +229,11 @@ export async function request(options: Readonly<RequestOptions>): Promise<Result
url += getQueryString(options.query);
}
// Append formData as body
// Append formData as body, this needs to be parsed to key=value pairs
// so the backend can parse this just like a regular HTML form.
if (options.formData) {
request.body = getFormData(options.formData);
headers.append('Content-Type', 'application/x-www-form-urlencoded');
} else if (options.body) {
// If this is blob data, then pass it directly to the body and set content type.
@ -2515,36 +2504,23 @@ export class ApiError extends Error {
}
}
export namespace ApiError {
export enum Message {
BAD_REQUEST = 'Bad Request',
UNAUTHORIZED = 'Unauthorized',
FORBIDDEN = 'Forbidden',
NOT_FOUND = 'Not Found',
INTERNAL_SERVER_ERROR = 'Internal Server Error',
BAD_GATEWAY = 'Bad Gateway',
SERVICE_UNAVAILABLE = 'Service Unavailable',
GENERIC_ERROR = 'Generic Error',
}
}
/**
* Catch common errors (based on status code).
* @param result
*/
export function catchGenericError(result: Result): void {
switch (result.status) {
case 400: throw new ApiError(result, ApiError.Message.BAD_REQUEST);
case 401: throw new ApiError(result, ApiError.Message.UNAUTHORIZED);
case 403: throw new ApiError(result, ApiError.Message.FORBIDDEN);
case 404: throw new ApiError(result, ApiError.Message.NOT_FOUND);
case 500: throw new ApiError(result, ApiError.Message.INTERNAL_SERVER_ERROR);
case 502: throw new ApiError(result, ApiError.Message.BAD_GATEWAY);
case 503: throw new ApiError(result, ApiError.Message.SERVICE_UNAVAILABLE);
case 400: throw new ApiError(result, 'Bad Request');
case 401: throw new ApiError(result, 'Unauthorized');
case 403: throw new ApiError(result, 'Forbidden');
case 404: throw new ApiError(result, 'Not Found');
case 500: throw new ApiError(result, 'Internal Server Error');
case 502: throw new ApiError(result, 'Bad Gateway');
case 503: throw new ApiError(result, 'Service Unavailable');
}
if (!isSuccess(result.status)) {
throw new ApiError(result, ApiError.Message.GENERIC_ERROR);
throw new ApiError(result, 'Generic Error');
}
}
"
@ -2732,9 +2708,11 @@ export async function request(options: Readonly<RequestOptions>): Promise<Result
url += getQueryString(options.query);
}
// Append formData as body
// Append formData as body, this needs to be parsed to key=value pairs
// so the backend can parse this just like a regular HTML form.
if (options.formData) {
request.body = getFormData(options.formData);
headers.append('Content-Type', 'application/x-www-form-urlencoded');
} else if (options.body) {
// If this is blob data, then pass it directly to the body and set content type.