Namespace and Ref Types (#128)

This commit is contained in:
sinclairzx81
2021-11-30 03:26:16 +09:00
committed by GitHub
parent e908f6e504
commit 0d64b9e489
11 changed files with 191 additions and 122 deletions
+9
View File
@@ -1,3 +1,12 @@
## [0.23.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.23.0)
Updates:
- The types `Type.Namespace()` and `Type.Ref()` are promoted to `Standard`.
- TypeBox adds a new type named `TRef<TSchema>` that is returned on calls to `Type.Ref(...)`. The `TRef` includes a `RefKind` symbol for introspection of the reference type.
- TypeBox now maintains an internal dictionary of all schemas passed that contain an `$id` property. This dictionary is checked whenever a user attempts to reference a type and will throw if attempting to reference a target schema with no $id.
- The types `Type.Partial(...)`, `Type.Required(...)`, `Type.Omit()` and `Type.Pick(...)` now support reference types. Note that when using these functions with references, TypeBox will replicate the source schema and apply the nessasary modifiers to the replication.
## [0.22.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.22.0)
Updates:
+9 -24
View File
@@ -1,30 +1,15 @@
import { TypeBuilder, TSchema, Static } from '@sinclair/typebox'
import { Type, Static } from '@sinclair/typebox'
// -----------------------------------------------------------
// Open API Extended Types
// -----------------------------------------------------------
const T = Type.Object({
name: Type.Optional(Type.String()),
order: Type.Number()
}, { $id: 'T' })
export type TNullable<T extends TSchema> = TSchema & {
['$static']: Static<T> | null
} & { nullable: true }
const R = Type.Ref(T)
export type TStringUnion<T extends string[]> = TSchema & {
['$static']: {[K in keyof T]: T[K] }[number]
enum: T
}
const P = Type.Omit(T, ['name'])
// -----------------------------------------------------------
// Open API TypeBuilder
// -----------------------------------------------------------
console.log(P)
export class OpenApiTypeBuilder extends TypeBuilder {
public Nullable<T extends TSchema>(schema: T): TNullable<T> {
return { ...schema, nullable: true } as any
}
type T = Static<typeof P>
public StringUnion<T extends string[]>(values: [...T]): TStringUnion<T> {
return { enum: values } as any
}
}
const Type = new OpenApiTypeBuilder()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@sinclair/typebox",
"version": "0.22.1",
"version": "0.23.0",
"description": "JSONSchema Type Builder with Static Type Resolution for TypeScript",
"keywords": [
"json-schema",
+8
View File
@@ -33,4 +33,12 @@ describe('Omit', () => {
strictEqual(A.additionalProperties, false)
strictEqual(T.additionalProperties, false)
})
it('Should construct new object when targetting reference', () => {
const T = Type.Object({ a: Type.String(), b: Type.String() }, { $id: 'T' })
const R = Type.Ref(T)
const P = Type.Omit(R, [])
strictEqual(P.properties.a.type, 'string')
strictEqual(P.properties.b.type, 'string')
})
})
+8
View File
@@ -41,4 +41,12 @@ describe('Partial', () => {
strictEqual(A.additionalProperties, false)
strictEqual(T.additionalProperties, false)
})
it('Should construct new object when targetting reference', () => {
const T = Type.Object({ a: Type.String(), b: Type.String() }, { $id: 'T' })
const R = Type.Ref(T)
const P = Type.Partial(R)
strictEqual(P.properties.a.type, 'string')
strictEqual(P.properties.b.type, 'string')
})
})
+7
View File
@@ -33,4 +33,11 @@ describe('Pick', () => {
strictEqual(A.additionalProperties, false)
strictEqual(T.additionalProperties, false)
})
it('Should construct new object when targetting reference', () => {
const T = Type.Object({ a: Type.String(), b: Type.String() }, { $id: 'T' })
const R = Type.Ref(T)
const P = Type.Pick(R, ['a', 'b'])
strictEqual(P.properties.a.type, 'string')
strictEqual(P.properties.b.type, 'string')
})
})
+12 -12
View File
@@ -30,18 +30,18 @@ describe('Ref', () => {
}, [T])
})
it('Should should not validate when not specifying an $id', () => {
const T = Type.Object({
x: Type.Number(),
y: Type.Number(),
z: Type.Number()
}, { })
const R = Type.Ref(T)
fail(R, {
x: 1,
y: 2,
z: 3
}, [T])
it('Should throw when not specifying an $id on target schema', () => {
try {
const T = Type.Object({
x: Type.Number(),
y: Type.Number(),
z: Type.Number()
}, { })
const R = Type.Ref(T)
} catch {
return
}
throw Error('Expected throw')
})
it('Should not validate when not adding additional schema', () => {
+8
View File
@@ -41,4 +41,12 @@ describe('Required', () => {
strictEqual(A.additionalPropeties, false)
strictEqual(T.additionalPropeties, false)
})
it('Should construct new object when targetting reference', () => {
const T = Type.Object({ a: Type.String(), b: Type.String() }, { $id: 'T' })
const R = Type.Ref(T)
const P = Type.Required(R)
strictEqual(P.properties.a.type, 'string')
strictEqual(P.properties.b.type, 'string')
})
})
+1 -1
View File
@@ -2,7 +2,7 @@ import * as Spec from './spec'
import { Type } from './typebox'
{
const T = Type.String()
const T = Type.String({ $id: 'T' })
const R = Type.Ref(T)
+25 -12
View File
@@ -28,6 +28,7 @@ export declare const BooleanKind: unique symbol;
export declare const NullKind: unique symbol;
export declare const UnknownKind: unique symbol;
export declare const AnyKind: unique symbol;
export declare const RefKind: unique symbol;
export interface CustomOptions {
$id?: string;
title?: string;
@@ -174,6 +175,11 @@ export interface TAny extends TSchema, CustomOptions {
$static: any;
kind: typeof AnyKind;
}
export interface TRef<T extends TSchema> extends TSchema, CustomOptions {
$static: Static<T>;
kind: typeof RefKind;
$ref: string;
}
export declare const ConstructorKind: unique symbol;
export declare const FunctionKind: unique symbol;
export declare const PromiseKind: unique symbol;
@@ -209,6 +215,9 @@ export interface TVoid extends TSchema, CustomOptions {
kind: typeof VoidKind;
type: 'void';
}
export declare type Pickable = TObject<TProperties> | TRef<TObject<TProperties>>;
export declare type PickablePropertyKeys<T extends Pickable> = T extends TObject<infer U> ? keyof U : T extends TRef<TObject<infer U>> ? keyof U : never;
export declare type PickableProperties<T extends Pickable> = T extends TObject<infer U> ? U : T extends TRef<TObject<infer U>> ? U : never;
export declare type UnionToIntersect<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
export declare type StaticReadonlyOptionalPropertyKeys<T extends TProperties> = {
[K in keyof T]: T[K] extends TReadonlyOptional<TSchema> ? K : never;
@@ -262,6 +271,7 @@ export declare type StaticFunction<T extends readonly TSchema[], U extends TSche
export declare type StaticPromise<T extends TSchema> = Promise<Static<T>>;
export declare type Static<T extends TSchema> = T['$static'];
export declare class TypeBuilder {
private readonly schemas;
/** `Standard` Modifies an object property to be both readonly and optional */
ReadonlyOptional<T extends TSchema>(item: T): TReadonlyOptional<T>;
/** `Standard` Modifies an object property to be readonly */
@@ -299,17 +309,17 @@ export declare class TypeBuilder {
/** `Standard` Creates an any type */
Any(options?: CustomOptions): TAny;
/** `Standard` Creates a keyof type from the given object */
KeyOf<T extends TObject<TProperties>>(schema: T, options?: CustomOptions): TKeyOf<(keyof T['properties'])[]>;
KeyOf<T extends TObject<TProperties>>(object: T, options?: CustomOptions): TKeyOf<(keyof T['properties'])[]>;
/** `Standard` Creates a record type */
Record<K extends TRecordKey, T extends TSchema>(key: K, value: T, options?: ObjectOptions): TRecord<K, T>;
/** `Standard` Makes all properties in the given object type required */
Required<T extends TObject<any>>(schema: T, options?: ObjectOptions): TObject<StaticRequired<T['properties']>>;
Required<T extends TObject<TProperties> | TRef<TObject<TProperties>>>(object: T, options?: ObjectOptions): TObject<StaticRequired<T['properties']>>;
/** `Standard` Makes all properties in the given object type optional */
Partial<T extends TObject<any>>(schema: T, options?: ObjectOptions): TObject<StaticPartial<T['properties']>>;
Partial<T extends TObject<TProperties> | TRef<TObject<TProperties>>>(object: T, options?: ObjectOptions): TObject<StaticPartial<T['properties']>>;
/** `Standard` Picks property keys from the given object type */
Pick<T extends TObject<TProperties>, K extends (keyof T['properties'])[]>(schema: T, keys: [...K], options?: ObjectOptions): TObject<Pick<T['properties'], K[number]>>;
Pick<T extends TObject<TProperties> | TRef<TObject<TProperties>>, K extends PickablePropertyKeys<T>[]>(object: T, keys: [...K], options?: ObjectOptions): TObject<Pick<PickableProperties<T>, K[number]>>;
/** `Standard` Omits property keys from the given object type */
Omit<T extends TObject<any>, K extends (keyof T['properties'])[]>(schema: T, keys: [...K], options?: ObjectOptions): TObject<Omit<T['properties'], K[number]>>;
Omit<T extends TObject<TProperties> | TRef<TObject<TProperties>>, K extends PickablePropertyKeys<T>[]>(object: T, keys: [...K], options?: ObjectOptions): TObject<Omit<PickableProperties<T>, K[number]>>;
/** `Standard` Omits the `kind` and `modifier` properties from the underlying schema */
Strict<T extends TSchema>(schema: T, options?: CustomOptions): T;
/** `Extended` Creates a constructor type */
@@ -322,14 +332,17 @@ export declare class TypeBuilder {
Undefined(options?: CustomOptions): TUndefined;
/** `Extended` Creates a void type */
Void(options?: CustomOptions): TVoid;
/** `Standard` Creates a namespace for a set of related types */
Namespace<T extends TDefinitions>($defs: T, options?: CustomOptions): TNamespace<T>;
/** `Standard` References a type within a namespace. The referenced namespace must specify an `$id` */
Ref<T extends TNamespace<TDefinitions>, K extends keyof T['$defs']>(box: T, key: K): TRef<T['$defs'][K]>;
/** `Standard` References type. The referenced type must specify an `$id` */
Ref<T extends TSchema>(schema: T): TRef<T>;
/** `Experimental` Creates a recursive type */
Rec<T extends TSchema>(callback: (self: TAny) => T, options?: CustomOptions): T;
/** `Experimental` Creates a recursive type. Pending https://github.com/ajv-validator/ajv/issues/1709 */
/** `Experimental` Creates a namespace for a set of related types */
Namespace<T extends TDefinitions>($defs: T, options?: CustomOptions): TNamespace<T>;
/** `Experimental` References a type within a namespace. The referenced namespace must specify an `$id` */
Ref<T extends TNamespace<TDefinitions>, K extends keyof T['$defs']>(box: T, key: K): T['$defs'][K];
/** `Experimental` References type. The referenced type must specify an `$id` */
Ref<T extends TSchema>(schema: T): T;
/** Stores this schema if it contains an $id. This function is used for later referencing. */
private Store;
/** Resolves a schema by $id. May resolve recursively if the target is a TRef. */
private Resolve;
}
export declare const Type: TypeBuilder;
+103 -72
View File
@@ -60,6 +60,7 @@ export const BooleanKind = Symbol('BooleanKind')
export const NullKind = Symbol('NullKind')
export const UnknownKind = Symbol('UnknownKind')
export const AnyKind = Symbol('AnyKind')
export const RefKind = Symbol('RefKind')
export interface CustomOptions {
$id?: string
@@ -147,6 +148,7 @@ export interface TBoolean extends TS
export interface TNull extends TSchema, CustomOptions { $static: null, kind: typeof NullKind, type: 'null' }
export interface TUnknown extends TSchema, CustomOptions { $static: unknown, kind: typeof UnknownKind }
export interface TAny extends TSchema, CustomOptions { $static: any, kind: typeof AnyKind }
export interface TRef<T extends TSchema> extends TSchema, CustomOptions { $static: Static<T>, kind: typeof RefKind, $ref: string }
// --------------------------------------------------------------------------
// Extended Schema Types
@@ -166,6 +168,9 @@ export interface TVoid extends T
// --------------------------------------------------------------------------
// Utility Types
// --------------------------------------------------------------------------
export type Pickable = TObject<TProperties> | TRef<TObject<TProperties>>
export type PickablePropertyKeys<T extends Pickable> = T extends TObject<infer U> ? keyof U : T extends TRef<TObject<infer U>> ? keyof U : never
export type PickableProperties <T extends Pickable> = T extends TObject<infer U> ? U : T extends TRef<TObject<infer U>> ? U : never
export type UnionToIntersect<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
export type StaticReadonlyOptionalPropertyKeys <T extends TProperties> = { [K in keyof T]: T[K] extends TReadonlyOptional<TSchema> ? K : never }[keyof T]
@@ -241,6 +246,7 @@ function clone(object: any): any {
// --------------------------------------------------------------------------
export class TypeBuilder {
private readonly schemas = new Map<string, TSchema>()
/** `Standard` Modifies an object property to be both readonly and optional */
public ReadonlyOptional<T extends TSchema>(item: T): TReadonlyOptional<T> {
@@ -262,9 +268,10 @@ export class TypeBuilder {
const additionalItems = false
const minItems = items.length
const maxItems = items.length
return ((items.length > 0)
const schema = ((items.length > 0)
? { ...options, kind: TupleKind, type: 'array', items, additionalItems, minItems, maxItems }
: { ...options, kind: TupleKind, type: 'array', minItems, maxItems }) as any
return this.Store(schema)
}
/** `Standard` Creates an object type with the given properties */
@@ -278,82 +285,82 @@ export class TypeBuilder {
})
const required_names = property_names.filter(name => !optional.includes(name))
const required = (required_names.length > 0) ? required_names : undefined
return ((required)
return this.Store(((required)
? { ...options, kind: ObjectKind, type: 'object', properties, required }
: { ...options, kind: ObjectKind, type: 'object', properties }) as any
: { ...options, kind: ObjectKind, type: 'object', properties }))
}
/** `Standard` Creates an intersect type. */
public Intersect<T extends TSchema[]>(items: [...T], options: IntersectOptions = {}): TIntersect<T> {
return { ...options, kind: IntersectKind, type: 'object', allOf: items } as any
return this.Store({ ...options, kind: IntersectKind, type: 'object', allOf: items })
}
/** `Standard` Creates a union type */
public Union<T extends TSchema[]>(items: [...T], options: CustomOptions = {}): TUnion<T> {
return { ...options, kind: UnionKind, anyOf: items } as any
return this.Store({ ...options, kind: UnionKind, anyOf: items })
}
/** `Standard` Creates an array type */
public Array<T extends TSchema>(items: T, options: ArrayOptions = {}): TArray<T> {
return { ...options, kind: ArrayKind, type: 'array', items } as any
return this.Store({ ...options, kind: ArrayKind, type: 'array', items })
}
/** `Standard` Creates an enum type from a TypeScript enum */
public Enum<T extends TEnumType>(item: T, options: CustomOptions = {}): TEnum<TEnumKey<T[keyof T]>[]> {
const values = Object.keys(item).filter(key => isNaN(key as any)).map(key => item[key]) as T[keyof T][]
const anyOf = values.map(value => typeof value === 'string' ? { type: 'string' as const, const: value } : { type: 'number' as const, const: value })
return { ...options, kind: EnumKind, anyOf } as any
return this.Store({ ...options, kind: EnumKind, anyOf })
}
/** `Standard` Creates a literal type. Supports string, number and boolean values only */
public Literal<T extends TValue>(value: T, options: CustomOptions = {}): TLiteral<T> {
return { ...options, kind: LiteralKind, const: value, type: typeof value as 'string' | 'number' | 'boolean' } as any
return this.Store({ ...options, kind: LiteralKind, const: value, type: typeof value as 'string' | 'number' | 'boolean' })
}
/** `Standard` Creates a string type */
public String<TCustomFormatOption extends string>(options: StringOptions<StringFormatOption | TCustomFormatOption> = {}): TString {
return { ...options, kind: StringKind, type: 'string' } as any
return this.Store({ ...options, kind: StringKind, type: 'string' })
}
/** `Standard` Creates a string type from a regular expression */
public RegEx(regex: RegExp, options: CustomOptions = {}): TString {
return this.String({ ...options, pattern: regex.source }) as any
return this.String({ ...options, pattern: regex.source })
}
/** `Standard` Creates a number type */
public Number(options: NumberOptions = {}): TNumber {
return { ...options, kind: NumberKind, type: 'number' } as any
return this.Store({ ...options, kind: NumberKind, type: 'number' })
}
/** `Standard` Creates an integer type */
public Integer(options: NumberOptions = {}): TInteger {
return { ...options, kind: IntegerKind, type: 'integer' } as any
return this.Store({ ...options, kind: IntegerKind, type: 'integer' })
}
/** `Standard` Creates a boolean type */
public Boolean(options: CustomOptions = {}): TBoolean {
return { ...options, kind: BooleanKind, type: 'boolean' } as any
return this.Store({ ...options, kind: BooleanKind, type: 'boolean' })
}
/** `Standard` Creates a null type */
public Null(options: CustomOptions = {}): TNull {
return { ...options, kind: NullKind, type: 'null' } as any
return this.Store({ ...options, kind: NullKind, type: 'null' })
}
/** `Standard` Creates an unknown type */
public Unknown(options: CustomOptions = {}): TUnknown {
return { ...options, kind: UnknownKind } as any
return this.Store({ ...options, kind: UnknownKind })
}
/** `Standard` Creates an any type */
public Any(options: CustomOptions = {}): TAny {
return { ...options, kind: AnyKind } as any
return this.Store({ ...options, kind: AnyKind })
}
/** `Standard` Creates a keyof type from the given object */
public KeyOf<T extends TObject<TProperties>>(schema: T, options: CustomOptions = {}): TKeyOf<(keyof T['properties'])[]> {
const keys = Object.keys(schema.properties)
return {...options, kind: KeyOfKind, type: 'string', enum: keys } as any
public KeyOf<T extends TObject<TProperties>>(object: T, options: CustomOptions = {}): TKeyOf<(keyof T['properties'])[]> {
const keys = Object.keys(object.properties)
return this.Store({...options, kind: KeyOfKind, type: 'string', enum: keys })
}
/** `Standard` Creates a record type */
@@ -367,15 +374,16 @@ export class TypeBuilder {
default: throw Error('Invalid Record Key')
}
})()
return { ...options, kind: RecordKind, type: 'object', patternProperties: { [pattern]: value } } as any
return this.Store({ ...options, kind: RecordKind, type: 'object', patternProperties: { [pattern]: value } })
}
/** `Standard` Makes all properties in the given object type required */
public Required<T extends TObject<any>>(schema: T, options: ObjectOptions = {}): TObject<StaticRequired<T['properties']>> {
const next = { ...clone(schema), ...options }
next.required = Object.keys(next.properties)
for(const key of Object.keys(next.properties)) {
const property = next.properties[key]
public Required<T extends TObject<TProperties> | TRef<TObject<TProperties>>>(object: T, options: ObjectOptions = {}): TObject<StaticRequired<T['properties']>> {
const source = this.Resolve(object)
const schema = { ...clone(source), ...options }
schema.required = Object.keys(schema.properties)
for(const key of Object.keys(schema.properties)) {
const property = schema.properties[key]
switch(property.modifier) {
case ReadonlyOptionalModifier: property.modifier = ReadonlyModifier; break;
case ReadonlyModifier: property.modifier = ReadonlyModifier; break;
@@ -383,15 +391,16 @@ export class TypeBuilder {
default: delete property.modifier; break;
}
}
return next
return this.Store(schema)
}
/** `Standard` Makes all properties in the given object type optional */
public Partial<T extends TObject<any>>(schema: T, options: ObjectOptions = {}): TObject<StaticPartial<T['properties']>> {
const next = { ...clone(schema), ...options }
delete next.required
for(const key of Object.keys(next.properties)) {
const property = next.properties[key]
public Partial<T extends TObject<TProperties> | TRef<TObject<TProperties>>>(object: T, options: ObjectOptions = {}): TObject<StaticPartial<T['properties']>> {
const source = this.Resolve(object)
const schema = { ...clone(source), ...options }
delete schema.required
for(const key of Object.keys(schema.properties)) {
const property = schema.properties[key]
switch(property.modifier) {
case ReadonlyOptionalModifier: property.modifier = ReadonlyOptionalModifier; break;
case ReadonlyModifier: property.modifier = ReadonlyOptionalModifier; break;
@@ -399,27 +408,29 @@ export class TypeBuilder {
default: property.modifier = OptionalModifier; break;
}
}
return next
return this.Store(schema)
}
/** `Standard` Picks property keys from the given object type */
public Pick<T extends TObject<TProperties>, K extends (keyof T['properties'])[]>(schema: T, keys: [...K], options: ObjectOptions = {}): TObject<Pick<T['properties'], K[number]>> {
const next = { ...clone(schema), ...options }
next.required = next.required ? next.required.filter((key: string) => keys.includes(key)) : undefined
for(const key of Object.keys(next.properties)) {
if(!keys.includes(key)) delete next.properties[key]
public Pick<T extends TObject<TProperties> | TRef<TObject<TProperties>>, K extends PickablePropertyKeys<T>[]>(object: T, keys: [...K], options: ObjectOptions = {}): TObject<Pick<PickableProperties<T>, K[number]>> {
const source = this.Resolve(object)
const schema = { ...clone(source), ...options }
schema.required = schema.required ? schema.required.filter((key: K) => keys.includes(key as any)) : undefined
for(const key of Object.keys(schema.properties)) {
if(!keys.includes(key as any)) delete schema.properties[key]
}
return next
return this.Store(schema)
}
/** `Standard` Omits property keys from the given object type */
public Omit<T extends TObject<any>, K extends (keyof T['properties'])[]>(schema: T, keys: [...K], options: ObjectOptions = {}):TObject<Omit<T['properties'], K[number]>> {
const next = { ...clone(schema), ...options }
next.required = next.required ? next.required.filter((key: string) => !keys.includes(key)) : undefined
for(const key of Object.keys(next.properties)) {
if(keys.includes(key)) delete next.properties[key]
public Omit<T extends TObject<TProperties> | TRef<TObject<TProperties>>, K extends PickablePropertyKeys<T>[]>(object: T, keys: [...K], options: ObjectOptions = {}):TObject<Omit<PickableProperties<T>, K[number]>> {
const source = this.Resolve(object)
const schema = { ...clone(source), ...options }
schema.required = schema.required ? schema.required.filter((key: string) => !keys.includes(key as any)) : undefined
for(const key of Object.keys(schema.properties)) {
if(keys.includes(key as any)) delete schema.properties[key]
}
return next
return this.Store(schema)
}
/** `Standard` Omits the `kind` and `modifier` properties from the underlying schema */
@@ -429,58 +440,78 @@ export class TypeBuilder {
/** `Extended` Creates a constructor type */
public Constructor<T extends TSchema[], U extends TSchema>(args: [...T], returns: U, options: CustomOptions = {}): TConstructor<T, U> {
return { ...options, kind: ConstructorKind, type: 'constructor', arguments: args, returns } as any
return this.Store({ ...options, kind: ConstructorKind, type: 'constructor', arguments: args, returns })
}
/** `Extended` Creates a function type */
public Function<T extends TSchema[], U extends TSchema>(args: [...T], returns: U, options: CustomOptions = {}): TFunction<T, U> {
return { ...options, kind: FunctionKind, type: 'function', arguments: args, returns } as any
return this.Store({ ...options, kind: FunctionKind, type: 'function', arguments: args, returns })
}
/** `Extended` Creates a promise type */
public Promise<T extends TSchema>(item: T, options: CustomOptions = {}): TPromise<T> {
return { ...options, type: 'promise', kind: PromiseKind, item } as any
return this.Store({ ...options, type: 'promise', kind: PromiseKind, item })
}
/** `Extended` Creates a undefined type */
public Undefined(options: CustomOptions = {}): TUndefined {
return { ...options, type: 'undefined', kind: UndefinedKind } as any
return this.Store({ ...options, type: 'undefined', kind: UndefinedKind })
}
/** `Extended` Creates a void type */
public Void(options: CustomOptions = {}): TVoid {
return { ...options, type: 'void', kind: VoidKind } as any
return this.Store({ ...options, type: 'void', kind: VoidKind })
}
/** `Standard` Creates a namespace for a set of related types */
public Namespace<T extends TDefinitions>($defs: T, options: CustomOptions = {}): TNamespace<T> {
return this.Store({ ...options, kind: BoxKind, $defs })
}
/** `Standard` References a type within a namespace. The referenced namespace must specify an `$id` */
public Ref<T extends TNamespace<TDefinitions>, K extends keyof T['$defs']>(box: T, key: K): TRef<T['$defs'][K]>
/** `Standard` References type. The referenced type must specify an `$id` */
public Ref<T extends TSchema>(schema: T): TRef<T>
public Ref(...args: any[]): any {
if(args.length === 2) {
const namespace = args[0] as TNamespace<TDefinitions>
const targetKey = args[1] as string
if(namespace.$id === undefined) throw new Error(`Referenced namespace has no $id`)
if(!this.schemas.has(namespace.$id)) throw new Error(`Unable to locate namespace with $id '${namespace.$id}'`)
return this.Store({ kind: RefKind, $ref: `${namespace.$id}#/$defs/${targetKey}` })
} else if(args.length === 1) {
const target = args[0] as any
if(target.$id === undefined) throw new Error(`Referenced schema has no $id`)
if(!this.schemas.has(target.$id)) throw new Error(`Unable to locate schema with $id '${target.$id}'`)
return this.Store({ kind: RefKind, $ref: target.$id })
} else {
throw new Error('Type.Ref: Invalid arguments')
}
}
/** `Experimental` Creates a recursive type */
public Rec<T extends TSchema>(callback: (self: TAny) => T, options: CustomOptions = {}): T {
const $id = options.$id || ''
const self = callback({ $ref: `${$id}#/$defs/self` } as any)
return { ...options, $ref: `${$id}#/$defs/self`, $defs: { self } } as unknown as T
return this.Store({ ...options, $ref: `${$id}#/$defs/self`, $defs: { self } })
}
/** `Experimental` Creates a recursive type. Pending https://github.com/ajv-validator/ajv/issues/1709 */
// public Rec<T extends TProperties>($id: string, callback: (self: TAny) => T, options: ObjectOptions = {}): TObject<T> {
// const properties = callback({ $recursiveRef: `${$id}` } as any)
// return { ...options, kind: ObjectKind, $id, $recursiveAnchor: true, type: 'object', properties }
// }
/** `Experimental` Creates a namespace for a set of related types */
public Namespace<T extends TDefinitions>($defs: T, options: CustomOptions = {}): TNamespace<T> {
return { ...options, kind: BoxKind, $defs } as any
/** Stores this schema if it contains an $id. This function is used for later referencing. */
private Store(schema: any): any {
if(!schema.$id) return schema
this.schemas.set(schema.$id, schema)
return schema
}
/** `Experimental` References a type within a namespace. The referenced namespace must specify an `$id` */
public Ref<T extends TNamespace<TDefinitions>, K extends keyof T['$defs']>(box: T, key: K): T['$defs'][K]
/** `Experimental` References type. The referenced type must specify an `$id` */
public Ref<T extends TSchema>(schema: T): T
public Ref(...args: any[]): any {
const $id = args[0]['$id'] || '' as string
const key = args[1] as string
return (args.length === 2) ? { $ref: `${$id}#/$defs/${key}` } : { $ref: $id }
/** Resolves a schema by $id. May resolve recursively if the target is a TRef. */
private Resolve(schema: any): any {
if(schema.kind !== RefKind) return schema
if(!this.schemas.has(schema.$ref)) throw Error(`Unable to locate schema with $id '${schema.$ref}'`)
return this.Resolve(this.schemas.get(schema.$ref)!)
}
}
export const Type = new TypeBuilder()