Skip to content

Commit

Permalink
special case empty object for jtd (#2158)
Browse files Browse the repository at this point in the history
fixes #2123
  • Loading branch information
erikbrinkman authored Jan 2, 2023
1 parent d2c57d9 commit dab8504
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 13 deletions.
32 changes: 21 additions & 11 deletions lib/types/jtd-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type EnumString<T> = [T] extends [never]
: null

/** true if type is a union of string literals */
type IsEnum<T> = null extends EnumString<Exclude<T, null>> ? false : true
type IsEnum<T> = null extends EnumString<T> ? 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
Expand All @@ -88,13 +88,18 @@ type IsElements<T> = false extends IsUnion<T>
: false

/** true if the the type is a values type */
type IsValues<T> = false extends IsUnion<Exclude<T, null>>
? TypeEquality<keyof Exclude<T, null>, string>
type IsValues<T> = false extends IsUnion<T> ? TypeEquality<keyof T, string> : false

/** true if type is a properties type and Union is false, or type is a discriminator type and Union is true */
type IsRecord<T, Union extends boolean> = Union extends IsUnion<T>
? null extends EnumString<keyof T>
? 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<T, Union extends boolean> = Union extends IsUnion<Exclude<T, null>>
? null extends EnumString<keyof Exclude<T, null>>
/** true if type represents an empty record */
type IsEmptyRecord<T> = [T] extends [Record<string, never>]
? [T] extends [never]
? false
: true
: false
Expand Down Expand Up @@ -131,7 +136,7 @@ export type JTDSchemaType<T, D extends Record<string, unknown> = Record<string,
? {type: "timestamp"}
: // enums - only accepts union of string literals
// TODO we can't actually check that everything in the union was specified
true extends IsEnum<T>
true extends IsEnum<Exclude<T, null>>
? {enum: EnumString<Exclude<T, null>>[]}
: // arrays - only accepts arrays, could be array of unions to be resolved later
true extends IsElements<Exclude<T, null>>
Expand All @@ -140,15 +145,20 @@ export type JTDSchemaType<T, D extends Record<string, unknown> = Record<string,
elements: JTDSchemaType<E, D>
}
: never
: // empty properties
true extends IsEmptyRecord<Exclude<T, null>>
?
| {properties: Record<string, never>; optionalProperties?: Record<string, never>}
| {optionalProperties: Record<string, never>}
: // values
true extends IsValues<T>
true extends IsValues<Exclude<T, null>>
? T extends Record<string, infer V>
? {
values: JTDSchemaType<V, D>
}
: never
: // properties
true extends IsRecord<T, false>
true extends IsRecord<Exclude<T, null>, false>
? ([RequiredKeys<Exclude<T, null>>] extends [never]
? {
properties?: Record<string, never>
Expand All @@ -168,15 +178,15 @@ export type JTDSchemaType<T, D extends Record<string, unknown> = Record<string,
additionalProperties?: boolean
}
: // discriminator
true extends IsRecord<T, true>
true extends IsRecord<Exclude<T, null>, true>
? {
[K in keyof Exclude<T, null>]-?: Exclude<T, null>[K] extends string
? {
discriminator: K
mapping: {
// TODO currently allows descriminator to be present in schema
[M in Exclude<T, null>[K]]: JTDSchemaType<
Omit<T extends {[C in K]: M} ? T : never, K>,
Omit<T extends Record<K, M> ? T : never, K>,
D
>
}
Expand Down
64 changes: 62 additions & 2 deletions spec/types/jtd-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
Expand All @@ -32,6 +38,14 @@ const mySchema: JTDSchemaType<MyData> = {
},
}

const missingSchema: JTDSchemaType<Missing> = {
discriminator: "type",
mapping: {
a: {properties: {a: {type: "float64"}}},
c: {properties: {}},
},
}

describe("JTDSchemaType", () => {
it("validation should prove the data type", () => {
const ajv = new _Ajv()
Expand Down Expand Up @@ -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<number> = {type: "float64"}
const numi: JTDSchemaType<number> = {type: "int32"}
Expand Down Expand Up @@ -286,12 +316,42 @@ describe("JTDSchemaType", () => {
const emptyButFull: JTDSchemaType<{a: string}> = {}
const emptyMeta: JTDSchemaType<unknown> = {metadata: {}}

// constant null not representable
// constant null representable as nullable empty object
const emptyNull: TypeEquality<JTDSchemaType<null>, 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<Record<string, never>> = {properties: {}}
const notNullable: JTDSchemaType<{}> = {properties: {}, nullable: false}

// can't be null
// @ts-expect-error
const nullable: JTDSchemaType<{}> = {properties: {}, nullable: true}

const emptyNullUnion: JTDSchemaType<null | {}> = {properties: {}, nullable: true}
const emptyNullRecord: JTDSchemaType<null | Record<string, never>> = {
properties: {},
nullable: true,
}

void [
emptyPro,
emptyOpt,
emptyBoth,
emptyRecord,
notNullable,
nullable,
emptyNullUnion,
emptyNullRecord,
]
})

it("should typecheck ref schemas", () => {
const refs: JTDSchemaType<number[], {num: number}> = {
definitions: {
Expand Down

0 comments on commit dab8504

Please sign in to comment.