Skip to content

Commit 1ef5a26

Browse files
authored
Merge pull request #63 from nicolas-chaulet/fix/remove-enums
fix(api): add enums flag
2 parents 3c4376f + a0a8d6e commit 1ef5a26

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+1025
-1087
lines changed

README.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,18 @@ $ openapi --help
5151
-c, --client <value> HTTP client to generate [fetch, xhr, node, axios, angular] (default: "fetch")
5252
--name <value> Custom client class name
5353
--useOptions <value> Use options instead of arguments (default: false)
54+
--no-autoformat Disable processing generated files with formatter
55+
--base <value> Manually set base in OpenAPI config instead of inferring from server value
56+
--enums Generate JavaScript objects from enum definitions (default: false)
5457
--exportCore <value> Write core files to disk (default: true)
5558
--exportServices <value> Write services to disk [true, false, regexp] (default: true)
5659
--exportModels <value> Write models to disk [true, false, regexp] (default: true)
5760
--exportSchemas <value> Write schemas to disk (default: false)
58-
--base <value> Manually set base in OpenAPI config instead of inferring from server value
59-
--no-autoformat Disable processing generated files with formatter
61+
--no-operationId Use path URL to generate operation ID
6062
--postfixServices Service name postfix (default: "Service")
6163
--postfixModels Model name postfix
6264
--request <value> Path to custom request file
6365
--useDateType <value> Output Date instead of string for the format "date-time" in the models (default: false)
64-
--useOperationId <value> Use operation id to generate operation names (default: true)
6566
-h, --help display help for command
6667
6768
Examples
@@ -79,6 +80,23 @@ openapi -i path/to/openapi.json -o src/client --no-autoformat
7980

8081
You can also prevent your client from being processed by formatters and linters by adding your output path to the tool's ignore file (e.g. `.eslintignore`, `.prettierignore`).
8182

