Skip to content

Commit

Permalink
Parse Pipeline and TypeCheck Schema and Reference Accessor Functions
Browse files Browse the repository at this point in the history
  • Loading branch information
sinclairzx81 committed Nov 26, 2024
1 parent 428f906 commit 4da6a67
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 33 deletions.
23 changes: 11 additions & 12 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -1325,27 +1325,26 @@ 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 process a value. The Parse function will call a specific sequence of Value functions (operations) to process the 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 A = Value.Parse(T, { }) // const A = { x: 0, y: 0 }

// Convert
const E = Value.Parse(Type.String(), undefined) // throws AssertError
```
const B = Value.Parse(T, { x: '1', y: '2' }) // const B = { x: 1, y: 2 }
The specific sequence of Value operations is `Clone` `Clean`, `Default`, `Convert`, `Assert` and `Decode`. You can specify user defined sequences in the following way.
// Clean
```typescript
// Same as calling 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
// Same as calling the Convert() then Assert() functions.

const D = Value.Parse(T, undefined) // throws AssertError
const S = Value.Parse(['Convert', 'Assert'], Type.String(), 12345)
```
When specifying user defined sequences, the return type of Parse will be unknown.
<a name='values-equal'></a>
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ export class TypeCheck<T extends TSchema> {
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)
Expand Down
98 changes: 79 additions & 19 deletions src/value/parse/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ 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'
Expand All @@ -36,33 +37,92 @@ import { Clean } from '../clean/clean'
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<string, TParseFunction>([
['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)
}
}
// ------------------------------------------------------------------
// ParseDefault: Default Sequence
// ------------------------------------------------------------------
// prettier-ignore
export const ParseDefault = [
'Clone',
'Clean',
'Default',
'Convert',
'Assert',
'Decode'
] as const

// ------------------------------------------------------------------
// ParseValue
// ------------------------------------------------------------------
function ParseValue<T extends TSchema, R = StaticDecode<T>>(schema: T, references: TSchema[], value: unknown): R {
return ParseReducer.reduce((value, reducer) => reducer(schema, references, value), value) as R
function ParseValue<Type extends TSchema, Result extends StaticDecode<Type> = StaticDecode<Type>>(keys: TParseOperation[], type: Type, references: TSchema[], value: unknown): Result {
return keys.reduce((value, key) => {
const operation = ParseRegistry.Get(key)
if (IsUndefined(operation)) throw new ParseError(`Unable to find Parse operation '${key}'`)
return operation(type, references, value)
}, value) as Result
}

// ------------------------------------------------------------------
// Parse
// ------------------------------------------------------------------
/** Parses a value or throws an `AssertError` if invalid. */
export function Parse<T extends TSchema, R = StaticDecode<T>>(schema: T, references: TSchema[], value: unknown): R
/** Parses a value or throws an `AssertError` if invalid. */
export function Parse<T extends TSchema, R = StaticDecode<T>>(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<Type extends TSchema, Output = StaticDecode<Type>, 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<Type extends TSchema, Output = StaticDecode<Type>, Result extends Output = Output>(schema: Type, value: unknown): Result
/** Parses a value using the specified operations. */
export function Parse<Type extends TSchema>(operations: TParseOperation[], schema: Type, references: TSchema[], value: unknown): unknown
/** Parses a value using the specified operations. */
export function Parse<Type extends TSchema>(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 [reducers, 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(reducers, schema, references, value)
}
20 changes: 20 additions & 0 deletions test/runtime/compiler/__members.ts
Original file line number Diff line number Diff line change
@@ -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()))
})
})
1 change: 1 addition & 0 deletions test/runtime/compiler/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './__members'
import './any'
import './array'
import './async-iterator'
Expand Down
39 changes: 37 additions & 2 deletions test/runtime/value/parse/parse.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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))
})
})

0 comments on commit 4da6a67

Please sign in to comment.