From f6fbef17941015b2c165332994af7ff34f029175 Mon Sep 17 00:00:00 2001 From: Dominik Moritz Date: Fri, 19 Apr 2024 17:30:27 -0400 Subject: [PATCH] make it work --- src/Type/LiteralType.ts | 4 + src/TypeFormatter/AnnotatedTypeFormatter.ts | 4 +- .../LiteralUnionTypeFormatter.ts | 63 +++++++---- src/TypeFormatter/UnionTypeFormatter.ts | 4 +- src/Utils/derefType.ts | 8 ++ test/invalid-data.test.ts | 14 +-- test/valid-data-other.test.ts | 1 + test/valid-data/string-literals-hack/main.ts | 13 +++ .../string-literals-hack/schema.json | 107 ++++++++++++++++++ 9 files changed, 179 insertions(+), 39 deletions(-) create mode 100644 test/valid-data/string-literals-hack/main.ts create mode 100644 test/valid-data/string-literals-hack/schema.json diff --git a/src/Type/LiteralType.ts b/src/Type/LiteralType.ts index e520d3c38..0b7840304 100644 --- a/src/Type/LiteralType.ts +++ b/src/Type/LiteralType.ts @@ -14,4 +14,8 @@ export class LiteralType extends BaseType { public getValue(): LiteralValue { return this.value; } + + public isString(): boolean { + return typeof this.value === "string"; + } } diff --git a/src/TypeFormatter/AnnotatedTypeFormatter.ts b/src/TypeFormatter/AnnotatedTypeFormatter.ts index 9012cc15f..06c18c8f7 100644 --- a/src/TypeFormatter/AnnotatedTypeFormatter.ts +++ b/src/TypeFormatter/AnnotatedTypeFormatter.ts @@ -61,9 +61,7 @@ export class AnnotatedTypeFormatter implements SubTypeFormatter { delete annotations.discriminator; } else { throw new Error( - `Cannot assign discriminator tag to type: ${JSON.stringify( - derefed, - )}. This tag can only be assigned to union types.`, + `Cannot assign discriminator tag to type: ${derefed.getName()}. This tag can only be assigned to union types.`, ); } } diff --git a/src/TypeFormatter/LiteralUnionTypeFormatter.ts b/src/TypeFormatter/LiteralUnionTypeFormatter.ts index e65f6019f..b0c8cd912 100644 --- a/src/TypeFormatter/LiteralUnionTypeFormatter.ts +++ b/src/TypeFormatter/LiteralUnionTypeFormatter.ts @@ -2,41 +2,48 @@ import { Definition } from "../Schema/Definition.js"; import { RawTypeName } from "../Schema/RawType.js"; import { SubTypeFormatter } from "../SubTypeFormatter.js"; import { BaseType } from "../Type/BaseType.js"; -import { LiteralType } from "../Type/LiteralType.js"; +import { LiteralType, LiteralValue } from "../Type/LiteralType.js"; import { NullType } from "../Type/NullType.js"; import { StringType } from "../Type/StringType.js"; import { UnionType } from "../Type/UnionType.js"; +import { derefAliasedType } from "../Utils/derefType.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 && this.isLiteralUnion(type); + return type instanceof UnionType && type.getTypes().length > 0 && isLiteralUnion(type); } public getDefinition(type: UnionType): Definition { let hasString = false; let preserveLiterals = false; - const types = type.getTypes().filter((t) => { + let allStrings = true; + + const flattenedTypes = flattenTypes(type); + + // filter out String types since we need to be more careful about them + const types = flattenedTypes.filter((t) => { if (t instanceof StringType) { hasString = true; preserveLiterals = preserveLiterals || t.getPreserveLiterals(); return false; } + + if (t instanceof LiteralType && !t.isString()) { + allStrings = false; + } + return true; }); - if (hasString && !preserveLiterals) { + if (allStrings && hasString && !preserveLiterals) { return { type: "string", }; } - const values = uniqueArray( - types.map((item: LiteralType | NullType | StringType) => this.getLiteralValue(item)), - ); - const typeNames = uniqueArray( - types.map((item: LiteralType | NullType | StringType) => this.getLiteralType(item)), - ); + const values = uniqueArray(types.map(getLiteralValue)); + const typeNames = uniqueArray(types.map(getLiteralType)); const ret = { type: typeNames.length === 1 ? typeNames[0] : typeNames, @@ -59,16 +66,30 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter { public getChildren(type: UnionType): BaseType[] { return []; } +} - protected isLiteralUnion(type: UnionType): boolean { - return type - .getTypes() - .every((item) => item instanceof LiteralType || item instanceof NullType || item instanceof StringType); - } - protected getLiteralValue(value: LiteralType | NullType): string | number | boolean | null { - return value instanceof LiteralType ? value.getValue() : null; - } - protected getLiteralType(value: LiteralType | NullType): RawTypeName { - return value instanceof LiteralType ? typeName(value.getValue()) : "null"; - } +function flattenTypes(type: UnionType): (StringType | LiteralType | NullType)[] { + return type + .getTypes() + .map(derefAliasedType) + .flatMap((t) => { + if (t instanceof UnionType) { + return flattenTypes(t); + } + return t as StringType | LiteralType | NullType; + }); +} + +function isLiteralUnion(type: UnionType): boolean { + return flattenTypes(type).every( + (item) => item instanceof LiteralType || item instanceof NullType || item instanceof StringType, + ); +} + +function getLiteralValue(value: LiteralType | NullType): LiteralValue | null { + return value instanceof LiteralType ? value.getValue() : null; +} + +function getLiteralType(value: LiteralType | NullType): RawTypeName { + return value instanceof LiteralType ? typeName(value.getValue()) : "null"; } diff --git a/src/TypeFormatter/UnionTypeFormatter.ts b/src/TypeFormatter/UnionTypeFormatter.ts index f34f6d42e..790f57436 100644 --- a/src/TypeFormatter/UnionTypeFormatter.ts +++ b/src/TypeFormatter/UnionTypeFormatter.ts @@ -40,9 +40,7 @@ export class UnionTypeFormatter implements SubTypeFormatter { if (undefinedIndex != -1) { throw new Error( - `Cannot find discriminator keyword "${discriminator}" in type ${JSON.stringify( - type.getTypes()[undefinedIndex], - )}.`, + `Cannot find discriminator keyword "${discriminator}" in type ${type.getTypes()[undefinedIndex].getName()}.`, ); } diff --git a/src/Utils/derefType.ts b/src/Utils/derefType.ts index c73a7699e..5e46b5ddb 100644 --- a/src/Utils/derefType.ts +++ b/src/Utils/derefType.ts @@ -25,3 +25,11 @@ export function derefAnnotatedType(type: BaseType): BaseType { return type; } + +export function derefAliasedType(type: BaseType): BaseType { + if (type instanceof AliasType) { + return derefAliasedType(type.getType()); + } + + return type; +} diff --git a/test/invalid-data.test.ts b/test/invalid-data.test.ts index aa82d2d6d..5c7437373 100644 --- a/test/invalid-data.test.ts +++ b/test/invalid-data.test.ts @@ -36,24 +36,14 @@ describe("invalid-data", () => { it("duplicates", assertSchema("duplicates", "MyType", `Type "A" has multiple definitions.`)); it( "missing-discriminator", - assertSchema( - "missing-discriminator", - "MyType", - 'Cannot find discriminator keyword "type" in type ' + - '{"name":"B","type":{"id":"interface-1119825560-40-63-1119825560-0-124",' + - '"baseTypes":[],"properties":[],"additionalProperties":false,"nonPrimitive":false}}.', - ), + assertSchema("missing-discriminator", "MyType", 'Cannot find discriminator keyword "type" in type B.'), ); it( "non-union-discriminator", assertSchema( "non-union-discriminator", "MyType", - "Cannot assign discriminator tag to type: " + - '{"id":"interface-2103469249-0-76-2103469249-0-77","baseTypes":[],' + - '"properties":[{"name":"name","type":{},"required":true}],' + - '"additionalProperties":false,"nonPrimitive":false}. ' + - "This tag can only be assigned to union types.", + "Cannot assign discriminator tag to type: interface-2103469249-0-76-2103469249-0-77. This tag can only be assigned to union types.", ), ); it( diff --git a/test/valid-data-other.test.ts b/test/valid-data-other.test.ts index 309c1c18b..e54f499f2 100644 --- a/test/valid-data-other.test.ts +++ b/test/valid-data-other.test.ts @@ -25,6 +25,7 @@ describe("valid-data-other", () => { it("string-literals-inline", assertValidSchema("string-literals-inline", "MyObject")); it("string-literals-intrinsic", assertValidSchema("string-literals-intrinsic", "MyObject")); it("string-literals-null", assertValidSchema("string-literals-null", "MyObject")); + it("string-literals-hack", assertValidSchema("string-literals-hack", "MyObject")); it("string-template-literals", assertValidSchema("string-template-literals", "MyObject")); it("string-template-expression-literals", assertValidSchema("string-template-expression-literals", "MyObject")); it( diff --git a/test/valid-data/string-literals-hack/main.ts b/test/valid-data/string-literals-hack/main.ts new file mode 100644 index 000000000..0e6fc4eab --- /dev/null +++ b/test/valid-data/string-literals-hack/main.ts @@ -0,0 +1,13 @@ +type Union = "a" | "b"; + +export type MyObject = { + literals: "foo" | "bar"; + stringWithNull: string | null; + literalWithNull: "foo" | "bar" | null; + literalWithString: "foo" | "bar" | string; + withRef: "foo" | Union; + withRefWithString: Union | string; + withHack: "foo" | "bar" | (string & {}); + withHackRecord: "foo" | "bar" | (string & Record); + withHackNull: "foo" | "bar" | null | (string & Record); +}; diff --git a/test/valid-data/string-literals-hack/schema.json b/test/valid-data/string-literals-hack/schema.json new file mode 100644 index 000000000..dff665091 --- /dev/null +++ b/test/valid-data/string-literals-hack/schema.json @@ -0,0 +1,107 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "literalWithNull": { + "enum": [ + "foo", + "bar", + null + ], + "type": [ + "string", + "null" + ] + }, + "literalWithString": { + "type": "string" + }, + "literals": { + "enum": [ + "foo", + "bar" + ], + "type": "string" + }, + "stringWithNull": { + "type": [ + "string", + "null" + ] + }, + "withHack": { + "anyOf": [ + { + "type": "string" + }, + { + "enum": [ + "foo", + "bar" + ], + "type": "string" + } + ] + }, + "withHackNull": { + "anyOf": [ + { + "type": "string" + }, + { + "enum": [ + "foo", + "bar", + null + ], + "type": [ + "string", + "null" + ] + } + ] + }, + "withHackRecord": { + "anyOf": [ + { + "type": "string" + }, + { + "enum": [ + "foo", + "bar" + ], + "type": "string" + } + ] + }, + "withRef": { + "enum": [ + "foo", + "a", + "b" + ], + "type": "string" + }, + "withRefWithString": { + "type": "string" + } + }, + "required": [ + "literals", + "stringWithNull", + "literalWithNull", + "literalWithString", + "withRef", + "withRefWithString", + "withHack", + "withHackRecord", + "withHackNull" + ], + "type": "object" + } + } +}