Skip to content

Commit

Permalink
address comments
Browse files Browse the repository at this point in the history
  • Loading branch information
erikbrinkman committed Feb 16, 2021
1 parent ea7774f commit f3a6b61
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 190 deletions.
1 change: 0 additions & 1 deletion lib/ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export {KeywordCxt}
export {DefinedError} from "./vocabularies/errors"
export {JSONType} from "./compile/rules"
export {JSONSchemaType} from "./types/json-schema"
export {JTDSchema} from "./types/jtd-schema"
export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen"

import type {AnySchemaObject} from "./types"
Expand Down
1 change: 1 addition & 0 deletions lib/jtd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {KeywordCxt}
export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen"

import type {AnySchemaObject} from "./types"
export {JTDSchemaType} from "./types/jtd-schema"
import AjvCore, {CurrentOptions} from "./core"
import jtdVocabulary from "./vocabularies/jtd"
import jtdMetaSchema from "./refs/jtd-schema"
Expand Down
190 changes: 119 additions & 71 deletions lib/types/jtd-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,34 @@ type OptionalKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? K : never
}[keyof T]

/** type is true if all elements of T extend T */
type AllExtend_<T, E> = T extends E ? true : false
type AllExtend<T, E> = false extends AllExtend_<T, E> ? false : true
/** type is true if T is a union type */
type IsUnion_<T, U extends T = T> = false extends (
T extends unknown ? ([U] extends [T] ? false : true) : never
)
? false
: true
type IsUnion<T> = IsUnion_<T>

/** type is true if type is identically string */
type IsString_<T> = T extends string ? (string extends T ? true : false) : false
type IsString<T> = false extends IsString_<T> ? false : true
/** type is true if T is identically E */
type TypeEquality<T, E> = [T] extends [E] ? ([E] extends [T] ? true : false) : false

/** gets only the string literals of a type or null if a type isn't a string literal */
type NullableStringLiterals<T> = T extends string ? (string extends T ? null : T) : null
type EnumString<T> = [T] extends [never]
? null
: T extends string
? string extends T
? null
: T
: null

/** 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
// have an index at every number
type IsElements<T> = [T] extends [readonly unknown[]]
? undefined extends T[0.5]
? false
: true
: false

/** numeric strings */
type NumberType = "float32" | "float64" | "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32"
Expand All @@ -26,78 +44,108 @@ type NumberType = "float32" | "float64" | "int8" | "uint8" | "int16" | "uint16"
type StringType = "string" | "timestamp"

