From e6723ff2d94d7e259698bf0c19c4ad203ee18218 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Mon, 6 Jan 2025 18:24:56 +0100 Subject: [PATCH 01/13] Add test case for regressed enum generation --- test/valid-data-other.test.ts | 1 + .../valid-data/enums-string-union/schema.json | 56 ++++++++ test/valid-data/enums-union/main.ts | 16 +++ test/valid-data/enums-union/schema.json | 124 ++++++++++++++++++ 4 files changed, 197 insertions(+) create mode 100644 test/valid-data/enums-string-union/schema.json create mode 100644 test/valid-data/enums-union/main.ts create mode 100644 test/valid-data/enums-union/schema.json diff --git a/test/valid-data-other.test.ts b/test/valid-data-other.test.ts index 2b6ddd181..bcb971627 100644 --- a/test/valid-data-other.test.ts +++ b/test/valid-data-other.test.ts @@ -9,6 +9,7 @@ describe("valid-data-other", () => { it("enums-mixed", assertValidSchema("enums-mixed", "Enum")); it("enums-member", assertValidSchema("enums-member", "MyObject")); it("enums-template-literal", assertValidSchema("enums-template-literal", "MyObject")); + it("enums-union", assertValidSchema("enums-union", "MyObject")); it("function-parameters-default-value", assertValidSchema("function-parameters-default-value", "myFunction")); it("function-parameters-declaration", assertValidSchema("function-parameters-declaration", "myFunction")); diff --git a/test/valid-data/enums-string-union/schema.json b/test/valid-data/enums-string-union/schema.json new file mode 100644 index 000000000..ce8a0cc50 --- /dev/null +++ b/test/valid-data/enums-string-union/schema.json @@ -0,0 +1,56 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "enumMemberWithLiteral": { + "anyOf": [ + { + "const": "alpha", + "type": "string" + }, + { + "const": "foo", + "type": "string" + } + ] + }, + "enumMemberWithLiteralAndNull": { + "anyOf": [ + { + "const": "alpha", + "type": "string" + }, + { + "const": "foo", + "type": "string" + }, + { + "type": "null" + } + ] + }, + "enumMembers": { + "anyOf": [ + { + "const": "alpha", + "type": "string" + }, + { + "const": "beta", + "type": "string" + } + ] + } + }, + "required": [ + "enumMembers", + "enumMemberWithLiteral", + "enumMemberWithLiteralAndNull" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/enums-union/main.ts b/test/valid-data/enums-union/main.ts new file mode 100644 index 000000000..10859a914 --- /dev/null +++ b/test/valid-data/enums-union/main.ts @@ -0,0 +1,16 @@ +enum Alphabet { + Alpha = "alpha", + Beta = "beta", + Omega = 666, +} + +export type MyObject = { + // All the members above should be output as enums, not anyOf + enumMembers: Alphabet.Alpha | Alphabet.Beta; + enumMemberWithLiteral: Alphabet.Alpha | "foo"; + enumMemberWithLiteralAndNull: Alphabet.Alpha | "foo" | null; + enumMembersWithNumber: Alphabet.Alpha | Alphabet.Omega; + wholeEnum: Alphabet; // Should output just all of Alphabet + wholeEnumWithLiteral: Alphabet | "bar"; // Should output all of Alphabet members (2 strings, 1 number) and "bar" + wholeEnumWithLiteralAndNull: Alphabet | "bar" | null; // Smae as above, but with null +}; diff --git a/test/valid-data/enums-union/schema.json b/test/valid-data/enums-union/schema.json new file mode 100644 index 000000000..cc4ecd92f --- /dev/null +++ b/test/valid-data/enums-union/schema.json @@ -0,0 +1,124 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "enumMemberWithLiteral": { + "anyOf": [ + { + "const": "alpha", + "type": "string" + }, + { + "const": "foo", + "type": "string" + } + ] + }, + "enumMemberWithLiteralAndNull": { + "anyOf": [ + { + "const": "alpha", + "type": "string" + }, + { + "const": "foo", + "type": "string" + }, + { + "type": "null" + } + ] + }, + "enumMembers": { + "anyOf": [ + { + "const": "alpha", + "type": "string" + }, + { + "const": "beta", + "type": "string" + } + ] + }, + "enumMembersWithNumber": { + "anyOf": [ + { + "const": "alpha", + "type": "string" + }, + { + "const": 666, + "type": "number" + } + ] + }, + "wholeEnum": { + "enum": [ + "alpha", + "beta", + 666 + ], + "type": [ + "string", + "number" + ] + }, + "wholeEnumWithLiteral": { + "anyOf": [ + { + "enum": [ + "alpha", + "beta", + 666 + ], + "type": [ + "string", + "number" + ] + }, + { + "const": "bar", + "type": "string" + } + ] + }, + "wholeEnumWithLiteralAndNull": { + "anyOf": [ + { + "enum": [ + "alpha", + "beta", + 666 + ], + "type": [ + "string", + "number" + ] + }, + { + "const": "bar", + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "enumMembers", + "enumMemberWithLiteral", + "enumMemberWithLiteralAndNull", + "enumMembersWithNumber", + "wholeEnum", + "wholeEnumWithLiteral", + "wholeEnumWithLiteralAndNull" + ], + "type": "object" + } + } +} From ff834ed75361aea03cea81f404559610179df7d8 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Mon, 6 Jan 2025 18:42:37 +0100 Subject: [PATCH 02/13] Run LiteralUnionTypeFormatter on unions including enum members --- .../LiteralUnionTypeFormatter.ts | 25 +++- test/valid-data/enums-union/main.ts | 2 +- test/valid-data/enums-union/schema.json | 121 +++++++----------- 3 files changed, 64 insertions(+), 84 deletions(-) diff --git a/src/TypeFormatter/LiteralUnionTypeFormatter.ts b/src/TypeFormatter/LiteralUnionTypeFormatter.ts index 937d2a0ed..67e402d08 100644 --- a/src/TypeFormatter/LiteralUnionTypeFormatter.ts +++ b/src/TypeFormatter/LiteralUnionTypeFormatter.ts @@ -2,6 +2,7 @@ import { Definition } from "../Schema/Definition.js"; import { RawTypeName } from "../Schema/RawType.js"; import { SubTypeFormatter } from "../SubTypeFormatter.js"; import { BaseType } from "../Type/BaseType.js"; +import { EnumType } from "../Type/EnumType.js"; import { LiteralType, LiteralValue } from "../Type/LiteralType.js"; import { NullType } from "../Type/NullType.js"; import { StringType } from "../Type/StringType.js"; @@ -43,8 +44,8 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter { }; } - const values = uniqueArray(types.map(getLiteralValue)); - const typeNames = uniqueArray(types.map(getLiteralType)); + const values = uniqueArray(types.flatMap(getLiteralValues)); + const typeNames = uniqueArray(types.flatMap(getLiteralTypes)); const ret = { type: typeNames.length === 1 ? typeNames[0] : typeNames, @@ -72,13 +73,23 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter { export function isLiteralUnion(type: UnionType): boolean { return type .getFlattenedTypes() - .every((item) => item instanceof LiteralType || item instanceof NullType || item instanceof StringType); + .every( + (item) => + item instanceof LiteralType || + item instanceof NullType || + item instanceof StringType || + item instanceof EnumType, + ); } -function getLiteralValue(value: LiteralType | NullType): LiteralValue | null { - return value instanceof LiteralType ? value.getValue() : null; +function getLiteralValues(value: LiteralType | EnumType | NullType): readonly (LiteralValue | null)[] { + return value instanceof LiteralType ? [value.getValue()] : value instanceof EnumType ? value.getValues() : [null]; } -function getLiteralType(value: LiteralType | NullType): RawTypeName { - return value instanceof LiteralType ? typeName(value.getValue()) : "null"; +function getLiteralTypes(value: LiteralType | EnumType | NullType): RawTypeName[] { + return value instanceof LiteralType + ? [typeName(value.getValue())] + : value instanceof EnumType + ? value.getValues().map(typeName) + : ["null"]; } diff --git a/test/valid-data/enums-union/main.ts b/test/valid-data/enums-union/main.ts index 10859a914..82c8aa5ea 100644 --- a/test/valid-data/enums-union/main.ts +++ b/test/valid-data/enums-union/main.ts @@ -12,5 +12,5 @@ export type MyObject = { enumMembersWithNumber: Alphabet.Alpha | Alphabet.Omega; wholeEnum: Alphabet; // Should output just all of Alphabet wholeEnumWithLiteral: Alphabet | "bar"; // Should output all of Alphabet members (2 strings, 1 number) and "bar" - wholeEnumWithLiteralAndNull: Alphabet | "bar" | null; // Smae as above, but with null + wholeEnumWithLiteralAndNull: Alphabet | "bar" | null; }; diff --git a/test/valid-data/enums-union/schema.json b/test/valid-data/enums-union/schema.json index cc4ecd92f..15312c271 100644 --- a/test/valid-data/enums-union/schema.json +++ b/test/valid-data/enums-union/schema.json @@ -6,54 +6,38 @@ "additionalProperties": false, "properties": { "enumMemberWithLiteral": { - "anyOf": [ - { - "const": "alpha", - "type": "string" - }, - { - "const": "foo", - "type": "string" - } - ] + "enum": [ + "alpha", + "foo" + ], + "type": "string" }, "enumMemberWithLiteralAndNull": { - "anyOf": [ - { - "const": "alpha", - "type": "string" - }, - { - "const": "foo", - "type": "string" - }, - { - "type": "null" - } + "enum": [ + "alpha", + "foo", + null + ], + "type": [ + "string", + "null" ] }, "enumMembers": { - "anyOf": [ - { - "const": "alpha", - "type": "string" - }, - { - "const": "beta", - "type": "string" - } - ] + "enum": [ + "alpha", + "beta" + ], + "type": "string" }, "enumMembersWithNumber": { - "anyOf": [ - { - "const": "alpha", - "type": "string" - }, - { - "const": 666, - "type": "number" - } + "enum": [ + "alpha", + 666 + ], + "type": [ + "string", + "number" ] }, "wholeEnum": { @@ -68,44 +52,29 @@ ] }, "wholeEnumWithLiteral": { - "anyOf": [ - { - "enum": [ - "alpha", - "beta", - 666 - ], - "type": [ - "string", - "number" - ] - }, - { - "const": "bar", - "type": "string" - } + "enum": [ + "alpha", + "beta", + 666, + "bar" + ], + "type": [ + "string", + "number" ] }, "wholeEnumWithLiteralAndNull": { - "anyOf": [ - { - "enum": [ - "alpha", - "beta", - 666 - ], - "type": [ - "string", - "number" - ] - }, - { - "const": "bar", - "type": "string" - }, - { - "type": "null" - } + "enum": [ + "alpha", + "beta", + 666, + "bar", + null + ], + "type": [ + "string", + "number", + "null" ] } }, From 09a9abb82146391785bcf3aa3048df5b41c3f081 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Mon, 6 Jan 2025 22:33:46 +0100 Subject: [PATCH 03/13] Delete leftover schema.json --- .../valid-data/enums-string-union/schema.json | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 test/valid-data/enums-string-union/schema.json diff --git a/test/valid-data/enums-string-union/schema.json b/test/valid-data/enums-string-union/schema.json deleted file mode 100644 index ce8a0cc50..000000000 --- a/test/valid-data/enums-string-union/schema.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "$ref": "#/definitions/MyObject", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "MyObject": { - "additionalProperties": false, - "properties": { - "enumMemberWithLiteral": { - "anyOf": [ - { - "const": "alpha", - "type": "string" - }, - { - "const": "foo", - "type": "string" - } - ] - }, - "enumMemberWithLiteralAndNull": { - "anyOf": [ - { - "const": "alpha", - "type": "string" - }, - { - "const": "foo", - "type": "string" - }, - { - "type": "null" - } - ] - }, - "enumMembers": { - "anyOf": [ - { - "const": "alpha", - "type": "string" - }, - { - "const": "beta", - "type": "string" - } - ] - } - }, - "required": [ - "enumMembers", - "enumMemberWithLiteral", - "enumMemberWithLiteralAndNull" - ], - "type": "object" - } - } -} From 5b71f3dd9dc8d1bbb5d9085618bf818338286b1f Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Mon, 6 Jan 2025 23:00:46 +0100 Subject: [PATCH 04/13] Add extra test cases --- test/valid-data/enums-union/main.ts | 10 +++++ test/valid-data/enums-union/schema.json | 50 ++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/test/valid-data/enums-union/main.ts b/test/valid-data/enums-union/main.ts index 82c8aa5ea..523220a6a 100644 --- a/test/valid-data/enums-union/main.ts +++ b/test/valid-data/enums-union/main.ts @@ -4,13 +4,23 @@ enum Alphabet { Omega = 666, } +enum FileAccess { + None, + Read = 1 << 1, + Write = 1 << 2, + ReadWrite = Read | Write, +} + export type MyObject = { // All the members above should be output as enums, not anyOf enumMembers: Alphabet.Alpha | Alphabet.Beta; enumMemberWithLiteral: Alphabet.Alpha | "foo"; enumMemberWithLiteralAndNull: Alphabet.Alpha | "foo" | null; + enumMemberWithInterface: Alphabet.Alpha | { abc: string }; enumMembersWithNumber: Alphabet.Alpha | Alphabet.Omega; wholeEnum: Alphabet; // Should output just all of Alphabet wholeEnumWithLiteral: Alphabet | "bar"; // Should output all of Alphabet members (2 strings, 1 number) and "bar" wholeEnumWithLiteralAndNull: Alphabet | "bar" | null; + twoDifferentEnumMembers: Alphabet.Alpha | FileAccess.Read; + twoDifferentWholeEnums: Alphabet | FileAccess; }; diff --git a/test/valid-data/enums-union/schema.json b/test/valid-data/enums-union/schema.json index 15312c271..1e6b686fe 100644 --- a/test/valid-data/enums-union/schema.json +++ b/test/valid-data/enums-union/schema.json @@ -5,6 +5,26 @@ "MyObject": { "additionalProperties": false, "properties": { + "enumMemberWithInterface": { + "anyOf": [ + { + "const": "alpha", + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "abc": { + "type": "string" + } + }, + "required": [ + "abc" + ], + "type": "object" + } + ] + }, "enumMemberWithLiteral": { "enum": [ "alpha", @@ -40,6 +60,31 @@ "number" ] }, + "twoDifferentEnumMembers": { + "enum": [ + "alpha", + 2 + ], + "type": [ + "string", + "number" + ] + }, + "twoDifferentWholeEnums": { + "enum": [ + "alpha", + "beta", + 666, + 0, + 2, + 4, + 6 + ], + "type": [ + "string", + "number" + ] + }, "wholeEnum": { "enum": [ "alpha", @@ -82,10 +127,13 @@ "enumMembers", "enumMemberWithLiteral", "enumMemberWithLiteralAndNull", + "enumMemberWithInterface", "enumMembersWithNumber", "wholeEnum", "wholeEnumWithLiteral", - "wholeEnumWithLiteralAndNull" + "wholeEnumWithLiteralAndNull", + "twoDifferentEnumMembers", + "twoDifferentWholeEnums" ], "type": "object" } From 680f2dc6c1004cbdb2d2b0bca50e4e73a0b53e88 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Mon, 6 Jan 2025 23:01:15 +0100 Subject: [PATCH 05/13] Improve clarity of code branches --- src/TypeFormatter/LiteralUnionTypeFormatter.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/TypeFormatter/LiteralUnionTypeFormatter.ts b/src/TypeFormatter/LiteralUnionTypeFormatter.ts index 67e402d08..a5fd8c9a6 100644 --- a/src/TypeFormatter/LiteralUnionTypeFormatter.ts +++ b/src/TypeFormatter/LiteralUnionTypeFormatter.ts @@ -83,13 +83,19 @@ export function isLiteralUnion(type: UnionType): boolean { } function getLiteralValues(value: LiteralType | EnumType | NullType): readonly (LiteralValue | null)[] { - return value instanceof LiteralType ? [value.getValue()] : value instanceof EnumType ? value.getValues() : [null]; + if (value instanceof EnumType) { + return value.getValues(); + } else if (value instanceof LiteralType) { + return [value.getValue()]; + } + return [null]; } function getLiteralTypes(value: LiteralType | EnumType | NullType): RawTypeName[] { - return value instanceof LiteralType - ? [typeName(value.getValue())] - : value instanceof EnumType - ? value.getValues().map(typeName) - : ["null"]; + if (value instanceof EnumType) { + return value.getValues().map(typeName); + } else if (value instanceof LiteralType) { + return [typeName(value.getValue())]; + } + return ["null"]; } From a84c527fdb06f61ff02185d38ea904b0411181e3 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette <47537704+arthurfiorette@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:54:17 -0300 Subject: [PATCH 06/13] Update src/TypeFormatter/LiteralUnionTypeFormatter.ts --- src/TypeFormatter/LiteralUnionTypeFormatter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/TypeFormatter/LiteralUnionTypeFormatter.ts b/src/TypeFormatter/LiteralUnionTypeFormatter.ts index a5fd8c9a6..a892860bf 100644 --- a/src/TypeFormatter/LiteralUnionTypeFormatter.ts +++ b/src/TypeFormatter/LiteralUnionTypeFormatter.ts @@ -94,7 +94,9 @@ function getLiteralValues(value: LiteralType | EnumType | NullType): readonly (L function getLiteralTypes(value: LiteralType | EnumType | NullType): RawTypeName[] { if (value instanceof EnumType) { return value.getValues().map(typeName); - } else if (value instanceof LiteralType) { + } + + if (value instanceof LiteralType) { return [typeName(value.getValue())]; } return ["null"]; From 38f7f3db958fd9f29cd540d633e9b6a029d8f5a1 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette <47537704+arthurfiorette@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:54:23 -0300 Subject: [PATCH 07/13] Update src/TypeFormatter/LiteralUnionTypeFormatter.ts --- src/TypeFormatter/LiteralUnionTypeFormatter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/TypeFormatter/LiteralUnionTypeFormatter.ts b/src/TypeFormatter/LiteralUnionTypeFormatter.ts index a892860bf..600ba5db2 100644 --- a/src/TypeFormatter/LiteralUnionTypeFormatter.ts +++ b/src/TypeFormatter/LiteralUnionTypeFormatter.ts @@ -85,7 +85,9 @@ export function isLiteralUnion(type: UnionType): boolean { function getLiteralValues(value: LiteralType | EnumType | NullType): readonly (LiteralValue | null)[] { if (value instanceof EnumType) { return value.getValues(); - } else if (value instanceof LiteralType) { + } + + if (value instanceof LiteralType) { return [value.getValue()]; } return [null]; From eb67a52c4a8ab4d3e8e2848c33f5219f382d011e Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Tue, 7 Jan 2025 16:00:03 -0300 Subject: [PATCH 08/13] fix: linting issues --- .../LiteralUnionTypeFormatter.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/TypeFormatter/LiteralUnionTypeFormatter.ts b/src/TypeFormatter/LiteralUnionTypeFormatter.ts index 600ba5db2..9174acfa5 100644 --- a/src/TypeFormatter/LiteralUnionTypeFormatter.ts +++ b/src/TypeFormatter/LiteralUnionTypeFormatter.ts @@ -1,9 +1,9 @@ -import { Definition } from "../Schema/Definition.js"; -import { RawTypeName } from "../Schema/RawType.js"; -import { SubTypeFormatter } from "../SubTypeFormatter.js"; -import { BaseType } from "../Type/BaseType.js"; +import type { Definition } from "../Schema/Definition.js"; +import type { RawTypeName } from "../Schema/RawType.js"; +import type { SubTypeFormatter } from "../SubTypeFormatter.js"; +import type { BaseType } from "../Type/BaseType.js"; import { EnumType } from "../Type/EnumType.js"; -import { LiteralType, LiteralValue } from "../Type/LiteralType.js"; +import { LiteralType, type LiteralValue } from "../Type/LiteralType.js"; import { NullType } from "../Type/NullType.js"; import { StringType } from "../Type/StringType.js"; import { UnionType } from "../Type/UnionType.js"; @@ -28,10 +28,14 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter { hasString = true; preserveLiterals = preserveLiterals || t.getPreserveLiterals(); return false; - } else if (t instanceof NullType) { + } + + if (t instanceof NullType) { hasNull = true; return true; - } else if (t instanceof LiteralType && !t.isString()) { + } + + if (t instanceof LiteralType && !t.isString()) { allStrings = false; } @@ -86,7 +90,7 @@ function getLiteralValues(value: LiteralType | EnumType | NullType): readonly (L if (value instanceof EnumType) { return value.getValues(); } - + if (value instanceof LiteralType) { return [value.getValue()]; } @@ -97,7 +101,7 @@ function getLiteralTypes(value: LiteralType | EnumType | NullType): RawTypeName[ if (value instanceof EnumType) { return value.getValues().map(typeName); } - + if (value instanceof LiteralType) { return [typeName(value.getValue())]; } From b88a1511e71af88a84cff1aab3c7e112ca6b96a6 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Tue, 7 Jan 2025 16:00:17 -0300 Subject: [PATCH 09/13] fix: remove inplicit statement --- test/valid-data/enums-union/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/valid-data/enums-union/main.ts b/test/valid-data/enums-union/main.ts index 523220a6a..7e768f474 100644 --- a/test/valid-data/enums-union/main.ts +++ b/test/valid-data/enums-union/main.ts @@ -5,7 +5,7 @@ enum Alphabet { } enum FileAccess { - None, + None = 0, Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write, From 6ae6379d4c92a79918d5b135281f0e5a8c940b58 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Tue, 7 Jan 2025 16:00:41 -0300 Subject: [PATCH 10/13] feat: added exported test cases --- test/valid-data-other.test.ts | 1 + test/valid-data/exported-enums-union/main.ts | 26 ++++ .../exported-enums-union/schema.json | 147 ++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 test/valid-data/exported-enums-union/main.ts create mode 100644 test/valid-data/exported-enums-union/schema.json diff --git a/test/valid-data-other.test.ts b/test/valid-data-other.test.ts index bcb971627..f41f09ff7 100644 --- a/test/valid-data-other.test.ts +++ b/test/valid-data-other.test.ts @@ -10,6 +10,7 @@ describe("valid-data-other", () => { it("enums-member", assertValidSchema("enums-member", "MyObject")); it("enums-template-literal", assertValidSchema("enums-template-literal", "MyObject")); it("enums-union", assertValidSchema("enums-union", "MyObject")); + it("exported-enums-union", assertValidSchema("exported-enums-union", "MyObject")); it("function-parameters-default-value", assertValidSchema("function-parameters-default-value", "myFunction")); it("function-parameters-declaration", assertValidSchema("function-parameters-declaration", "myFunction")); diff --git a/test/valid-data/exported-enums-union/main.ts b/test/valid-data/exported-enums-union/main.ts new file mode 100644 index 000000000..797602dce --- /dev/null +++ b/test/valid-data/exported-enums-union/main.ts @@ -0,0 +1,26 @@ +export enum Alphabet { + Alpha = "alpha", + Beta = "beta", + Omega = 666, +} + +export enum FileAccess { + None = 0, + Read = 1 << 1, + Write = 1 << 2, + ReadWrite = Read | Write, +} + +export type MyObject = { + // All the members above should be output as enums, not anyOf + enumMembers: Alphabet.Alpha | Alphabet.Beta; + enumMemberWithLiteral: Alphabet.Alpha | "foo"; + enumMemberWithLiteralAndNull: Alphabet.Alpha | "foo" | null; + enumMemberWithInterface: Alphabet.Alpha | { abc: string }; + enumMembersWithNumber: Alphabet.Alpha | Alphabet.Omega; + wholeEnum: Alphabet; // Should output just all of Alphabet + wholeEnumWithLiteral: Alphabet | "bar"; // Should output all of Alphabet members (2 strings, 1 number) and "bar" + wholeEnumWithLiteralAndNull: Alphabet | "bar" | null; + twoDifferentEnumMembers: Alphabet.Alpha | FileAccess.Read; + twoDifferentWholeEnums: Alphabet | FileAccess; +}; diff --git a/test/valid-data/exported-enums-union/schema.json b/test/valid-data/exported-enums-union/schema.json new file mode 100644 index 000000000..994331bb2 --- /dev/null +++ b/test/valid-data/exported-enums-union/schema.json @@ -0,0 +1,147 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Alphabet": { + "enum": [ + "alpha", + "beta", + 666 + ], + "type": [ + "string", + "number" + ] + }, + "FileAccess": { + "enum": [ + 0, + 2, + 4, + 6 + ], + "type": "number" + }, + "MyObject": { + "additionalProperties": false, + "properties": { + "enumMemberWithInterface": { + "anyOf": [ + { + "const": "alpha", + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "abc": { + "type": "string" + } + }, + "required": [ + "abc" + ], + "type": "object" + } + ] + }, + "enumMemberWithLiteral": { + "enum": [ + "alpha", + "foo" + ], + "type": "string" + }, + "enumMemberWithLiteralAndNull": { + "enum": [ + "alpha", + "foo", + null + ], + "type": [ + "string", + "null" + ] + }, + "enumMembers": { + "enum": [ + "alpha", + "beta" + ], + "type": "string" + }, + "enumMembersWithNumber": { + "enum": [ + "alpha", + 666 + ], + "type": [ + "string", + "number" + ] + }, + "twoDifferentEnumMembers": { + "enum": [ + "alpha", + 2 + ], + "type": [ + "string", + "number" + ] + }, + "twoDifferentWholeEnums": { + "anyOf": [ + { + "$ref": "#/definitions/Alphabet" + }, + { + "$ref": "#/definitions/FileAccess" + } + ] + }, + "wholeEnum": { + "$ref": "#/definitions/Alphabet" + }, + "wholeEnumWithLiteral": { + "anyOf": [ + { + "$ref": "#/definitions/Alphabet" + }, + { + "const": "bar", + "type": "string" + } + ] + }, + "wholeEnumWithLiteralAndNull": { + "anyOf": [ + { + "$ref": "#/definitions/Alphabet" + }, + { + "const": "bar", + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "enumMembers", + "enumMemberWithLiteral", + "enumMemberWithLiteralAndNull", + "enumMemberWithInterface", + "enumMembersWithNumber", + "wholeEnum", + "wholeEnumWithLiteral", + "wholeEnumWithLiteralAndNull", + "twoDifferentEnumMembers", + "twoDifferentWholeEnums" + ], + "type": "object" + } + } +} From 351e144c059c0352a1157f1daf0860405ae50ceb Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Tue, 7 Jan 2025 16:20:39 -0300 Subject: [PATCH 11/13] refactor: sets and for loop --- .../LiteralUnionTypeFormatter.ts | 74 ++++++++----------- src/Utils/uniqueArray.ts | 8 +- 2 files changed, 31 insertions(+), 51 deletions(-) diff --git a/src/TypeFormatter/LiteralUnionTypeFormatter.ts b/src/TypeFormatter/LiteralUnionTypeFormatter.ts index 9174acfa5..e719590b5 100644 --- a/src/TypeFormatter/LiteralUnionTypeFormatter.ts +++ b/src/TypeFormatter/LiteralUnionTypeFormatter.ts @@ -8,12 +8,12 @@ import { NullType } from "../Type/NullType.js"; import { StringType } from "../Type/StringType.js"; import { UnionType } from "../Type/UnionType.js"; import { typeName } from "../Utils/typeName.js"; -import { uniqueArray } from "../Utils/uniqueArray.js"; export class LiteralUnionTypeFormatter implements SubTypeFormatter { public supportsType(type: BaseType): boolean { return type instanceof UnionType && type.getTypes().length > 0 && isLiteralUnion(type); } + public getDefinition(type: UnionType): Definition { let hasString = false; let preserveLiterals = false; @@ -26,7 +26,7 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter { const types = literals.filter((t) => { if (t instanceof StringType) { hasString = true; - preserveLiterals = preserveLiterals || t.getPreserveLiterals(); + preserveLiterals ||= t.getPreserveLiterals(); return false; } @@ -43,33 +43,41 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter { }); if (allStrings && hasString && !preserveLiterals) { - return { - type: hasNull ? ["string", "null"] : "string", - }; + return hasNull ? { type: ["string", "null"] } : { type: "string" }; } - const values = uniqueArray(types.flatMap(getLiteralValues)); - const typeNames = uniqueArray(types.flatMap(getLiteralTypes)); + const typeValues: Set = new Set(); + const typeNames: Set = new Set(); - const ret = { - type: typeNames.length === 1 ? typeNames[0] : typeNames, - enum: values, - }; + for (const type of types) { + if (type instanceof EnumType) { + for (const value of type.getValues()) { + typeValues.add(value); + typeNames.add(typeName(value)); + } + + continue; + } + + if (type instanceof LiteralType) { + typeValues.add(type.getValue()); + typeNames.add(typeName(type.getValue())); + continue; + } - if (preserveLiterals) { - return { - anyOf: [ - { - type: "string", - }, - ret, - ], - }; + typeValues.add(null); + typeNames.add("null"); } - return ret; + const schema = { + type: typeNames.size === 1 ? typeNames.values().next().value : Array.from(typeNames), + enum: Array.from(typeValues), + }; + + return preserveLiterals ? { anyOf: [{ type: "string" }, schema] } : schema; } - public getChildren(type: UnionType): BaseType[] { + + public getChildren(): BaseType[] { return []; } } @@ -85,25 +93,3 @@ export function isLiteralUnion(type: UnionType): boolean { item instanceof EnumType, ); } - -function getLiteralValues(value: LiteralType | EnumType | NullType): readonly (LiteralValue | null)[] { - if (value instanceof EnumType) { - return value.getValues(); - } - - if (value instanceof LiteralType) { - return [value.getValue()]; - } - return [null]; -} - -function getLiteralTypes(value: LiteralType | EnumType | NullType): RawTypeName[] { - if (value instanceof EnumType) { - return value.getValues().map(typeName); - } - - if (value instanceof LiteralType) { - return [typeName(value.getValue())]; - } - return ["null"]; -} diff --git a/src/Utils/uniqueArray.ts b/src/Utils/uniqueArray.ts index b20a18031..120bb9c17 100644 --- a/src/Utils/uniqueArray.ts +++ b/src/Utils/uniqueArray.ts @@ -1,9 +1,3 @@ export function uniqueArray(array: readonly T[]): T[] { - return array.reduce((result: T[], item: T) => { - if (!result.includes(item)) { - result.push(item); - } - - return result; - }, []); + return array.filter((value, index) => array.indexOf(value) === index); } From 18fe0972974ce6ed65798258bd0276a6ad13f30c Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Tue, 7 Jan 2025 16:22:45 -0300 Subject: [PATCH 12/13] style: lint --- src/TypeFormatter/LiteralUnionTypeFormatter.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/TypeFormatter/LiteralUnionTypeFormatter.ts b/src/TypeFormatter/LiteralUnionTypeFormatter.ts index e719590b5..d4803c9f3 100644 --- a/src/TypeFormatter/LiteralUnionTypeFormatter.ts +++ b/src/TypeFormatter/LiteralUnionTypeFormatter.ts @@ -14,28 +14,28 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter { return type instanceof UnionType && type.getTypes().length > 0 && isLiteralUnion(type); } - public getDefinition(type: UnionType): Definition { + public getDefinition(unionType: UnionType): Definition { let hasString = false; let preserveLiterals = false; let allStrings = true; let hasNull = false; - const literals = type.getFlattenedTypes(); + const literals = unionType.getFlattenedTypes(); // filter out String types since we need to be more careful about them - const types = literals.filter((t) => { - if (t instanceof StringType) { + const types = literals.filter((literal) => { + if (literal instanceof StringType) { hasString = true; - preserveLiterals ||= t.getPreserveLiterals(); + preserveLiterals ||= literal.getPreserveLiterals(); return false; } - if (t instanceof NullType) { + if (literal instanceof NullType) { hasNull = true; return true; } - if (t instanceof LiteralType && !t.isString()) { + if (literal instanceof LiteralType && !literal.isString()) { allStrings = false; } From 436548391b6d1b8ef2175c958c07ca1e4a0774b8 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Tue, 7 Jan 2025 16:34:16 -0300 Subject: [PATCH 13/13] refactor: separate methods --- src/TypeFormatter/EnumTypeFormatter.ts | 16 ++++- .../LiteralUnionTypeFormatter.ts | 62 +++++++++++++------ 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/TypeFormatter/EnumTypeFormatter.ts b/src/TypeFormatter/EnumTypeFormatter.ts index b720f491d..e8a986563 100644 --- a/src/TypeFormatter/EnumTypeFormatter.ts +++ b/src/TypeFormatter/EnumTypeFormatter.ts @@ -1,3 +1,4 @@ +import type { JSONSchema7TypeName } from "json-schema"; import type { Definition } from "../Schema/Definition.js"; import type { SubTypeFormatter } from "../SubTypeFormatter.js"; import type { BaseType } from "../Type/BaseType.js"; @@ -17,11 +18,20 @@ export class EnumTypeFormatter implements SubTypeFormatter { // However, this formatter is used both for enum members and enum types, // so the side effect is that an enum type that contains just a single // value is represented as "const" too. - return values.length === 1 - ? { type: types[0], const: values[0] } - : { type: types.length === 1 ? types[0] : types, enum: values }; + return values.length === 1 ? { type: types[0], const: values[0] } : { type: toEnumType(types), enum: values }; } public getChildren(type: EnumType): BaseType[] { return []; } } + +/** + * Unwraps the array if it contains only one type. + */ +export function toEnumType(types: JSONSchema7TypeName[]) { + if (types.length === 1) { + return types[0]; + } + + return types; +} diff --git a/src/TypeFormatter/LiteralUnionTypeFormatter.ts b/src/TypeFormatter/LiteralUnionTypeFormatter.ts index d4803c9f3..e6f69df00 100644 --- a/src/TypeFormatter/LiteralUnionTypeFormatter.ts +++ b/src/TypeFormatter/LiteralUnionTypeFormatter.ts @@ -8,6 +8,7 @@ import { NullType } from "../Type/NullType.js"; import { StringType } from "../Type/StringType.js"; import { UnionType } from "../Type/UnionType.js"; import { typeName } from "../Utils/typeName.js"; +import { toEnumType } from "./EnumTypeFormatter.js"; export class LiteralUnionTypeFormatter implements SubTypeFormatter { public supportsType(type: BaseType): boolean { @@ -50,27 +51,12 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter { const typeNames: Set = new Set(); for (const type of types) { - if (type instanceof EnumType) { - for (const value of type.getValues()) { - typeValues.add(value); - typeNames.add(typeName(value)); - } - - continue; - } - - if (type instanceof LiteralType) { - typeValues.add(type.getValue()); - typeNames.add(typeName(type.getValue())); - continue; - } - - typeValues.add(null); - typeNames.add("null"); + appendTypeNames(type, typeNames); + appendTypeValues(type, typeValues); } const schema = { - type: typeNames.size === 1 ? typeNames.values().next().value : Array.from(typeNames), + type: toEnumType(Array.from(typeNames)), enum: Array.from(typeValues), }; @@ -93,3 +79,43 @@ export function isLiteralUnion(type: UnionType): boolean { item instanceof EnumType, ); } + +/** + * Appends all possible type names of a type to the given set. + */ +function appendTypeNames(type: BaseType, names: Set) { + if (type instanceof EnumType) { + for (const value of type.getValues()) { + names.add(typeName(value)); + } + + return; + } + + if (type instanceof LiteralType) { + names.add(typeName(type.getValue())); + return; + } + + names.add(typeName(null)); +} + +/** + * Appends all possible values of a type to the given set. + */ +function appendTypeValues(type: BaseType, values: Set) { + if (type instanceof EnumType) { + for (const value of type.getValues()) { + values.add(value); + } + + return; + } + + if (type instanceof LiteralType) { + values.add(type.getValue()); + return; + } + + values.add(null); +}