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

Add new configuration option to preserve javascript enum names #356

Closed
Closed
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
5 changes: 5 additions & 0 deletions .changeset/curvy-beans-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-ts-docs": patch
---

Add documentation for new enums configuration option
5 changes: 5 additions & 0 deletions .changeset/quick-ladybugs-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/openapi-ts": minor
---

Add configuration option to closely preserve JavaScript enum names
21 changes: 21 additions & 0 deletions docs/openapi-ts/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ You can also prevent your client from being processed by linters by adding your

## Enums

> Omitting the `enums` configuration value will emit unions instead of enums.

If you need to iterate through possible field values without manually typing arrays, you can export enums with

```js{2}
Expand All @@ -140,6 +142,25 @@ export const FooEnum = {
} as const;
```

There is an additional JavaScript option for enums:

```js{2}
export default {
enums: 'javascript-preserve-name,
input: 'path/to/openapi.json',
output: 'src/client',
}
```

As the name indicates this will not rename the enums from your input and will preserve the original names while maintaing the type-safety of the JavaScript object as a constant.

```js
export const MY_FOO_ENUM = {
FOO: 'foo',
BAR: 'bar',
} as const;
```

We discourage generating [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 really need TypeScript enums, you can export them with

```js{2}
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-ts/bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const params = program
.option('-d, --debug', 'Run in debug mode?')
.option('--base [value]', 'Manually set base in OpenAPI config instead of inferring from server value')
.option('--dry-run [value]', 'Skip writing files to disk?')
.option('--enums <value>', 'Export enum definitions (javascript, typescript)')
.option('--enums <value>', 'Export enum definitions (javascript, javascript-preserve-name, typescript)')
.option('--exportCore [value]', 'Write core files to disk')
.option('--exportModels [value]', 'Write models to disk')
.option('--exportServices [value]', 'Write services to disk')
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-ts/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface UserConfig {
* Export enum definitions?
* @default false
*/
enums?: 'javascript' | 'typescript' | false;
enums?: 'javascript' | 'javascript-preserve-name' | 'typescript' | false;
/**
* Generate core client classes?
* @default true
Expand Down
38 changes: 27 additions & 11 deletions packages/openapi-ts/src/utils/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,9 @@ export const enumKey = (value?: string | number, customName?: string) => {
return key.toUpperCase();
};

/**
* Enums can't contain hyphens in their name. Additionally, name might've been
* already escaped, so we need to remove quotes around it.
* {@link https://github.com/ferdikoomen/openapi-typescript-codegen/issues/1969}
*/
export const enumName = (client: Client, name?: string) => {
if (!name) {
return name;
}
const escapedName = unescapeName(name).replace(/[-_]([a-z])/gi, ($0, $1: string) => $1.toLocaleUpperCase());
let result = `${escapedName.charAt(0).toLocaleUpperCase() + escapedName.slice(1)}Enum`;
const updateClientEnums = (client: Client, currentEnum: string) => {
let index = 1;
let result = currentEnum;
while (client.enumNames.includes(result)) {
if (result.endsWith(index.toString())) {
result = result.slice(0, result.length - index.toString().length);
Expand All @@ -56,9 +47,34 @@ export const enumName = (client: Client, name?: string) => {
result = result + index.toString();
}
client.enumNames = [...client.enumNames, result];
};

/**
* Enums can't contain hyphens in their name. Additionally, name might've been
* already escaped, so we need to remove quotes around it.
* {@link https://github.com/ferdikoomen/openapi-typescript-codegen/issues/1969}
*/
export const javascriptEnumName = (client: Client, name?: string) => {
if (!name) {
return name;
}
const escapedName = unescapeName(name).replace(/[-_]([a-z])/gi, ($0, $1: string) => $1.toLocaleUpperCase());
const result = `${escapedName.charAt(0).toLocaleUpperCase() + escapedName.slice(1)}Enum`;
updateClientEnums(client, result);
return result;
};

export const javascriptPreservedEnumName = (client: Client, name?: string) => {
if (!name) {
return name;
}

// Remove all invalid characters
const cleanEnum = unescapeName(name).replace(/[^a-zA-Z0-9_$]/gi, '');
updateClientEnums(client, cleanEnum);
return cleanEnum;
};

export const enumUnionType = (enums: Enum[]) =>
enums
.map(enumerator => enumValue(enumerator.value))
Expand Down
10 changes: 7 additions & 3 deletions packages/openapi-ts/src/utils/write/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type Comments, compiler, type Node, TypeScriptFile } from '../../compil
import type { Model, OpenApi, OperationParameter, Service } from '../../openApi';
import type { Client } from '../../types/client';
import { getConfig } from '../config';
import { enumKey, enumName, enumUnionType, enumValue } from '../enum';
import { enumKey, enumUnionType, enumValue, javascriptEnumName, javascriptPreservedEnumName } from '../enum';
import { escapeComment } from '../escape';
import { serviceExportedNamespace } from '../handlebars';
import { sortByName } from '../sort';
Expand Down Expand Up @@ -62,13 +62,17 @@ const processEnum = (client: Client, model: Model, exportType: boolean) => {
}
}

if (config.enums === 'javascript') {
if (['javascript', 'javascript-preserve-name'].includes(config.enums as string)) {
const expression = compiler.types.object(properties, {
comments,
multiLine: true,
unescape: true,
});
nodes = [...nodes, compiler.export.asConst(enumName(client, model.name)!, expression)];
const preserveName = config.enums === 'javascript-preserve-name';
const outputEnumName = preserveName
? javascriptPreservedEnumName(client, model.name)!
: javascriptEnumName(client, model.name)!;
nodes = [...nodes, compiler.export.asConst(outputEnumName, expression)];
}

return nodes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,26 @@ export const EnumWithExtensionsEnum = {
CUSTOM_ERROR: 500,
} as const;

/**
* This is a simple enum with a non-PascalCase name.
*/
export type UPPER_SNAKE_ENUM = 'UPPER_SNAKE_0' | 'UPPER_SNAKE_1' | 'UPPER_SNAKE_2';

export const UPPERSNAKEENUMEnum = {
/**
* UPPER_SNAKE_0
*/
UPPER_SNAKE_0: 'UPPER_SNAKE_0',
/**
* UPPER_SNAKE_1
*/
UPPER_SNAKE_1: 'UPPER_SNAKE_1',
/**
* UPPER_SNAKE_2
*/
UPPER_SNAKE_2: 'UPPER_SNAKE_2',
} as const;

/**
* This is a simple array with numbers
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ export const $EnumWithExtensions = {
],
} as const;

export const $UPPER_SNAKE_ENUM = {
description: 'This is a simple enum with a non-PascalCase name.',
enum: ['UPPER_SNAKE_0', 'UPPER_SNAKE_1', 'UPPER_SNAKE_2'],
'x-enum-varnames': [0, 1, 2],
'x-enum-descriptions': ['UPPER_SNAKE_0', 'UPPER_SNAKE_1', 'UPPER_SNAKE_2'],
} as const;

export const $ArrayWithNumbers = {
description: 'This is a simple array with numbers',
type: 'array',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,26 @@ export const EnumWithXEnumNamesEnum = {
two: 2,
} as const;

/**
* This is a simple enum with a non-PascalCase name.
*/
export type UPPER_SNAKE_ENUM = 'UPPER_SNAKE_0' | 'UPPER_SNAKE_1' | 'UPPER_SNAKE_2';

export const UPPERSNAKEENUMEnum = {
/**
* UPPER_SNAKE_0
*/
UPPER_SNAKE_0: 'UPPER_SNAKE_0',
/**
* UPPER_SNAKE_1
*/
UPPER_SNAKE_1: 'UPPER_SNAKE_1',
/**
* UPPER_SNAKE_2
*/
UPPER_SNAKE_2: 'UPPER_SNAKE_2',
} as const;

/**
* This is a simple array with numbers
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ export const $EnumWithXEnumNames = {
'x-enumNames': ['zero', 'one', 'two'],
} as const;

export const $UPPER_SNAKE_ENUM = {
description: 'This is a simple enum with a non-PascalCase name.',
enum: ['UPPER_SNAKE_0', 'UPPER_SNAKE_1', 'UPPER_SNAKE_2'],
'x-enum-varnames': [0, 1, 2],
'x-enum-descriptions': ['UPPER_SNAKE_0', 'UPPER_SNAKE_1', 'UPPER_SNAKE_2'],
} as const;

export const $ArrayWithNumbers = {
description: 'This is a simple array with numbers',
type: 'array',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ export type EnumWithExtensions = 200 | 400 | 500;

export type EnumWithXEnumNames = 0 | 1 | 2;

/**
* This is a simple enum with a non-PascalCase name.
*/
export type UPPER_SNAKE_ENUM = 'UPPER_SNAKE_0' | 'UPPER_SNAKE_1' | 'UPPER_SNAKE_2';

/**
* This is a simple array with numbers
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ export const $EnumWithXEnumNames = {
'x-enumNames': ['zero', 'one', 'two'],
} as const;

export const $UPPER_SNAKE_ENUM = {
description: 'This is a simple enum with a non-PascalCase name.',
enum: ['UPPER_SNAKE_0', 'UPPER_SNAKE_1', 'UPPER_SNAKE_2'],
'x-enum-varnames': [0, 1, 2],
'x-enum-descriptions': ['UPPER_SNAKE_0', 'UPPER_SNAKE_1', 'UPPER_SNAKE_2'],
} as const;

export const $ArrayWithNumbers = {
description: 'This is a simple array with numbers',
type: 'array',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,26 @@ export const EnumWithXEnumNamesEnum = {
two: 2,
} as const;

/**
* This is a simple enum with a non-PascalCase name.
*/
export type UPPER_SNAKE_ENUM = 'UPPER_SNAKE_0' | 'UPPER_SNAKE_1' | 'UPPER_SNAKE_2';

export const UPPERSNAKEENUMEnum = {
/**
* UPPER_SNAKE_0
*/
UPPER_SNAKE_0: 'UPPER_SNAKE_0',
/**
* UPPER_SNAKE_1
*/
UPPER_SNAKE_1: 'UPPER_SNAKE_1',
/**
* UPPER_SNAKE_2
*/
UPPER_SNAKE_2: 'UPPER_SNAKE_2',
} as const;

/**
* This is a simple array with numbers
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ export const $EnumWithXEnumNames = {
'x-enumNames': ['zero', 'one', 'two'],
} as const;

export const $UPPER_SNAKE_ENUM = {
description: 'This is a simple enum with a non-PascalCase name.',
enum: ['UPPER_SNAKE_0', 'UPPER_SNAKE_1', 'UPPER_SNAKE_2'],
'x-enum-varnames': [0, 1, 2],
'x-enum-descriptions': ['UPPER_SNAKE_0', 'UPPER_SNAKE_1', 'UPPER_SNAKE_2'],
} as const;

export const $ArrayWithNumbers = {
description: 'This is a simple array with numbers',
type: 'array',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';

export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: unknown;
public readonly request: ApiRequestOptions;

constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);

this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type ApiRequestOptions = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, unknown>;
readonly cookies?: Record<string, unknown>;
readonly headers?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly formData?: Record<string, unknown>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ApiResult<TData = any> = {
readonly body: TData;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly url: string;
};
Loading
Loading