From 2cf4215a877749803751344da9e5f4e8ecafc4a1 Mon Sep 17 00:00:00 2001 From: Derrick Pang Date: Wed, 17 Apr 2024 12:53:32 -0700 Subject: [PATCH 1/2] Fix for union types of union types in Records. Existing code was just allowing LiteralType through. --- src/NodeParser/MappedTypeNodeParser.ts | 32 +++++++++++++++ test/config.test.ts | 1 + test/valid-data-type.test.ts | 1 + .../type-mapped-union-union/main.ts | 6 +++ .../type-mapped-union-union/schema.json | 40 +++++++++++++++++++ 5 files changed, 80 insertions(+) create mode 100644 test/valid-data/type-mapped-union-union/main.ts create mode 100644 test/valid-data/type-mapped-union-union/schema.json diff --git a/src/NodeParser/MappedTypeNodeParser.ts b/src/NodeParser/MappedTypeNodeParser.ts index cba966942..4df61e831 100644 --- a/src/NodeParser/MappedTypeNodeParser.ts +++ b/src/NodeParser/MappedTypeNodeParser.ts @@ -18,6 +18,7 @@ import { derefAnnotatedType, derefType } from "../Utils/derefType.js"; import { getKey } from "../Utils/nodeKey.js"; import { preserveAnnotation } from "../Utils/preserveAnnotation.js"; import { removeUndefined } from "../Utils/removeUndefined.js"; +import { AliasType } from "../Type/AliasType.js"; export class MappedTypeNodeParser implements SubNodeParser { public constructor( @@ -104,7 +105,38 @@ export class MappedTypeNodeParser implements SubNodeParser { protected getProperties(node: ts.MappedTypeNode, keyListType: UnionType, context: Context): ObjectProperty[] { return keyListType .getTypes() + .flatMap((type) => { + if (type instanceof LiteralType) { + return type; + } else if (type instanceof AliasType) { + const itemsToProcess = [type.getType()]; + const processedTypes = []; + while (itemsToProcess.length > 0) { + const currentType = itemsToProcess[0]; + if (currentType instanceof LiteralType) { + processedTypes.push(currentType); + } else if (currentType instanceof AliasType) { + itemsToProcess.push(currentType.getType()); + } else if (currentType instanceof UnionType) { + itemsToProcess.push(...currentType.getTypes()); + } + itemsToProcess.shift(); + } + return processedTypes; + } + return []; + }) .filter((type): type is LiteralType => type instanceof LiteralType) + .reduce((acc: LiteralType[], curr: LiteralType) => { + if ( + acc.findIndex((val: LiteralType) => { + return val.getId() === curr.getId(); + }) < 0 + ) { + acc.push(curr); + } + return acc; + }, []) .map((type) => [type, this.mapKey(node, type, context)]) .filter((value): value is [LiteralType, LiteralType] => value[1] instanceof LiteralType) .reduce((result: ObjectProperty[], [key, mappedKey]: [LiteralType, LiteralType]) => { diff --git a/test/config.test.ts b/test/config.test.ts index a9fc245ab..e82473826 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -64,6 +64,7 @@ function assertSchema( // skip full check if we are not encoding refs validateFormats: config.encodeRefs === false ? undefined : true, keywords: config.markdownDescription ? ["markdownDescription"] : undefined, + allowUnionTypes: true, }); addFormats(validator); diff --git a/test/valid-data-type.test.ts b/test/valid-data-type.test.ts index d91be0ac2..1f2192447 100644 --- a/test/valid-data-type.test.ts +++ b/test/valid-data-type.test.ts @@ -102,6 +102,7 @@ describe("valid-data-type", () => { it("type-mapped-additional-props", assertValidSchema("type-mapped-additional-props", "MyObject")); it("type-mapped-array", assertValidSchema("type-mapped-array", "MyObject")); it("type-mapped-union-intersection", assertValidSchema("type-mapped-union-intersection", "MyObject")); + it("type-mapped-union-union", assertValidSchema("type-mapped-union-union", "MyType")); it("type-mapped-enum", assertValidSchema("type-mapped-enum", "MyObject")); it("type-mapped-enum-optional", assertValidSchema("type-mapped-enum-optional", "MyObject")); it("type-mapped-enum-null", assertValidSchema("type-mapped-enum-null", "MyObject")); diff --git a/test/valid-data/type-mapped-union-union/main.ts b/test/valid-data/type-mapped-union-union/main.ts new file mode 100644 index 000000000..54c23563c --- /dev/null +++ b/test/valid-data/type-mapped-union-union/main.ts @@ -0,0 +1,6 @@ +type MyType1 = "s1"; +type MyType2 = MyType1 | "s2" | "s3"; +type MyType3 = MyType2 | "s4" | "s5"; +type MyType10 = MyType3 | MyType2 | 's6'; + +export type MyType = Record; diff --git a/test/valid-data/type-mapped-union-union/schema.json b/test/valid-data/type-mapped-union-union/schema.json new file mode 100644 index 000000000..720a444a4 --- /dev/null +++ b/test/valid-data/type-mapped-union-union/schema.json @@ -0,0 +1,40 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "additionalProperties": { + "type": "string" + }, + "properties": { + "s1": { + "type": "string" + }, + "s2": { + "type": "string" + }, + "s3": { + "type": "string" + }, + "s4": { + "type": "string" + }, + "s5": { + "type": "string" + }, + "s6": { + "type": "string" + } + }, + "required": [ + "s4", + "s5", + "s2", + "s3", + "s1", + "s6" + ], + "type": "object" + } + } +} From 4d5af8078d407806d8b41ff14dec9556bfc7cdb0 Mon Sep 17 00:00:00 2001 From: Derrick Pang Date: Sat, 20 Apr 2024 12:14:35 -0700 Subject: [PATCH 2/2] Moved flattening of types into UnionType class and removed logic from MappedTypeNodeParser. --- src/NodeParser/MappedTypeNodeParser.ts | 34 +---------------------- src/Type/UnionType.ts | 37 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/NodeParser/MappedTypeNodeParser.ts b/src/NodeParser/MappedTypeNodeParser.ts index 4df61e831..42016388b 100644 --- a/src/NodeParser/MappedTypeNodeParser.ts +++ b/src/NodeParser/MappedTypeNodeParser.ts @@ -18,7 +18,6 @@ import { derefAnnotatedType, derefType } from "../Utils/derefType.js"; import { getKey } from "../Utils/nodeKey.js"; import { preserveAnnotation } from "../Utils/preserveAnnotation.js"; import { removeUndefined } from "../Utils/removeUndefined.js"; -import { AliasType } from "../Type/AliasType.js"; export class MappedTypeNodeParser implements SubNodeParser { public constructor( @@ -104,39 +103,8 @@ export class MappedTypeNodeParser implements SubNodeParser { protected getProperties(node: ts.MappedTypeNode, keyListType: UnionType, context: Context): ObjectProperty[] { return keyListType - .getTypes() - .flatMap((type) => { - if (type instanceof LiteralType) { - return type; - } else if (type instanceof AliasType) { - const itemsToProcess = [type.getType()]; - const processedTypes = []; - while (itemsToProcess.length > 0) { - const currentType = itemsToProcess[0]; - if (currentType instanceof LiteralType) { - processedTypes.push(currentType); - } else if (currentType instanceof AliasType) { - itemsToProcess.push(currentType.getType()); - } else if (currentType instanceof UnionType) { - itemsToProcess.push(...currentType.getTypes()); - } - itemsToProcess.shift(); - } - return processedTypes; - } - return []; - }) + .getFlattenedTypes() .filter((type): type is LiteralType => type instanceof LiteralType) - .reduce((acc: LiteralType[], curr: LiteralType) => { - if ( - acc.findIndex((val: LiteralType) => { - return val.getId() === curr.getId(); - }) < 0 - ) { - acc.push(curr); - } - return acc; - }, []) .map((type) => [type, this.mapKey(node, type, context)]) .filter((value): value is [LiteralType, LiteralType] => value[1] instanceof LiteralType) .reduce((result: ObjectProperty[], [key, mappedKey]: [LiteralType, LiteralType]) => { diff --git a/src/Type/UnionType.ts b/src/Type/UnionType.ts index 2d36da255..a36bdf193 100644 --- a/src/Type/UnionType.ts +++ b/src/Type/UnionType.ts @@ -2,6 +2,8 @@ import { BaseType } from "./BaseType.js"; import { uniqueTypeArray } from "../Utils/uniqueTypeArray.js"; import { NeverType } from "./NeverType.js"; import { derefType } from "../Utils/derefType.js"; +import { LiteralType } from "./LiteralType.js"; +import { AliasType } from "./AliasType.js"; export class UnionType extends BaseType { private readonly types: BaseType[]; @@ -41,6 +43,41 @@ export class UnionType extends BaseType { return this.types; } + public getFlattenedTypes(): LiteralType[] { + return this.types + .flatMap((type) => { + if (type instanceof LiteralType) { + return type; + } else if (type instanceof AliasType) { + const itemsToProcess = [type.getType()]; + const processedTypes = []; + while (itemsToProcess.length > 0) { + const currentType = itemsToProcess[0]; + if (currentType instanceof LiteralType) { + processedTypes.push(currentType); + } else if (currentType instanceof AliasType) { + itemsToProcess.push(currentType.getType()); + } else if (currentType instanceof UnionType) { + itemsToProcess.push(...currentType.getTypes()); + } + itemsToProcess.shift(); + } + return processedTypes; + } + return []; + }) + .reduce((acc: LiteralType[], curr: LiteralType) => { + if ( + acc.findIndex((val: LiteralType) => { + return val.getId() === curr.getId(); + }) < 0 + ) { + acc.push(curr); + } + return acc; + }, []); + } + public normalize(): BaseType { if (this.types.length === 0) { return new NeverType();