83+
## Enums
84+
85+
We do not generate TypeScript [enums](https://www.typescriptlang.org/docs/handbook/enums.html) because they are not standard JavaScript and pose [typing challenges](https://dev.to/ivanzm123/dont-use-enums-in-typescript-they-are-very-dangerous-57bh). If you want to iterate through possible field values without manually typing arrays, you can export enums by running
86+
87+
```sh
88+
openapi -i path/to/openapi.json -o src/client --enums
89+
```
90+
91+
This will export your enums as plain JavaScript objects. For example, `Foo` will generate the following
92+
93+
```ts
94+
export const FooEnum = {
95+
FOO: 'foo',
96+
BAR: 'bar',
97+
} as const;
98+
```
99+
82100
## Contributing
83101

84102
This section is WIP.

bin/index.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,26 @@ const params = program
1515
.option('-c, --client <value>', 'HTTP client to generate [fetch, xhr, node, axios, angular]', 'fetch')
1616
.option('--name <value>', 'Custom client class name')
1717
.option('--useOptions [value]', 'Use options instead of arguments', false)
18-
.option('--base [value]', 'Manually set base in OpenAPI config instead of inferring from server value')
1918
.option('--no-autoformat', 'Disable processing generated files with formatter')
19+
.option('--base [value]', 'Manually set base in OpenAPI config instead of inferring from server value')
20+
.option('--enums', 'Generate JavaScript objects from enum definitions', false)
2021
.option('--exportCore <value>', 'Write core files to disk', true)
2122
.option('--exportServices <value>', 'Write services to disk', true)
2223
.option('--exportModels <value>', 'Write models to disk', true)
2324
.option('--exportSchemas <value>', 'Write schemas to disk', false)
25+
.option('--no-operationId', 'Use path URL to generate operation ID')
2426
.option('--postfixServices <value>', 'Service name postfix', 'Service')
2527
.option('--serviceResponse [value]', 'Define shape of returned value from service calls')
2628
.option('--useDateType <value>', 'Output Date instead of string for the format "date-time" in the models', false)
27-
.option('--useOperationId <value>', 'Use operation id to generate operation names', true)
2829
.option('--postfixModels <value>', 'Model name postfix')
2930
.option('--request <value>', 'Path to custom request file')
31+
.option('--no-write', 'Skip writing files to disk (used for testing)')
3032
.parse(process.argv)
3133
.opts();
3234

3335
const OpenAPI = require(path.resolve(__dirname, '../dist/index.js'));
3436

35-
const parseBooleanOrString = value => {
36-
try {
37-
return JSON.parse(value) === true;
38-
} catch (error) {
39-
return value;
40-
}
41-
};
37+
const parseBooleanOrString = value => (value === true || value === 'true' ? true : value);
4238

4339
if (OpenAPI) {
4440
OpenAPI.generate({
@@ -50,7 +46,6 @@ if (OpenAPI) {
5046
exportServices: parseBooleanOrString(params.exportServices),
5147
httpClient: params.client,
5248
useDateType: JSON.parse(params.useDateType) === true,
53-
useOperationId: JSON.parse(params.useOperationId) === true,
5449
useOptions: JSON.parse(params.useOptions) === true,
5550
})
5651
.then(() => {

bin/index.spec.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
const crossSpawn = require('cross-spawn');
1+
const { sync } = require('cross-spawn');
22

33
describe('bin', () => {
44
it('it should support minimal params', async () => {
5-
const result = crossSpawn.sync('node', [
5+
const result = sync('node', [
66
'./bin/index.js',
77
'--input',
88
'./test/spec/v3.json',
99
'--output',
1010
'./test/generated/bin',
11+
'--no-write',
1112
]);
1213
expect(result.stdout.toString()).toBe('');
1314
expect(result.stderr.toString()).toBe('');
1415
});
1516

1617
it('it should support all params', async () => {
17-
const result = crossSpawn.sync('node', [
18+
const result = sync('node', [
1819
'./bin/index.js',
1920
'--input',
2021
'./test/spec/v3.json',
@@ -35,13 +36,14 @@ describe('bin', () => {
3536
'Service',
3637
'--postfixModels',
3738
'Dto',
39+
'--no-write',
3840
]);
3941
expect(result.stdout.toString()).toBe('');
4042
expect(result.stderr.toString()).toBe('');
4143
});
4244

4345
it('it should support regexp params', async () => {
44-
const result = crossSpawn.sync('node', [
46+
const result = sync('node', [
4547
'./bin/index.js',
4648
'--input',
4749
'./test/spec/v3.json',
@@ -51,44 +53,47 @@ describe('bin', () => {
5153
'^(Simple|Types)',
5254
'--exportModels',
5355
'^(Simple|Types)',
56+
'--no-write',
5457
]);
5558
expect(result.stdout.toString()).toBe('');
5659
expect(result.stderr.toString()).toBe('');
5760
});
5861

5962
it('should autoformat with Prettier', async () => {
60-
const result = crossSpawn.sync('node', [
63+
const result = sync('node', [
6164
'./bin/index.js',
6265
'--input',
6366
'./test/spec/v3.json',
6467
'--output',
6568
'./test/generated/bin',
69+
'--no-write',
6670
]);
6771
expect(result.stdout.toString()).toBe('');
6872
expect(result.stderr.toString()).toBe('');
6973
});
7074

7175
it('it should throw error without params', async () => {
72-
const result = crossSpawn.sync('node', ['./bin/index.js']);
76+
const result = sync('node', ['./bin/index.js', '--no-write']);
7377
expect(result.stdout.toString()).toBe('');
7478
expect(result.stderr.toString()).toContain(`error: required option '-i, --input <value>' not specified`);
7579
});
7680

7781
it('it should throw error with wrong params', async () => {
78-
const result = crossSpawn.sync('node', [
82+
const result = sync('node', [
7983
'./bin/index.js',
8084
'--input',
8185
'./test/spec/v3.json',
8286
'--output',
8387
'./test/generated/bin',
8488
'--unknown',
89+
'--no-write',
8590
]);
8691
expect(result.stdout.toString()).toBe('');
8792
expect(result.stderr.toString()).toContain(`error: unknown option '--unknown'`);
8893
});
8994

9095
it('it should display help', async () => {
91-
const result = crossSpawn.sync('node', ['./bin/index.js', '--help']);
96+
const result = sync('node', ['./bin/index.js', '--help', '--no-write']);
9297
expect(result.stdout.toString()).toContain(`Usage: openapi [options]`);
9398
expect(result.stdout.toString()).toContain(`-i, --input <value>`);
9499
expect(result.stdout.toString()).toContain(`-o, --output <value>`);

jest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const config: Config.InitialOptions = {
55
{
66
displayName: 'UNIT',
77
testEnvironment: 'node',
8-
testMatch: ['<rootDir>/src/**/*.spec.ts', '<rootDir>/test/index.spec.ts', '<rootDir>/bin/index.spec.js'],
8+
testMatch: ['<rootDir>/test/index.spec.ts', '<rootDir>/bin/index.spec.js', '<rootDir>/src/**/*.spec.ts'],
99
moduleFileExtensions: ['js', 'ts', 'd.ts'],
1010
moduleNameMapper: {
1111
'\\.hbs$': '<rootDir>/src/templates/__mocks__/index.ts',

rollup.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ const handlebarsPlugin = () => ({
4343
exactArray: true,
4444
ifdef: true,
4545
ifOperationDataOptional: true,
46-
inlineEnum: true,
4746
intersection: true,
4847
modelImports: true,
48+
modelsExports: true,
4949
modelUnionType: true,
5050
nameOperationDataType: true,
5151
notEquals: true,

src/client/interfaces/Options.d.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export interface Options {
1515
* Custom client class name
1616
*/
1717
clientName?: string;
18+
/**
19+
* Generate JavaScript objects from enum definitions
20+
*/
21+
enums?: boolean;
1822
/**
1923
* Generate core client classes
2024
*/
@@ -39,6 +43,10 @@ export interface Options {
3943
* The relative location of the OpenAPI spec
4044
*/
4145
input: string | Record<string, any>;
46+
/**
47+
* Use operation ID to generate operation names?
48+
*/
49+
operationId?: boolean;
4250
/**
4351
* The relative location of the output directory
4452
*/
@@ -63,10 +71,6 @@ export interface Options {
6371
* Output Date instead of string for the format "date-time" in the models
6472
*/
6573
useDateType?: boolean;
66-
/**
67-
* Should the operationId be used when generating operation names?
68-
*/
69-
useOperationId?: boolean;
7074
/**
7175
* Use options or arguments functions
7276
*/

src/index.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getOpenApiSpec } from './utils/getOpenApiSpec';
44
import { getOpenApiSpecParser } from './utils/getOpenApiSpecParser';
55
import { postProcessClient } from './utils/postProcessClient';
66
import { registerHandlebarTemplates } from './utils/registerHandlebarTemplates';
7-
import { writeClient } from './utils/writeClient';
7+
import { writeClient } from './utils/write/client';
88

99
export { HttpClient } from './HttpClient';
1010

@@ -17,43 +17,58 @@ export { HttpClient } from './HttpClient';
1717
export const generate = async (options: Options): Promise<void> => {
1818
const {
1919
autoformat = true,
20+
base,
21+
clientName,
22+
enums = false,
2023
exportCore = true,
2124
exportModels = true,
2225
exportSchemas = false,
2326
exportServices = true,
2427
httpClient = HttpClient.FETCH,
28+
input,
29+
operationId = true,
30+
output,
2531
postfixModels = '',
2632
postfixServices = 'Service',
33+
request,
2734
serviceResponse = 'body',
2835
useDateType = false,
2936
useOptions = false,
3037
write = true,
3138
} = options;
32-
const openApi = typeof options.input === 'string' ? await getOpenApiSpec(options.input) : options.input;
33-
const parser = getOpenApiSpecParser(openApi);
34-
const templates = registerHandlebarTemplates(openApi, {
39+
40+
const defaultOptions: Omit<Required<Options>, 'base' | 'clientName' | 'request'> &
41+
Pick<Options, 'base' | 'clientName' | 'request'> = {
42+
autoformat,
43+
base,
44+
clientName,
45+
enums,
46+
exportCore,
47+
exportModels,
48+
exportSchemas,
49+
exportServices,
3550
httpClient,
51+
input,
52+
operationId,
53+
output,
54+
postfixModels,
55+
postfixServices,
56+
request,
3657
serviceResponse,
58+
useDateType,
3759
useOptions,
38-
});
60+
write,
61+
};
62+
63+
const openApi =
64+
typeof defaultOptions.input === 'string' ? await getOpenApiSpec(defaultOptions.input) : defaultOptions.input;
65+
const parser = getOpenApiSpecParser(openApi);
66+
const templates = registerHandlebarTemplates(openApi, defaultOptions);
3967

40-
const client = parser(openApi, options);
68+
const client = parser(openApi, defaultOptions);
4169
const clientFinal = postProcessClient(client);
4270
if (write) {
43-
await writeClient(clientFinal, templates, {
44-
...options,
45-
autoformat,
46-
exportCore,
47-
exportModels,
48-
exportSchemas,
49-
exportServices,
50-
httpClient,
51-
postfixModels,
52-
postfixServices,
53-
serviceResponse,
54-
useDateType,
55-
useOptions,
56-
});
71+
await writeClient(clientFinal, templates, defaultOptions);
5772
}
5873
};
5974

src/openApi/v2/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import { getServiceVersion } from './parser/getServiceVersion';
1212
* @param openApi The OpenAPI spec that we have loaded from disk.
1313
* @param options Options passed to the generate method
1414
*/
15-
export const parse = (openApi: OpenApi, options: Options): Client => {
15+
export const parse = (
16+
openApi: OpenApi,
17+
options: Pick<Required<Options>, 'operationId'> & Omit<Options, 'operationId'>
18+
): Client => {
1619
const version = getServiceVersion(openApi.info.version);
1720
const server = getServer(openApi);
1821
const models = getModels(openApi);

src/openApi/v2/parser/getModel.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const getModel = (
6060
const enums = getEnums(definition, definition.enum);
6161
if (enums.length) {
6262
model.base = 'string';
63-
model.enum.push(...enums);
63+
model.enum = [...model.enum, ...enums];
6464
model.export = 'enum';
6565
model.type = 'string';
6666
return model;
@@ -114,7 +114,7 @@ export const getModel = (
114114
model.export = composition.export;
115115
model.imports.push(...composition.imports);
116116
model.properties.push(...composition.properties);
117-
model.enums.push(...composition.enums);
117+
model.enums = [...model.enums, ...composition.enums];
118118
return model;
119119
}
120120

@@ -127,10 +127,10 @@ export const getModel = (
127127
const modelProperties = getModelProperties(openApi, definition, getModel);
128128
modelProperties.forEach(modelProperty => {
129129
model.imports.push(...modelProperty.imports);
130-
model.enums.push(...modelProperty.enums);
130+
model.enums = [...model.enums, ...modelProperty.enums];
131131
model.properties.push(modelProperty);
132132
if (modelProperty.export === 'enum') {
133-
model.enums.push(modelProperty);
133+
model.enums = [...model.enums, modelProperty];
134134
}
135135
});
136136
}

src/openApi/v2/parser/getOperation.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { Operation } from '../../../client/interfaces/Operation';
22
import type { OperationParameters } from '../../../client/interfaces/OperationParameters';
33
import type { Options } from '../../../client/interfaces/Options';
4+
import { getOperationName } from '../../../utils/operation';
45
import type { OpenApi } from '../interfaces/OpenApi';
56
import type { OpenApiOperation } from '../interfaces/OpenApiOperation';
67
import { getOperationErrors } from './getOperationErrors';
7-
import { getOperationName } from './getOperationName';
88
import { getOperationParameters } from './getOperationParameters';
99
import { getOperationResponseHeader } from './getOperationResponseHeader';
1010
import { getOperationResponses } from './getOperationResponses';
@@ -19,16 +19,16 @@ export const getOperation = (
1919
tag: string,
2020
op: OpenApiOperation,
2121
pathParams: OperationParameters,
22-
options: Options
22+
options: Pick<Required<Options>, 'operationId'> & Omit<Options, 'operationId'>
2323
): Operation => {
2424
const serviceName = getServiceName(tag);
25-
const operationName = getOperationName(url, method, options, op.operationId);
25+
const name = getOperationName(url, method, options, op.operationId);
2626

2727
// Create a new operation object for this method.
2828
const operation: Operation = {
2929
$refs: [],
3030
service: serviceName,
31-
name: operationName,
31+
name,
3232
summary: op.summary || null,
3333
description: op.description || null,
3434
deprecated: op.deprecated === true,

0 commit comments

Comments
 (0)