Skip to content

Feature/pe 2152 #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ test/e2e/generated
samples/generated
samples/swagger-codegen-cli-v2.jar
samples/swagger-codegen-cli-v3.jar
example
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ $ openapi --help
--exportServices <value> Write services to disk (default: true)
--exportModels <value> Write models to disk (default: true)
--exportSchemas <value> Write schemas to disk (default: false)
--exportClient <value> Generate and write client class to disk (default: false)
--name <value> Custom client class name (default: "AppClient")

Examples
$ openapi --input ./spec.json
Expand Down Expand Up @@ -392,6 +394,29 @@ const getToken = async () => {
OpenAPI.TOKEN = getToken;
```

### Generate client instance with `--exportClient` option

The OpenAPI generator allows to create 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, use `--exportClient` option. To set a custom name to the client class, use `--name` option.

```
openapi --input ./spec.json --output ./dist --exportClient true --name DemoAppClient
```

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 DemoAppClient({ BASE: 'http://server-host.com', TOKEN: '1234' });

// use the client instance to make the API call
const res: OrganizationResponse = await appClient.organizations.createOrganization({
name: 'OrgName',
description: 'OrgDescription',
});
```

### References

Local references to schema definitions (those beginning with `#/definitions/schemas/`)
Expand Down
4 changes: 4 additions & 0 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ const params = program
.option('--exportServices <value>', 'Write services to disk', true)
.option('--exportModels <value>', 'Write models to disk', true)
.option('--exportSchemas <value>', 'Write schemas to disk', false)
.option('--exportClient <value>', 'Generate and write client class to disk', false)
.option('--request <value>', 'Path to custom request file')
.option('--name <value>', 'Custom client class name', 'AppClient')
.parse(process.argv)
.opts();

Expand All @@ -36,6 +38,8 @@ if (OpenAPI) {
exportServices: JSON.parse(params.exportServices) === true,
exportModels: JSON.parse(params.exportModels) === true,
exportSchemas: JSON.parse(params.exportSchemas) === true,
exportClient: JSON.parse(params.exportClient) === true,
clientName: params.name,
request: params.request,
})
.then(() => {
Expand Down
12 changes: 2 additions & 10 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ module.exports = {
{
displayName: 'UNIT',
testEnvironment: 'node',
testMatch: [
'<rootDir>/src/**/*.spec.ts',
'<rootDir>/test/index.spec.js',
],
testMatch: ['<rootDir>/src/**/*.spec.ts', '<rootDir>/test/index.spec.js', '<rootDir>/test/index.client.spec.js'],
moduleFileExtensions: ['js', 'ts', 'd.ts'],
moduleNameMapper: {
'\\.hbs$': '<rootDir>/src/templates/__mocks__/index.js',
Expand All @@ -29,10 +26,5 @@ module.exports = {
],
},
],
collectCoverageFrom: [
'<rootDir>/src/**/*.ts',
'!<rootDir>/src/**/*.d.ts',
'!<rootDir>/bin',
'!<rootDir>/dist',
],
collectCoverageFrom: ['<rootDir>/src/**/*.ts', '!<rootDir>/src/**/*.d.ts', '!<rootDir>/bin', '!<rootDir>/dist'],
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openapi-typescript-codegen",
"version": "0.9.3",
"name": "@parsable/openapi-typescript-codegen",
"version": "0.0.1-alpha-2",
"description": "Library that generates Typescript clients based on the OpenAPI specification.",
"author": "Ferdi Koomen",
"homepage": "https://github.com/ferdikoomen/openapi-typescript-codegen",
Expand Down
14 changes: 10 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export type Options = {
exportServices?: boolean;
exportModels?: boolean;
exportSchemas?: boolean;
exportClient?: boolean;
request?: string;
clientName?: string;
write?: boolean;
};

Expand All @@ -37,6 +39,8 @@ export type Options = {
* @param exportServices: Generate services
* @param exportModels: Generate models
* @param exportSchemas: Generate schemas
* @param exportClient: Generate client class
* @param clientName: Custom client class name
* @param request: Path to custom request file
* @param write Write the files to disk (true or false)
*/
Expand All @@ -50,6 +54,8 @@ export async function generate({
exportServices = true,
exportModels = true,
exportSchemas = false,
exportClient = false,
clientName = 'AppClient',
request,
write = true,
}: Options): Promise<void> {
Expand All @@ -64,17 +70,17 @@ export async function generate({
switch (openApiVersion) {
case OpenApiVersion.V2: {
const client = parseV2(openApi);
const clientFinal = postProcessClient(client);
const clientFinal = postProcessClient(client, exportClient);
if (!write) break;
await writeClient(clientFinal, templates, output, httpClient, useOptions, useUnionTypes, exportCore, exportServices, exportModels, exportSchemas, request);
await writeClient(clientFinal, templates, output, httpClient, useOptions, useUnionTypes, exportCore, exportServices, exportModels, exportSchemas, exportClient, clientName, request);
break;
}

case OpenApiVersion.V3: {
const client = parseV3(openApi);
const clientFinal = postProcessClient(client);
const clientFinal = postProcessClient(client, exportClient);
if (!write) break;
await writeClient(clientFinal, templates, output, httpClient, useOptions, useUnionTypes, exportCore, exportServices, exportModels, exportSchemas, request);
await writeClient(clientFinal, templates, output, httpClient, useOptions, useUnionTypes, exportCore, exportServices, exportModels, exportSchemas, exportClient, clientName, request);
break;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/openApi/v2/parser/getOperationPath.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { getOperationPath } from './getOperationPath';

describe('getOperationPath', () => {
it('should produce correct result', () => {
expect(getOperationPath('/api/v{api-version}/list/{id}/{type}')).toEqual('/api/v${OpenAPI.VERSION}/list/${id}/${type}');
expect(getOperationPath('/api/v{api-version}/list/{id}')).toEqual('/api/v${OpenAPI.VERSION}/list/${id}');
expect(getOperationPath('/api/v{api-version}/list/{id}/{type}')).toEqual('/api/v${apiVersion}/list/${id}/${type}');
expect(getOperationPath('/api/v{api-version}/list/{id}')).toEqual('/api/v${apiVersion}/list/${id}');
expect(getOperationPath('/api/v1/list/{id}')).toEqual('/api/v1/list/${id}');
expect(getOperationPath('/api/{foobar}')).toEqual('/api/${foobar}');
expect(getOperationPath('/api/{fooBar}')).toEqual('/api/${fooBar}');
Expand Down
8 changes: 3 additions & 5 deletions src/openApi/v2/parser/getOperationPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import { getOperationParameterName } from './getOperationParameterName';
* @param path
*/
export function getOperationPath(path: string): string {
return path
.replace(/\{(.*?)\}/g, (_, w: string) => {
return `\${${getOperationParameterName(w)}}`;
})
.replace('${apiVersion}', '${OpenAPI.VERSION}');
return path.replace(/\{(.*?)\}/g, (_, w: string) => {
return `\${${getOperationParameterName(w)}}`;
});
}
4 changes: 2 additions & 2 deletions src/openApi/v3/parser/getOperationPath.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { getOperationPath } from './getOperationPath';

describe('getOperationPath', () => {
it('should produce correct result', () => {
expect(getOperationPath('/api/v{api-version}/list/{id}/{type}')).toEqual('/api/v${OpenAPI.VERSION}/list/${id}/${type}');
expect(getOperationPath('/api/v{api-version}/list/{id}')).toEqual('/api/v${OpenAPI.VERSION}/list/${id}');
expect(getOperationPath('/api/v{api-version}/list/{id}/{type}')).toEqual('/api/v${apiVersion}/list/${id}/${type}');
expect(getOperationPath('/api/v{api-version}/list/{id}')).toEqual('/api/v${apiVersion}/list/${id}');
expect(getOperationPath('/api/v1/list/{id}')).toEqual('/api/v1/list/${id}');
expect(getOperationPath('/api/{foobar}')).toEqual('/api/${foobar}');
expect(getOperationPath('/api/{fooBar}')).toEqual('/api/${fooBar}');
Expand Down
8 changes: 3 additions & 5 deletions src/openApi/v3/parser/getOperationPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import { getOperationParameterName } from './getOperationParameterName';
* @param path
*/
export function getOperationPath(path: string): string {
return path
.replace(/\{(.*?)\}/g, (_, w: string) => {
return `\${${getOperationParameterName(w)}}`;
})
.replace('${apiVersion}', '${OpenAPI.VERSION}');
return path.replace(/\{(.*?)\}/g, (_, w: string) => {
return `\${${getOperationParameterName(w)}}`;
});
}
17 changes: 17 additions & 0 deletions src/templates/core/BaseHttpRequest.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{{>header}}

import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import type { OpenAPIConfig } from './OpenAPI';

export class BaseHttpRequest {
readonly openApiConfig: OpenAPIConfig;

constructor(openApiConfig: OpenAPIConfig) {
this.openApiConfig = openApiConfig;
}

async request(options: ApiRequestOptions): Promise<ApiResult> {
throw new Error('Not Implemented');
}
}
13 changes: 7 additions & 6 deletions src/templates/core/OpenAPI.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ import type { ApiRequestOptions } from './ApiRequestOptions';
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;

type Config = {
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
export type OpenAPIConfig = {
BASE?: string;
VERSION?: string;
WITH_CREDENTIALS?: boolean;
TOKEN?: string | Resolver<string>;
USERNAME?: string | Resolver<string>;
PASSWORD?: string | Resolver<string>;
HEADERS?: Headers | Resolver<Headers>;
}

export const OpenAPI: Config = {
{{#unless @root.exportClient}}
export const OpenAPI: OpenAPIConfig = {
BASE: '{{{server}}}',
VERSION: '{{{version}}}',
WITH_CREDENTIALS: false,
Expand All @@ -24,3 +24,4 @@ export const OpenAPI: Config = {
PASSWORD: undefined,
HEADERS: undefined,
};
{{/unless}}
10 changes: 5 additions & 5 deletions src/templates/core/fetch/getHeaders.hbs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
const token = await resolve(options, OpenAPI.TOKEN);
const username = await resolve(options, OpenAPI.USERNAME);
const password = await resolve(options, OpenAPI.PASSWORD);
const defaultHeaders = await resolve(options, OpenAPI.HEADERS);
async function getHeaders(options: ApiRequestOptions, config: OpenAPIConfig): Promise<Headers> {
const token = await resolve(options, config.TOKEN);
const username = await resolve(options, config.USERNAME);
const password = await resolve(options, config.PASSWORD);
const defaultHeaders = await resolve(options, config.HEADERS);

const headers = new Headers({
Accept: 'application/json',
Expand Down
41 changes: 39 additions & 2 deletions src/templates/core/fetch/request.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import type { OpenAPIConfig } from './OpenAPI';
{{#if @root.exportClient}}
import { BaseHttpRequest } from './BaseHttpRequest';
{{else}}
import { OpenAPI } from './OpenAPI';
{{/if}}

{{>functions/isDefined}}

Expand Down Expand Up @@ -47,15 +52,46 @@ import { OpenAPI } from './OpenAPI';
{{>functions/catchErrors}}


{{#if @root.exportClient}}
export class FetchHttpRequest extends BaseHttpRequest {
constructor(openApiConfig: OpenAPIConfig) {
super(openApiConfig);
}

/**
* Request using fetch client
* @param options The request options from the the service
* @returns ApiResult
* @throws ApiError
*/
async request(options: ApiRequestOptions): Promise<ApiResult> {
const url = getUrl(options, this.openApiConfig);
const response = await sendRequest(options, this.openApiConfig, url);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);

const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};

catchErrors(options, result);
return result;
}
}
{{else}}
/**
* Request using fetch client
* @param options The request options from the the service
* @returns ApiResult
* @throws ApiError
*/
export async function request(options: ApiRequestOptions): Promise<ApiResult> {
const url = getUrl(options);
const response = await sendRequest(options, url);
const url = getUrl(options, OpenAPI);
const response = await sendRequest(options, OpenAPI, url);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);

Expand All @@ -70,3 +106,4 @@ export async function request(options: ApiRequestOptions): Promise<ApiResult> {
catchErrors(options, result);
return result;
}
{{/if}}
6 changes: 3 additions & 3 deletions src/templates/core/fetch/sendRequest.hbs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
async function sendRequest(options: ApiRequestOptions, url: string): Promise<Response> {
async function sendRequest(options: ApiRequestOptions, config: OpenAPIConfig, url: string): Promise<Response> {
const request: RequestInit = {
method: options.method,
headers: await getHeaders(options),
headers: await getHeaders(options, config),
body: getRequestBody(options),
};
if (OpenAPI.WITH_CREDENTIALS) {
if (config.WITH_CREDENTIALS) {
request.credentials = 'include';
}
return await fetch(url, request);
Expand Down
4 changes: 2 additions & 2 deletions src/templates/core/functions/getUrl.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
function getUrl(options: ApiRequestOptions): string {
function getUrl(options: ApiRequestOptions, config: OpenAPIConfig): string {
const path = options.path.replace(/[:]/g, '_');
const url = `${OpenAPI.BASE}${path}`;
const url = `${config.BASE}${path}`;

if (options.query) {
return `${url}${getQueryString(options.query)}`;
Expand Down
10 changes: 5 additions & 5 deletions src/templates/core/node/getHeaders.hbs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
const token = await resolve(options, OpenAPI.TOKEN);
const username = await resolve(options, OpenAPI.USERNAME);
const password = await resolve(options, OpenAPI.PASSWORD);
const defaultHeaders = await resolve(options, OpenAPI.HEADERS);
async function getHeaders(options: ApiRequestOptions, config: OpenAPIConfig): Promise<Headers> {
const token = await resolve(options, config.TOKEN);
const username = await resolve(options, config.USERNAME);
const password = await resolve(options, config.PASSWORD);
const defaultHeaders = await resolve(options, config.HEADERS);

const headers = new Headers({
Accept: 'application/json',
Expand Down
Loading