From 87dadd569aa2fcd39ac6f603f97478b87496c930 Mon Sep 17 00:00:00 2001 From: Erik Brinkman Date: Sun, 13 Nov 2022 22:53:31 -0500 Subject: [PATCH] special case empty object for jtd fixes #2123 --- lib/types/jtd-schema.ts | 32 ++++++++++++------ spec/types/jtd-schema.spec.ts | 64 +++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/lib/types/jtd-schema.ts b/lib/types/jtd-schema.ts index 3d5ae4ac9..61b2bde81 100644 --- a/lib/types/jtd-schema.ts +++ b/lib/types/jtd-schema.ts @@ -74,7 +74,7 @@ type EnumString = [T] extends [never] : null /** true if type is a union of string literals */ -type IsEnum = null extends EnumString> ? false : true +type IsEnum = null extends EnumString ? false : true /** true only if all types are array types (not tuples) */ // NOTE relies on the fact that tuples don't have an index at 0.5, but arrays @@ -88,13 +88,18 @@ type IsElements = false extends IsUnion : false /** true if the the type is a values type */ -type IsValues = false extends IsUnion> - ? TypeEquality, string> +type IsValues = false extends IsUnion ? TypeEquality : false + +/** true if type is a properties type and Union is false, or type is a discriminator type and Union is true */ +type IsRecord = Union extends IsUnion + ? null extends EnumString + ? false + : true : false -/** true if type is a proeprties type and Union is false, or type is a discriminator type and Union is true */ -type IsRecord = Union extends IsUnion> - ? null extends EnumString> +/** true if type represents an empty record */ +type IsEmptyRecord = [T] extends [Record] + ? [T] extends [never] ? false : true : false @@ -131,7 +136,7 @@ export type JTDSchemaType = Record + true extends IsEnum> ? {enum: EnumString>[]} : // arrays - only accepts arrays, could be array of unions to be resolved later true extends IsElements> @@ -140,15 +145,20 @@ export type JTDSchemaType = Record } : never + : // empty properties + true extends IsEmptyRecord> + ? + | {properties: Record; optionalProperties?: Record} + | {optionalProperties: Record} : // values - true extends IsValues + true extends IsValues> ? T extends Record ? { values: JTDSchemaType } : never : // properties - true extends IsRecord + true extends IsRecord, false> ? ([RequiredKeys>] extends [never] ? { properties?: Record @@ -168,7 +178,7 @@ export type JTDSchemaType = Record + true extends IsRecord, true> ? { [K in keyof Exclude]-?: Exclude[K] extends string ? { @@ -176,7 +186,7 @@ export type JTDSchemaType = Record[K]]: JTDSchemaType< - Omit, + Omit ? T : never, K>, D > } diff --git a/spec/types/jtd-schema.spec.ts b/spec/types/jtd-schema.spec.ts index c763a8eb3..450577e10 100644 --- a/spec/types/jtd-schema.spec.ts +++ b/spec/types/jtd-schema.spec.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-empty-interface,no-void */ +/* eslint-disable @typescript-eslint/no-empty-interface,no-void,@typescript-eslint/ban-types */ import _Ajv from "../ajv_jtd" import type {JTDSchemaType, SomeJTDSchemaType, JTDDataType} from "../../dist/jtd" import chai from "../chai" @@ -17,8 +17,14 @@ interface B { b?: string } +interface C { + type: "c" +} + type MyData = A | B +type Missing = A | C + interface LinkedList { val: number next?: LinkedList @@ -32,6 +38,14 @@ const mySchema: JTDSchemaType = { }, } +const missingSchema: JTDSchemaType = { + discriminator: "type", + mapping: { + a: {properties: {a: {type: "float64"}}}, + c: {properties: {}}, + }, +} + describe("JTDSchemaType", () => { it("validation should prove the data type", () => { const ajv = new _Ajv() @@ -69,6 +83,22 @@ describe("JTDSchemaType", () => { serialize(invalidData) }) + it("validation should prove the data type for missingSchema", () => { + const ajv = new _Ajv() + const validate = ajv.compile(missingSchema) + const validData: unknown = {type: "c"} + + if (validate(validData)) { + validData.type.should.equal("c") + } + should.not.exist(validate.errors) + + if (ajv.validate(missingSchema, validData)) { + validData.type.should.equal("c") + } + should.not.exist(validate.errors) + }) + it("should typecheck number schemas", () => { const numf: JTDSchemaType = {type: "float64"} const numi: JTDSchemaType = {type: "int32"} @@ -286,12 +316,42 @@ describe("JTDSchemaType", () => { const emptyButFull: JTDSchemaType<{a: string}> = {} const emptyMeta: JTDSchemaType = {metadata: {}} - // constant null not representable + // constant null representable as nullable empty object const emptyNull: TypeEquality, never> = true void [empty, emptyUnknown, falseUnknown, emptyButFull, emptyMeta, emptyNull] }) + it("should typecheck empty records", () => { + // empty record variants + const emptyPro: JTDSchemaType<{}> = {properties: {}} + const emptyOpt: JTDSchemaType<{}> = {optionalProperties: {}} + const emptyBoth: JTDSchemaType<{}> = {properties: {}, optionalProperties: {}} + const emptyRecord: JTDSchemaType> = {properties: {}} + const notNullable: JTDSchemaType<{}> = {properties: {}, nullable: false} + + // can't be null + // @ts-expect-error + const nullable: JTDSchemaType<{}> = {properties: {}, nullable: true} + + const emptyNullUnion: JTDSchemaType = {properties: {}, nullable: true} + const emptyNullRecord: JTDSchemaType> = { + properties: {}, + nullable: true, + } + + void [ + emptyPro, + emptyOpt, + emptyBoth, + emptyRecord, + notNullable, + nullable, + emptyNullUnion, + emptyNullRecord, + ] + }) + it("should typecheck ref schemas", () => { const refs: JTDSchemaType = { definitions: {