Skip to content

Commit

Permalink
Merge pull request #19 from nicolas-chaulet/refactor/options
Browse files Browse the repository at this point in the history
refactor(parser): start passing options object around instead of positional parameters
  • Loading branch information
mrlubos authored Feb 20, 2024
2 parents 8b1a42c + cc86849 commit ab12c4f
Show file tree
Hide file tree
Showing 18 changed files with 192 additions and 157 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Mainly, it's because the original project maintainer [doesn't have time](https:/
- ability to select which services to export and naming strategies for generated methods
- support for non-ASCII characters
- support for x-body-name header (compatible with Connexion v3.x)
- ability to autoformat output with Prettier

# OpenAPI Typescript Codegen

Expand Down
20 changes: 10 additions & 10 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ const params = program
.option('--exportCore <value>', 'Write core files to disk', true)
.option('--exportServices <value>', 'Write services to disk', true)
.option('--exportModels <value>', 'Write models to disk', true)
.option('--useOperationId <value>', 'Use operation id to generate operation names', true)
.option('--exportSchemas <value>', 'Write schemas to disk', false)
.option('--indent <value>', 'Indentation options [4, 2, tabs]', '4')
.option('--postfixServices <value>', 'Service name postfix', 'Service')
.option('--useOperationId <value>', 'Use operation id to generate operation names', true)
.option('--postfixModels <value>', 'Model name postfix')
.option('--request <value>', 'Path to custom request file')
.parse(process.argv)
Expand All @@ -41,22 +41,22 @@ const parseBooleanOrString = value => {

if (OpenAPI) {
OpenAPI.generate({
input: params.input,
output: params.output,
httpClient: params.client,
clientName: params.name,
useOptions: params.useOptions,
useUnionTypes: params.useUnionTypes,
autoformat: JSON.parse(params.autoformat) === true,
clientName: params.name,
exportCore: JSON.parse(params.exportCore) === true,
exportServices: parseBooleanOrString(params.exportServices),
exportModels: parseBooleanOrString(params.exportModels),
exportSchemas: JSON.parse(params.exportSchemas) === true,
useOperationId: JSON.parse(params.useOperationId) === true,
exportServices: parseBooleanOrString(params.exportServices),
httpClient: params.client,
indent: params.indent,
postfixServices: params.postfixServices,
input: params.input,
output: params.output,
postfixModels: params.postfixModels,
postfixServices: params.postfixServices,
request: params.request,
useOperationId: JSON.parse(params.useOperationId) === true,
useOptions: params.useOptions,
useUnionTypes: params.useUnionTypes,
})
.then(() => {
process.exit(0);
Expand Down
19 changes: 19 additions & 0 deletions src/client/interfaces/Options.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface Options {
autoformat?: boolean;
clientName?: string;
exportCore?: boolean;
exportModels?: boolean | string;
exportSchemas?: boolean;
exportServices?: boolean | string;
httpClient?: HttpClient;
indent?: Indent;
input: string | Record<string, any>;
output: string;
postfixModels?: string;
postfixServices?: string;
request?: string;
useOperationId?: boolean;
useOptions?: boolean;
useUnionTypes?: boolean;
write?: boolean;
}
64 changes: 19 additions & 45 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Options } from './client/interfaces/Options';
import { HttpClient } from './HttpClient';
import { Indent } from './Indent';
import { parse as parseV2 } from './openApi/v2';
Expand All @@ -12,26 +13,6 @@ import { writeClient } from './utils/writeClient';
export { HttpClient } from './HttpClient';
export { Indent } from './Indent';

export type Options = {
input: string | Record<string, any>;
output: string;
httpClient?: HttpClient;
clientName?: string;
useOptions?: boolean;
useUnionTypes?: boolean;
autoformat?: boolean;
exportCore?: boolean;
exportServices?: boolean | string;
exportModels?: boolean | string;
exportSchemas?: boolean;
useOperationId?: boolean;
indent?: Indent;
postfixServices?: string;
postfixModels?: string;
request?: string;
write?: boolean;
};

/**
* Generate the OpenAPI client. This method will read the OpenAPI specification and based on the
* given language it will generate the client, including the typed models, validation schemas,
Expand All @@ -54,26 +35,21 @@ export type Options = {
* @param request Path to custom request file
* @param write Write the files to disk (true or false)
*/
export const generate = async ({
input,
output,
httpClient = HttpClient.FETCH,
clientName,
useOptions = false,
useUnionTypes = false,
autoformat = false,
exportCore = true,
exportServices = true,
exportModels = true,
exportSchemas = false,
useOperationId = true,
indent = Indent.SPACE_4,
postfixServices = 'Service',
postfixModels = '',
request,
write = true,
}: Options): Promise<void> => {
const openApi = isString(input) ? await getOpenApiSpec(input) : input;
export const generate = async (options: Options): Promise<void> => {
const {
httpClient = HttpClient.FETCH,
useOptions = false,
useUnionTypes = false,
exportCore = true,
exportServices = true,
exportModels = true,
exportSchemas = false,
indent = Indent.SPACE_4,
postfixServices = 'Service',
postfixModels = '',
write = true,
} = options;
const openApi = isString(options.input) ? await getOpenApiSpec(options.input) : options.input;
const openApiVersion = getOpenApiVersion(openApi);
const templates = registerHandlebarTemplates({
httpClient,
Expand All @@ -96,26 +72,24 @@ export const generate = async ({
}

if (parser) {
const client = parser(openApi, useOperationId);
const client = parser(openApi, options);
const clientFinal = postProcessClient(client);
if (write) {
await writeClient(
clientFinal,
templates,
output,
options.output,
httpClient,
useOptions,
useUnionTypes,
autoformat,
exportCore,
exportServices,
exportModels,
exportSchemas,
indent,
postfixServices,
postfixModels,
clientName,
request
options
);
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/openApi/v2/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Client } from '../../client/interfaces/Client';
import type { Options } from '../../client/interfaces/Options';
import type { OpenApi } from './interfaces/OpenApi';
import { getModels } from './parser/getModels';
import { getServer } from './parser/getServer';
Expand All @@ -9,13 +10,13 @@ import { getServiceVersion } from './parser/getServiceVersion';
* Parse the OpenAPI specification to a Client model that contains
* all the models, services and schema's we should output.
* @param openApi The OpenAPI spec that we have loaded from disk.
* @param useOperationId should the operationId be used when generating operation names
* @param options Options passed to the generate method
*/
export const parse = (openApi: OpenApi, useOperationId: boolean): Client => {
export const parse = (openApi: OpenApi, options: Options): Client => {
const version = getServiceVersion(openApi.info.version);
const server = getServer(openApi);
const models = getModels(openApi);
const services = getServices(openApi, useOperationId);
const services = getServices(openApi, options);

return { version, server, models, services };
};
5 changes: 3 additions & 2 deletions src/openApi/v2/parser/getOperation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Operation } from '../../../client/interfaces/Operation';
import type { OperationParameters } from '../../../client/interfaces/OperationParameters';
import type { Options } from '../../../client/interfaces/Options';
import type { OpenApi } from '../interfaces/OpenApi';
import type { OpenApiOperation } from '../interfaces/OpenApiOperation';
import { getOperationErrors } from './getOperationErrors';
Expand All @@ -18,10 +19,10 @@ export const getOperation = (
tag: string,
op: OpenApiOperation,
pathParams: OperationParameters,
useOperationId: boolean
options: Options
): Operation => {
const serviceName = getServiceName(tag);
const operationName = getOperationName(url, method, useOperationId, op.operationId);
const operationName = getOperationName(url, method, options, op.operationId);

// Create a new operation object for this method.
const operation: Operation = {
Expand Down
64 changes: 41 additions & 23 deletions src/openApi/v2/parser/getOperationName.spec.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@
import type { Options } from '../../../client/interfaces/Options';
import { getOperationName } from './getOperationName';

describe('getOperationName', () => {
it('should produce correct result', () => {
expect(getOperationName('/api/v{api-version}/users', 'GET', true, 'GetAllUsers')).toEqual('getAllUsers');
expect(getOperationName('/api/v{api-version}/users', 'GET', true, undefined)).toEqual('getApiUsers');
expect(getOperationName('/api/v{api-version}/users', 'POST', true, undefined)).toEqual('postApiUsers');
expect(getOperationName('/api/v1/users', 'GET', true, 'GetAllUsers')).toEqual('getAllUsers');
expect(getOperationName('/api/v1/users', 'GET', true, undefined)).toEqual('getApiV1Users');
expect(getOperationName('/api/v1/users', 'POST', true, undefined)).toEqual('postApiV1Users');
expect(getOperationName('/api/v1/users/{id}', 'GET', true, undefined)).toEqual('getApiV1UsersById');
expect(getOperationName('/api/v1/users/{id}', 'POST', true, undefined)).toEqual('postApiV1UsersById');
const options: Options = {
input: '',
output: '',
};
expect(getOperationName('/api/v{api-version}/users', 'GET', options, 'GetAllUsers')).toEqual('getAllUsers');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, undefined)).toEqual('getApiUsers');
expect(getOperationName('/api/v{api-version}/users', 'POST', options, undefined)).toEqual('postApiUsers');
expect(getOperationName('/api/v1/users', 'GET', options, 'GetAllUsers')).toEqual('getAllUsers');
expect(getOperationName('/api/v1/users', 'GET', options, undefined)).toEqual('getApiV1Users');
expect(getOperationName('/api/v1/users', 'POST', options, undefined)).toEqual('postApiV1Users');
expect(getOperationName('/api/v1/users/{id}', 'GET', options, undefined)).toEqual('getApiV1UsersById');
expect(getOperationName('/api/v1/users/{id}', 'POST', options, undefined)).toEqual('postApiV1UsersById');

expect(getOperationName('/api/v{api-version}/users', 'GET', true, 'fooBar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', true, 'FooBar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', true, 'Foo Bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', true, 'foo bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', true, 'foo-bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', true, 'foo_bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', true, 'foo.bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', true, '@foo.bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', true, '$foo.bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', true, '_foo.bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', true, '-foo.bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', true, '123.foo.bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, 'fooBar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, 'FooBar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, 'Foo Bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, 'foo bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, 'foo-bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, 'foo_bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, 'foo.bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, '@foo.bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, '$foo.bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, '_foo.bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, '-foo.bar')).toEqual('fooBar');
expect(getOperationName('/api/v{api-version}/users', 'GET', options, '123.foo.bar')).toEqual('fooBar');

expect(getOperationName('/api/v1/users', 'GET', false, 'GetAllUsers')).toEqual('getApiV1Users');
expect(getOperationName('/api/v{api-version}/users', 'GET', false, 'fooBar')).toEqual('getApiUsers');
const optionsIgnoreOperationId: Options = {
...options,
useOperationId: false,
};
expect(getOperationName('/api/v1/users', 'GET', optionsIgnoreOperationId, 'GetAllUsers')).toEqual(
'getApiV1Users'
);
expect(getOperationName('/api/v{api-version}/users', 'GET', optionsIgnoreOperationId, 'fooBar')).toEqual(
'getApiUsers'
);
expect(
getOperationName('/api/v{api-version}/users/{userId}/location/{locationId}', 'GET', false, 'fooBar')
getOperationName(
'/api/v{api-version}/users/{userId}/location/{locationId}',
'GET',
optionsIgnoreOperationId,
'fooBar'
)
).toEqual('getApiUsersByUserIdLocationByLocationId');
});
});
9 changes: 3 additions & 6 deletions src/openApi/v2/parser/getOperationName.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import camelCase from 'camelcase';

import type { Options } from '../../../client/interfaces/Options';
import sanitizeOperationName from '../../../utils/sanitizeOperationName';

/**
* Convert the input value to a correct operation (method) classname.
* This will use the operation ID - if available - and otherwise fallback
* on a generated name from the URL
*/
export const getOperationName = (
url: string,
method: string,
useOperationId: boolean,
operationId?: string
): string => {
export const getOperationName = (url: string, method: string, options: Options, operationId?: string): string => {
const { useOperationId = true } = options;
if (useOperationId && operationId) {
return camelCase(sanitizeOperationName(operationId).trim());
}
Expand Down
8 changes: 7 additions & 1 deletion src/openApi/v2/parser/getServices.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { Options } from '../../../client/interfaces/Options';
import { getServices } from './getServices';

describe('getServices', () => {
it('should create a unnamed service if tags are empty', () => {
const options: Options = {
input: '',
output: '',
useOperationId: false,
};
const services = getServices(
{
swagger: '2.0',
Expand All @@ -25,7 +31,7 @@ describe('getServices', () => {
},
},
},
false
options
);

expect(services).toHaveLength(1);
Expand Down
13 changes: 3 additions & 10 deletions src/openApi/v2/parser/getServices.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Options } from '../../../client/interfaces/Options';
import type { Service } from '../../../client/interfaces/Service';
import { unique } from '../../../utils/unique';
import type { OpenApi } from '../interfaces/OpenApi';
Expand All @@ -7,7 +8,7 @@ import { getOperationParameters } from './getOperationParameters';
/**
* Get the OpenAPI services
*/
export const getServices = (openApi: OpenApi, useOperationId: boolean): Service[] => {
export const getServices = (openApi: OpenApi, options: Options): Service[] => {
const services = new Map<string, Service>();
for (const url in openApi.paths) {
if (openApi.paths.hasOwnProperty(url)) {
Expand All @@ -30,15 +31,7 @@ export const getServices = (openApi: OpenApi, useOperationId: boolean): Service[
const op = path[method]!;
const tags = op.tags?.length ? op.tags.filter(unique) : ['Default'];
tags.forEach(tag => {
const operation = getOperation(
openApi,
url,
method,
tag,
op,
pathParams,
useOperationId
);
const operation = getOperation(openApi, url, method, tag, op, pathParams, options);

// If we have already declared a service, then we should fetch that and
// append the new method to it. Otherwise we should create a new service object.
Expand Down
7 changes: 4 additions & 3 deletions src/openApi/v3/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Client } from '../../client/interfaces/Client';
import type { Options } from '../../client/interfaces/Options';
import type { OpenApi } from './interfaces/OpenApi';
import { getModels } from './parser/getModels';
import { getServer } from './parser/getServer';
Expand All @@ -9,13 +10,13 @@ import { getServiceVersion } from './parser/getServiceVersion';
* Parse the OpenAPI specification to a Client model that contains
* all the models, services and schema's we should output.
* @param openApi The OpenAPI spec that we have loaded from disk.
* @param useOperationId should the operationId be used when generating operation names
* @param options Options passed to the generate method
*/
export const parse = (openApi: OpenApi, useOperationId: boolean): Client => {
export const parse = (openApi: OpenApi, options: Options): Client => {
const version = getServiceVersion(openApi.info.version);
const server = getServer(openApi);
const models = getModels(openApi);
const services = getServices(openApi, useOperationId);
const services = getServices(openApi, options);

return { version, server, models, services };
};
Loading

0 comments on commit ab12c4f

Please sign in to comment.