Skip to content

Commit

Permalink
ChangeLog
Browse files Browse the repository at this point in the history
  • Loading branch information
sinclairzx81 committed Oct 27, 2023
1 parent e496eff commit 852b200
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 78 deletions.
53 changes: 53 additions & 0 deletions changelog/0.32.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
## [0.32.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.32.0)

## Overview

Revision 0.32.0 focuses primarily on enhancements to the Value module. It includes two new functions, Default and Clean which can be used for pre and post processing values before and after validation. It also includes a new namespace called ValueGuard which provides type safe guards for JavaScript primitive and object values.

This revision also carries out updates to existing Value functions, and relaxes some of the rules around Types requiring type registration (limiting this requirement to only operations that type check during processing). Other updates include general refactoring and maintaince of existing Value functions.

This revision contains no breaking changes.

## Default

Revision 0.32.0 adds a new Default function for preprocessing values prior to valdiation. This function accepts a schema + value, and will return a new value patched with any specified default values. This function works similar to Ajv's `useDefaults`, but is explicitly called rather than configured and does not mutate the original value. It works exclusively on the optional `default` schema annotation.

The Default function makes a "best effort" attempt to patch values with defaults if it can, but respects any internal value passed on the original value except for `undefined`. This approach is intended to help ensure the caller passing an incorrect value is informed of the incorrect value post validation.

The following shows usage and mapping behaviors.

```typescript
const T = Type.Object({
x: Type.Number({ default: 100 }),
y: Type.Number({ default: 200 })
}, {
default: {}
})

Value.Default(T, undefined) // { x: 100, y: 200 } - undefined, use default {} into { x: 100, y: 200 }
Value.Default(T, null) // null - (null respected)
Value.Default(T, {}) // { x: 100, y: 200 } - empty {} into default x, y
Value.Default(T, { x: 1 }) // { x: 1, y: 200 } - retain x, default y
Value.Default(T, { x: 1, y: 2 }) // { x: 1, y: 2 } - retain x, y
Value.Default(T, { x: 1, y: null }) // { x: 1, y: null } - retain x, y (null respected)
```
The Default function performs no type checking at all and may return incomplete or incorrect values. Because of this, the Default function returns `unknown` and should be checked prior to use. Applications can setup a validation pipeline in the following way.
```ts
import { TSchema, Static, Type } from '@sinclair/typebox'

function Parse<T extends TSchema>(schema: T, value: unknown): Static<T> {
const withDefaults = Value.Default(schema, value)
const valid = Value.Check(schema, withDefaults)
if(!valid) throw new Error(Value.Errors(schema, withDefaults).First()!)
return withDefaults
}

const A = Parse(Type.Object({ // const A = { x: 1, y: 0 }
x: Type.Number({ default: 0 }),
y: Type.Number({ default: 0 }),
}), { x: 1 })
```

## Clean

## ValueGuard
58 changes: 35 additions & 23 deletions examples/index.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,47 @@
import { TypeSystem } from '@sinclair/typebox/system'
import { TypeCompiler } from '@sinclair/typebox/compiler'
import { Value, ValuePointer } from '@sinclair/typebox/value'
import { Value, ValueGuard } from '@sinclair/typebox/value'
import { Type, TypeGuard, Kind, Static, TSchema } from '@sinclair/typebox'

// Todo: Investigate Union Default (initialize interior types)
// Todo: Implement Value.Clean() Tests

const T = Type.Record(Type.Number(), Type.String({ default: 1 }), {
additionalProperties: Type.Any({ default: 1000 }),
})
function Parse<T extends TSchema>(schema: T, value: unknown): Static<T> {
const withDefaults = Value.Default(schema, value)
const valid = Value.Check(schema, withDefaults)
if (!valid) throw new Error(Value.Errors(schema, withDefaults).First()!.message)
return withDefaults
}

