Skip to content

add flag for exporting enum value arrays #1616 #1661

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 1 commit into from
May 16, 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 docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ The following flags are supported in the CLI:
| `--properties-required-by-default` | | `false` | Treat schema objects without `required` as having all properties required. |
| `--empty-objects-unknown` | | `false` | Allow arbitrary properties for schema objects with no specified properties, and no specified `additionalProperties` |
| `--enum` | | `false` | Generate true [TS enums](https://www.typescriptlang.org/docs/handbook/enums.html) rather than string unions. |
| `--enum-values` | | `false` | Export enum values as arrays. |
| `--exclude-deprecated` | | `false` | Exclude deprecated fields from types |
| `--export-type` | `-t` | `false` | Export `type` instead of `interface` |
| `--immutable` | | `false` | Generates immutable types (readonly properties and readonly array) |
Expand Down
1 change: 1 addition & 0 deletions docs/zh/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ CLI 支持以下参数:
| `--default-non-nullable` | | `false` | 将带有默认值的模式对象视为非可空 |
| `--empty-objects-unknown` | | `false` | 允许在未指定属性和未指定 `additionalProperties` 的情况下,为模式对象设置任意属性 |
| `--enum` | | `false` | 生成真实的 [TS 枚举](https://www.typescriptlang.org/docs/handbook/enums.html),而不是字符串联合。 |
| `--enum-values | | `false` | |
| `--exclude-deprecated` | | `false` | 从类型中排除已弃用的字段 |
| `--export-type` | `-t` | `false` | 导出 `type` 而不是 `interface` |
| `--immutable` | | `false` | 生成不可变类型(只读属性和只读数组) |
Expand Down
7 changes: 5 additions & 2 deletions packages/openapi-typescript/bin/cli.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env node

import { loadConfig, findConfig, createConfig } from "@redocly/openapi-core";
import { createConfig, findConfig, loadConfig } from "@redocly/openapi-core";
import fs from "node:fs";
import path from "node:path";
import parser from "yargs-parser";
import openapiTS, { astToString, c, COMMENT_HEADER, error, formatTime, warn } from "../dist/index.js";
import openapiTS, { COMMENT_HEADER, astToString, c, error, formatTime, warn } from "../dist/index.js";

const HELP = `Usage
$ openapi-typescript [input] [options]
Expand All @@ -15,6 +15,7 @@ Options
--redocly [path], -c Specify path to Redocly config (default: redocly.yaml)
--output, -o Specify output file (if not specified in redocly.yaml)
--enum Export true TS enums instead of unions
--enum-values Export enum values as arrays
--export-type, -t Export top-level \`type\` instead of \`interface\`
--immutable Generate readonly types
--additional-properties Treat schema objects as if \`additionalProperties: true\` is set
Expand Down Expand Up @@ -62,6 +63,7 @@ const flags = parser(args, {
"propertiesRequiredByDefault",
"emptyObjectsUnknown",
"enum",
"enumValues",
"excludeDeprecated",
"exportType",
"help",
Expand Down Expand Up @@ -91,6 +93,7 @@ async function generateSchema(schema, { redocly, silent = false }) {
defaultNonNullable: flags.defaultNonNullable,
emptyObjectsUnknown: flags.emptyObjectsUnknown,
enum: flags.enum,
enumValues: flags.enumValues,
excludeDeprecated: flags.excludeDeprecated,
exportType: flags.exportType,
immutable: flags.immutable,
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export default async function openapiTS(
discriminators: scanDiscriminators(schema, options),
emptyObjectsUnknown: options.emptyObjectsUnknown ?? false,
enum: options.enum ?? false,
enumValues: options.enumValues ?? false,
excludeDeprecated: options.excludeDeprecated ?? false,
exportType: options.exportType ?? false,
immutable: options.immutable ?? false,
Expand Down
64 changes: 57 additions & 7 deletions packages/openapi-typescript/src/lib/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,7 @@ export function tsEnum(
metadata?: { name?: string; description?: string }[],
options?: { export?: boolean },
) {
let enumName = name.replace(JS_ENUM_INVALID_CHARS_RE, (c) => {
const last = c[c.length - 1];
return JS_PROPERTY_INDEX_INVALID_CHARS_RE.test(last) ? "" : last.toUpperCase();
});
if (Number(name[0]) >= 0) {
enumName = `Value${name}`;
}
let enumName = sanitizeMemberName(name);
enumName = `${enumName[0].toUpperCase()}${enumName.substring(1)}`;
return ts.factory.createEnumDeclaration(
/* modifiers */ options ? tsModifiers({ export: options.export ?? false }) : undefined,
Expand All @@ -221,6 +215,62 @@ export function tsEnum(
);
}

/** Create an exported TS array literal expression */
export function tsArrayLiteralExpression(
name: string,
elementType: ts.TypeNode,
values: (string | number)[],
options?: { export?: boolean; readonly?: boolean },
) {
let variableName = sanitizeMemberName(name);
variableName = `${variableName[0].toLowerCase()}${variableName.substring(1)}`;

const arrayType = options?.readonly
? ts.factory.createTypeReferenceNode("ReadonlyArray", [elementType])
: ts.factory.createArrayTypeNode(elementType);

return ts.factory.createVariableStatement(
options ? tsModifiers({ export: options.export ?? false }) : undefined,
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
variableName,
undefined,
arrayType,
ts.factory.createArrayLiteralExpression(
values.map((value) => {
if (typeof value === "number") {
if (value < 0) {
return ts.factory.createPrefixUnaryExpression(
ts.SyntaxKind.MinusToken,
ts.factory.createNumericLiteral(Math.abs(value)),
);
} else {
return ts.factory.createNumericLiteral(value);
}
} else {
return ts.factory.createStringLiteral(value);
}
}),
),
),
],
ts.NodeFlags.Const,
),
);
}

function sanitizeMemberName(name: string) {
let sanitizedName = name.replace(JS_ENUM_INVALID_CHARS_RE, (c) => {
const last = c[c.length - 1];
return JS_PROPERTY_INDEX_INVALID_CHARS_RE.test(last) ? "" : last.toUpperCase();
});
if (Number(name[0]) >= 0) {
sanitizedName = `Value${name}`;
}
return sanitizedName;
}

/** Sanitize TS enum member expression */
export function tsEnumMember(value: string | number, metadata: { name?: string; description?: string } = {}) {
let name = metadata.name ?? String(value);
Expand Down
26 changes: 25 additions & 1 deletion packages/openapi-typescript/src/transform/schema-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
UNKNOWN,
addJSDocComment,
oapiRef,
tsArrayLiteralExpression,
tsEnum,
tsIntersection,
tsIsPrimitive,
Expand Down Expand Up @@ -118,7 +119,30 @@ export function transformSchemaObjectWithComposition(
if ((Array.isArray(schemaObject.type) && schemaObject.type.includes("null")) || schemaObject.nullable) {
enumType.push(NULL);
}
return tsUnion(enumType);

const unionType = tsUnion(enumType);

// hoist array with valid enum values to top level if string/number enum and option is enabled
if (options.ctx.enumValues && schemaObject.enum.every((v) => typeof v === "string" || typeof v === "number")) {
let enumValuesVariableName = parseRef(options.path ?? "").pointer.join("/");
// allow #/components/schemas to have simpler names
enumValuesVariableName = enumValuesVariableName.replace("components/schemas", "");
enumValuesVariableName = `${enumValuesVariableName}Values`;

const enumValuesArray = tsArrayLiteralExpression(
enumValuesVariableName,
oapiRef(options.path ?? ""),
schemaObject.enum as (string | number)[],
{
export: true,
readonly: true,
},
);

options.ctx.injectFooter.push(enumValuesArray);
}

return unionType;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/openapi-typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,8 @@ export interface OpenAPITSOptions {
exportType?: boolean;
/** Export true TypeScript enums instead of unions */
enum?: boolean;
/** Export union values as arrays */
enumValues?: boolean;
/** (optional) Substitute path parameter names with their respective types */
pathParamsAsTypes?: boolean;
/** Treat all objects as if they have \`required\` set to all properties by default (default: false) */
Expand All @@ -673,6 +675,7 @@ export interface GlobalContext {
};
emptyObjectsUnknown: boolean;
enum: boolean;
enumValues: boolean;
excludeDeprecated: boolean;
exportType: boolean;
immutable: boolean;
Expand Down
55 changes: 55 additions & 0 deletions packages/openapi-typescript/test/lib/ts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
STRING,
astToString,
oapiRef,
tsArrayLiteralExpression,
tsEnum,
tsIsPrimitive,
tsLiteral,
Expand Down Expand Up @@ -201,6 +202,60 @@ describe("tsEnum", () => {
});
});

describe("tsArrayLiteralExpression", () => {
test("string members", () => {
expect(
astToString(
tsArrayLiteralExpression("-my-color-Values", oapiRef("#/components/schemas/Color"), ["green", "red", "blue"]),
).trim(),
).toBe(`const myColorValues: components["schemas"]["Color"][] = ["green", "red", "blue"];`);
});

test("with setting: export", () => {
expect(
astToString(
tsArrayLiteralExpression("-my-color-Values", oapiRef("#/components/schemas/Color"), ["green", "red", "blue"], {
export: true,
}),
).trim(),
).toBe(`export const myColorValues: components["schemas"]["Color"][] = ["green", "red", "blue"];`);
});

test("with setting: readonly", () => {
expect(
astToString(
tsArrayLiteralExpression("-my-color-Values", oapiRef("#/components/schemas/Color"), ["green", "red", "blue"], {
readonly: true,
}),
).trim(),
).toBe(`const myColorValues: ReadonlyArray<components["schemas"]["Color"]> = ["green", "red", "blue"];`);
});

test("name from path", () => {
expect(
astToString(
tsArrayLiteralExpression(
"#/paths/url/get/parameters/query/status/Values",
oapiRef("#/components/schemas/Status"),
["active", "inactive"],
),
).trim(),
).toBe(`const pathsUrlGetParametersQueryStatusValues: components["schemas"]["Status"][] = ["active", "inactive"];`);
});

test("number members", () => {
expect(
astToString(
tsArrayLiteralExpression(
".Error.code.Values",
oapiRef("#/components/schemas/ErrorCode"),
[100, 101, 102, -100],
),
).trim(),
).toBe(`const errorCodeValues: components["schemas"]["ErrorCode"][] = [100, 101, 102, -100];`);
});
});

describe("tsPropertyIndex", () => {
test("numbers -> number literals", () => {
expect(astToString(tsPropertyIndex(200)).trim()).toBe("200");
Expand Down
86 changes: 86 additions & 0 deletions packages/openapi-typescript/test/node-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,92 @@ export type operations = Record<string, never>;`,
options: { enum: true },
},
],
[
"options > enumValues",
{
given: {
openapi: "3.1",
info: { title: "Test", version: "1.0" },
paths: {
"/url": {
get: {
parameters: [
{
name: "status",
in: "query",
schema: {
type: "string",
enum: ["active", "inactive"],
},
},
],
},
},
},
components: {
schemas: {
Status: {
type: "string",
enum: ["active", "inactive"],
},
ErrorCode: {
type: "number",
enum: [100, 101, 102, 103, 104, 105],
},
},
},
},
want: `export interface paths {
"/url": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: {
status?: "active" | "inactive";
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: never;
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** @enum {string} */
Status: "active" | "inactive";
/** @enum {number} */
ErrorCode: 100 | 101 | 102 | 103 | 104 | 105;
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export const pathsUrlGetParametersQueryStatusValues: ReadonlyArray<paths["/url"]["get"]["parameters"]["query"]["status"]> = ["active", "inactive"];
export const statusValues: ReadonlyArray<components["schemas"]["Status"]> = ["active", "inactive"];
export const errorCodeValues: ReadonlyArray<components["schemas"]["ErrorCode"]> = [100, 101, 102, 103, 104, 105];
export type operations = Record<string, never>;`,
options: { enumValues: true },
},
],
[
"snapshot > GitHub",
{
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-typescript/test/test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const DEFAULT_CTX: GlobalContext = {
},
emptyObjectsUnknown: false,
enum: false,
enumValues: false,
excludeDeprecated: false,
exportType: false,
immutable: false,
Expand Down
Loading