mirror of
https://github.com/ferdikoomen/openapi-typescript-codegen.git
synced 2025-12-08 20:16:21 +00:00
- Working on babel support (union types)
This commit is contained in:
parent
12c46cf686
commit
0f10589581
146
README.md
146
README.md
@ -6,37 +6,68 @@
|
||||
[](https://codecov.io/gh/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
|
||||
|
||||
16
bin/index.js
16
bin/index.js
@ -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'));
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user