From cd9712c463a385ac3df8b2980b6adafaab249a66 Mon Sep 17 00:00:00 2001 From: Haydn Paterson Date: Tue, 26 Nov 2024 19:42:36 +0900 Subject: [PATCH] Revision 0.34.9 (#1101) * Parse Pipeline and TypeCheck Schema and Reference Accessor Functions * Documentation * Version --- changelog/0.34.0.md | 3 + package-lock.json | 4 +- package.json | 2 +- readme.md | 22 +++--- src/compiler/compiler.ts | 8 +++ src/value/parse/parse.ts | 106 ++++++++++++++++++++++------- test/runtime/compiler/__members.ts | 20 ++++++ test/runtime/compiler/index.ts | 1 + test/runtime/value/parse/parse.ts | 39 ++++++++++- 9 files changed, 167 insertions(+), 38 deletions(-) create mode 100644 test/runtime/compiler/__members.ts diff --git a/changelog/0.34.0.md b/changelog/0.34.0.md index 755d04bf3..7e5e46939 100644 --- a/changelog/0.34.0.md +++ b/changelog/0.34.0.md @@ -1,4 +1,7 @@ ### 0.34.0 +- [Revision 0.34.9](https://github.com/sinclairzx81/typebox/pull/1101) + - User Defined Parse Pipelines + - Access to Schema and References on TypeCheck - [Revision 0.34.8](https://github.com/sinclairzx81/typebox/pull/1098) - Fix for Computed Readonly and Optional Properties - [Revision 0.34.7](https://github.com/sinclairzx81/typebox/pull/1093) diff --git a/package-lock.json b/package-lock.json index 8d9666303..a962edf2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.34.8", + "version": "0.34.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.34.8", + "version": "0.34.9", "license": "MIT", "devDependencies": { "@arethetypeswrong/cli": "^0.13.2", diff --git a/package.json b/package.json index b841d7895..279b3dd59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.34.8", + "version": "0.34.9", "description": "Json Schema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", diff --git a/readme.md b/readme.md index 4004ece9f..6dc0ceb83 100644 --- a/readme.md +++ b/readme.md @@ -1325,26 +1325,28 @@ const B = Value.Encode(Type.String(), 42) // throw ### Parse -Use the Parse function to parse a value or throw if invalid. This function internally uses Default, Clean, Convert and Decode to make a best effort attempt to parse the value into the expected type. This function should not be used in performance critical code paths. +Use the Parse function to parse a value. This function calls the `Clone` `Clean`, `Default`, `Convert`, `Assert` and `Decode` Value functions in this exact order to process a value. ```typescript -const T = Type.Object({ x: Type.Number({ default: 0 }), y: Type.Number({ default: 0 }) }) +const R = Value.Parse(Type.String(), 'hello') // const R: string = "hello" -// Default +const E = Value.Parse(Type.String(), undefined) // throws AssertError +``` -const A = Value.Parse(T, { }) // const A = { x: 0, y: 0 } +You can override the order in which functions are are run, or omit functions entirely in the following way. -// Convert +```typescript +// Runs no functions. -const B = Value.Parse(T, { x: '1', y: '2' }) // const B = { x: 1, y: 2 } +const R = Value.Parse([], Type.String(), 12345) -// Clean +// Runs the Assert() function. -const C = Value.Parse(T, { x: 1, y: 2, z: 3 }) // const C = { x: 1, y: 2 } +const E = Value.Parse(['Assert'], Type.String(), 12345) -// Assert +// Runs the Convert() function followed by the Assert() function. -const D = Value.Parse(T, undefined) // throws AssertError +const S = Value.Parse(['Convert', 'Assert'], Type.String(), 12345) ``` diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 6e438702b..7450c8aab 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -97,6 +97,14 @@ export class TypeCheck { public Code(): string { return this.code } + /** Returns the schema type used to validate */ + public Schema(): T { + return this.schema + } + /** Returns reference types used to validate */ + public References(): TSchema[] { + return this.references + } /** Returns an iterator for each error in this value. */ public Errors(value: unknown): ValueErrorIterator { return Errors(this.schema, this.references, value) diff --git a/src/value/parse/parse.ts b/src/value/parse/parse.ts index 13477a4c9..be07f5b81 100644 --- a/src/value/parse/parse.ts +++ b/src/value/parse/parse.ts @@ -26,43 +26,103 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -import { TransformDecode, HasTransform } from '../transform/index' +import { TypeBoxError } from '../../type/error/index' +import { TransformDecode, TransformEncode, HasTransform } from '../transform/index' import { TSchema } from '../../type/schema/index' import { StaticDecode } from '../../type/static/index' -import { Assert } from '../assert/assert' -import { Default } from '../default/default' -import { Convert } from '../convert/convert' -import { Clean } from '../clean/clean' +import { Assert } from '../assert/index' +import { Default } from '../default/index' +import { Convert } from '../convert/index' +import { Clean } from '../clean/index' import { Clone } from '../clone/index' // ------------------------------------------------------------------ -// ParseReducer +// Guards // ------------------------------------------------------------------ -type ReducerFunction = (schema: TSchema, references: TSchema[], value: unknown) => unknown +import { IsArray, IsUndefined } from '../guard/index' + +// ------------------------------------------------------------------ +// Error +// ------------------------------------------------------------------ +export class ParseError extends TypeBoxError { + constructor(message: string) { + super(message) + } +} + +// ------------------------------------------------------------------ +// ParseRegistry +// ------------------------------------------------------------------ +export type TParseOperation = 'Clone' | 'Clean' | 'Default' | 'Convert' | 'Assert' | 'Decode' | ({} & string) +export type TParseFunction = (type: TSchema, references: TSchema[], value: unknown) => unknown // prettier-ignore -const ParseReducer: ReducerFunction[] = [ - (_schema, _references, value) => Clone(value), - (schema, references, value) => Default(schema, references, value), - (schema, references, value) => Clean(schema, references, value), - (schema, references, value) => Convert(schema, references, value), - (schema, references, value) => { Assert(schema, references, value); return value }, - (schema, references, value) => (HasTransform(schema, references) ? TransformDecode(schema, references, value) : value), -] +export namespace ParseRegistry { + const registry = new Map([ + ['Clone', (_type, _references, value: unknown) => Clone(value)], + ['Clean', (type, references, value: unknown) => Clean(type, references, value)], + ['Default', (type, references, value: unknown) => Default(type, references, value)], + ['Convert', (type, references, value: unknown) => Convert(type, references, value)], + ['Assert', (type, references, value: unknown) => { Assert(type, references, value); return value }], + ['Decode', (type, references, value: unknown) => (HasTransform(type, references) ? TransformDecode(type, references, value) : value)], + ['Encode', (type, references, value: unknown) => (HasTransform(type, references) ? TransformEncode(type, references, value) : value)], + ]) + // Deletes an entry from the registry + export function Delete(key: string): void { + registry.delete(key) + } + // Sets an entry in the registry + export function Set(key: string, callback: TParseFunction): void { + registry.set(key, callback) + } + // Gets an entry in the registry + export function Get(key: string): TParseFunction | undefined { + return registry.get(key) + } +} +// ------------------------------------------------------------------ +// Default Parse Sequence +// ------------------------------------------------------------------ +// prettier-ignore +export const ParseDefault = [ + 'Clone', + 'Clean', + 'Default', + 'Convert', + 'Assert', + 'Decode' +] as const + // ------------------------------------------------------------------ // ParseValue // ------------------------------------------------------------------ -function ParseValue>(schema: T, references: TSchema[], value: unknown): R { - return ParseReducer.reduce((value, reducer) => reducer(schema, references, value), value) as R +function ParseValue = StaticDecode>(operations: TParseOperation[], type: Type, references: TSchema[], value: unknown): Result { + return operations.reduce((value, operationKey) => { + const operation = ParseRegistry.Get(operationKey) + if (IsUndefined(operation)) throw new ParseError(`Unable to find Parse operation '${operationKey}'`) + return operation(type, references, value) + }, value) as Result } + // ------------------------------------------------------------------ // Parse // ------------------------------------------------------------------ -/** Parses a value or throws an `AssertError` if invalid. */ -export function Parse>(schema: T, references: TSchema[], value: unknown): R -/** Parses a value or throws an `AssertError` if invalid. */ -export function Parse>(schema: T, value: unknown): R -/** Parses a value or throws an `AssertError` if invalid. */ +/** Parses a value using the default parse pipeline. Will throws an `AssertError` if invalid. */ +export function Parse, Result extends Output = Output>(schema: Type, references: TSchema[], value: unknown): Result +/** Parses a value using the default parse pipeline. Will throws an `AssertError` if invalid. */ +export function Parse, Result extends Output = Output>(schema: Type, value: unknown): Result +/** Parses a value using the specified operations. */ +export function Parse(operations: TParseOperation[], schema: Type, references: TSchema[], value: unknown): unknown +/** Parses a value using the specified operations. */ +export function Parse(operations: TParseOperation[], schema: Type, value: unknown): unknown +/** Parses a value */ export function Parse(...args: any[]): unknown { - return args.length === 3 ? ParseValue(args[0], args[1], args[2]) : ParseValue(args[0], [], args[1]) + // prettier-ignore + const [operations, schema, references, value] = ( + args.length === 4 ? [args[0], args[1], args[2], args[3]] : + args.length === 3 ? IsArray(args[0]) ? [args[0], args[1], [], args[2]] : [ParseDefault, args[0], args[1], args[2]] : + args.length === 2 ? [ParseDefault, args[0], [], args[1]] : + (() => { throw new ParseError('Invalid Arguments') })() + ) + return ParseValue(operations, schema, references, value) } diff --git a/test/runtime/compiler/__members.ts b/test/runtime/compiler/__members.ts new file mode 100644 index 000000000..6ade7f40b --- /dev/null +++ b/test/runtime/compiler/__members.ts @@ -0,0 +1,20 @@ +import { TypeCompiler } from '@sinclair/typebox/compiler' +import { Type, TypeGuard, ValueGuard } from '@sinclair/typebox' +import { Assert } from '../assert/index' + +describe('compiler/TypeCheckMembers', () => { + it('Should return Schema', () => { + const A = TypeCompiler.Compile(Type.Number(), [Type.String(), Type.Boolean()]) + Assert.IsTrue(TypeGuard.IsNumber(A.Schema())) + }) + it('Should return References', () => { + const A = TypeCompiler.Compile(Type.Number(), [Type.String(), Type.Boolean()]) + Assert.IsTrue(TypeGuard.IsNumber(A.Schema())) + Assert.IsTrue(TypeGuard.IsString(A.References()[0])) + Assert.IsTrue(TypeGuard.IsBoolean(A.References()[1])) + }) + it('Should return Code', () => { + const A = TypeCompiler.Compile(Type.Number(), [Type.String(), Type.Boolean()]) + Assert.IsTrue(ValueGuard.IsString(A.Code())) + }) +}) diff --git a/test/runtime/compiler/index.ts b/test/runtime/compiler/index.ts index 21decd7fa..a39e51760 100644 --- a/test/runtime/compiler/index.ts +++ b/test/runtime/compiler/index.ts @@ -1,3 +1,4 @@ +import './__members' import './any' import './array' import './async-iterator' diff --git a/test/runtime/value/parse/parse.ts b/test/runtime/value/parse/parse.ts index 5c8be9dd7..2bf7106de 100644 --- a/test/runtime/value/parse/parse.ts +++ b/test/runtime/value/parse/parse.ts @@ -1,5 +1,5 @@ -import { Value, AssertError } from '@sinclair/typebox/value' -import { Type } from '@sinclair/typebox' +import { Value, AssertError, ParseRegistry } from '@sinclair/typebox/value' +import { Type, TypeGuard } from '@sinclair/typebox' import { Assert } from '../../assert/index' // prettier-ignore @@ -87,4 +87,39 @@ describe('value/Parse', () => { const X = Value.Parse(T, 'world') Assert.IsEqual(X, 'hello') }) + // ---------------------------------------------------------------- + // Operations + // ---------------------------------------------------------------- + it('Should run operations 1', () => { + const A = Type.Object({ x: Type.Number() }) + const I = { x: 1 } + const O = Value.Parse([], A, I) + Assert.IsTrue(I === O) + }) + it('Should run operations 2', () => { + const A = Type.Object({ x: Type.Number() }) + const I = { x: 1 } + const O = Value.Parse(['Clone'], A, I) + Assert.IsTrue(I !== O) + }) + it('Should run operations 3', () => { + ParseRegistry.Set('Intercept', ( schema, references, value) => { throw 1 }) + const A = Type.Object({ x: Type.Number() }) + Assert.Throws(() => Value.Parse(['Intercept'], A, null)) + ParseRegistry.Delete('Intercept') + const F = ParseRegistry.Get('Intercept') + Assert.IsEqual(F, undefined) + }) + it('Should run operations 4', () => { + ParseRegistry.Set('Intercept', ( schema, references, value) => { + Assert.IsEqual(value, 12345) + Assert.IsTrue(TypeGuard.IsNumber(schema)) + Assert.IsTrue(TypeGuard.IsString(references[0])) + }) + Value.Parse(['Intercept'], Type.Number(), [Type.String()], 12345) + ParseRegistry.Delete('Intercept') + }) + it('Should run operations 5', () => { + Assert.Throws(() => Value.Parse(['Intercept'], Type.String(), null)) + }) })