export const Loose = Type.Object({
number: Type.Number(),
negNumber: Type.Number(),
maxNumber: Type.Number(),
string: Type.String(),
longString: Type.String(),
boolean: Type.Boolean(),
deeplyNested: Type.Object({
foo: Type.String(),
num: Type.Number(),
bool: Type.Boolean(),
const A = Parse(
Type.Object({
// const A = { x: 1, y: 0 }
x: Type.Number({ default: 0 }),
y: Type.Number({ default: 0 }),
}),
})
{ x: 1 },
)
console.log(A)
// const T = Type.Record(Type.Number(), Type.String({ default: 1 }), {
// additionalProperties: Type.Any({ default: 1000 }),
// })

const C = TypeCompiler.Compile(Loose)
// export const Loose = Type.Object({
// number: Type.Number(),
// negNumber: Type.Number(),
// maxNumber: Type.Number(),
// string: Type.String(),
// longString: Type.String(),
// boolean: Type.Boolean(),
// deeplyNested: Type.Object({
// foo: Type.String(),
// num: Type.Number(),
// bool: Type.Boolean(),
// }),
// })

const A = Value.Create(Loose)
// const C = TypeCompiler.Compile(Loose)

let S = Date.now()
for (let i = 0; i < 1_000_000; i++) {
Value.Clean(Loose, A)
}
console.log(Date.now() - S)
// const A = Value.Clone(new Map())

// console.log(A)
52 changes: 36 additions & 16 deletions src/value/clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,57 @@ THE SOFTWARE.
---------------------------------------------------------------------------*/

import { IsArray, IsDate, IsObject, IsTypedArray, IsValueType } from './guard'
import type { ObjectType, ArrayType, TypedArrayType, ValueType } from './guard'
import { IsArray, IsDate, IsMap, IsSet, IsObject, IsTypedArray, IsValueType } from './guard'
import * as Types from '../typebox'

// --------------------------------------------------------------------------
// Errors
// --------------------------------------------------------------------------
export class CloneError extends Types.TypeBoxError {
constructor(public readonly value: unknown) {
super('Unable to clone value')
}
}
// --------------------------------------------------------------------------
// Clonable
// --------------------------------------------------------------------------
function ObjectType(value: ObjectType): any {
const keys = [...Object.getOwnPropertyNames(value), ...Object.getOwnPropertySymbols(value)]
return keys.reduce((acc, key) => ({ ...acc, [key]: Clone(value[key]) }), {})
function CloneTypedArray(value: TypedArrayType): any {
return value.slice()
}
function ArrayType(value: ArrayType): any {
function CloneArray(value: ArrayType): any {
return value.map((element: any) => Clone(element))
}
function TypedArrayType(value: TypedArrayType): any {
return value.slice()
function CloneDate(value: Date): any {
return new Date(value.getTime())
}
function CloneMap(value: Map<unknown, unknown>): any {
return new Map(value.entries())
}
function CloneSet(value: Set<unknown>): any {
return new Set(value.values())
}
function DateType(value: Date): any {
return new Date(value.toISOString())
function CloneObject(value: ObjectType): any {
const keys = [...Object.getOwnPropertyNames(value), ...Object.getOwnPropertySymbols(value)]
return keys.reduce((acc, key) => ({ ...acc, [key]: Clone(value[key]) }), {})
}
function ValueType(value: ValueType): any {
function CloneValueType(value: ValueType): any {
return value
}
// --------------------------------------------------------------------------
// Clone
// --------------------------------------------------------------------------
/** Returns a structural clone of the given value */
export function Clone<T extends unknown>(value: T): T {
if (IsTypedArray(value)) return TypedArrayType(value)
if (IsArray(value)) return ArrayType(value)
if (IsDate(value)) return DateType(value)
if (IsObject(value)) return ObjectType(value)
if (IsValueType(value)) return ValueType(value)
throw new Error('ValueClone: Unable to clone value')
// prettier-ignore
return (
IsTypedArray(value) ? CloneTypedArray(value) :
IsMap(value) ? CloneMap(value) :
IsSet(value) ? CloneSet(value) :
IsDate(value) ? CloneDate(value) :
IsArray(value) ? CloneArray(value) :
IsObject(value) ? CloneObject(value) :
IsValueType(value) ? CloneValueType(value) :
(() => { throw new CloneError(value) })
)
}
68 changes: 48 additions & 20 deletions src/value/equal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,41 +27,69 @@ THE SOFTWARE.
---------------------------------------------------------------------------*/

import type { ObjectType, ArrayType, TypedArrayType, ValueType } from './guard'
import { IsObject, IsInstanceObject, IsDate, IsArray, IsTypedArray, IsValueType } from './guard'
import { IsObject, IsInstanceObject, IsSet, IsMap, IsDate, IsArray, IsTypedArray, IsValueType } from './guard'

// --------------------------------------------------------------------------
// Equality Checks
// Equality
// --------------------------------------------------------------------------
function ObjectType(left: ObjectType, right: unknown): boolean {
if (!IsObject(right) || IsInstanceObject(right)) return false
const leftKeys = [...Object.keys(left), ...Object.getOwnPropertySymbols(left)]
const rightKeys = [...Object.keys(right), ...Object.getOwnPropertySymbols(right)]
if (leftKeys.length !== rightKeys.length) return false
return leftKeys.every((key) => Equal(left[key], right[key]))
function EqualsTypedArray(left: TypedArrayType, right: unknown): any {
if (!IsTypedArray(right) || left.length !== right.length || Object.getPrototypeOf(left).constructor.name !== Object.getPrototypeOf(right).constructor.name) return false
return left.every((value, index) => Equal(value, right[index]))
}
function DateType(left: Date, right: unknown): any {
function EqualsDate(left: Date, right: unknown): any {
return IsDate(right) && left.getTime() === right.getTime()
}
function ArrayType(left: ArrayType, right: unknown): any {
function EqualsMap(left: Map<unknown, unknown>, right: unknown) {
if (!IsMap(right) || left.size !== right.size) return false
for (const [key, value] of left) {
if (!Equal(value, right.get(key))) return false
}
return true
}
function EqualsSet(left: Set<unknown>, right: unknown) {
if (!IsSet(right) || left.size !== right.size) return false
for (const leftValue of left) {
let found = false
for (const rightValue of right) {
if (Equal(leftValue, rightValue)) {
found = true
break
}
}
if (!found) {
return false
}
}
return true
}
function EqualsArray(left: ArrayType, right: unknown): any {
if (!IsArray(right) || left.length !== right.length) return false
return left.every((value, index) => Equal(value, right[index]))
}
function TypedArrayType(left: TypedArrayType, right: unknown): any {
if (!IsTypedArray(right) || left.length !== right.length || Object.getPrototypeOf(left).constructor.name !== Object.getPrototypeOf(right).constructor.name) return false
return left.every((value, index) => Equal(value, right[index]))
function EqualsObject(left: ObjectType, right: unknown): boolean {
if (!IsObject(right) || IsInstanceObject(right)) return false
const leftKeys = [...Object.keys(left), ...Object.getOwnPropertySymbols(left)]
const rightKeys = [...Object.keys(right), ...Object.getOwnPropertySymbols(right)]
if (leftKeys.length !== rightKeys.length) return false
return leftKeys.every((key) => Equal(left[key], right[key]))
}
function ValueType(left: ValueType, right: unknown): any {
function EqualsValueType(left: ValueType, right: unknown): any {
return left === right
}
// --------------------------------------------------------------------------
// Equal
// --------------------------------------------------------------------------
/** Returns true if left and right values are structurally equal */
export function Equal<T>(left: T, right: unknown): right is T {
if (IsTypedArray(left)) return TypedArrayType(left, right)
if (IsDate(left)) return DateType(left, right)
if (IsArray(left)) return ArrayType(left, right)
if (IsObject(left)) return ObjectType(left, right)
if (IsValueType(left)) return ValueType(left, right)
throw new Error('ValueEquals: Unable to compare value')
// prettier-ignore
return (
IsTypedArray(left) ? EqualsTypedArray(left, right) :
IsDate(left) ? EqualsDate(left, right) :
IsMap(left) ? EqualsMap(left, right) :
IsSet(left) ? EqualsSet(left, right) :
IsArray(left) ? EqualsArray(left, right) :
IsObject(left) ? EqualsObject(left, right) :
IsValueType(left) ? EqualsValueType(left, right) :
false
)
}
41 changes: 23 additions & 18 deletions src/value/guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,35 +57,40 @@ export function IsIterator(value: unknown): value is IterableIterator<any> {
return IsObject(value) && Symbol.iterator in value
}
// --------------------------------------------------------------------------
// Nominal
// Standard
// --------------------------------------------------------------------------
/** Returns true if this value is a typed array */
export function IsTypedArray(value: unknown): value is TypedArrayType {
return ArrayBuffer.isView(value)
/** Returns true if this value has this property key */
export function HasPropertyKey<K extends PropertyKey>(value: Record<any, unknown>, key: K): value is ObjectType & Record<K, unknown> {
return key in value
}
/** Returns true if this value is a Promise */
export function IsPromise(value: unknown): value is Promise<unknown> {
return value instanceof Promise
/** Returns true if this value is an object instance extending anything other than Object */
export function IsInstanceObject(value: unknown): value is ObjectType {
return IsObject(value) && IsFunction(value.constructor) && value.constructor.name !== 'Object'
}
/** Returns true if the value is a Uint8Array */
export function IsUint8Array(value: unknown): value is Uint8Array {
return value instanceof Uint8Array
}
/** Returns true if this value is a Promise */
export function IsPromise(value: unknown): value is Promise<unknown> {
return value instanceof Promise
}
/** Returns true if this value is an instance of Map<K, T> */
export function IsMap(value: unknown): value is Map<unknown, unknown> {
return value instanceof Map
}
/** Returns true if this value is an instance of Set<T> */
export function IsSet(value: unknown): value is Set<unknown> {
return value instanceof Set
}
/** Returns true if this value is a typed array */
export function IsTypedArray(value: unknown): value is TypedArrayType {
return ArrayBuffer.isView(value)
}
/** Returns true if this value is a Date */
export function IsDate(value: unknown): value is Date {
return value instanceof Date && Number.isFinite(value.getTime())
}
// --------------------------------------------------------------------------
// Standard
// --------------------------------------------------------------------------
/** Returns true if this value has this property key */
export function HasPropertyKey<K extends PropertyKey>(value: Record<any, unknown>, key: K): value is ObjectType & Record<K, unknown> {
return key in value
}
/** Returns true if this object is an instance of anything other than Object */
export function IsInstanceObject(value: unknown): value is ObjectType {
return IsObject(value) && IsFunction(value.constructor) && value.constructor.name !== 'Object'
}
/** Returns true of this value is an object type */
export function IsObject(value: unknown): value is ObjectType {
return value !== null && typeof value === 'object'
Expand Down
1 change: 1 addition & 0 deletions src/value/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ export { Edit, Insert, Update, Delete } from './delta'
export { Mutable } from './mutate'
export { ValuePointer } from './pointer'
export { Value } from './value'
export * as ValueGuard from './guard'
2 changes: 1 addition & 1 deletion src/value/value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export namespace Value {
export function Errors(...args: any[]) {
return ValueErrors.Errors.apply(ValueErrors, args as any)
}
/** Returns true if left and right values are structurally equal */
/** Returns true if left and right values are deeply equal */
export function Equal<T>(left: T, right: unknown): right is T {
return ValueEqual.Equal(left, right)
}
Expand Down
22 changes: 22 additions & 0 deletions test/runtime/value/clone/clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,26 @@ describe('value/clone/Clone', () => {
const R = Value.Clone(V)
Assert.IsEqual(R, V)
})
// --------------------------------------------
// Map
// --------------------------------------------
it('Should clone Map 1', () => {
const V = new Map([
['A', 1],
['B', 2],
['C', 3],
])
const R = Value.Clone(V)
Assert.IsEqual(V, R)
Assert.IsTrue(V !== R)
})
// --------------------------------------------
// Set
// --------------------------------------------
it('Should clone Set 1', () => {
const V = new Set([1, 2, 3])
const R = Value.Clone(V)
Assert.IsEqual(V, R)
Assert.IsTrue(V !== R)
})
})
Loading

0 comments on commit 852b200

Please sign in to comment.