Skip to content
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

refactor(parser): start passing options object around instead of positional parameters #19

Merged
merged 1 commit into from
Feb 20, 2024
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 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
Loading