Skip to content

Commit

Permalink
Merge pull request #1458 from erikbrinkman/jtd-data
Browse files Browse the repository at this point in the history
[RFC] JTDDataType
  • Loading branch information
epoberezkin committed Mar 6, 2021
2 parents 937643e + d5b4aa7 commit 22aa683
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 3 deletions.
4 changes: 2 additions & 2 deletions lib/jtd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export {KeywordCxt}
export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen"

import type {AnySchemaObject, SchemaObject, JTDParser} from "./types"
import type {JTDSchemaType} from "./types/jtd-schema"
export {JTDSchemaType}
import type {JTDSchemaType, JTDDataType} from "./types/jtd-schema"
export {JTDSchemaType, JTDDataType}
import AjvCore, {CurrentOptions} from "./core"
import jtdVocabulary from "./vocabularies/jtd"
import jtdMetaSchema from "./refs/jtd-schema"
Expand Down
66 changes: 66 additions & 0 deletions lib/types/jtd-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,69 @@ export type JTDSchemaType<T, D extends Record<string, unknown> = Record<string,
// TODO these should only be allowed at the top level
definitions?: {[K in keyof D]: JTDSchemaType<D[K], D>}
}

type JTDDataDef<S, D extends Record<string, unknown>> =
| (// ref
S extends {ref: string}
? JTDDataDef<D[S["ref"]], D>
: // type
S extends {type: NumberType}
? number
: S extends {type: "string"}
? string
: S extends {type: "timestamp"}
? string | Date
: // enum
S extends {enum: readonly (infer E)[]}
? string extends E
? never
: [E] extends [string]
? E
: never
: // elements
S extends {elements: infer E}
? JTDDataDef<E, D>[]
: // properties
S extends {
properties: Record<string, unknown>
optionalProperties?: Record<string, unknown>
additionalProperties?: boolean
}
? {-readonly [K in keyof S["properties"]]-?: JTDDataDef<S["properties"][K], D>} &
{
-readonly [K in keyof S["optionalProperties"]]+?: JTDDataDef<
S["optionalProperties"][K],
D
>
} &
([S["additionalProperties"]] extends [true] ? Record<string, unknown> : unknown)
: S extends {
properties?: Record<string, unknown>
optionalProperties: Record<string, unknown>
additionalProperties?: boolean
}
? {-readonly [K in keyof S["properties"]]-?: JTDDataDef<S["properties"][K], D>} &
{
-readonly [K in keyof S["optionalProperties"]]+?: JTDDataDef<
S["optionalProperties"][K],
D
>
} &
([S["additionalProperties"]] extends [true] ? Record<string, unknown> : unknown)
: // values
S extends {values: infer V}
? Record<string, JTDDataDef<V, D>>
: // discriminator
S extends {discriminator: infer M; mapping: Record<string, unknown>}
? [M] extends [string]
? {
[K in keyof S["mapping"]]: JTDDataDef<S["mapping"][K], D> & {[KM in M]: K}
}[keyof S["mapping"]]
: never
: // empty
unknown)
| (S extends {nullable: true} ? null : never)

export type JTDDataType<S> = S extends {definitions: Record<string, unknown>}
? JTDDataDef<S, S["definitions"]>
: JTDDataDef<S, Record<string, never>>
133 changes: 132 additions & 1 deletion spec/types/jtd-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-empty-interface,no-void */
import _Ajv from "../ajv_jtd"
import type {JTDSchemaType} from "../../dist/jtd"
import type {JTDSchemaType, JTDDataType} from "../../dist/jtd"
import chai from "../chai"
const should = chai.should()

