From 4d7ef420d19af8d133ad651285845e73e17198ab Mon Sep 17 00:00:00 2001 From: sinclairzx81 Date: Mon, 9 Jan 2023 23:26:24 +0900 Subject: [PATCH] Revised User Defined Type and Format API (#308) --- package.json | 2 +- readme.md | 232 +++++++++++++---------- src/system/system.ts | 45 ++++- test/runtime/system/AllowArrayObjects.ts | 2 +- test/runtime/system/AllowNaN.ts | 2 +- test/runtime/system/CreateFormat.ts | 17 ++ test/runtime/system/CreateType.ts | 23 +++ test/runtime/system/index.ts | 2 + 8 files changed, 211 insertions(+), 114 deletions(-) create mode 100644 test/runtime/system/CreateFormat.ts create mode 100644 test/runtime/system/CreateType.ts diff --git a/package.json b/package.json index 295a2de..fd8c28e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.25.18", + "version": "0.25.19", "description": "JSONSchema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", diff --git a/readme.md b/readme.md index ac6ea81..3a576e9 100644 --- a/readme.md +++ b/readme.md @@ -96,10 +96,11 @@ License MIT - [Errors](#values-errors) - [Pointer](#values-pointer) - [TypeCheck](#typecheck) - - [Ajv](#typecheck-ajv) - [TypeCompiler](#typecheck-typecompiler) - - [Custom Types](#typecheck-custom-types) - - [Custom Formats](#typecheck-custom-formats) + - [Ajv](#typecheck-ajv) +- [TypeSystem](#typecheck) + - [Types](#typesystem-types) + - [Formats](#typesystem-formats) - [Benchmark](#benchmark) - [Compile](#benchmark-compile) - [Validate](#benchmark-validate) @@ -936,6 +937,71 @@ TypeBox targets JSON Schema Draft 6 and is built and tested against the Ajv JSON The following sections detail using these validators. + + +### TypeCompiler + +TypeBox includes an high performance just-in-time (JIT) compiler and type checker that can be used in applications that require extremely fast validation. Note that this compiler is optimized for TypeBox types only where the schematics are known in advance. + +The compiler module is provided as an optional import. + +```typescript +import { TypeCompiler } from '@sinclair/typebox/compiler' +``` + +Use the `Compile(...)` function to compile a type. + +```typescript +const C = TypeCompiler.Compile(Type.Object({ // const C: TypeCheck> + +const R = C.Check({ x: 1, y: 2, z: 3 }) // const R = true +``` + +Validation errors can be read with the `Errors(...)` function. + +```typescript +const C = TypeCompiler.Compile(Type.Object({ // const C: TypeCheck> + +const value = { } + +const errors = [...C.Errors(value)] // const errors = [{ + // schema: { type: 'number' }, + // path: '/x', + // value: undefined, + // message: 'Expected number' + // }, { + // schema: { type: 'number' }, + // path: '/y', + // value: undefined, + // message: 'Expected number' + // }, { + // schema: { type: 'number' }, + // path: '/z', + // value: undefined, + // message: 'Expected number' + // }] +``` + +Compiled routines can be inspected with the `.Code()` function. + +```typescript +const C = TypeCompiler.Compile(Type.String()) // const C: TypeCheck + +console.log(C.Code()) // return function check(value) { + // return ( + // (typeof value === 'string') + // ) + // } +``` + ### Ajv @@ -1066,125 +1132,87 @@ const R = ajv.validate(Type.Object({ // const R = true - + -### TypeCompiler +## TypeSystem -TypeBox provides an optional high performance just-in-time (JIT) compiler and type checker that can be used in applications that require extremely fast validation. Note that this compiler is optimized for TypeBox types only where the schematics are known in advance. If defining custom types with `Type.Unsafe` please consider Ajv. +TypeBox provides an extensible TypeSystem module that enables developers to register additional types above and beyond the standard or extended type set. This module also allows developers to define custom string formats as well as override certain type checking behaviours. -The compiler module is provided as an optional import. +The TypeSystem module is provided as an optional import. ```typescript -import { TypeCompiler } from '@sinclair/typebox/compiler' +import { TypeSystem } from '@sinclair/typebox/system' ``` -Use the `Compile(...)` function to compile a type. + + +### Types + +Use the `CreateType(...)` function to specify and return a custom type. This function will return a type factory function that can be used to construct the type. The following creates and registers a BigNumber type which will statically infer as `bigint`. ```typescript -const C = TypeCompiler.Compile(Type.Object({ // const C: TypeCheck> +//-------------------------------------------------------------------------------------------- +// +// Use TypeSystem.CreateType(...) to define and return a type factory function +// +//-------------------------------------------------------------------------------------------- -const R = C.Check({ x: 1, y: 2, z: 3 }) // const R = true +type BigNumberOptions = { minimum?: bigint; maximum?: bigint } + +const BigNumber = TypeSystem.CreateType( + 'BigNumber', + (options, value) => { + if (typeof value !== 'bigint') return false + if (options.maximum !== undefined && value > options.maximum) return false + if (options.minimum !== undefined && value < options.minimum) return false + return true + } +) + +//-------------------------------------------------------------------------------------------- +// +// Use the custom type like any other type +// +//-------------------------------------------------------------------------------------------- + +const T = BigNumber({ minimum: 10n, maximum: 20n }) // const T = { + // minimum: 10n, + // maximum: 20n, + // [Symbol(TypeBox.Kind)]: 'BigNumber' + // } + +const C = TypeCompiler.Compile(T) +const X = C.Check(15n) // const X = true +const Y = C.Check(5n) // const Y = false +const Z = C.Check(25n) // const Z = false ``` -Validation errors can be read with the `Errors(...)` function. + + +### Formats + +Use the `CreateFormat(...)` function to specify user defined string formats. The following creates a custom string format that checks for lowercase. ```typescript -const C = TypeCompiler.Compile(Type.Object({ // const C: TypeCheck> +//-------------------------------------------------------------------------------------------- +// +// Use TypeSystem.CreateFormat(...) to define a custom string format +// +//-------------------------------------------------------------------------------------------- -const value = { } +TypeSystem.CreateFormat('lowercase', value => value === value.toLowerCase()) -const errors = [...C.Errors(value)] // const errors = [{ - // schema: { type: 'number' }, - // path: '/x', - // value: undefined, - // message: 'Expected number' - // }, { - // schema: { type: 'number' }, - // path: '/y', - // value: undefined, - // message: 'Expected number' - // }, { - // schema: { type: 'number' }, - // path: '/z', - // value: undefined, - // message: 'Expected number' - // }] -``` +//-------------------------------------------------------------------------------------------- +// +// Use the format by creating string types with the 'format' option +// +//-------------------------------------------------------------------------------------------- -Compiled routines can be inspected with the `.Code()` function. - -```typescript -const C = TypeCompiler.Compile(Type.String()) // const C: TypeCheck - -console.log(C.Code()) // return function check(value) { - // return ( - // (typeof value === 'string') - // ) - // } -``` - - - -### Custom Types - -Use the custom module to create user defined types. User defined types require a `[Kind]` symbol property which is used to match the schema against a registered type. Custom types are specific to TypeBox and can only be used with the TypeCompiler and Value modules. - -The custom module is an optional import. - -```typescript -import { Custom } from '@sinclair/typebox/custom' -``` - -The following creates a `bigint` type. - -```typescript -import { Type, Kind } from '@sinclair/typebox' - -Custom.Set('bigint', (schema, value) => typeof value === 'bigint') -// ▲ -// │ -// └────────────────────────────┐ -// │ -const T = Type.Unsafe({ [Kind]: 'bigint' }) // const T = { [Kind]: 'BigInt' } - -const A = TypeCompiler.Compile(T).Check(1n) // const A = true - -const B = Value.Check(T, 1) // const B = false -``` - - - -### Custom Formats - -Use the format module to create user defined string formats. The format module is specific to TypeBox can only be used with the TypeCompiler and Value modules. If using Ajv, please refer to the official Ajv format documentation located [here](https://ajv.js.org/guide/formats.html). - -The format module is an optional import. - -```typescript -import { Format } from '@sinclair/typebox/format' -``` - -The following creates a custom string format that checks for lowercase. - -```typescript -Format.Set('lowercase', value => value === value.toLowerCase()) -// ▲ -// │ -// └────────────────────┐ -// │ const T = Type.String({ format: 'lowercase' }) -const A = TypeCompiler.Compile(T).Check('action') // const A = true +const A = Value.Check(T, 'action') // const A = true -const B = Value.Check(T, 'Action') // const B = false +const B = Value.Check(T, 'ACTION') // const B = false ``` diff --git a/src/system/system.ts b/src/system/system.ts index 0ddd2a1..6148660 100644 --- a/src/system/system.ts +++ b/src/system/system.ts @@ -26,13 +26,40 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -export namespace TypeSystem { - /** - * Sets whether arrays should be treated as kinds of objects. The default is `false` - */ - export let AllowArrayObjects: boolean = false - /** - * Sets whether numeric checks should consider NaN a valid number type. The default is `false` - */ - export let AllowNaN: boolean = false +import { Kind, Type } from '@sinclair/typebox' +import { Custom } from '../custom/index' +import { Format } from '../format/index' + +export class TypeSystemDuplicateTypeKind extends Error { + constructor(kind: string) { + super(`Duplicate kind '${kind}' detected`) + } +} +export class TypeSystemDuplicateFormat extends Error { + constructor(kind: string) { + super(`Duplicate format '${kind}' detected`) + } +} + +/** Creates user defined types and formats and provides overrides for value checking behaviours */ +export namespace TypeSystem { + /** Sets whether arrays should be treated as kinds of objects. The default is `false` */ + export let AllowArrayObjects: boolean = false + + /** Sets whether numeric checks should consider NaN a valid number type. The default is `false` */ + export let AllowNaN: boolean = false + + /** Creates a custom type */ + export function CreateType(kind: string, callback: (options: Options, value: unknown) => boolean) { + if (Custom.Has(kind)) throw new TypeSystemDuplicateTypeKind(kind) + Custom.Set(kind, callback) + return (options: Partial = {}) => Type.Unsafe({ ...options, [Kind]: kind }) + } + + /** Creates a custom string format */ + export function CreateFormat(format: string, callback: (value: string) => boolean) { + if (Format.Has(format)) throw new TypeSystemDuplicateFormat(format) + Format.Set(format, callback) + return callback + } } diff --git a/test/runtime/system/AllowArrayObjects.ts b/test/runtime/system/AllowArrayObjects.ts index 9ee0c46..0641398 100644 --- a/test/runtime/system/AllowArrayObjects.ts +++ b/test/runtime/system/AllowArrayObjects.ts @@ -2,7 +2,7 @@ import { Ok, Fail } from '../compiler/validate' import { TypeSystem } from '@sinclair/typebox/system' import { Type } from '@sinclair/typebox' -describe('TypeSystem/AllowArrayObjects', () => { +describe('system/TypeSystem/AllowArrayObjects', () => { before(() => { TypeSystem.AllowArrayObjects = true }) diff --git a/test/runtime/system/AllowNaN.ts b/test/runtime/system/AllowNaN.ts index bcf0d9e..4795e79 100644 --- a/test/runtime/system/AllowNaN.ts +++ b/test/runtime/system/AllowNaN.ts @@ -2,7 +2,7 @@ import { Ok, Fail } from '../compiler/validate' import { TypeSystem } from '@sinclair/typebox/system' import { Type } from '@sinclair/typebox' -describe('TypeSystem/AllowNaN', () => { +describe('system/TypeSystem/AllowNaN', () => { before(() => { TypeSystem.AllowNaN = true }) diff --git a/test/runtime/system/CreateFormat.ts b/test/runtime/system/CreateFormat.ts new file mode 100644 index 0000000..751eb34 --- /dev/null +++ b/test/runtime/system/CreateFormat.ts @@ -0,0 +1,17 @@ +import { Ok, Fail } from '../compiler/validate' +import { Assert } from '../assert/index' +import { TypeSystem } from '@sinclair/typebox/system' +import { Type } from '@sinclair/typebox' + +describe('system/TypeSystem/CreateFormat', () => { + it('Should create and validate a format', () => { + TypeSystem.CreateFormat('CreateFormat0', (value) => value === value.toLowerCase()) + const T = Type.String({ format: 'CreateFormat0' }) + Ok(T, 'action') + Fail(T, 'ACTION') + }) + it('Should throw if registering the same format twice', () => { + TypeSystem.CreateFormat('CreateFormat1', (value) => true) + Assert.throws(() => TypeSystem.CreateFormat('CreateFormat1', (value) => true)) + }) +}) diff --git a/test/runtime/system/CreateType.ts b/test/runtime/system/CreateType.ts new file mode 100644 index 0000000..60626b7 --- /dev/null +++ b/test/runtime/system/CreateType.ts @@ -0,0 +1,23 @@ +import { Ok, Fail } from '../compiler/validate' +import { Assert } from '../assert/index' +import { TypeSystem } from '@sinclair/typebox/system' + +describe('system/TypeSystem/CreateType', () => { + it('Should create and validate a type', () => { + type BigNumberOptions = { minimum?: bigint; maximum?: bigint } + const BigNumber = TypeSystem.CreateType('CreateType0', (options, value) => { + if (typeof value !== 'bigint') return false + if (options.maximum !== undefined && value > options.maximum) return false + if (options.minimum !== undefined && value < options.minimum) return false + return true + }) + const T = BigNumber({ minimum: 10n, maximum: 20n }) + Ok(T, 15n) + Fail(T, 5n) + Fail(T, 25n) + }) + it('Should throw if registering the same type twice', () => { + TypeSystem.CreateType('CreateType1', () => true) + Assert.throws(() => TypeSystem.CreateType('CreateType1', () => true)) + }) +}) diff --git a/test/runtime/system/index.ts b/test/runtime/system/index.ts index fde988a..72bdb9c 100644 --- a/test/runtime/system/index.ts +++ b/test/runtime/system/index.ts @@ -1,2 +1,4 @@ import './AllowArrayObjects' import './AllowNaN' +import './CreateFormat' +import './CreateType'