/** actual schema */
export type JTDSchema<T, D extends Record<string, unknown> = Record<string, never>> = (
| // refs - accepts any key in definitions of the right type
{[K in keyof D]: T extends D[K] ? {ref: K} : never}[keyof D]
// numbers - only accepts number or union of number literals
| (AllExtend<T, number | null> extends true ? {type: NumberType} : never)
// booleans - accepts any boolean, incluting just true or false literals
| (AllExtend<T, boolean | null> extends true ? {type: "boolean"} : never)
// strings - only accepts the type string
// TODO accept Date?
| (IsString<Exclude<T, null>> extends true ? {type: StringType} : never)
// enums - only accepts union of string literals
| (// TODO we can't actually check that everything in the union was specified
null extends NullableStringLiterals<Exclude<T, null>>
? never
: {enum: NullableStringLiterals<Exclude<T, null>>[]})
// arrays - only accepts arrays, could be array of unions to be resolved later
| (T extends (infer E)[]
export type JTDSchemaType<T, D extends Record<string, unknown> = Record<string, never>> = (
| // refs - where null wasn't specified, must match exactly
({[K in keyof D]: [T] extends [D[K]] ? {ref: K} : never}[keyof D] & {nullable?: false})
// nulled refs - if ref is nullable and nullable is specified, then it can
// match either null or non-null definitions
| (null extends T
? {
elements: JTDSchema<E, D>
}
[K in keyof D]: [Exclude<T, null>] extends [Exclude<D[K], null>] ? {ref: K} : never
}[keyof D] & {nullable: true}
: never)
// all object like
| (keyof T extends string
?
| // values
{
values: JTDSchema<T[keyof T], D>
}
// explicit keys
| ((RequiredKeys<Exclude<T, null>> extends never
// all other types
| ((
| // numbers - only accepts the type number
(true extends TypeEquality<Exclude<T, null>, number> ? {type: NumberType} : never)
// booleans - accepts the type boolean
| (true extends TypeEquality<Exclude<T, null>, boolean> ? {type: "boolean"} : never)
// strings - only accepts the type string
| (true extends TypeEquality<Exclude<T, null>, string> ? {type: StringType} : never)
// strings - only accepts the type Date
| (true extends TypeEquality<Exclude<T, null>, Date> ? {type: "timestamp"} : never)
// enums - only accepts union of string literals
// TODO we can't actually check that everything in the union was specified
| (null extends EnumString<Exclude<T, null>> ? never : {enum: EnumString<Exclude<T, null>>[]})
// arrays - only accepts arrays, could be array of unions to be resolved later
| (false extends IsUnion<Exclude<T, null>>
? true extends IsElements<Exclude<T, null>>
? T extends readonly (infer E)[]
? {
elements: JTDSchemaType<E, D>
}
: never
: never
: never)
// values
| (false extends IsUnion<Exclude<T, null>>
? true extends TypeEquality<keyof Exclude<T, null>, string>
? T extends Record<string, infer V>
? {
properties?: Record<string, never>
values: JTDSchemaType<V>
}
: {
properties: {[K in RequiredKeys<T>]: JTDSchema<T[K], D>}
}) &
(OptionalKeys<Exclude<T, null>> extends never
: never
: never
: never)
// properties
| (false extends IsUnion<Exclude<T, null>>
? null extends EnumString<keyof Exclude<T, null>>
? never
: ([RequiredKeys<Exclude<T, null>>] extends [never]
? {
optionalProperties?: Record<string, never>
properties?: Record<string, never>
}
: {
optionalProperties: {
[K in OptionalKeys<T>]: JTDSchema<Exclude<T[K], undefined>, D>
properties: {[K in RequiredKeys<T>]: JTDSchemaType<T[K], D>}
}) &
([OptionalKeys<Exclude<T, null>>] extends [never]
? {
optionalProperties?: Record<string, never>
}
}) & {
additionalProperties?: boolean
})
// tagged unions
| {
[K in keyof T]-?: T[K] extends string
? {
discriminator: K
mapping: {
// TODO currently allows descriminator to be present in schema
[M in T[K]]: JTDSchema<Omit<T extends {[D in K]: M} ? T : never, K>, D>
: {
optionalProperties: {
[K in OptionalKeys<T>]: JTDSchemaType<Exclude<T[K], undefined>, D>
}
}) & {
additionalProperties?: boolean
}
: never)
// discriminator
| (true extends IsUnion<Exclude<T, null>>
? null extends EnumString<keyof Exclude<T, null>>
? never
: {
[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>,
D
>
}
}
}
: never
}[keyof T]
// empty
// TODO as written, and object like could get an empty schema which
// potentially breaks guarantees, but does match the spec
| Record<string, never>
: never)
: never
}[keyof Exclude<T, null>]
: never)
// empty schema
// NOTE there should only be one type that extends Record<string, never> so unions
// shouldn't be a worry
| (T extends Record<string, never> ? unknown : never)
// null
// NOTE we have to check this too because null as an exclusive type also
// qualifies for the empty schema
| (true extends TypeEquality<T, null> ? unknown : never)
) &
(null extends T
? {
nullable: true
}
: {nullable?: false}))
) & {
// extra properties
// TODO handle ajv extensions to metadata?
metadata?: {[name: string]: unknown}
// TODO these should only be allowed at the top level, but typescript doesn't like saying no
definitions?: {[K in keyof D]: JTDSchema<D[K], D>}
} & (null extends T
? {
nullable: true
}
: {nullable?: false})
metadata?: Record<string, unknown>
// TODO these should only be allowed at the top level
definitions?: {[K in keyof D]: JTDSchemaType<D[K], D>}
}
Loading

0 comments on commit f3a6b61

Please sign in to comment.