Expand All @@ -19,6 +19,11 @@ interface B {

type MyData = A | B

interface LinkedList {
val: number
next?: LinkedList
}

const mySchema: JTDSchemaType<MyData> = {
discriminator: "type",
mapping: {
Expand Down Expand Up @@ -315,3 +320,129 @@ describe("JTDSchemaType", () => {
void [meta, emptyMeta, unknownMeta]
})
})

describe("JTDDataType", () => {
it("should typecheck number schemas", () => {
const numSchema = {type: "float64"} as const
const num: TypeEquality<JTDDataType<typeof numSchema>, number> = true

void [num]
})

it("should typecheck string schemas", () => {
const strSchema = {type: "string"} as const
const str: TypeEquality<JTDDataType<typeof strSchema>, string> = true

void [str]
})

it("should typecheck timestamp schemas", () => {
const timeSchema = {type: "timestamp"} as const
const time: TypeEquality<JTDDataType<typeof timeSchema>, string | Date> = true

void [time]
})

it("should typecheck enum schemas", () => {
const enumSchema = {enum: ["a", "b"]} as const
const enumerated: TypeEquality<JTDDataType<typeof enumSchema>, "a" | "b"> = true

// if you forget const on an enum it will error
const enumStringSchema = {enum: ["a", "b"]}
const enumString: TypeEquality<JTDDataType<typeof enumStringSchema>, never> = true
// also if not a string
const enumNumSchema = {enum: [3]} as const
const enumNum: TypeEquality<JTDDataType<typeof enumNumSchema>, never> = true

void [enumerated, enumString, enumNum]
})

it("should typecheck elements schemas", () => {
const elementsSchema = {elements: {type: "float64"}} as const
const elem: TypeEquality<JTDDataType<typeof elementsSchema>, number[]> = true

void [elem]
})

it("should typecheck properties schemas", () => {
const bothPropsSchema = {
properties: {a: {type: "float64"}},
optionalProperties: {b: {type: "string"}},
} as const
const both: TypeEquality<JTDDataType<typeof bothPropsSchema>, {a: number; b?: string}> = true

const reqPropsSchema = {properties: {a: {type: "float64"}}} as const
const req: TypeEquality<JTDDataType<typeof reqPropsSchema>, {a: number}> = true

const optPropsSchema = {optionalProperties: {b: {type: "string"}}} as const
const opt: TypeEquality<JTDDataType<typeof optPropsSchema>, {b?: string}> = true

const noAddSchema = {
optionalProperties: {b: {type: "string"}},
additionalProperties: false,
} as const
const noAdd: TypeEquality<JTDDataType<typeof noAddSchema>, {b?: string}> = true

const addSchema = {
optionalProperties: {b: {type: "string"}},
additionalProperties: true,
} as const
const add: TypeEquality<
JTDDataType<typeof addSchema>,
{b?: string; [key: string]: unknown}
> = true
const addVal: JTDDataType<typeof addSchema> = {b: "b", additional: 6}

void [both, req, opt, noAdd, add, addVal]
})

it("should typecheck values schemas", () => {
const valuesSchema = {values: {type: "float64"}} as const
const values: TypeEquality<JTDDataType<typeof valuesSchema>, Record<string, number>> = true

void [values]
})

it("should typecheck discriminator schemas", () => {
const discriminatorSchema = {
discriminator: "type",
mapping: {
a: {properties: {a: {type: "float64"}}},
b: {optionalProperties: {b: {type: "string"}}},
},
} as const
const disc: TypeEquality<JTDDataType<typeof discriminatorSchema>, A | B> = true

void [disc]
})

it("should typecheck ref schemas", () => {
const refSchema = {
definitions: {num: {type: "float64", nullable: true}},
ref: "num",
nullable: true,
} as const
const ref: TypeEquality<JTDDataType<typeof refSchema>, number | null> = true

// works for recursive schemas
const llSchema = {
definitions: {
node: {
properties: {val: {type: "float64"}},
optionalProperties: {next: {ref: "node"}},
},
},
ref: "node",
} as const
const list: TypeEquality<JTDDataType<typeof llSchema>, LinkedList> = true

void [ref, list]
})

it("should typecheck empty schemas", () => {
const emptySchema = {metadata: {}} as const
const empty: TypeEquality<JTDDataType<typeof emptySchema>, unknown> = true

void [empty]
})
})

0 comments on commit 22aa683

Please sign in to comment.