Skip to content

Commit aecaa73

Browse files
authored
feat: enum support (#153)
* feat(config-base): adding useEnums to config * feat(utils): adding getEnumProperties for parsing schemas * feat(generateschematypes): handling enum config in generateSchemaTypes * feat(enum-declaration): splitting enum declaration in its own context * feat(type-declaration): adding enum support to schemaToTypeAliasDeclaration * test(schema-to-enum): adding more test cases * perf(schema-to-type): improving performance when getting schema name * test(snapshots): updating new test snapshots * test(convert-numbers-to-words): adding unit tests for converting numbers into words
1 parent c6a3372 commit aecaa73

File tree

9 files changed

+645
-36
lines changed

9 files changed

+645
-36
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { OpenAPIObject, SchemaObject } from "openapi3-ts";
2+
import ts from "typescript";
3+
import { schemaToEnumDeclaration } from "./schemaToEnumDeclaration";
4+
import { OpenAPIComponentType } from "./schemaToTypeAliasDeclaration";
5+
6+
describe("schemaToTypeAliasDeclaration", () => {
7+
it("should generate a string enums", () => {
8+
const schema: SchemaObject = {
9+
type: "string",
10+
enum: ["AVAILABLE", "PENDING", "SOLD"],
11+
};
12+
13+
expect(printSchema(schema)).toMatchInlineSnapshot(`
14+
"export enum Test {
15+
AVAILABLE = \\"AVAILABLE\\",
16+
PENDING = \\"PENDING\\",
17+
SOLD = \\"SOLD\\"
18+
}"
19+
`);
20+
});
21+
22+
it("should generate a int enum", () => {
23+
const schema: SchemaObject = {
24+
type: "string",
25+
enum: [1, 2, 3],
26+
};
27+
28+
expect(printSchema(schema)).toMatchInlineSnapshot(`
29+
"export enum Test {
30+
ONE = 1,
31+
TWO = 2,
32+
THREE = 3
33+
}"
34+
`);
35+
});
36+
37+
it("should generate a int enum (using big numbers)", () => {
38+
const schema: SchemaObject = {
39+
type: "string",
40+
enum: [0, 7, 15, 100, 1000, 1456, 3217],
41+
};
42+
43+
expect(printSchema(schema)).toMatchInlineSnapshot(`
44+
"export enum Test {
45+
ZERO = 0,
46+
SEVEN = 7,
47+
FIFTEEN = 15,
48+
ONE_HUNDRED = 100,
49+
ONE_THOUSAND = 1000,
50+
ONE_THOUSAND_FOUR_HUNDRED_FIFTY_SIX = 1456,
51+
THREE_THOUSAND_TWO_HUNDRED_SEVENTEEN = 3217
52+
}"
53+
`);
54+
});
55+
56+
it("should generate a boolean enum", () => {
57+
const schema: SchemaObject = {
58+
type: "string",
59+
enum: [true, false],
60+
};
61+
62+
expect(printSchema(schema)).toMatchInlineSnapshot(`
63+
"export enum Test {
64+
True,
65+
False
66+
}"
67+
`);
68+
});
69+
});
70+
71+
const printSchema = (
72+
schema: SchemaObject,
73+
currentComponent: OpenAPIComponentType = "schemas",
74+
components?: OpenAPIObject["components"]
75+
) => {
76+
const nodes = schemaToEnumDeclaration("Test", schema, {
77+
currentComponent,
78+
openAPIDocument: { components },
79+
});
80+
81+
const sourceFile = ts.createSourceFile(
82+
"index.ts",
83+
"",
84+
ts.ScriptTarget.Latest
85+
);
86+
87+
const printer = ts.createPrinter({
88+
newLine: ts.NewLineKind.LineFeed,
89+
removeComments: false,
90+
});
91+
92+
return nodes
93+
.map((node: ts.Node) =>
94+
printer.printNode(ts.EmitHint.Unspecified, node, sourceFile)
95+
)
96+
.join("\n");
97+
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { pascal } from "case";
2+
import { SchemaObject } from "openapi3-ts";
3+
import ts, { factory as f } from "typescript";
4+
import { convertNumberToWord } from "../utils/getEnumProperties";
5+
import { Context, getJSDocComment } from "./schemaToTypeAliasDeclaration";
6+
7+
/**
8+
* Add Enum support when transforming an OpenAPI Schema Object to Typescript Nodes.
9+
*
10+
* @param name Name of the schema
11+
* @param schema OpenAPI Schema object
12+
* @param context Context
13+
*/
14+
export const schemaToEnumDeclaration = (
15+
name: string,
16+
schema: SchemaObject,
17+
context: Context
18+
): ts.Node[] => {
19+
const jsDocNode = getJSDocComment(schema, context);
20+
const members = getEnumMembers(schema, context);
21+
const declarationNode = f.createEnumDeclaration(
22+
[f.createModifier(ts.SyntaxKind.ExportKeyword)],
23+
pascal(name),
24+
members
25+
);
26+
27+
return jsDocNode ? [jsDocNode, declarationNode] : [declarationNode];
28+
};
29+
30+
function getEnumMembers(
31+
schema: SchemaObject,
32+
context: Context
33+
): ts.EnumMember[] {
34+
if (!schema.enum || !Array.isArray(schema.enum)) {
35+
throw new Error(
36+
"The provided schema does not have an 'enum' property or it is not an array."
37+
);
38+
}
39+
40+
return schema.enum.map((enumValue, index) => {
41+
let enumName: string;
42+
let enumValueNode: ts.Expression | undefined = undefined;
43+
44+
if (typeof enumValue === "string") {
45+
enumName = enumValue;
46+
enumValueNode = f.createStringLiteral(enumValue);
47+
} else if (typeof enumValue === "number") {
48+
enumName = convertNumberToWord(enumValue)
49+
.toUpperCase()
50+
.replace(/[-\s]/g, "_");
51+
enumValueNode = f.createNumericLiteral(enumValue);
52+
} else if (typeof enumValue === "boolean") {
53+
enumName = enumValue ? "True" : "False";
54+
} else {
55+
throw new Error(`Unsupported enum value type: ${typeof enumValue}`);
56+
}
57+
58+
return f.createEnumMember(f.createIdentifier(enumName), enumValueNode);
59+
});
60+
}

plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,32 @@ describe("schemaToTypeAliasDeclaration", () => {
6969
);
7070
});
7171

72+
it("should reference to an previously created enum", () => {
73+
const schema: SchemaObject = {
74+
type: "object",
75+
properties: {
76+
status: {
77+
type: "string",
78+
enum: ["AVAILABLE", "PENDING", "SOLD"],
79+
},
80+
},
81+
xml: { name: "pet" },
82+
};
83+
84+
const components: OpenAPIObject["components"] = {
85+
schemas: {
86+
Pet: schema,
87+
},
88+
};
89+
90+
expect(printSchema(schema, "schemas", components, true))
91+
.toMatchInlineSnapshot(`
92+
"export type Test = {
93+
status?: TestStatus;
94+
};"
95+
`);
96+
});
97+
7298
it("should generate nullable enums (strings)", () => {
7399
const schema: SchemaObject = {
74100
type: "string",
@@ -879,12 +905,18 @@ describe("schemaToTypeAliasDeclaration", () => {
879905
const printSchema = (
880906
schema: SchemaObject,
881907
currentComponent: OpenAPIComponentType = "schemas",
882-
components?: OpenAPIObject["components"]
908+
components?: OpenAPIObject["components"],
909+
useEnums?: boolean
883910
) => {
884-
const nodes = schemaToTypeAliasDeclaration("Test", schema, {
885-
currentComponent,
886-
openAPIDocument: { components },
887-
});
911+
const nodes = schemaToTypeAliasDeclaration(
912+
"Test",
913+
schema,
914+
{
915+
currentComponent,
916+
openAPIDocument: { components },
917+
},
918+
useEnums
919+
);
888920

889921
const sourceFile = ts.createSourceFile(
890922
"index.ts",

plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import { findKey, get, merge, intersection, omit } from "lodash";
2-
import { singular } from "pluralize";
1+
import { pascal } from "case";
2+
import { findKey, get, intersection, merge, omit } from "lodash";
33
import {
44
ComponentsObject,
55
DiscriminatorObject,
6-
isReferenceObject,
7-
isSchemaObject,
86
OpenAPIObject,
97
ReferenceObject,
108
SchemaObject,
9+
isReferenceObject,
10+
isSchemaObject,
1111
} from "openapi3-ts";
12-
import ts, { factory as f } from "typescript";
12+
import { singular } from "pluralize";
1313
import { isValidIdentifier } from "tsutils";
14-
import { pascal } from "case";
14+
import ts, { factory as f } from "typescript";
1515
import { getReferenceSchema } from "./getReferenceSchema";
1616

1717
type RemoveIndex<T> = {
@@ -37,24 +37,28 @@ export type Context = {
3737
currentComponent: OpenAPIComponentType | null;
3838
};
3939

40+
let useEnumsConfigBase: boolean | undefined;
41+
4042
/**
4143
* Transform an OpenAPI Schema Object to Typescript Nodes (comment & declaration).
4244
*
43-
* @param name Name of the schema
45+
* @param name Name of the schema
4446
* @param schema OpenAPI Schema object
4547
* @param context Context
4648
*/
4749
export const schemaToTypeAliasDeclaration = (
4850
name: string,
4951
schema: SchemaObject,
50-
context: Context
52+
context: Context,
53+
useEnums?: boolean
5154
): ts.Node[] => {
55+
useEnumsConfigBase = useEnums;
5256
const jsDocNode = getJSDocComment(schema, context);
5357
const declarationNode = f.createTypeAliasDeclaration(
5458
[f.createModifier(ts.SyntaxKind.ExportKeyword)],
5559
pascal(name),
5660
undefined,
57-
getType(schema, context)
61+
getType(schema, context, name)
5862
);
5963

6064
return jsDocNode ? [jsDocNode, declarationNode] : [declarationNode];
@@ -68,7 +72,9 @@ export const schemaToTypeAliasDeclaration = (
6872
*/
6973
export const getType = (
7074
schema: SchemaObject | ReferenceObject,
71-
context: Context
75+
context: Context,
76+
name?: string,
77+
isNodeEnum?: boolean
7278
): ts.TypeNode => {
7379
if (isReferenceObject(schema)) {
7480
const [hash, topLevel, namespace, name] = schema.$ref.split("/");
@@ -126,7 +132,11 @@ export const getType = (
126132
}
127133

128134
if (schema.enum) {
129-
return f.createUnionTypeNode([
135+
if (isNodeEnum) {
136+
return f.createTypeReferenceNode(f.createIdentifier(name || ""));
137+
}
138+
139+
const unionTypes = f.createUnionTypeNode([
130140
...schema.enum.map((value) => {
131141
if (typeof value === "string") {
132142
return f.createLiteralTypeNode(f.createStringLiteral(value));
@@ -143,6 +153,8 @@ export const getType = (
143153
}),
144154
...(schema.nullable ? [f.createLiteralTypeNode(f.createNull())] : []),
145155
]);
156+
157+
return unionTypes;
146158
}
147159

148160
// Handle implicit object
@@ -198,6 +210,8 @@ export const getType = (
198210
const members: ts.TypeElement[] = Object.entries(
199211
schema.properties || {}
200212
).map(([key, property]) => {
213+
const isEnum = "enum" in property && useEnumsConfigBase;
214+
201215
const propertyNode = f.createPropertySignature(
202216
undefined,
203217
isValidIdentifier(key)
@@ -206,7 +220,12 @@ export const getType = (
206220
schema.required?.includes(key)
207221
? undefined
208222
: f.createToken(ts.SyntaxKind.QuestionToken),
209-
getType(property, context)
223+
getType(
224+
property,
225+
context,
226+
`${name}${pascal(key)}`.replace(/[^a-zA-Z0-9 ]/g, ""),
227+
isEnum
228+
)
210229
);
211230
const jsDocNode = getJSDocComment(property, context);
212231
if (jsDocNode) addJSDocToNode(propertyNode, jsDocNode);
@@ -528,7 +547,7 @@ const keysToExpressAsJsDocProperty: Array<keyof RemoveIndex<SchemaObject>> = [
528547
* @param context
529548
* @returns JSDoc node
530549
*/
531-
const getJSDocComment = (
550+
export const getJSDocComment = (
532551
schema: SchemaObject,
533552
context: Context
534553
): ts.JSDoc | undefined => {

0 commit comments

Comments
 (0)