Merge pull request #944 from ferdikoomen/feature/angular

Feature/angular
This commit is contained in:
Ferdi Koomen 2022-01-28 17:35:50 +01:00 committed by GitHub
commit 8d5e458c08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 5278 additions and 902 deletions

482
README.md
View File

@ -14,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, [Node-Fetch](#node-fetch-support), [Axios](#axios-support) and XHR http clients
- Supports generations of Fetch, [Node-Fetch](#node-fetch-support), [Axios](#axios-support), [Angular](#angular-support) 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
@ -28,7 +28,6 @@
npm install openapi-typescript-codegen --save-dev
```
## Usage
```
@ -40,7 +39,7 @@ $ openapi --help
-V, --version output the version number
-i, --input <value> OpenAPI specification, can be a path, url or string content (required)
-o, --output <value> Output directory (required)
-c, --client <value> HTTP client to generate [fetch, xhr, axios, node] (default: "fetch")
-c, --client <value> HTTP client to generate [fetch, xhr, node, axios, angular] (default: "fetch")
--name <value> Custom client class name
--useOptions Use options instead of arguments
--useUnionTypes Use union types instead of enums
@ -54,468 +53,29 @@ $ openapi --help
-h, --help display help for command
Examples
$ openapi --input ./spec.json
$ openapi --input ./spec.json --output ./dist
$ openapi --input ./spec.json --output ./dist --client xhr
$ openapi --input ./spec.json --output ./generated
$ openapi --input ./spec.json --output ./generated --client xhr
```
## Example
**package.json**
```json
{
"scripts": {
"generate": "openapi --input ./spec.json --output ./dist"
}
}
```
**NPX**
```
npx openapi-typescript-codegen --input ./spec.json --output ./dist
```
**Node.js API**
```javascript
const OpenAPI = require('openapi-typescript-codegen');
OpenAPI.generate({
input: './spec.json',
output: './dist'
});
// Or by providing the content of the spec directly 🚀
OpenAPI.generate({
input: require('./spec.json'),
output: './dist'
});
```
## Features
### Generate client instance with `--name` option
The OpenAPI generator allows creation of client instances to support the multiple backend services use case.
The generated client uses an instance of the server configuration and not the global `OpenAPI` constant.
To generate a client instance, set a custom name to the client class, use `--name` option.
```
openapi --input ./spec.json --output ./dist ---name AppClient
```
The generated client will be exported from the `index` file and can be used as shown below:
```typescript
// Create the client instance with server and authentication details
const appClient = new AppClient({
BASE: 'http://server-host.com',
TOKEN: '1234'
});
// Use the client instance to make the API call
const response = await appClient.organizations.createOrganization({
name: 'OrgName',
description: 'OrgDescription',
});
```
### 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:**
```typescript
function createUser(name: string, password: string, type?: string, address?: string) {
// ...
}
// Usage
createUser('Jack', '123456', undefined, 'NY US');
```
**Object-style:**
```typescript
function createUser({ name, password, type, address }: {
name: string,
password: string,
type?: string
address?: string
}) {
// ...
}
// Usage
createUser({
name: 'Jack',
password: '123456',
address: 'NY US'
});
```
### 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).
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 `--useUnionTypes` 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:
```json
{
"MyModel": {
"required": [
"key",
"name"
],
"type": "object",
"properties": {
"key": {
"maxLength": 64,
"pattern": "^[a-zA-Z0-9_]*$",
"type": "string"
},
"name": {
"maxLength": 255,
"type": "string"
},
"enabled": {
"type": "boolean",
"readOnly": true
},
"modified": {
"type": "string",
"format": "date-time",
"readOnly": true
}
}
}
}
```
This will generate the following interface:
```typescript
export interface MyModel {
key: string;
name: string;
readonly enabled?: boolean;
readonly modified?: string;
}
```
The interface does not contain any properties like `maxLength` or `pattern`. However, they could be useful
if we wanted to create some form where a user could create such a model. In that form you would iterate
over the properties to render form fields based on their type and validate the input based on the `maxLength`
or `pattern` property. This requires us to have this information somewhere... For this we can use the
flag `--exportSchemas` to generate a runtime model next to the normal interface:
```typescript
export const $MyModel = {
properties: {
key: {
type: 'string',
isRequired: true,
maxLength: 64,
pattern: '^[a-zA-Z0-9_]*$',
},
name: {
type: 'string',
isRequired: true,
maxLength: 255,
},
enabled: {
type: 'boolean',
isReadOnly: true,
},
modified: {
type: 'string',
isReadOnly: true,
format: 'date-time',
},
},
} as const;
```
These runtime object are prefixed with a `$` character and expose all the interesting attributes of a model
and its properties. We can now use this object to generate the form:
```typescript jsx
import { $MyModel } from './generated';
// Some pseudo code to iterate over the properties and return a form field
// the form field could be some abstract component that renders the correct
// field type and validation rules based on the given input.
const formFields = Object.entries($MyModel.properties).map(([key, value]) => (
<FormField
name={key}
type={value.type}
format={value.format}
maxLength={value.maxLength}
pattern={value.pattern}
isReadOnly={value.isReadOnly}
/>
));
const MyForm = () => (
<form>
{formFields}
</form>
);
```
### Enum with custom names and descriptions
You can use `x-enum-varnames` and `x-enum-descriptions` in your spec to generate enum with custom names and descriptions.
It's not in official [spec](https://github.com/OAI/OpenAPI-Specification/issues/681) yet. But it's a supported extension
that can help developers use more meaningful enumerators.
```json
{
"EnumWithStrings": {
"description": "This is a simple enum with strings",
"enum": [
0,
1,
2
],
"x-enum-varnames": [
"Success",
"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 an error"
]
}
}
```
Generated code:
```typescript
enum EnumWithStrings {
/*
* Used when the status of something is successful
*/
Success = 0,
/*
* Used when the status of something has a warning
*/
Waring = 1,
/*
* Used when the status of something has an error
*/
Error = 2,
}
```
### Nullable in OpenAPI v2
In the OpenAPI v3 spec you can create properties that can be NULL, by providing a `nullable: true` in your schema.
However, the v2 spec does not allow you to do this. You can use the unofficial `x-nullable` in your specification
to generate nullable properties in OpenApi v2.
```json
{
"ModelWithNullableString": {
"required": ["requiredProp"],
"description": "This is a model with one string property",
"type": "object",
"properties": {
"prop": {
"description": "This is a simple string property",
"type": "string",
"x-nullable": true
},
"requiredProp": {
"description": "This is a simple string property",
"type": "string",
"x-nullable": true
}
}
}
}
```
Generated code:
```typescript
interface ModelWithNullableString {
prop?: string | null,
requiredProp: string | null,
}
```
### 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:
```typescript
import { OpenAPI } from './generated';
OpenAPI.TOKEN = 'some-bearer-token';
```
Alternatively, we also support an async method that provides the token for each request.
You can simply assign this method to the same `TOKEN `property in the global OpenAPI object.
```typescript
import { OpenAPI } from './generated';
const getToken = async () => {
// Some code that requests a token...
return 'SOME_TOKEN';
}
OpenAPI.TOKEN = getToken;
```
### References
Local references to schema definitions (those beginning with `#/definitions/schemas/`)
will be converted to type references to the equivalent, generated top-level type.
The OpenAPI generator also supports external references, which allows you to break
down your openapi.yml into multiple sub-files, or incorporate third-party schemas
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' }`
* *remote references* - fully qualified references to another remote location
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.
External references may also contain internal paths in the external schema (e.g.
`schemas/collection.yml#/definitions/schemas/Customer`) and back-references to
the base openapi file or between files (so that you can reference another
schema in the main file as a type of an object or array property, for example).
At start-up, an OpenAPI or Swagger file with external references will be "bundled",
so that all external references and back-references will be resolved (but local
references preserved).
FAQ
Documentation
===
- [Basic usage](docs/basic-usage.md)
- [OpenAPI object](docs/openapi-object.md)
- [Client instances](docs/client-instances.md) `--name`
- [Argument vs. Object style](docs/arguments-vs-object-style.md) `--useOptions`
- [Enums vs. Union types](docs/enum-vs-union-types.md) `--useUnionTypes`
- [Runtime schemas](docs/runtime-schemas.md) `--exportSchemas`
- [Enum with custom names and descriptions](docs/custom-enums.md)
- [Nullable props (OpenAPI v2)](docs/nullable-props.md)
- [Authorization](docs/authorization.md)
- [External references](docs/external-references.md)
### 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, the [@babel/plugin-transform-typescript](https://babeljs.io/docs/en/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---useuniontypes).
**Note:** If you are using Babel 7 and Typescript 3.8 (or higher) then you should enable the `onlyRemoveTypeImports` to
ignore any 'type only' imports, see https://babeljs.io/docs/en/babel-preset-typescript#onlyremovetypeimports for more info
```javascript
module.exports = {
presets: [
['@babel/preset-typescript', {
onlyRemoveTypeImports: true,
}],
],
};
```
### Axios support
This tool allows you to generate a client based on the [`axios`](https://www.npmjs.com/package/axios) client.
The advantage of the Axios client is that it works in both NodeJS and Browser based environments.
If you want to generate the Axios based client then you can specify `--client axios` in the openapi call:
`openapi --input ./spec.json --output ./dist --client axios`
The only downside is that this client needs some additional dependencies to work (due to the missing Blob and FormData
classes in NodeJS).
```
npm install axios --save-dev
npm install form-data@4.x --save-dev
```
In order to compile the project and resolve the imports, you will need to enable the `allowSyntheticDefaultImports`
in your `tsconfig.json` file.
### Node-Fetch support
By default, this tool will generate a client that is compatible with the (browser based) Fetch API.
However, this client will not work inside the Node.js environment. If you want to generate the Node.js compatible
client then 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 might need to install the `node-fetch@2.x` dependencies.
> 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!
```
npm install @types/node-fetch@2.x --save-dev
npm install abort-controller@3.x --save-dev
npm install form-data@4.x --save-dev
npm install node-fetch@2.x --save-dev
```
In order to compile the project and resolve the imports, you will need to enable the `allowSyntheticDefaultImports`
in your `tsconfig.json` file.
Support
===
- [Babel support](docs/babel-support.md)
- [Axios support](docs/axios-support.md)
- [Angular support](docs/angular-support.md)
- [Node-Fetch support](docs/node-fetch-support.md)
[npm-url]: https://npmjs.org/package/openapi-typescript-codegen
[npm-image]: https://img.shields.io/npm/v/openapi-typescript-codegen.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, axios]', 'fetch')
.option('-c, --client <value>', 'HTTP client to generate [fetch, xhr, node, axios, angular]', 'fetch')
.option('--name <value>', 'Custom client class name')
.option('--useOptions', 'Use options instead of arguments')
.option('--useUnionTypes', 'Use union types instead of enums')

84
docs/angular-support.md Normal file
View File

@ -0,0 +1,84 @@
# Angular support
This tool allows you to generate a client based on the [`Angular HttpClient`](https://angular.io/guide/http).
The generated services are fully injectable and make use of the [RxJS](https://rxjs.dev/) Observer pattern.
If you want to generate the Angular based client then you can specify `--client angular` in the openapi call:
`openapi --input ./spec.json --output ./generated --client angular`
The Angular client has been tested with the following versions:
```
"@angular/common": "13.1.3",
"@angular/core": "13.1.3",
"rxjs": "7.5.2",
```
## Example
In the AppModule you can import the services and add them to the list of injectable services:
```typescript
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { OrganizationService } from './generated/services/OrganizationService';
@NgModule({
imports: [
BrowserModule,
HttpClientModule,
],
providers: [
OrganizationService,
],
bootstrap: [
AppComponent,
],
})
export class AppModule {}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
```
Inside the component you can inject the service and just use it as you would with any observable:
```typescript
import { Component } from '@angular/core';
import type { OrganizationService } from './generated/services/OrganizationService';
@Component({
selector: 'app-root',
template: `<div>Angular is ready</div>`,
})
export class AppComponent {
constructor(private readonly organizationService: OrganizationService) {
// Make a call
this.organizationService
.createOrganization({
name: 'OrgName',
description: 'OrgDescription',
})
.subscribe(organization => {
console.log(organization);
});
// Or even map result and retry on error
this.organizationService
.getOrganizations()
.pipe(
map(organizations => organizations[0]),
retryWhen(error => error)
)
.subscribe(organization => {
console.log(organization);
});
}
}
```

View File

@ -0,0 +1,37 @@
# Arguments vs. Object style
**Flag:** `--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:**
```typescript
const createUser = (name: string, password: string, type?: string, address?: string) => {
// ...
};
// Usage
createUser('Jack', '123456', undefined, 'NY US');
```
**Object style:**
```typescript
const createUser = ({ name, password, type, address }: {
name: string,
password: string,
type?: string
address?: string
}) => {
// ...
};
// Usage
createUser({
name: 'Jack',
password: '123456',
address: 'NY US'
});
```

24
docs/authorization.md Normal file
View File

@ -0,0 +1,24 @@
# 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:
```typescript
import { OpenAPI } from './generated';
OpenAPI.TOKEN = 'some-bearer-token';
```
Alternatively, we also support an async method that provides the token for each request.
You can simply assign this method to the same `TOKEN `property in the global OpenAPI object.
```typescript
import { OpenAPI } from './generated';
const getToken = async () => {
// Some code that requests a token...
return 'SOME_TOKEN';
};
OpenAPI.TOKEN = getToken;
```

18
docs/axios-support.md Normal file
View File

@ -0,0 +1,18 @@
# Axios support
This tool allows you to generate a client based on the [`Axios`](https://www.npmjs.com/package/axios) client.
The advantage of the Axios client is that it works in both Node.js and Browser based environments.
If you want to generate the Axios based client then you can specify `--client axios` in the openapi call:
`openapi --input ./spec.json --output ./generated --client axios`
The only downside is that this client needs some additional dependencies to work (due to the missing FormData
classes in Node.js).
```
npm install axios --save-dev
npm install form-data@4.x --save-dev
```
In order to compile the project and resolve the imports, you will need to enable the `allowSyntheticDefaultImports`
in your `tsconfig.json` file.

19
docs/babel-support.md Normal file
View File

@ -0,0 +1,19 @@
# 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, the [@babel/plugin-transform-typescript](https://babeljs.io/docs/en/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---useuniontypes).
**Note:** If you are using Babel 7 and Typescript 3.8 (or higher) then you should enable the `onlyRemoveTypeImports` to
ignore any 'type only' imports, see https://babeljs.io/docs/en/babel-preset-typescript#onlyremovetypeimports for more info
```javascript
module.exports = {
presets: [
['@babel/preset-typescript', {
onlyRemoveTypeImports: true,
}],
],
};
```

61
docs/basic-usage.md Normal file
View File

@ -0,0 +1,61 @@
# Basic 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, node, axios, angular] (default: "fetch")
--name <value> Custom client class name
--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)
--indent <value> Indentation options [4, 2, tab] (default: "5")
--postfix <value> Service name postfix (default: "Service")
--request <value> Path to custom request file
-h, --help display help for command
Examples
$ openapi --input ./spec.json --output ./generated
```
## Example
**package.json**
```json
{
"scripts": {
"generate": "openapi --input ./spec.json --output ./generated"
}
}
```
**NPX**
```
npx openapi-typescript-codegen --input ./spec.json --output ./generated
```
**Node.js**
```javascript
const OpenAPI = require('openapi-typescript-codegen');
OpenAPI.generate({
input: './spec.json',
output: './generated',
});
// Or by providing the content of the spec directly 🚀
OpenAPI.generate({
input: require('./spec.json'),
output: './generated',
});
```

27
docs/client-instances.md Normal file
View File

@ -0,0 +1,27 @@
# Client instances
**Flag:** `--name`
The OpenAPI generator allows creation of client instances to support the multiple backend services use case.
The generated client uses an instance of the server configuration and not the global `OpenAPI` constant.
To generate a client instance, set a custom name to the client class, use `--name` option.
```
openapi --input ./spec.json --output ./generated ---name AppClient
```
The generated client will be exported from the `index` file and can be used as shown below:
```typescript
// Create the client instance with server and authentication details
const appClient = new AppClient({
BASE: 'http://server-host.com',
TOKEN: '1234',
});
// Use the client instance to make the API call
const response = await appClient.organizations.createOrganization({
name: 'OrgName',
description: 'OrgDescription',
});
```

47
docs/custom-enums.md Normal file
View File

@ -0,0 +1,47 @@
# Enum with custom names and descriptions
You can use `x-enum-varnames` and `x-enum-descriptions` in your spec to generate enum with custom names and descriptions.
It's not in official [spec](https://github.com/OAI/OpenAPI-Specification/issues/681) yet. But it's a supported extension
that can help developers use more meaningful enumerators.
```json
{
"EnumWithStrings": {
"description": "This is a simple enum with strings",
"enum": [
0,
1,
2
],
"x-enum-varnames": [
"Success",
"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 an error"
]
}
}
```
Generated code:
```typescript
enum EnumWithStrings {
/*
* Used when the status of something is successful
*/
Success = 0,
/*
* Used when the status of something has a warning
*/
Waring = 1,
/*
* Used when the status of something has an error
*/
Error = 2,
}
```

View File

@ -0,0 +1,54 @@
# Enums vs. Union types
**Flag:** `--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).
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 `--useUnionTypes` 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 type 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 type Order = {
id?: number;
quantity?: number;
status?: 'placed' | 'approved' | 'delivered';
};
// Usage
const order: Order = {
id: 1,
quantity: 40,
status: 'placed',
};
```

View File

@ -0,0 +1,28 @@
# External references
Local references to schema definitions (those beginning with `#/definitions/schemas/`)
will be converted to type references to the equivalent, generated top-level type.
The OpenAPI generator also supports external references, which allows you to break
down your openapi.yml into multiple sub-files, or incorporate third-party schemas
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' }`
* *remote references* - fully qualified references to another remote location 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.
External references may also contain internal paths in the external schema (e.g.
`schemas/collection.yml#/definitions/schemas/Customer`) and back-references to
the base openapi file or between files (so that you can reference another
schema in the main file as a type of an object or array property, for example).
At start-up, an OpenAPI or Swagger file with external references will be "bundled",
so that all external references and back-references will be resolved (but local
references preserved).

View File

@ -0,0 +1,23 @@
# Node-Fetch support
By default, this tool will generate a client that is compatible with the (browser based) Fetch API.
However, this client will not work inside the Node.js environment. If you want to generate the Node.js compatible
client then you can specify `--client node` in the openapi call:
`openapi --input ./spec.json --output ./generated --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 might need to install the `node-fetch@2.x` dependencies.
> 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!
```
npm install @types/node-fetch@2.x --save-dev
npm install abort-controller@3.x --save-dev
npm install form-data@4.x --save-dev
npm install node-fetch@2.x --save-dev
```
In order to compile the project and resolve the imports, you will need to enable the `allowSyntheticDefaultImports`
in your `tsconfig.json` file.

38
docs/nullable-props.md Normal file
View File

@ -0,0 +1,38 @@
# Nullable props (OpenAPI v2)
In the OpenAPI v3 spec you can create properties that can be `NULL`, by providing a `nullable: true` in your schema.
However, the v2 spec does not allow you to do this. You can use the unofficial `x-nullable` in your specification
to generate nullable properties in OpenApi v2.
```json
{
"ModelWithNullableString": {
"required": [
"requiredProp"
],
"description": "This is a model with one string property",
"type": "object",
"properties": {
"prop": {
"description": "This is a simple string property",
"type": "string",
"x-nullable": true
},
"requiredProp": {
"description": "This is a simple string property",
"type": "string",
"x-nullable": true
}
}
}
}
```
Generated code:
```typescript
export type ModelWithNullableString = {
prop?: string | null;
requiredProp: string | null;
};
```

144
docs/openapi-object.md Normal file
View File

@ -0,0 +1,144 @@
# OpenAPI object
The library exposes a global OpenAPI object that can be used to configure the requests,
below you can find the properties and their usage.
**Example:**
```typescript
export const OpenAPI: OpenAPIConfig = {
BASE: 'http://localhost:3000/api',
VERSION: '2.0',
WITH_CREDENTIALS: false,
CREDENTIALS: 'include',
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};
```
Properties
===
### `OpenAPI.BASE`
The base path of the OpenAPI server, this is generated from the spec,
but can be overwritten to switch servers.
```typescript
if (process.env === 'development') {
OpenAPI.BASE = 'http://staging.company.com:3000/api';
}
if (process.env === 'production') {
OpenAPI.BASE = '/api';
}
```
### `OpenAPI.VERSION`
The version param in the OpenAPI paths `{api-version}`. The version is taken from the spec,
but can be updated to call multiple versions of the same OpenAPI backend.
### `OpenAPI.WITH_CREDENTIALS`
Similar to the [withCredentials](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)
property of the XHR specification. When set to true, cross-site requests should be made
using credentials such as cookies, authorization headers, etc.
### `OpenAPI.CREDENTIALS`
Similar to the [credentials](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included)
property of the Fetch specification. When `OpenAPI.WITH_CREDENTIALS` is set to true,
this property controls the specific implementation for Fetch and Node-Fetch clients.
Valid values are: `include`, `omit` and `same-origin`.
### `OpenAPI.TOKEN`
Set the Bearer authentication token to use for the requests. This needs to be a valid
(non-expired) token, otherwise the request will fail. The property can be updated as often
as you want, this is useful for scenario's where the token would automatically refresh
after x minutes. This property also allows you to use an `async` method that will be resolved
before requests are made.
```typescript
OpenAPI.TOKEN = 'MY_TOKEN';
OpenAPI.TOKEN = async () => {
// Note: loading this from a JSON file is not recommended ;-)
const response = await fetch('configuration.json');
const { token } = response.json();
return token;
};
```
### `OpenAPI.USERNAME`
Set the basic authentication username, although not recommended, the basic authentication
header is still supported. The username and password hash will be calculated by the client
before sending the request. This property also allows you to use an `async` method that
will be resolved before requests are made.
```typescript
OpenAPI.USERNAME = 'john';
OpenAPI.USERNAME = async () => {
// Note: loading this from a JSON file is not recommended ;-)
const response = await fetch('configuration.json');
const { username } = response.json();
return username;
};
```
### `OpenAPI.PASSWORD`
Set the basic authentication password. See `OpenAPI.USERNAME` for more info.
```typescript
OpenAPI.PASSWORD = 'welcome123';
OpenAPI.PASSWORD = async () => {
// Note: loading this from a JSON file is not recommended ;-)
const response = await fetch('configuration.json');
const { password } = response.json();
return password;
};
```
### `OpenAPI.HEADERS`
This property allows you to specify additional headers to send for each request. This can be useful
for adding headers that are not generated through the spec. Or adding headers for tracking purposes.
This property also allows you to use an `async` method that will be resolved before requests are made.
```typescript
OpenAPI.HEADERS = {
'x-navigator': window.navigator.appVersion,
'x-environment': process.env,
'last-modified': 'Wed, 21 Oct 2015 07:28:00 GMT',
};
OpenAPI.HEADERS = async () => {
// Note: loading this from a JSON file is not recommended ;-)
const response = await fetch('configuration.json');
const { headers } = response.json();
return headers;
};
```
### `OpenAPI.ENCODE_PATH`
By default, all path parameters are encoded using the `encodeURI` method. This will convert invalid
URL characters, for example spaces, backslashes, etc. However, you might want to make the encoding
more strict due to security restrictions. So you can set this to `encodeURIComponent` to encode
most non-alphanumerical characters to percentage encoding. Or set a customer encoder that just
replaces some special characters.
```typescript
OpenAPI.ENCODE_PATH = encodeURIComponent;
OpenAPI.ENCODE_PATH = (value: string) => {
return value.replace(':', '_');
};
```

113
docs/runtime-schemas.md Normal file
View File

@ -0,0 +1,113 @@
# Runtime schemas
**Flag:** `--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:
```json
{
"MyModel": {
"required": [
"key",
"name"
],
"type": "object",
"properties": {
"key": {
"maxLength": 64,
"pattern": "^[a-zA-Z0-9_]*$",
"type": "string"
},
"name": {
"maxLength": 255,
"type": "string"
},
"enabled": {
"type": "boolean",
"readOnly": true
},
"modified": {
"type": "string",
"format": "date-time",
"readOnly": true
}
}
}
}
```
This will generate the following interface:
```typescript
export type MyModel = {
key: string;
name: string;
readonly enabled?: boolean;
readonly modified?: string;
}
```
The interface does not contain any properties like `maxLength` or `pattern`. However, they could be useful
if we wanted to create some form where a user could create such a model. In that form you would iterate
over the properties to render form fields based on their type and validate the input based on the `maxLength`
or `pattern` property. This requires us to have this information somewhere... For this we can use the
flag `--exportSchemas` to generate a runtime model next to the normal interface:
```typescript
export const $MyModel = {
properties: {
key: {
type: 'string',
isRequired: true,
maxLength: 64,
pattern: '^[a-zA-Z0-9_]*$',
},
name: {
type: 'string',
isRequired: true,
maxLength: 255,
},
enabled: {
type: 'boolean',
isReadOnly: true,
},
modified: {
type: 'string',
isReadOnly: true,
format: 'date-time',
},
},
} as const;
```
These runtime object are prefixed with a `$` character and expose all the interesting attributes of a model
and its properties. We can now use this object to generate the form:
```typescript jsx
import { $MyModel } from './generated';
// Some pseudo code to iterate over the properties and return a form field
// the form field could be some abstract component that renders the correct
// field type and validation rules based on the given input.
const formFields = Object.entries($MyModel.properties)
.map(([key, value]) => (
<FormField
name={key}
type={value.type}
format={value.format}
maxLength={value.maxLength}
pattern={value.pattern}
isReadOnly={value.isReadOnly}
/>
));
const MyForm = () => (
<form>
{formFields}
</form>
);
```

View File

@ -20,18 +20,20 @@ const config: Config.InitialOptions = {
'<rootDir>/test/e2e/v2.node.spec.ts',
'<rootDir>/test/e2e/v2.axios.spec.ts',
'<rootDir>/test/e2e/v2.babel.spec.ts',
'<rootDir>/test/e2e/v2.angular.spec.ts',
'<rootDir>/test/e2e/v3.fetch.spec.ts',
'<rootDir>/test/e2e/v3.xhr.spec.ts',
'<rootDir>/test/e2e/v3.node.spec.ts',
'<rootDir>/test/e2e/v3.axios.spec.ts',
'<rootDir>/test/e2e/v3.babel.spec.ts',
'<rootDir>/test/e2e/v3.angular.spec.ts',
'<rootDir>/test/e2e/client.fetch.spec.ts',
'<rootDir>/test/e2e/client.xhr.spec.ts',
'<rootDir>/test/e2e/client.node.spec.ts',
'<rootDir>/test/e2e/client.axios.spec.ts',
'<rootDir>/test/e2e/client.babel.spec.ts',
],
modulePathIgnorePatterns: ['<rootDir>/test/e2e/generated'],
modulePathIgnorePatterns: ['<rootDir>/test/e2e/generated'],
},
],
collectCoverageFrom: ['<rootDir>/src/**/*.ts', '!<rootDir>/src/**/*.d.ts', '!<rootDir>/bin', '!<rootDir>/dist'],

View File

@ -51,24 +51,30 @@
"test:update": "jest --selectProjects UNIT --updateSnapshot",
"test:watch": "jest --selectProjects UNIT --watch",
"test:coverage": "jest --selectProjects UNIT --coverage",
"test:e2e": "jest --selectProjects E2E --runInBand",
"test:e2e": "jest --selectProjects E2E --runInBand --verbose",
"eslint": "eslint .",
"eslint:fix": "eslint . --fix",
"prepublishOnly": "yarn run clean && yarn run release",
"codecov": "codecov --token=66c30c23-8954-4892-bef9-fbaed0a2e42b"
},
"dependencies": {
"@types/node-fetch": "^2.5.12",
"abort-controller": "^3.0.0",
"axios": "^0.25.0",
"camelcase": "^6.3.0",
"commander": "^8.3.0",
"form-data": "^4.0.0",
"handlebars": "^4.7.6",
"json-schema-ref-parser": "^9.0.7",
"node-fetch": "^2.6.6"
"json-schema-ref-parser": "^9.0.7"
},
"devDependencies": {
"@angular-devkit/build-angular": "13.1.4",
"@angular/animations": "13.1.3",
"@angular/cli": "13.1.4",
"@angular/common": "13.1.3",
"@angular/compiler": "13.1.3",
"@angular/compiler-cli": "13.1.3",
"@angular/core": "13.1.3",
"@angular/forms": "13.1.3",
"@angular/platform-browser": "13.1.3",
"@angular/platform-browser-dynamic": "13.1.3",
"@angular/router": "13.1.3",
"@babel/cli": "7.16.8",
"@babel/core": "7.16.12",
"@babel/preset-env": "7.16.11",
@ -76,22 +82,29 @@
"@rollup/plugin-commonjs": "21.0.1",
"@rollup/plugin-node-resolve": "13.1.3",
"@rollup/plugin-typescript": "8.3.0",
"@types/cross-spawn": "6.0.2",
"@types/express": "4.17.13",
"@types/glob": "7.2.0",
"@types/jest": "27.4.0",
"@types/node": "17.0.12",
"@types/node-fetch": "^2.5.12",
"@types/qs": "6.9.7",
"@typescript-eslint/eslint-plugin": "5.10.1",
"@typescript-eslint/parser": "5.10.1",
"abort-controller": "^3.0.0",
"axios": "^0.25.0",
"codecov": "3.8.3",
"cross-spawn": "7.0.3",
"eslint": "8.7.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-simple-import-sort": "7.0.0",
"express": "4.17.2",
"form-data": "^4.0.0",
"glob": "7.2.0",
"jest": "27.4.7",
"jest-cli": "27.4.7",
"node-fetch": "^2.6.6",
"prettier": "2.5.1",
"puppeteer": "13.1.2",
"qs": "6.10.3",
@ -99,8 +112,10 @@
"rollup": "2.66.1",
"rollup-plugin-node-externals": "3.1.2",
"rollup-plugin-terser": "7.0.2",
"rxjs": "7.5.2",
"ts-node": "10.4.0",
"tslib": "2.3.1",
"typescript": "4.5.5"
"typescript": "4.5.5",
"zone.js": "0.11.4"
}
}

View File

@ -3,4 +3,5 @@ export enum HttpClient {
XHR = 'xhr',
NODE = 'node',
AXIOS = 'axios',
ANGULAR = 'angular',
}

View File

@ -4,6 +4,7 @@ import { parse as parseV2 } from './openApi/v2';
import { parse as parseV3 } from './openApi/v3';
import { getOpenApiSpec } from './utils/getOpenApiSpec';
import { getOpenApiVersion, OpenApiVersion } from './utils/getOpenApiVersion';
import { isDefined } from './utils/isDefined';
import { isString } from './utils/isString';
import { postProcessClient } from './utils/postProcessClient';
import { registerHandlebarTemplates } from './utils/registerHandlebarTemplates';
@ -64,6 +65,10 @@ export const generate = async ({
request,
write = true,
}: Options): Promise<void> => {
if (httpClient === HttpClient.ANGULAR && isDefined(clientName)) {
throw new Error('Angular client does not support --name property');
}
const openApi = isString(input) ? await getOpenApiSpec(input) : input;
const openApiVersion = getOpenApiVersion(openApi);
const templates = registerHandlebarTemplates({

View File

@ -0,0 +1,44 @@
const getHeaders = (config: OpenAPIConfig, options: ApiRequestOptions): Observable<HttpHeaders> => {
return forkJoin({
token: resolve(options, config.TOKEN),
username: resolve(options, config.USERNAME),
password: resolve(options, config.PASSWORD),
additionalHeaders: resolve(options, config.HEADERS),
}).pipe(
map(({ token, username, password, additionalHeaders }) => {
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(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}`;
}
if (options.body) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
} else if (isBlob(options.body)) {
headers['Content-Type'] = options.body.type || 'application/octet-stream';
} else if (isString(options.body)) {
headers['Content-Type'] = 'text/plain';
} else if (!isFormData(options.body)) {
headers['Content-Type'] = 'application/json';
}
}
return new HttpHeaders(headers);
}),
);
};

View File

@ -0,0 +1,12 @@
const getRequestBody = (options: ApiRequestOptions): any => {
if (options.body) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);
}
}
return;
};

View File

@ -0,0 +1,6 @@
const getResponseBody = <T>(response: HttpResponse<T>): T | undefined => {
if (response.status !== 204 && response.body !== null) {
return response.body;
}
return;
};

View File

@ -0,0 +1,9 @@
const getResponseHeader = <T>(response: HttpResponse<T>, responseHeader?: string): string | undefined => {
if (responseHeader) {
const value = response.headers.get(responseHeader);
if (isString(value)) {
return value;
}
}
return;
};

View File

@ -0,0 +1,109 @@
{{>header}}
import { HttpClient, HttpHeaders } from '@angular/common/http';
import type { HttpResponse, HttpErrorResponse } from '@angular/common/http';
import { catchError, forkJoin, map, switchMap, of, throwError } from 'rxjs';
import type { Observable } from 'rxjs';
import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import type { OpenAPIConfig } from './OpenAPI';
{{>functions/isDefined}}
{{>functions/isString}}
{{>functions/isStringWithValue}}
{{>functions/isBlob}}
{{>functions/isFormData}}
{{>functions/base64}}
{{>functions/getQueryString}}
{{>functions/getUrl}}
{{>functions/getFormData}}
{{>functions/resolve}}
{{>angular/getHeaders}}
{{>angular/getRequestBody}}
{{>angular/sendRequest}}
{{>angular/getResponseHeader}}
{{>angular/getResponseBody}}
{{>functions/catchErrorCodes}}
/**
* Request method
* @param config The OpenAPI configuration object
* @param http The Angular HTTP client
* @param options The request options from the service
* @returns Observable<T>
* @throws ApiError
*/
export const request = <T>(config: OpenAPIConfig, http: HttpClient, options: ApiRequestOptions): Observable<T> => {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
return getHeaders(config, options).pipe(
switchMap(headers => {
return sendRequest<T>(config, options, http, url, formData, body, headers);
}),
map(response => {
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
return {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
} as ApiResult;
}),
catchError((error: HttpErrorResponse) => {
if (!error.status) {
return throwError(error);
}
return of({
url,
ok: error.ok,
status: error.status,
statusText: error.statusText,
body: error.error ?? error.statusText,
} as ApiResult);
}),
map(result => {
catchErrorCodes(options, result);
return result.body as T;
}),
catchError((error: ApiError) => {
return throwError(error);
}),
);
};

View File

@ -0,0 +1,16 @@
export const sendRequest = <T>(
config: OpenAPIConfig,
options: ApiRequestOptions,
http: HttpClient,
url: string,
body: any,
formData: FormData | undefined,
headers: HttpHeaders
): Observable<HttpResponse<T>> => {
return http.request<T>(options.method, url, {
headers,
body: body ?? formData,
withCredentials: config.WITH_CREDENTIALS,
observe: 'response',
});
};

View File

@ -55,7 +55,7 @@ import type { OpenAPIConfig } from './OpenAPI';
{{>axios/getResponseBody}}
{{>functions/catchErrors}}
{{>functions/catchErrorCodes}}
/**
@ -86,7 +86,7 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}

View File

@ -52,7 +52,7 @@ import type { OpenAPIConfig } from './OpenAPI';
{{>fetch/getResponseBody}}
{{>functions/catchErrors}}
{{>functions/catchErrorCodes}}
/**
@ -83,7 +83,7 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}

View File

@ -1,4 +1,4 @@
const catchErrors = (options: ApiRequestOptions, result: ApiResult): void => {
const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',

View File

@ -56,7 +56,7 @@ import type { OpenAPIConfig } from './OpenAPI';
{{>node/getResponseBody}}
{{>functions/catchErrors}}
{{>functions/catchErrorCodes}}
/**
@ -87,7 +87,7 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}

View File

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

View File

@ -55,7 +55,7 @@ import type { OpenAPIConfig } from './OpenAPI';
{{>xhr/getResponseBody}}
{{>functions/catchErrors}}
{{>functions/catchErrorCodes}}
/**
@ -86,7 +86,7 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}

View File

@ -1,12 +1,20 @@
{{>header}}
{{#equals @root.httpClient 'angular'}}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import type { Observable } from 'rxjs';
{{/equals}}
{{#if imports}}
{{#each imports}}
import type { {{{this}}} } from '../models/{{{this}}}';
{{/each}}
{{/if}}
{{#notEquals @root.httpClient 'angular'}}
import type { CancelablePromise } from '../core/CancelablePromise';
{{/notEquals}}
{{#if @root.exportClient}}
import type { BaseHttpRequest } from '../core/BaseHttpRequest';
{{else}}
@ -14,11 +22,18 @@ import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
{{/if}}
{{#equals @root.httpClient 'angular'}}
@Injectable()
{{/equals}}
export class {{{name}}}{{{@root.postfix}}} {
{{#if @root.exportClient}}
constructor(private readonly httpRequest: BaseHttpRequest) {}
{{/if}}
{{#equals @root.httpClient 'angular'}}
constructor(private readonly http: HttpClient) {}
{{/equals}}
{{#each operations}}
/**
@ -47,8 +62,13 @@ export class {{{name}}}{{{@root.postfix}}} {
public {{{name}}}({{>parameters}}): CancelablePromise<{{>result}}> {
return this.httpRequest.request({
{{else}}
{{#equals @root.httpClient 'angular'}}
public {{{name}}}({{>parameters}}): Observable<{{>result}}> {
return __request(OpenAPI, this.http, {
{{else}}
public static {{{name}}}({{>parameters}}): CancelablePromise<{{>result}}> {
return __request(OpenAPI, {
{{/equals}}
{{/if}}
method: '{{{method}}}',
url: '{{{path}}}',

View File

@ -2,6 +2,7 @@
{{~#equals @root.httpClient 'fetch'}}Blob{{/equals~}}
{{~#equals @root.httpClient 'xhr'}}Blob{{/equals~}}
{{~#equals @root.httpClient 'axios'}}Blob{{/equals~}}
{{~#equals @root.httpClient 'angular'}}Blob{{/equals~}}
{{~#equals @root.httpClient 'node'}}Blob{{/equals~}}
{{~else~}}
{{{base}}}

View File

@ -14,5 +14,7 @@ export const getHttpRequestName = (httpClient: HttpClient): string => {
return 'NodeHttpRequest';
case HttpClient.AXIOS:
return 'AxiosHttpRequest';
case HttpClient.ANGULAR:
return 'AngularHttpRequest';
}
};

View File

@ -2,6 +2,12 @@ import Handlebars from 'handlebars/runtime';
import { HttpClient } from '../HttpClient';
import templateClient from '../templates/client.hbs';
import angularGetHeaders from '../templates/core/angular/getHeaders.hbs';
import angularGetRequestBody from '../templates/core/angular/getRequestBody.hbs';
import angularGetResponseBody from '../templates/core/angular/getResponseBody.hbs';
import angularGetResponseHeader from '../templates/core/angular/getResponseHeader.hbs';
import angularRequest from '../templates/core/angular/request.hbs';
import angularSendRequest from '../templates/core/angular/sendRequest.hbs';
import templateCoreApiError from '../templates/core/ApiError.hbs';
import templateCoreApiRequestOptions from '../templates/core/ApiRequestOptions.hbs';
import templateCoreApiResult from '../templates/core/ApiResult.hbs';
@ -20,7 +26,7 @@ import fetchGetResponseHeader from '../templates/core/fetch/getResponseHeader.hb
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 functionCatchErrorCodes from '../templates/core/functions/catchErrorCodes.hbs';
import functionGetFormData from '../templates/core/functions/getFormData.hbs';
import functionGetQueryString from '../templates/core/functions/getQueryString.hbs';
import functionGetUrl from '../templates/core/functions/getUrl.hbs';
@ -161,7 +167,7 @@ export const registerHandlebarTemplates = (root: {
Handlebars.registerPartial('base', Handlebars.template(partialBase));
// Generic functions used in 'request' file @see src/templates/core/request.hbs for more info
Handlebars.registerPartial('functions/catchErrors', Handlebars.template(functionCatchErrors));
Handlebars.registerPartial('functions/catchErrorCodes', Handlebars.template(functionCatchErrorCodes));
Handlebars.registerPartial('functions/getFormData', Handlebars.template(functionGetFormData));
Handlebars.registerPartial('functions/getQueryString', Handlebars.template(functionGetQueryString));
Handlebars.registerPartial('functions/getUrl', Handlebars.template(functionGetUrl));
@ -206,5 +212,13 @@ export const registerHandlebarTemplates = (root: {
Handlebars.registerPartial('axios/sendRequest', Handlebars.template(axiosSendRequest));
Handlebars.registerPartial('axios/request', Handlebars.template(axiosRequest));
// Specific files for the angular client implementation
Handlebars.registerPartial('angular/getHeaders', Handlebars.template(angularGetHeaders));
Handlebars.registerPartial('angular/getRequestBody', Handlebars.template(angularGetRequestBody));
Handlebars.registerPartial('angular/getResponseBody', Handlebars.template(angularGetResponseBody));
Handlebars.registerPartial('angular/getResponseHeader', Handlebars.template(angularGetResponseHeader));
Handlebars.registerPartial('angular/sendRequest', Handlebars.template(angularSendRequest));
Handlebars.registerPartial('angular/request', Handlebars.template(angularRequest));
return templates;
};

View File

@ -469,7 +469,7 @@ const getResponseBody = async (response: Response): Promise<any> => {
return;
};
const catchErrors = (options: ApiRequestOptions, result: ApiResult): void => {
const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
@ -519,7 +519,7 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}
@ -3370,7 +3370,7 @@ const getResponseBody = async (response: Response): Promise<any> => {
return;
};
const catchErrors = (options: ApiRequestOptions, result: ApiResult): void => {
const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
@ -3420,7 +3420,7 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}

View File

@ -0,0 +1,91 @@
import { HttpClientModule } from '@angular/common/http';
import { Component, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { OpenAPI } from './core/OpenAPI';
import { CollectionFormatService } from './services/CollectionFormatService';
import { ComplexService } from './services/ComplexService';
import { DefaultService } from './services/DefaultService';
import { DefaultsService } from './services/DefaultsService';
import { DuplicateService } from './services/DuplicateService';
import { ErrorService } from './services/ErrorService';
import { HeaderService } from './services/HeaderService';
import { MultipleTags1Service } from './services/MultipleTags1Service';
import { MultipleTags2Service } from './services/MultipleTags2Service';
import { MultipleTags3Service } from './services/MultipleTags3Service';
import { NoContentService } from './services/NoContentService';
import { ParametersService } from './services/ParametersService';
import { ResponseService } from './services/ResponseService';
import { SimpleService } from './services/SimpleService';
import { TypesService } from './services/TypesService';
@Component({
selector: 'app-root',
template: `<div>Angular is ready</div>`,
})
export class AppComponent {
constructor(
private readonly collectionFormatService: CollectionFormatService,
private readonly complexService: ComplexService,
private readonly defaultService: DefaultService,
private readonly defaultsService: DefaultsService,
private readonly duplicateService: DuplicateService,
private readonly errorService: ErrorService,
private readonly headerService: HeaderService,
private readonly multipleTags1Service: MultipleTags1Service,
private readonly multipleTags2Service: MultipleTags2Service,
private readonly multipleTags3Service: MultipleTags3Service,
private readonly noContentService: NoContentService,
private readonly parametersService: ParametersService,
private readonly responseService: ResponseService,
private readonly simpleService: SimpleService,
private readonly typesService: TypesService
) {
(window as any).api = {
OpenAPI,
CollectionFormatService: collectionFormatService,
ComplexService: complexService,
DefaultService: defaultService,
DefaultsService: defaultsService,
DuplicateService: duplicateService,
ErrorService: errorService,
HeaderService: headerService,
MultipleTags1Service: multipleTags1Service,
MultipleTags2Service: multipleTags2Service,
MultipleTags3Service: multipleTags3Service,
NoContentService: noContentService,
ParametersService: parametersService,
ResponseService: responseService,
SimpleService: simpleService,
TypesService: typesService,
};
}
}
@NgModule({
imports: [BrowserModule, HttpClientModule],
providers: [
CollectionFormatService,
ComplexService,
DefaultService,
DefaultsService,
DuplicateService,
ErrorService,
HeaderService,
MultipleTags1Service,
MultipleTags2Service,
MultipleTags3Service,
NoContentService,
ParametersService,
ResponseService,
SimpleService,
TypesService,
],
bootstrap: [AppComponent],
})
export class AppModule {}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));

View File

@ -3,7 +3,7 @@ import { compileWithTypescript } from './scripts/compileWithTypescript';
import { generateClient } from './scripts/generateClient';
import server from './scripts/server';
describe('v3.node', () => {
describe('client.axios', () => {
beforeAll(async () => {
cleanup('client/axios');
await generateClient('client/axios', 'v3', 'axios', false, false, 'AppClient');
@ -83,4 +83,68 @@ describe('v3.node', () => {
}
expect(error).toContain('Request aborted');
});
it('should throw known error (500)', async () => {
let error;
try {
const { AppClient } = require('./generated/client/axios/index.js');
const client = new AppClient();
await client.error.testErrorCode(500);
} catch (e) {
const err = e as any;
error = JSON.stringify({
name: err.name,
message: err.message,
url: err.url,
status: err.status,
statusText: err.statusText,
body: err.body,
});
}
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Custom message: Internal Server Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: {
status: 500,
message: 'hello world',
},
})
);
});
it('should throw unknown error (409)', async () => {
let error;
try {
const { AppClient } = require('./generated/client/axios/index.js');
const client = new AppClient();
await client.error.testErrorCode(409);
} catch (e) {
const err = e as any;
error = JSON.stringify({
name: err.name,
message: err.message,
url: err.url,
status: err.status,
statusText: err.statusText,
body: err.body,
});
}
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Generic Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: {
status: 409,
message: 'hello world',
},
})
);
});
});

View File

@ -5,7 +5,7 @@ import { copyAsset } from './scripts/copyAsset';
import { generateClient } from './scripts/generateClient';
import server from './scripts/server';
describe('v3.babel', () => {
describe('client.babel', () => {
beforeAll(async () => {
cleanup('client/babel');
await generateClient('client/babel', 'v3', 'fetch', true, true, 'AppClient');
@ -53,13 +53,123 @@ describe('v3.babel', () => {
const { AppClient } = (window as any).api;
const client = new AppClient();
return await client.complex.complexTypes({
first: {
second: {
third: 'Hello World!',
parameterObject: {
first: {
second: {
third: 'Hello World!',
},
},
},
});
});
expect(result).toBeDefined();
});
it('support form data', async () => {
const result = await browser.evaluate(async () => {
const { AppClient } = (window as any).api;
const client = new AppClient();
return await client.parameters.callWithParameters({
parameterHeader: 'valueHeader',
parameterQuery: 'valueQuery',
parameterForm: 'valueForm',
parameterCookie: 'valueCookie',
parameterPath: 'valuePath',
requestBody: {
prop: 'valueBody',
},
});
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
let error;
try {
await browser.evaluate(async () => {
const { AppClient } = (window as any).api;
const client = new AppClient();
const promise = client.simple.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
});
} catch (e) {
error = (e as Error).message;
}
expect(error).toContain('CancelError: Request aborted');
});
it('should throw known error (500)', async () => {
const error = await browser.evaluate(async () => {
try {
const { AppClient } = (window as any).api;
const client = new AppClient();
await client.error.testErrorCode({
status: 500,
});
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Custom message: Internal Server Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: {
status: 500,
message: 'hello world',
},
})
);
});
it('should throw unknown error (409)', async () => {
const error = await browser.evaluate(async () => {
try {
const { AppClient } = (window as any).api;
const client = new AppClient();
await client.error.testErrorCode({
status: 409,
});
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Generic Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: {
status: 409,
message: 'hello world',
},
})
);
});
});

View File

@ -5,7 +5,7 @@ import { copyAsset } from './scripts/copyAsset';
import { generateClient } from './scripts/generateClient';
import server from './scripts/server';
describe('v3.fetch', () => {
describe('client.fetch', () => {
beforeAll(async () => {
cleanup('client/fetch');
await generateClient('client/fetch', 'v3', 'fetch', false, false, 'AppClient');
@ -126,7 +126,10 @@ describe('v3.fetch', () => {
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: 'Internal Server Error',
body: {
status: 500,
message: 'hello world',
},
})
);
});
@ -157,7 +160,10 @@ describe('v3.fetch', () => {
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: 'Conflict',
body: {
status: 409,
message: 'hello world',
},
})
);
});

View File

@ -3,7 +3,7 @@ import { compileWithTypescript } from './scripts/compileWithTypescript';
import { generateClient } from './scripts/generateClient';
import server from './scripts/server';
describe('v3.node', () => {
describe('client.node', () => {
beforeAll(async () => {
cleanup('client/node');
await generateClient('client/node', 'v3', 'node', false, false, 'AppClient');
@ -108,7 +108,10 @@ describe('v3.node', () => {
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: 'Internal Server Error',
body: {
status: 500,
message: 'hello world',
},
})
);
});
@ -137,7 +140,10 @@ describe('v3.node', () => {
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: 'Conflict',
body: {
status: 409,
message: 'hello world',
},
})
);
});

View File

@ -5,7 +5,7 @@ import { copyAsset } from './scripts/copyAsset';
import { generateClient } from './scripts/generateClient';
import server from './scripts/server';
describe('v3.xhr', () => {
describe('client.xhr', () => {
beforeAll(async () => {
cleanup('client/xhr');
await generateClient('client/xhr', 'v3', 'xhr', false, false, 'AppClient');
@ -63,6 +63,24 @@ describe('v3.xhr', () => {
expect(result).toBeDefined();
});
it('support form data', async () => {
const result = await browser.evaluate(async () => {
const { AppClient } = (window as any).api;
const client = new AppClient();
return await client.parameters.callWithParameters(
'valueHeader',
'valueQuery',
'valueForm',
'valueCookie',
'valuePath',
{
prop: 'valueBody',
}
);
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
let error;
try {
@ -107,7 +125,10 @@ describe('v3.xhr', () => {
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: 'Internal Server Error',
body: {
status: 500,
message: 'hello world',
},
})
);
});
@ -138,7 +159,10 @@ describe('v3.xhr', () => {
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: 'Conflict',
body: {
status: 409,
message: 'hello world',
},
})
);
});

View File

@ -0,0 +1,24 @@
import { sync } from 'cross-spawn';
import { resolve as resolvePath } from 'path';
export const buildAngularProject = (dir: string, name: string, output: string) => {
const cwd = `./test/e2e/generated/${dir}/${name}/`;
sync(
'ng',
[
'build',
'--output-path',
output,
'--optimization',
'false',
'--configuration',
'development',
'--source-map',
'false',
],
{
cwd: resolvePath(cwd),
stdio: 'inherit',
}
);
};

View File

@ -0,0 +1,43 @@
import { sync } from 'cross-spawn';
import { mkdirSync, rmSync } from 'fs';
import { resolve as resolvePath } from 'path';
export const createAngularProject = (dir: string, name: string) => {
const cwd = `./test/e2e/generated/${dir}/`;
mkdirSync(cwd, {
recursive: true,
});
sync(
'ng',
[
'new',
name,
'--minimal',
'true',
'--style',
'css',
'--inline-style',
'true',
'--inline-template',
'true',
'--routing',
'false',
'--skip-tests',
'true',
'--skip-install',
'true',
'--skip-git',
'true',
'--commit',
'false',
'--force',
],
{
cwd: resolvePath(cwd),
stdio: 'inherit',
}
);
rmSync(`${cwd}/${name}/src/app/`, {
recursive: true,
});
};

View File

@ -28,6 +28,25 @@ const start = async (dir: string) => {
})
);
// Serve static assets
_app.get('/runtime.js', (req, res) => {
res.sendFile(resolvePath(`./test/e2e/generated/${dir}/runtime.js`));
});
_app.get('/polyfills.js', (req, res) => {
res.sendFile(resolvePath(`./test/e2e/generated/${dir}/polyfills.js`));
});
_app.get('/vendor.js', (req, res) => {
res.sendFile(resolvePath(`./test/e2e/generated/${dir}/vendor.js`));
});
_app.get('/main.js', (req, res) => {
res.sendFile(resolvePath(`./test/e2e/generated/${dir}/main.js`));
});
_app.get('/styles.css', (req, res) => {
res.sendFile(resolvePath(`./test/e2e/generated/${dir}/styles.css`));
});
_app.get('/favicon.ico', (req, res) => {
res.sendFile(resolvePath(`./test/e2e/generated/${dir}/favicon.ico`));
});
_app.get('/', (req, res) => {
res.sendFile(resolvePath(`./test/e2e/generated/${dir}/index.html`));
});
@ -37,7 +56,10 @@ const start = async (dir: string) => {
// See the spec files for more information.
_app.all('/base/api/v1.0/error', (req, res) => {
const status = parseInt(String(req.query.status));
res.sendStatus(status);
res.status(status).json({
status,
message: 'hello world',
});
});
// Register an 'echo' server that just returns all data from the API calls.

View File

@ -0,0 +1,52 @@
import browser from './scripts/browser';
import { buildAngularProject } from './scripts/buildAngularProject';
import { cleanup } from './scripts/cleanup';
import { copyAsset } from './scripts/copyAsset';
import { createAngularProject } from './scripts/createAngularProject';
import { generateClient } from './scripts/generateClient';
import server from './scripts/server';
describe('v2.angular', () => {
beforeAll(async () => {
cleanup('v2/angular');
createAngularProject('v2/angular', 'app');
await generateClient('v2/angular/app/src', 'v2', 'angular');
copyAsset('main-angular.ts', 'v2/angular/app/src/main.ts');
buildAngularProject('v2/angular', 'app', 'dist');
await server.start('v2/angular/app/dist');
await browser.start();
}, 30000);
afterAll(async () => {
await browser.stop();
await server.stop();
});
it('requests token', async () => {
await browser.exposeFunction('tokenRequest', jest.fn().mockResolvedValue('MY_TOKEN'));
const result = await browser.evaluate(async () => {
return await new Promise<any>(resolve => {
const { OpenAPI, SimpleService } = (window as any).api;
OpenAPI.TOKEN = (window as any).tokenRequest;
SimpleService.getCallWithoutParametersAndResponse().subscribe(resolve);
});
});
expect(result.headers.authorization).toBe('Bearer MY_TOKEN');
});
it('supports complex params', async () => {
const result = await browser.evaluate(async () => {
return await new Promise<any>(resolve => {
const { ComplexService } = (window as any).api;
ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!',
},
},
}).subscribe(resolve);
});
});
expect(result).toBeDefined();
});
});

View File

@ -3,7 +3,7 @@ import { compileWithTypescript } from './scripts/compileWithTypescript';
import { generateClient } from './scripts/generateClient';
import server from './scripts/server';
describe('v2.node', () => {
describe('v2.axios', () => {
beforeAll(async () => {
cleanup('v2/axios');
await generateClient('v2/axios', 'v2', 'axios');
@ -35,19 +35,4 @@ describe('v2.node', () => {
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
let error;
try {
const { SimpleService } = require('./generated/v2/axios/index.js');
const promise = SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
} catch (e) {
error = (e as Error).message;
}
expect(error).toContain('Request aborted');
});
});

View File

@ -35,9 +35,11 @@ describe('v2.babel', () => {
const result = await browser.evaluate(async () => {
const { ComplexService } = (window as any).api;
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!',
parameterObject: {
first: {
second: {
third: 'Hello World!',
},
},
},
});

View File

@ -44,82 +44,4 @@ describe('v2.fetch', () => {
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
let error;
try {
await browser.evaluate(async () => {
const { SimpleService } = (window as any).api;
const promise = SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
});
} catch (e) {
error = (e as Error).message;
}
expect(error).toContain('CancelError: Request aborted');
});
it('should throw known error (500)', async () => {
const error = await browser.evaluate(async () => {
try {
const { ErrorService } = (window as any).api;
await ErrorService.testErrorCode(500);
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Custom message: Internal Server Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: 'Internal Server Error',
})
);
});
it('should throw unknown error (409)', async () => {
const error = await browser.evaluate(async () => {
try {
const { ErrorService } = (window as any).api;
await ErrorService.testErrorCode(409);
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Generic Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: 'Conflict',
})
);
});
});

View File

@ -50,60 +50,4 @@ describe('v2.node', () => {
}
expect(error).toContain('Request aborted');
});
it('should throw known error (500)', async () => {
let error;
try {
const { ErrorService } = require('./generated/v2/node/index.js');
await ErrorService.testErrorCode(500);
} catch (e) {
const err = e as any;
error = JSON.stringify({
name: err.name,
message: err.message,
url: err.url,
status: err.status,
statusText: err.statusText,
body: err.body,
});
}
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Custom message: Internal Server Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: 'Internal Server Error',
})
);
});
it('should throw unknown error (409)', async () => {
let error;
try {
const { ErrorService } = require('./generated/v2/node/index.js');
await ErrorService.testErrorCode(409);
} catch (e) {
const err = e as any;
error = JSON.stringify({
name: err.name,
message: err.message,
url: err.url,
status: err.status,
statusText: err.statusText,
body: err.body,
});
}
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Generic Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: 'Conflict',
})
);
});
});

View File

@ -44,82 +44,4 @@ describe('v2.xhr', () => {
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
let error;
try {
await browser.evaluate(async () => {
const { SimpleService } = (window as any).api;
const promise = SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
});
} catch (e) {
error = (e as Error).message;
}
expect(error).toContain('CancelError: Request aborted');
});
it('should throw known error (500)', async () => {
const error = await browser.evaluate(async () => {
try {
const { ErrorService } = (window as any).api;
await ErrorService.testErrorCode(500);
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Custom message: Internal Server Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: 'Internal Server Error',
})
);
});
it('should throw unknown error (409)', async () => {
const error = await browser.evaluate(async () => {
try {
const { ErrorService } = (window as any).api;
await ErrorService.testErrorCode(409);
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Generic Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: 'Conflict',
})
);
});
});

158
test/e2e/v3.angular.spec.ts Normal file
View File

@ -0,0 +1,158 @@
import browser from './scripts/browser';
import { buildAngularProject } from './scripts/buildAngularProject';
import { cleanup } from './scripts/cleanup';
import { copyAsset } from './scripts/copyAsset';
import { createAngularProject } from './scripts/createAngularProject';
import { generateClient } from './scripts/generateClient';
import server from './scripts/server';
describe('v3.angular', () => {
beforeAll(async () => {
cleanup('v3/angular');
createAngularProject('v3/angular', 'app');
await generateClient('v3/angular/app/src', 'v3', 'angular');
copyAsset('main-angular.ts', 'v3/angular/app/src/main.ts');
buildAngularProject('v3/angular', 'app', 'dist');
await server.start('v3/angular/app/dist');
await browser.start();
}, 30000);
afterAll(async () => {
await browser.stop();
await server.stop();
});
it('requests token', async () => {
await browser.exposeFunction('tokenRequest', jest.fn().mockResolvedValue('MY_TOKEN'));
const result = await browser.evaluate(async () => {
return await new Promise<any>(resolve => {
const { OpenAPI, SimpleService } = (window as any).api;
OpenAPI.TOKEN = (window as any).tokenRequest;
OpenAPI.USERNAME = undefined;
OpenAPI.PASSWORD = undefined;
SimpleService.getCallWithoutParametersAndResponse().subscribe(resolve);
});
});
expect(result.headers.authorization).toBe('Bearer MY_TOKEN');
});
it('uses credentials', async () => {
const result = await browser.evaluate(async () => {
return await new Promise<any>(resolve => {
const { OpenAPI, SimpleService } = (window as any).api;
OpenAPI.TOKEN = undefined;
OpenAPI.USERNAME = 'username';
OpenAPI.PASSWORD = 'password';
SimpleService.getCallWithoutParametersAndResponse().subscribe(resolve);
});
});
expect(result.headers.authorization).toBe('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
});
it('supports complex params', async () => {
const result = await browser.evaluate(async () => {
return await new Promise<any>(resolve => {
const { ComplexService } = (window as any).api;
ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!',
},
},
}).subscribe(resolve);
});
});
expect(result).toBeDefined();
});
it('support form data', async () => {
const result = await browser.evaluate(async () => {
return await new Promise<any>(resolve => {
const { ParametersService } = (window as any).api;
ParametersService.callWithParameters(
'valueHeader',
'valueQuery',
'valueForm',
'valueCookie',
'valuePath',
{
prop: 'valueBody',
}
).subscribe(resolve);
});
});
expect(result).toBeDefined();
});
it('should throw known error (500)', async () => {
const error = await browser.evaluate(async () => {
try {
await new Promise<any>((resolve, reject) => {
const { ErrorService } = (window as any).api;
ErrorService.testErrorCode(500).subscribe(resolve, reject);
});
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Custom message: Internal Server Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: {
status: 500,
message: 'hello world',
},
})
);
});
it('should throw unknown error (409)', async () => {
const error = await browser.evaluate(async () => {
const { ErrorService } = (window as any).api;
ErrorService.testErrorCode(409).subscribe(console.log, console.log);
try {
await new Promise<any>((resolve, reject) => {
// const { ErrorService } = (window as any).api;
ErrorService.testErrorCode(409).subscribe(resolve, reject);
});
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Generic Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: {
status: 409,
message: 'hello world',
},
})
);
});
});

View File

@ -3,7 +3,7 @@ import { compileWithTypescript } from './scripts/compileWithTypescript';
import { generateClient } from './scripts/generateClient';
import server from './scripts/server';
describe('v3.node', () => {
describe('v3.axios', () => {
beforeAll(async () => {
cleanup('v3/axios');
await generateClient('v3/axios', 'v3', 'axios');
@ -76,4 +76,66 @@ describe('v3.node', () => {
}
expect(error).toContain('Request aborted');
});
it('should throw known error (500)', async () => {
let error;
try {
const { ErrorService } = require('./generated/v3/axios/index.js');
await ErrorService.testErrorCode(500);
} catch (e) {
const err = e as any;
error = JSON.stringify({
name: err.name,
message: err.message,
url: err.url,
status: err.status,
statusText: err.statusText,
body: err.body,
});
}
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Custom message: Internal Server Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: {
status: 500,
message: 'hello world',
},
})
);
});
it('should throw unknown error (409)', async () => {
let error;
try {
const { ErrorService } = require('./generated/v3/axios/index.js');
await ErrorService.testErrorCode(409);
} catch (e) {
const err = e as any;
error = JSON.stringify({
name: err.name,
message: err.message,
url: err.url,
status: err.status,
statusText: err.statusText,
body: err.body,
});
}
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Generic Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: {
status: 409,
message: 'hello world',
},
})
);
});
});

View File

@ -48,13 +48,120 @@ describe('v3.babel', () => {
const result = await browser.evaluate(async () => {
const { ComplexService } = (window as any).api;
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!',
parameterObject: {
first: {
second: {
third: 'Hello World!',
},
},
},
});
});
expect(result).toBeDefined();
});
it('support form data', async () => {
const result = await browser.evaluate(async () => {
const { ParametersService } = (window as any).api;
return await ParametersService.callWithParameters({
parameterHeader: 'valueHeader',
parameterQuery: 'valueQuery',
parameterForm: 'valueForm',
parameterCookie: 'valueCookie',
parameterPath: 'valuePath',
requestBody: {
prop: 'valueBody',
},
});
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
let error;
try {
await browser.evaluate(async () => {
const { SimpleService } = (window as any).api;
const promise = SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
});
} catch (e) {
error = (e as Error).message;
}
expect(error).toContain('CancelError: Request aborted');
});
it('should throw known error (500)', async () => {
const error = await browser.evaluate(async () => {
try {
const { ErrorService } = (window as any).api;
await ErrorService.testErrorCode({
status: 500,
});
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Custom message: Internal Server Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: {
status: 500,
message: 'hello world',
},
})
);
});
it('should throw unknown error (409)', async () => {
const error = await browser.evaluate(async () => {
try {
const { ErrorService } = (window as any).api;
await ErrorService.testErrorCode({
status: 409,
});
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Generic Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: {
status: 409,
message: 'hello world',
},
})
);
});
});

View File

@ -118,7 +118,10 @@ describe('v3.fetch', () => {
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: 'Internal Server Error',
body: {
status: 500,
message: 'hello world',
},
})
);
});
@ -148,7 +151,10 @@ describe('v3.fetch', () => {
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: 'Conflict',
body: {
status: 409,
message: 'hello world',
},
})
);
});

View File

@ -100,7 +100,10 @@ describe('v3.node', () => {
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: 'Internal Server Error',
body: {
status: 500,
message: 'hello world',
},
})
);
});
@ -128,7 +131,10 @@ describe('v3.node', () => {
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: 'Conflict',
body: {
status: 409,
message: 'hello world',
},
})
);
});

View File

@ -58,6 +58,23 @@ describe('v3.xhr', () => {
expect(result).toBeDefined();
});
it('support form data', async () => {
const result = await browser.evaluate(async () => {
const { ParametersService } = (window as any).api;
return await ParametersService.callWithParameters(
'valueHeader',
'valueQuery',
'valueForm',
'valueCookie',
'valuePath',
{
prop: 'valueBody',
}
);
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
let error;
try {
@ -100,7 +117,10 @@ describe('v3.xhr', () => {
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: 'Internal Server Error',
body: {
status: 500,
message: 'hello world',
},
})
);
});
@ -130,7 +150,10 @@ describe('v3.xhr', () => {
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: 'Conflict',
body: {
status: 409,
message: 'hello world',
},
})
);
});

View File

@ -14,7 +14,8 @@
"noImplicitAny": true,
"strict": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true
},
"files": [

3
types/index.d.ts vendored
View File

@ -3,6 +3,7 @@ export declare enum HttpClient {
XHR = 'xhr',
NODE = 'node',
AXIOS = 'axios',
ANGULAR = 'angular',
}
export declare enum Indent {
@ -14,7 +15,7 @@ export declare enum Indent {
export type Options = {
input: string | Record<string, any>;
output: string;
httpClient?: HttpClient | 'fetch' | 'xhr' | 'node' | 'axios';
httpClient?: HttpClient | 'fetch' | 'xhr' | 'node' | 'axios' | 'angular';
clientName?: string;
useOptions?: boolean;
useUnionTypes?: boolean;

3577
yarn.lock

File diff suppressed because it is too large Load Diff