From ee72178a8fb1f9c41e77003505fc6c96aba384cc Mon Sep 17 00:00:00 2001 From: sinclair Date: Wed, 26 Jul 2023 17:28:18 +0900 Subject: [PATCH] Revision 0.30.0 Compression --- example/index.ts | 74 ++- package.json | 11 + src/compiler/compiler.ts | 4 +- src/errors/errors.ts | 63 ++- src/system/system.ts | 8 +- src/value/cast.ts | 479 ++++++++++---------- src/value/check.ts | 778 ++++++++++++++++---------------- src/value/clone.ts | 85 ++-- src/value/convert.ts | 617 ++++++++++++------------- src/value/create.ts | 682 ++++++++++++++-------------- src/value/delta.ts | 234 +++++----- src/value/equal.ts | 73 ++- src/value/guard.ts | 181 ++++++++ src/value/hash.ts | 258 +++++------ src/value/index.ts | 7 +- src/value/is.ts | 59 --- src/value/mutate.ts | 124 ++--- src/value/pointer.ts | 13 +- src/value/value.ts | 44 +- test/runtime/value/hash/hash.ts | 104 ++--- tsconfig.json | 33 ++ 21 files changed, 2003 insertions(+), 1928 deletions(-) create mode 100644 src/value/guard.ts delete mode 100644 src/value/is.ts diff --git a/example/index.ts b/example/index.ts index 82d0e49ef..0ef2a7acc 100644 --- a/example/index.ts +++ b/example/index.ts @@ -8,23 +8,39 @@ import { Type, TypeGuard, Kind, Static, TSchema } from '@sinclair/typebox' // - Add Readonly Symbol // - Add Optional Symbol // TypeExtends: -// - implemented StructuralRight() +// - implemented StructuralRight() - (done) // deprecate Type.RegExp() -// - Deprecated Type.RegEx() -// - Add Type.RegExp() +// - Deprecated Type.RegEx() - (done) +// - Add Type.RegExp() - (done) // Array: contains, minContains, maxContains -// - typecompiler - (done) -// - value - (done) -// - error - (done) -// - create - (done) -// - tests - (done) +// - typecompiler - (done) +// - value - (done) +// - error - (done) +// - create - (done) +// - tests - (done) // Iterators: Type.Iterator & Type.IteratorAsync -// - typecompiler - (done) -// - value - (done) -// - error - (done) -// - create - (done) -// - cast - (done) -// - tests - (done - not create) +// - typecompiler - (done) +// - value - (done) +// - error - (done) +// - create - (done) +// - cast - (done) +// - tests - (done - not create) +// Value +// - remove namespaces - (done) +// - retain value pointer - (done) +// - add common value guard utility - (done) +// - figure out what to do with non-nomimal-object + +// 0.29.0 +// ┌──────────────────────┬────────────┬────────────┬─────────────┐ +// │ (index) │ Compiled │ Minified │ Compression │ +// ├──────────────────────┼────────────┼────────────┼─────────────┤ +// │ typebox/compiler │ '130.3 kb' │ ' 58.2 kb' │ '2.24 x' │ +// │ typebox/errors │ '113.3 kb' │ ' 49.8 kb' │ '2.27 x' │ +// │ typebox/system │ ' 78.8 kb' │ ' 32.2 kb' │ '2.45 x' │ +// │ typebox/value │ '180.0 kb' │ ' 77.7 kb' │ '2.32 x' │ +// │ typebox │ ' 77.7 kb' │ ' 31.7 kb' │ '2.45 x' │ +// └──────────────────────┴────────────┴────────────┴─────────────┘ // ┌──────────────────────┬────────────┬────────────┬─────────────┐ // │ (index) │ Compiled │ Minified │ Compression │ @@ -35,3 +51,33 @@ import { Type, TypeGuard, Kind, Static, TSchema } from '@sinclair/typebox' // │ typebox/value │ '181.7 kb' │ ' 79.2 kb' │ '2.29 x' │ // │ typebox │ ' 74.3 kb' │ ' 30.8 kb' │ '2.41 x' │ // └──────────────────────┴────────────┴────────────┴─────────────┘ + +// ┌──────────────────────┬────────────┬────────────┬─────────────┐ +// │ (index) │ Compiled │ Minified │ Compression │ +// ├──────────────────────┼────────────┼────────────┼─────────────┤ +// │ typebox/compiler │ '129.4 kb' │ ' 58.7 kb' │ '2.20 x' │ +// │ typebox/errors │ '111.1 kb' │ ' 49.8 kb' │ '2.23 x' │ +// │ typebox/system │ ' 75.4 kb' │ ' 31.3 kb' │ '2.41 x' │ +// │ typebox/value │ '175.6 kb' │ ' 77.9 kb' │ '2.25 x' │ +// │ typebox │ ' 74.3 kb' │ ' 30.8 kb' │ '2.41 x' │ +// └──────────────────────┴────────────┴────────────┴─────────────┘ + +// ┌──────────────────────┬────────────┬────────────┬─────────────┐ +// │ (index) │ Compiled │ Minified │ Compression │ +// ├──────────────────────┼────────────┼────────────┼─────────────┤ +// │ typebox/compiler │ '129.4 kb' │ ' 58.7 kb' │ '2.20 x' │ +// │ typebox/errors │ '111.1 kb' │ ' 49.8 kb' │ '2.23 x' │ +// │ typebox/system │ ' 75.4 kb' │ ' 31.3 kb' │ '2.41 x' │ +// │ typebox/value │ '175.5 kb' │ ' 77.7 kb' │ '2.26 x' │ +// │ typebox │ ' 74.3 kb' │ ' 30.8 kb' │ '2.41 x' │ +// └──────────────────────┴────────────┴────────────┴─────────────┘ + +// ┌──────────────────────┬────────────┬────────────┬─────────────┐ +// │ (index) │ Compiled │ Minified │ Compression │ +// ├──────────────────────┼────────────┼────────────┼─────────────┤ +// │ typebox/compiler │ '130.3 kb' │ ' 59.1 kb' │ '2.20 x' │ +// │ typebox/errors │ '112.0 kb' │ ' 50.2 kb' │ '2.23 x' │ +// │ typebox/system │ ' 75.4 kb' │ ' 31.3 kb' │ '2.41 x' │ +// │ typebox/value │ '175.2 kb' │ ' 77.0 kb' │ '2.28 x' │ +// │ typebox │ ' 74.3 kb' │ ' 30.8 kb' │ '2.41 x' │ +// └──────────────────────┴────────────┴────────────┴─────────────┘ diff --git a/package.json b/package.json index 8a0edf3e0..378d6074c 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,17 @@ "./compiler": "./compiler/index.js", "./errors": "./errors/index.js", "./system": "./system/index.js", + "./value/cast": "./value/cast.js", + "./value/check": "./value/check.js", + "./value/clone": "./value/clone.js", + "./value/convert": "./value/convert.js", + "./value/create": "./value/create.js", + "./value/delta": "./value/delta.js", + "./value/equal": "./value/equal.js", + "./value/guard": "./value/guard.js", + "./value/hash": "./value/hash.js", + "./value/mutate": "./value/mutate.js", + "./value/pointer": "./value/pointer.js", "./value": "./value/index.js", ".": "./typebox.js" }, diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index da4a06735..9e3ae82f6 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -29,7 +29,7 @@ THE SOFTWARE. import * as Types from '../typebox' import { ValueErrors, ValueErrorIterator } from '../errors/index' import { TypeSystem } from '../system/index' -import { ValueHash } from '../value/hash' +import * as ValueHash from '../value/hash' // ------------------------------------------------------------------- // CheckFunction @@ -554,7 +554,7 @@ export namespace TypeCompiler { return checkFunc(value) } function valueHashFunction(value: unknown) { - return ValueHash.Create(value) + return ValueHash.Hash(value) } const checkFunction = compiledFunction(typeRegistryFunction, formatRegistryFunction, valueHashFunction) return new TypeCheck(schema, references, checkFunction, code) diff --git a/src/errors/errors.ts b/src/errors/errors.ts index f1417e1c3..46f4ec852 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -26,8 +26,9 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ import * as Types from '../typebox' +import * as ValueHash from '../value/hash' +import * as ValueGuard from '../value/guard' import { TypeSystem } from '../system/index' -import { ValueHash } from '../value/hash' // ------------------------------------------------------------------- // ValueErrorType @@ -140,15 +141,6 @@ export namespace ValueErrors { // ---------------------------------------------------------------------- // Guards // ---------------------------------------------------------------------- - function IsBigInt(value: unknown): value is bigint { - return typeof value === 'bigint' - } - function IsInteger(value: unknown): value is number { - return globalThis.Number.isInteger(value) - } - function IsString(value: unknown): value is string { - return typeof value === 'string' - } function IsDefined(value: unknown): value is T { return value !== undefined } @@ -159,27 +151,26 @@ export namespace ValueErrors { return TypeSystem.ExactOptionalPropertyTypes ? key in value : value[key] !== undefined } function IsObject(value: unknown): value is Record { - const result = typeof value === 'object' && value !== null - return TypeSystem.AllowArrayObjects ? result : result && !globalThis.Array.isArray(value) + const isObject = ValueGuard.IsObject(value) + return TypeSystem.AllowArrayObjects ? isObject : isObject && !ValueGuard.IsArray(value) } function IsRecordObject(value: unknown): value is Record { return IsObject(value) && !(value instanceof globalThis.Date) && !(value instanceof globalThis.Uint8Array) } function IsNumber(value: unknown): value is number { - const result = typeof value === 'number' - return TypeSystem.AllowNaN ? result : result && globalThis.Number.isFinite(value) + const isNumber = ValueGuard.IsNumber(value) + return TypeSystem.AllowNaN ? isNumber : isNumber && globalThis.Number.isFinite(value) } function IsVoid(value: unknown): value is void { - const result = value === undefined - return TypeSystem.AllowVoidNull ? result || value === null : result + const isUndefined = ValueGuard.IsUndefined(value) + return TypeSystem.AllowVoidNull ? isUndefined || value === null : isUndefined } - // ---------------------------------------------------------------------- // Types // ---------------------------------------------------------------------- function* Any(schema: Types.TAny, references: Types.TSchema[], path: string, value: any): IterableIterator {} function* Array(schema: Types.TArray, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!globalThis.Array.isArray(value)) { + if (!ValueGuard.IsArray(value)) { return yield { type: ValueErrorType.Array, schema, path, value, message: `Expected array` } } if (IsDefined(schema.minItems) && !(value.length >= schema.minItems)) { @@ -193,7 +184,7 @@ export namespace ValueErrors { yield* Visit(schema.items, references, `${path}/${i}`, value[i]) } // prettier-ignore - if (schema.uniqueItems === true && !((function () { const set = new Set(); for (const element of value) { const hashed = ValueHash.Create(element); if (set.has(hashed)) { return false } else { set.add(hashed) } } return true })())) { + if (schema.uniqueItems === true && !((function () { const set = new Set(); for (const element of value) { const hashed = ValueHash.Hash(element); if (set.has(hashed)) { return false } else { set.add(hashed) } } return true })())) { yield { type: ValueErrorType.ArrayUniqueItems, schema, path, value, message: `Expected array elements to be unique` } } // contains @@ -201,24 +192,24 @@ export namespace ValueErrors { return } const containsSchema = IsDefined(schema.contains) ? schema.contains : Types.Type.Never() - const containsCount = value.reduce((acc, value, index) => (Visit(containsSchema, references, `${path}${index}`, value).next().done === true ? acc + 1 : acc), 0) + const containsCount = value.reduce((acc: number, value, index) => (Visit(containsSchema, references, `${path}${index}`, value).next().done === true ? acc + 1 : acc), 0) if (containsCount === 0) { yield { type: ValueErrorType.ArrayContains, schema, path, value, message: `Expected array to contain at least one matching type` } } - if (IsNumber(schema.minContains) && containsCount < schema.minContains) { + if (ValueGuard.IsNumber(schema.minContains) && containsCount < schema.minContains) { yield { type: ValueErrorType.ArrayMinContains, schema, path, value, message: `Expected array to contain at least ${schema.minContains} matching types` } } - if (IsNumber(schema.maxContains) && containsCount > schema.maxContains) { + if (ValueGuard.IsNumber(schema.maxContains) && containsCount > schema.maxContains) { yield { type: ValueErrorType.ArrayMaxContains, schema, path, value, message: `Expected array to contain no more than ${schema.maxContains} matching types` } } } function* AsyncIterator(schema: Types.TAsyncIterator, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!(IsObject(value) && globalThis.Symbol.asyncIterator in value)) { + if (!ValueGuard.IsAsyncIterator(value)) { yield { type: ValueErrorType.AsyncIterator, schema, path, value, message: `Expected value to be an async iterator` } } } function* BigInt(schema: Types.TBigInt, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!IsBigInt(value)) { + if (!ValueGuard.IsBigInt(value)) { return yield { type: ValueErrorType.BigInt, schema, path, value, message: `Expected bigint` } } if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === globalThis.BigInt(0))) { @@ -238,7 +229,7 @@ export namespace ValueErrors { } } function* Boolean(schema: Types.TBoolean, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!(typeof value === 'boolean')) { + if (!ValueGuard.IsBoolean(value)) { return yield { type: ValueErrorType.Boolean, schema, path, value, message: `Expected boolean` } } } @@ -246,7 +237,7 @@ export namespace ValueErrors { yield* Visit(schema.returns, references, path, value.prototype) } function* Date(schema: Types.TDate, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!(value instanceof globalThis.Date)) { + if (!ValueGuard.IsDate(value)) { return yield { type: ValueErrorType.Date, schema, path, value, message: `Expected Date object` } } if (!globalThis.isFinite(value.getTime())) { @@ -266,12 +257,12 @@ export namespace ValueErrors { } } function* Function(schema: Types.TFunction, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!(typeof value === 'function')) { + if (!ValueGuard.IsFunction(value)) { return yield { type: ValueErrorType.Function, schema, path, value, message: `Expected function` } } } function* Integer(schema: Types.TInteger, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!IsInteger(value)) { + if (!ValueGuard.IsInteger(value)) { return yield { type: ValueErrorType.Integer, schema, path, value, message: `Expected integer` } } if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === 0)) { @@ -341,7 +332,7 @@ export namespace ValueErrors { } } function* Null(schema: Types.TNull, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!(value === null)) { + if (!ValueGuard.IsNull(value)) { return yield { type: ValueErrorType.Null, schema, path, value, message: `Expected null` } } } @@ -410,7 +401,7 @@ export namespace ValueErrors { } } function* Promise(schema: Types.TPromise, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!(typeof value === 'object' && typeof value.then === 'function')) { + if (!ValueGuard.IsPromise(value)) { yield { type: ValueErrorType.Promise, schema, path, value, message: `Expected Promise` } } } @@ -448,7 +439,7 @@ export namespace ValueErrors { yield* Visit(target, references, path, value) } function* String(schema: Types.TString, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!IsString(value)) { + if (!ValueGuard.IsString(value)) { return yield { type: ValueErrorType.String, schema, path, value, message: 'Expected string' } } if (IsDefined(schema.minLength) && !(value.length >= schema.minLength)) { @@ -457,13 +448,13 @@ export namespace ValueErrors { if (IsDefined(schema.maxLength) && !(value.length <= schema.maxLength)) { yield { type: ValueErrorType.StringMaxLength, schema, path, value, message: `Expected string length less or equal to ${schema.maxLength}` } } - if (schema.pattern !== undefined) { + if (ValueGuard.IsString(schema.pattern)) { const regex = new RegExp(schema.pattern) if (!regex.test(value)) { yield { type: ValueErrorType.StringPattern, schema, path, value, message: `Expected string to match pattern ${schema.pattern}` } } } - if (schema.format !== undefined) { + if (ValueGuard.IsString(schema.format)) { if (!Types.FormatRegistry.Has(schema.format)) { yield { type: ValueErrorType.StringFormatUnknown, schema, path, value, message: `Unknown string format '${schema.format}'` } } else { @@ -475,12 +466,12 @@ export namespace ValueErrors { } } function* Symbol(schema: Types.TSymbol, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!(typeof value === 'symbol')) { + if (!ValueGuard.IsSymbol(value)) { return yield { type: ValueErrorType.Symbol, schema, path, value, message: 'Expected symbol' } } } function* TemplateLiteral(schema: Types.TTemplateLiteral, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!IsString(value)) { + if (!ValueGuard.IsString(value)) { return yield { type: ValueErrorType.String, schema, path, value, message: 'Expected string' } } const regex = new RegExp(schema.pattern) @@ -531,7 +522,7 @@ export namespace ValueErrors { } } function* Uint8Array(schema: Types.TUint8Array, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (!(value instanceof globalThis.Uint8Array)) { + if (!ValueGuard.IsUint8Array(value)) { return yield { type: ValueErrorType.Uint8Array, schema, path, value, message: `Expected Uint8Array` } } if (IsDefined(schema.maxByteLength) && !(value.length <= schema.maxByteLength)) { diff --git a/src/system/system.ts b/src/system/system.ts index 880f800e4..4b5291fd2 100644 --- a/src/system/system.ts +++ b/src/system/system.ts @@ -38,12 +38,8 @@ export class TypeSystemDuplicateFormat extends Error { super(`Duplicate string format '${kind}' detected`) } } - /** Creates user defined types and formats and provides overrides for value checking behaviours */ export namespace TypeSystem { - // ------------------------------------------------------------------------ - // Assertion Policies - // ------------------------------------------------------------------------ /** Sets whether TypeBox should assert optional properties using the TypeScript `exactOptionalPropertyTypes` assertion policy. The default is `false` */ export let ExactOptionalPropertyTypes: boolean = false /** Sets whether arrays should be treated as a kind of objects. The default is `false` */ @@ -52,9 +48,7 @@ export namespace TypeSystem { export let AllowNaN: boolean = false /** Sets whether `null` should validate for void types. The default is `false` */ export let AllowVoidNull: boolean = false - // ------------------------------------------------------------------------ - // String Formats and Types - // ------------------------------------------------------------------------ + /** Creates a new type */ export function Type(kind: string, check: (options: Options, value: unknown) => boolean) { if (Types.TypeRegistry.Has(kind)) throw new TypeSystemDuplicateTypeKind(kind) diff --git a/src/value/cast.ts b/src/value/cast.ts index 1fbde60f1..7112b1eed 100644 --- a/src/value/cast.ts +++ b/src/value/cast.ts @@ -27,13 +27,14 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ import * as Types from '../typebox' -import { ValueCreate } from './create' -import { ValueCheck } from './check' -import { ValueClone } from './clone' +import * as ValueCreate from './create' +import * as ValueCheck from './check' +import * as ValueClone from './clone' +import * as ValueGuard from './guard' -// ---------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------- // Errors -// ---------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------- export class ValueCastReferenceTypeError extends Error { constructor(public readonly schema: Types.TRef | Types.TThis) { super(`ValueCast: Cannot locate referenced schema with $id '${schema.$ref}'`) @@ -64,17 +65,18 @@ export class ValueCastDereferenceError extends Error { super(`ValueCast: Unable to dereference schema with $id '${schema.$ref}'`) } } -// ---------------------------------------------------------------------------------------------- -// The following will score a schema against a value. For objects, the score is the tally of -// points awarded for each property of the value. Property points are (1.0 / propertyCount) -// to prevent large property counts biasing results. Properties that match literal values are -// maximally awarded as literals are typically used as union discriminator fields. -// ---------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------- +// The following will score a schema against a value. For objects, the score +// is the tally of points awarded for each property of the value. Property +// points are (1.0 / propertyCount) to prevent large property counts biasing +// results. Properties that match literal values are maximally awarded as +// literals are typically used as union discriminator fields. +// -------------------------------------------------------------------------- namespace UnionCastCreate { function Score(schema: Types.TSchema, references: Types.TSchema[], value: any): number { - if (schema[Types.Kind] === 'Object' && typeof value === 'object' && value !== null) { + if (schema[Types.Kind] === 'Object' && typeof value === 'object' && !ValueGuard.IsNull(value)) { const object = schema as Types.TObject - const keys = Object.keys(value) + const keys = globalThis.Object.getOwnPropertyNames(value) const entries = globalThis.Object.entries(object.properties) const [point, max] = [1 / entries.length, entries.length] return entries.reduce((acc, [key, schema]) => { @@ -99,247 +101,230 @@ namespace UnionCastCreate { return select } export function Create(union: Types.TUnion, references: Types.TSchema[], value: any) { - if (union.default !== undefined) { + if ('default' in union) { return union.default } else { const schema = Select(union, references, value) - return ValueCast.Cast(schema, references, value) + return Cast(schema, references, value) } } } - -export namespace ValueCast { - // ---------------------------------------------------------------------------------------------- - // Guards - // ---------------------------------------------------------------------------------------------- - function IsObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !globalThis.Array.isArray(value) - } - function IsArray(value: unknown): value is unknown[] { - return typeof value === 'object' && globalThis.Array.isArray(value) - } - function IsNumber(value: unknown): value is number { - return typeof value === 'number' && !isNaN(value) - } - function IsString(value: unknown): value is string { - return typeof value === 'string' - } - // ---------------------------------------------------------------------------------------------- - // Cast - // ---------------------------------------------------------------------------------------------- - function Any(schema: Types.TAny, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) - } - function Array(schema: Types.TArray, references: Types.TSchema[], value: any): any { - if (ValueCheck.Check(schema, references, value)) return ValueClone.Clone(value) - const created = IsArray(value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) - const minimum = IsNumber(schema.minItems) && created.length < schema.minItems ? [...created, ...globalThis.Array.from({ length: schema.minItems - created.length }, () => null)] : created - const maximum = IsNumber(schema.maxItems) && minimum.length > schema.maxItems ? minimum.slice(0, schema.maxItems) : minimum - const casted = maximum.map((value: unknown) => Visit(schema.items, references, value)) - if (schema.uniqueItems !== true) return casted - const unique = [...new Set(casted)] - if (!ValueCheck.Check(schema, references, unique)) throw new ValueCastArrayUniqueItemsTypeError(schema, unique) - return unique - } - function AsyncIterator(schema: Types.TAsyncIterator, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) - } - function BigInt(schema: Types.TBigInt, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) - } - function Boolean(schema: Types.TBoolean, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) - } - function Constructor(schema: Types.TConstructor, references: Types.TSchema[], value: any): any { - if (ValueCheck.Check(schema, references, value)) return ValueCreate.Create(schema, references) - const required = new Set(schema.returns.required || []) - const result = function () {} - for (const [key, property] of globalThis.Object.entries(schema.returns.properties)) { - if (!required.has(key) && value.prototype[key] === undefined) continue - result.prototype[key] = Visit(property as Types.TSchema, references, value.prototype[key]) - } - return result - } - function Date(schema: Types.TDate, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) - } - function Function(schema: Types.TFunction, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) - } - function Integer(schema: Types.TInteger, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) - } - function Intersect(schema: Types.TIntersect, references: Types.TSchema[], value: any): any { - const created = ValueCreate.Create(schema, references) - const mapped = IsObject(created) && IsObject(value) ? { ...(created as any), ...value } : value - return ValueCheck.Check(schema, references, mapped) ? mapped : ValueCreate.Create(schema, references) - } - function Iterator(schema: Types.TIterator, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) - } - function Literal(schema: Types.TLiteral, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) - } - function Never(schema: Types.TNever, references: Types.TSchema[], value: any): any { - throw new ValueCastNeverTypeError(schema) - } - function Not(schema: Types.TNot, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) - } - function Null(schema: Types.TNull, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) - } - function Number(schema: Types.TNumber, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) - } - function Object(schema: Types.TObject, references: Types.TSchema[], value: any): any { - if (ValueCheck.Check(schema, references, value)) return value - if (value === null || typeof value !== 'object') return ValueCreate.Create(schema, references) - const required = new Set(schema.required || []) - const result = {} as Record - for (const [key, property] of globalThis.Object.entries(schema.properties)) { - if (!required.has(key) && value[key] === undefined) continue - result[key] = Visit(property, references, value[key]) - } - // additional schema properties - if (typeof schema.additionalProperties === 'object') { - const propertyNames = globalThis.Object.getOwnPropertyNames(schema.properties) - for (const propertyName of globalThis.Object.getOwnPropertyNames(value)) { - if (propertyNames.includes(propertyName)) continue - result[propertyName] = Visit(schema.additionalProperties, references, value[propertyName]) - } - } - return result - } - function Promise(schema: Types.TSchema, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) - } - function Record(schema: Types.TRecord, references: Types.TSchema[], value: any): any { - if (ValueCheck.Check(schema, references, value)) return ValueClone.Clone(value) - if (value === null || typeof value !== 'object' || globalThis.Array.isArray(value) || value instanceof globalThis.Date) return ValueCreate.Create(schema, references) - const subschemaPropertyName = globalThis.Object.getOwnPropertyNames(schema.patternProperties)[0] - const subschema = schema.patternProperties[subschemaPropertyName] - const result = {} as Record - for (const [propKey, propValue] of globalThis.Object.entries(value)) { - result[propKey] = Visit(subschema, references, propValue) - } - return result - } - function Ref(schema: Types.TRef, references: Types.TSchema[], value: any): any { - const index = references.findIndex((foreign) => foreign.$id === schema.$ref) - if (index === -1) throw new ValueCastDereferenceError(schema) - const target = references[index] - return Visit(target, references, value) - } - function String(schema: Types.TString, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) - } - function Symbol(schema: Types.TSymbol, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) - } - function TemplateLiteral(schema: Types.TSymbol, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) - } - function This(schema: Types.TThis, references: Types.TSchema[], value: any): any { - const index = references.findIndex((foreign) => foreign.$id === schema.$ref) - if (index === -1) throw new ValueCastDereferenceError(schema) - const target = references[index] - return Visit(target, references, value) - } - function Tuple(schema: Types.TTuple, references: Types.TSchema[], value: any): any { - if (ValueCheck.Check(schema, references, value)) return ValueClone.Clone(value) - if (!globalThis.Array.isArray(value)) return ValueCreate.Create(schema, references) - if (schema.items === undefined) return [] - return schema.items.map((schema, index) => Visit(schema, references, value[index])) - } - function Undefined(schema: Types.TUndefined, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) - } - function Union(schema: Types.TUnion, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : UnionCastCreate.Create(schema, references, value) - } - function Uint8Array(schema: Types.TUint8Array, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) - } - function Unknown(schema: Types.TUnknown, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) - } - function Void(schema: Types.TVoid, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) - } - function UserDefined(schema: Types.TSchema, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) - } - export function Visit(schema: Types.TSchema, references: Types.TSchema[], value: any): any { - const references_ = IsString(schema.$id) ? [...references, schema] : references - const schema_ = schema as any - switch (schema[Types.Kind]) { - case 'Any': - return Any(schema_, references_, value) - case 'Array': - return Array(schema_, references_, value) - case 'AsyncIterator': - return AsyncIterator(schema_, references_, value) - case 'BigInt': - return BigInt(schema_, references_, value) - case 'Boolean': - return Boolean(schema_, references_, value) - case 'Constructor': - return Constructor(schema_, references_, value) - case 'Date': - return Date(schema_, references_, value) - case 'Function': - return Function(schema_, references_, value) - case 'Integer': - return Integer(schema_, references_, value) - case 'Intersect': - return Intersect(schema_, references_, value) - case 'Iterator': - return Iterator(schema_, references_, value) - case 'Literal': - return Literal(schema_, references_, value) - case 'Never': - return Never(schema_, references_, value) - case 'Not': - return Not(schema_, references_, value) - case 'Null': - return Null(schema_, references_, value) - case 'Number': - return Number(schema_, references_, value) - case 'Object': - return Object(schema_, references_, value) - case 'Promise': - return Promise(schema_, references_, value) - case 'Record': - return Record(schema_, references_, value) - case 'Ref': - return Ref(schema_, references_, value) - case 'String': - return String(schema_, references_, value) - case 'Symbol': - return Symbol(schema_, references_, value) - case 'TemplateLiteral': - return TemplateLiteral(schema_, references_, value) - case 'This': - return This(schema_, references_, value) - case 'Tuple': - return Tuple(schema_, references_, value) - case 'Undefined': - return Undefined(schema_, references_, value) - case 'Union': - return Union(schema_, references_, value) - case 'Uint8Array': - return Uint8Array(schema_, references_, value) - case 'Unknown': - return Unknown(schema_, references_, value) - case 'Void': - return Void(schema_, references_, value) - default: - if (!Types.TypeRegistry.Has(schema_[Types.Kind])) throw new ValueCastUnknownTypeError(schema_) - return UserDefined(schema_, references_, value) +// -------------------------------------------------------------------------- +// Cast +// -------------------------------------------------------------------------- +function Any(schema: Types.TAny, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) +} +function Array(schema: Types.TArray, references: Types.TSchema[], value: any): any { + if (ValueCheck.Check(schema, references, value)) return ValueClone.Clone(value) + const created = ValueGuard.IsArray(value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) + const minimum = ValueGuard.IsNumber(schema.minItems) && created.length < schema.minItems ? [...created, ...globalThis.Array.from({ length: schema.minItems - created.length }, () => null)] : created + const maximum = ValueGuard.IsNumber(schema.maxItems) && minimum.length > schema.maxItems ? minimum.slice(0, schema.maxItems) : minimum + const casted = maximum.map((value: unknown) => Visit(schema.items, references, value)) + if (schema.uniqueItems !== true) return casted + const unique = [...new Set(casted)] + if (!ValueCheck.Check(schema, references, unique)) throw new ValueCastArrayUniqueItemsTypeError(schema, unique) + return unique +} +function AsyncIterator(schema: Types.TAsyncIterator, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) +} +function BigInt(schema: Types.TBigInt, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) +} +function Boolean(schema: Types.TBoolean, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) +} +function Constructor(schema: Types.TConstructor, references: Types.TSchema[], value: any): any { + if (ValueCheck.Check(schema, references, value)) return ValueCreate.Create(schema, references) + const required = new Set(schema.returns.required || []) + const result = function () {} + for (const [key, property] of globalThis.Object.entries(schema.returns.properties)) { + if (!required.has(key) && value.prototype[key] === undefined) continue + result.prototype[key] = Visit(property as Types.TSchema, references, value.prototype[key]) + } + return result +} +function Date(schema: Types.TDate, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) +} +function Function(schema: Types.TFunction, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) +} +function Integer(schema: Types.TInteger, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) +} +function Intersect(schema: Types.TIntersect, references: Types.TSchema[], value: any): any { + const created = ValueCreate.Create(schema, references) + const mapped = ValueGuard.IsNonNominalObject(created) && ValueGuard.IsNonNominalObject(value) ? { ...(created as any), ...value } : value + return ValueCheck.Check(schema, references, mapped) ? mapped : ValueCreate.Create(schema, references) +} +function Iterator(schema: Types.TIterator, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) +} +function Literal(schema: Types.TLiteral, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) +} +function Never(schema: Types.TNever, references: Types.TSchema[], value: any): any { + throw new ValueCastNeverTypeError(schema) +} +function Not(schema: Types.TNot, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) +} +function Null(schema: Types.TNull, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) +} +function Number(schema: Types.TNumber, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) +} +function Object(schema: Types.TObject, references: Types.TSchema[], value: any): any { + if (ValueCheck.Check(schema, references, value)) return value + if (value === null || typeof value !== 'object') return ValueCreate.Create(schema, references) + const required = new Set(schema.required || []) + const result = {} as Record + for (const [key, property] of globalThis.Object.entries(schema.properties)) { + if (!required.has(key) && value[key] === undefined) continue + result[key] = Visit(property, references, value[key]) + } + // additional schema properties + if (typeof schema.additionalProperties === 'object') { + const propertyNames = globalThis.Object.getOwnPropertyNames(schema.properties) + for (const propertyName of globalThis.Object.getOwnPropertyNames(value)) { + if (propertyNames.includes(propertyName)) continue + result[propertyName] = Visit(schema.additionalProperties, references, value[propertyName]) } } - export function Cast(schema: T, references: Types.TSchema[], value: any): Types.Static { - return Visit(schema, references, ValueClone.Clone(value)) + return result +} +function Promise(schema: Types.TSchema, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) +} +function Record(schema: Types.TRecord, references: Types.TSchema[], value: any): any { + if (ValueCheck.Check(schema, references, value)) return ValueClone.Clone(value) + if (value === null || typeof value !== 'object' || globalThis.Array.isArray(value) || value instanceof globalThis.Date) return ValueCreate.Create(schema, references) + const subschemaPropertyName = globalThis.Object.getOwnPropertyNames(schema.patternProperties)[0] + const subschema = schema.patternProperties[subschemaPropertyName] + const result = {} as Record + for (const [propKey, propValue] of globalThis.Object.entries(value)) { + result[propKey] = Visit(subschema, references, propValue) + } + return result +} +function Ref(schema: Types.TRef, references: Types.TSchema[], value: any): any { + const index = references.findIndex((foreign) => foreign.$id === schema.$ref) + if (index === -1) throw new ValueCastDereferenceError(schema) + const target = references[index] + return Visit(target, references, value) +} +function String(schema: Types.TString, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) +} +function Symbol(schema: Types.TSymbol, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) +} +function TemplateLiteral(schema: Types.TSymbol, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) +} +function This(schema: Types.TThis, references: Types.TSchema[], value: any): any { + const index = references.findIndex((foreign) => foreign.$id === schema.$ref) + if (index === -1) throw new ValueCastDereferenceError(schema) + const target = references[index] + return Visit(target, references, value) +} +function Tuple(schema: Types.TTuple, references: Types.TSchema[], value: any): any { + if (ValueCheck.Check(schema, references, value)) return ValueClone.Clone(value) + if (!ValueGuard.IsArray(value)) return ValueCreate.Create(schema, references) + if (schema.items === undefined) return [] + return schema.items.map((schema, index) => Visit(schema, references, value[index])) +} +function Undefined(schema: Types.TUndefined, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) +} +function Union(schema: Types.TUnion, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : UnionCastCreate.Create(schema, references, value) +} +function Uint8Array(schema: Types.TUint8Array, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) +} +function Unknown(schema: Types.TUnknown, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) +} +function Void(schema: Types.TVoid, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) +} +function UserDefined(schema: Types.TSchema, references: Types.TSchema[], value: any): any { + return ValueCheck.Check(schema, references, value) ? ValueClone.Clone(value) : ValueCreate.Create(schema, references) +} +export function Visit(schema: Types.TSchema, references: Types.TSchema[], value: any): any { + const references_ = ValueGuard.IsString(schema.$id) ? [...references, schema] : references + const schema_ = schema as any + switch (schema[Types.Kind]) { + case 'Any': + return Any(schema_, references_, value) + case 'Array': + return Array(schema_, references_, value) + case 'AsyncIterator': + return AsyncIterator(schema_, references_, value) + case 'BigInt': + return BigInt(schema_, references_, value) + case 'Boolean': + return Boolean(schema_, references_, value) + case 'Constructor': + return Constructor(schema_, references_, value) + case 'Date': + return Date(schema_, references_, value) + case 'Function': + return Function(schema_, references_, value) + case 'Integer': + return Integer(schema_, references_, value) + case 'Intersect': + return Intersect(schema_, references_, value) + case 'Iterator': + return Iterator(schema_, references_, value) + case 'Literal': + return Literal(schema_, references_, value) + case 'Never': + return Never(schema_, references_, value) + case 'Not': + return Not(schema_, references_, value) + case 'Null': + return Null(schema_, references_, value) + case 'Number': + return Number(schema_, references_, value) + case 'Object': + return Object(schema_, references_, value) + case 'Promise': + return Promise(schema_, references_, value) + case 'Record': + return Record(schema_, references_, value) + case 'Ref': + return Ref(schema_, references_, value) + case 'String': + return String(schema_, references_, value) + case 'Symbol': + return Symbol(schema_, references_, value) + case 'TemplateLiteral': + return TemplateLiteral(schema_, references_, value) + case 'This': + return This(schema_, references_, value) + case 'Tuple': + return Tuple(schema_, references_, value) + case 'Undefined': + return Undefined(schema_, references_, value) + case 'Union': + return Union(schema_, references_, value) + case 'Uint8Array': + return Uint8Array(schema_, references_, value) + case 'Unknown': + return Unknown(schema_, references_, value) + case 'Void': + return Void(schema_, references_, value) + default: + if (!Types.TypeRegistry.Has(schema_[Types.Kind])) throw new ValueCastUnknownTypeError(schema_) + return UserDefined(schema_, references_, value) } } +/** Casts a value into a given type. The return value will retain as much information of the original value as possible. Cast will convert string, number, boolean and date values if a reasonable conversion is possible. */ +export function Cast(schema: T, references: Types.TSchema[], value: any): Types.Static { + return Visit(schema, references, ValueClone.Clone(value)) +} diff --git a/src/value/check.ts b/src/value/check.ts index 028c06666..62e52af38 100644 --- a/src/value/check.ts +++ b/src/value/check.ts @@ -28,11 +28,12 @@ THE SOFTWARE. import * as Types from '../typebox' import { TypeSystem } from '../system/index' -import { ValueHash } from './hash' +import * as ValueGuard from './guard' +import * as ValueHash from './hash' -// ------------------------------------------------------------------------- +// -------------------------------------------------------------------------- // Errors -// ------------------------------------------------------------------------- +// -------------------------------------------------------------------------- export class ValueCheckUnknownTypeError extends Error { constructor(public readonly schema: Types.TSchema) { super(`ValueCheck: ${schema[Types.Kind] ? `Unknown type '${schema[Types.Kind]}'` : 'Unknown type'}`) @@ -43,448 +44,437 @@ export class ValueCheckDereferenceError extends Error { super(`ValueCheck: Unable to dereference schema with $id '${schema.$ref}'`) } } -export namespace ValueCheck { - // ---------------------------------------------------------------------- - // Guards - // ---------------------------------------------------------------------- - function IsBigInt(value: unknown): value is bigint { - return typeof value === 'bigint' +// -------------------------------------------------------------------------- +// TypeGuards +// -------------------------------------------------------------------------- +function IsAnyOrUnknown(schema: Types.TSchema) { + return schema[Types.Kind] === 'Any' || schema[Types.Kind] === 'Unknown' +} +// -------------------------------------------------------------------------- +// Guards +// -------------------------------------------------------------------------- +function IsDefined(value: unknown): value is T { + return value !== undefined +} +// -------------------------------------------------------------------------- +// Policies +// -------------------------------------------------------------------------- +function IsExactOptionalProperty(value: Record, key: string) { + return TypeSystem.ExactOptionalPropertyTypes ? key in value : value[key] !== undefined +} +function IsObject(value: unknown): value is Record { + const isObject = ValueGuard.IsObject(value) + return TypeSystem.AllowArrayObjects ? isObject : isObject && !ValueGuard.IsArray(value) +} +function IsRecordObject(value: unknown): value is Record { + return IsObject(value) && !(value instanceof globalThis.Date) && !(value instanceof globalThis.Uint8Array) +} +function IsNumber(value: unknown): value is number { + const isNumber = ValueGuard.IsNumber(value) + return TypeSystem.AllowNaN ? isNumber : isNumber && globalThis.Number.isFinite(value) +} +function IsVoid(value: unknown): value is void { + const isUndefined = ValueGuard.IsUndefined(value) + return TypeSystem.AllowVoidNull ? isUndefined || value === null : isUndefined +} +// -------------------------------------------------------------------------- +// Types +// -------------------------------------------------------------------------- +function Any(schema: Types.TAny, references: Types.TSchema[], value: any): boolean { + return true +} +function Array(schema: Types.TArray, references: Types.TSchema[], value: any): boolean { + if (!globalThis.Array.isArray(value)) { + return false } - function IsInteger(value: unknown): value is number { - return globalThis.Number.isInteger(value) + if (IsDefined(schema.minItems) && !(value.length >= schema.minItems)) { + return false } - function IsString(value: unknown): value is string { - return typeof value === 'string' + if (IsDefined(schema.maxItems) && !(value.length <= schema.maxItems)) { + return false } - function IsDefined(value: unknown): value is T { - return value !== undefined + if (!value.every((value) => Visit(schema.items, references, value))) { + return false } - // ---------------------------------------------------------------------- - // SchemaGuards - // ---------------------------------------------------------------------- - function IsAnyOrUnknown(schema: Types.TSchema) { - return schema[Types.Kind] === 'Any' || schema[Types.Kind] === 'Unknown' + // prettier-ignore + if (schema.uniqueItems === true && !((function() { const set = new Set(); for(const element of value) { const hashed = ValueHash.Hash(element); if(set.has(hashed)) { return false } else { set.add(hashed) } } return true })())) { + return false + } + // contains + if (!(IsDefined(schema.contains) || IsNumber(schema.minContains) || IsNumber(schema.maxContains))) { + return true // exit } - // ---------------------------------------------------------------------- - // Policies - // ---------------------------------------------------------------------- - function IsExactOptionalProperty(value: Record, key: string) { - return TypeSystem.ExactOptionalPropertyTypes ? key in value : value[key] !== undefined + const containsSchema = IsDefined(schema.contains) ? schema.contains : Types.Type.Never() + const containsCount = value.reduce((acc, value) => (Visit(containsSchema, references, value) ? acc + 1 : acc), 0) + if (containsCount === 0) { + return false } - function IsObject(value: unknown): value is Record { - const result = typeof value === 'object' && value !== null - return TypeSystem.AllowArrayObjects ? result : result && !globalThis.Array.isArray(value) + if (IsNumber(schema.minContains) && containsCount < schema.minContains) { + return false } - function IsRecordObject(value: unknown): value is Record { - return IsObject(value) && !(value instanceof globalThis.Date) && !(value instanceof globalThis.Uint8Array) + if (IsNumber(schema.maxContains) && containsCount > schema.maxContains) { + return false } - function IsNumber(value: unknown): value is number { - const result = typeof value === 'number' - return TypeSystem.AllowNaN ? result : result && globalThis.Number.isFinite(value) + return true +} +function AsyncIterator(schema: Types.TAsyncIterator, references: Types.TSchema[], value: any): boolean { + return IsObject(value) && globalThis.Symbol.asyncIterator in value +} +function BigInt(schema: Types.TBigInt, references: Types.TSchema[], value: any): boolean { + if (!ValueGuard.IsBigInt(value)) { + return false } - function IsVoid(value: unknown): value is void { - const result = value === undefined - return TypeSystem.AllowVoidNull ? result || value === null : result + if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === globalThis.BigInt(0))) { + return false } - // ---------------------------------------------------------------------- - // Types - // ---------------------------------------------------------------------- - function Any(schema: Types.TAny, references: Types.TSchema[], value: any): boolean { - return true + if (IsDefined(schema.exclusiveMinimum) && !(value > schema.exclusiveMinimum)) { + return false } - function Array(schema: Types.TArray, references: Types.TSchema[], value: any): boolean { - if (!globalThis.Array.isArray(value)) { - return false - } - if (IsDefined(schema.minItems) && !(value.length >= schema.minItems)) { - return false - } - if (IsDefined(schema.maxItems) && !(value.length <= schema.maxItems)) { - return false - } - if (!value.every((value) => Visit(schema.items, references, value))) { - return false - } - // prettier-ignore - if (schema.uniqueItems === true && !((function() { const set = new Set(); for(const element of value) { const hashed = ValueHash.Create(element); if(set.has(hashed)) { return false } else { set.add(hashed) } } return true })())) { - return false - } - // contains - if (!(IsDefined(schema.contains) || IsNumber(schema.minContains) || IsNumber(schema.maxContains))) { - return true // exit - } - const containsSchema = IsDefined(schema.contains) ? schema.contains : Types.Type.Never() - const containsCount = value.reduce((acc, value) => (Visit(containsSchema, references, value) ? acc + 1 : acc), 0) - if (containsCount === 0) { - return false - } - if (IsNumber(schema.minContains) && containsCount < schema.minContains) { - return false - } - if (IsNumber(schema.maxContains) && containsCount > schema.maxContains) { - return false - } - return true + if (IsDefined(schema.exclusiveMaximum) && !(value < schema.exclusiveMaximum)) { + return false } - function AsyncIterator(schema: Types.TAsyncIterator, references: Types.TSchema[], value: any): boolean { - return IsObject(value) && globalThis.Symbol.asyncIterator in value + if (IsDefined(schema.minimum) && !(value >= schema.minimum)) { + return false } - function BigInt(schema: Types.TBigInt, references: Types.TSchema[], value: any): boolean { - if (!IsBigInt(value)) { - return false - } - if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === globalThis.BigInt(0))) { - return false - } - if (IsDefined(schema.exclusiveMinimum) && !(value > schema.exclusiveMinimum)) { - return false - } - if (IsDefined(schema.exclusiveMaximum) && !(value < schema.exclusiveMaximum)) { - return false - } - if (IsDefined(schema.minimum) && !(value >= schema.minimum)) { - return false - } - if (IsDefined(schema.maximum) && !(value <= schema.maximum)) { - return false - } - return true + if (IsDefined(schema.maximum) && !(value <= schema.maximum)) { + return false } - function Boolean(schema: Types.TBoolean, references: Types.TSchema[], value: any): boolean { - return typeof value === 'boolean' + return true +} +function Boolean(schema: Types.TBoolean, references: Types.TSchema[], value: any): boolean { + return typeof value === 'boolean' +} +function Constructor(schema: Types.TConstructor, references: Types.TSchema[], value: any): boolean { + return Visit(schema.returns, references, value.prototype) +} +function Date(schema: Types.TDate, references: Types.TSchema[], value: any): boolean { + if (!(value instanceof globalThis.Date)) { + return false } - function Constructor(schema: Types.TConstructor, references: Types.TSchema[], value: any): boolean { - return Visit(schema.returns, references, value.prototype) + if (!IsNumber(value.getTime())) { + return false } - function Date(schema: Types.TDate, references: Types.TSchema[], value: any): boolean { - if (!(value instanceof globalThis.Date)) { - return false - } - if (!IsNumber(value.getTime())) { - return false - } - if (IsDefined(schema.exclusiveMinimumTimestamp) && !(value.getTime() > schema.exclusiveMinimumTimestamp)) { - return false - } - if (IsDefined(schema.exclusiveMaximumTimestamp) && !(value.getTime() < schema.exclusiveMaximumTimestamp)) { - return false - } - if (IsDefined(schema.minimumTimestamp) && !(value.getTime() >= schema.minimumTimestamp)) { - return false - } - if (IsDefined(schema.maximumTimestamp) && !(value.getTime() <= schema.maximumTimestamp)) { - return false - } - return true + if (IsDefined(schema.exclusiveMinimumTimestamp) && !(value.getTime() > schema.exclusiveMinimumTimestamp)) { + return false } - function Function(schema: Types.TFunction, references: Types.TSchema[], value: any): boolean { - return typeof value === 'function' + if (IsDefined(schema.exclusiveMaximumTimestamp) && !(value.getTime() < schema.exclusiveMaximumTimestamp)) { + return false } - function Integer(schema: Types.TInteger, references: Types.TSchema[], value: any): boolean { - if (!IsInteger(value)) { - return false - } - if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === 0)) { - return false - } - if (IsDefined(schema.exclusiveMinimum) && !(value > schema.exclusiveMinimum)) { - return false - } - if (IsDefined(schema.exclusiveMaximum) && !(value < schema.exclusiveMaximum)) { - return false - } - if (IsDefined(schema.minimum) && !(value >= schema.minimum)) { - return false - } - if (IsDefined(schema.maximum) && !(value <= schema.maximum)) { - return false - } - return true + if (IsDefined(schema.minimumTimestamp) && !(value.getTime() >= schema.minimumTimestamp)) { + return false } - function Intersect(schema: Types.TIntersect, references: Types.TSchema[], value: any): boolean { - const check1 = schema.allOf.every((schema) => Visit(schema, references, value)) - if (schema.unevaluatedProperties === false) { - const keyPattern = new RegExp(Types.KeyResolver.ResolvePattern(schema)) - const check2 = globalThis.Object.getOwnPropertyNames(value).every((key) => keyPattern.test(key)) - return check1 && check2 - } else if (Types.TypeGuard.TSchema(schema.unevaluatedProperties)) { - const keyCheck = new RegExp(Types.KeyResolver.ResolvePattern(schema)) - const check2 = globalThis.Object.getOwnPropertyNames(value).every((key) => keyCheck.test(key) || Visit(schema.unevaluatedProperties as Types.TSchema, references, value[key])) - return check1 && check2 - } else { - return check1 - } + if (IsDefined(schema.maximumTimestamp) && !(value.getTime() <= schema.maximumTimestamp)) { + return false } - function Iterator(schema: Types.TIterator, references: Types.TSchema[], value: any): boolean { - return IsObject(value) && globalThis.Symbol.iterator in value + return true +} +function Function(schema: Types.TFunction, references: Types.TSchema[], value: any): boolean { + return typeof value === 'function' +} +function Integer(schema: Types.TInteger, references: Types.TSchema[], value: any): boolean { + if (!ValueGuard.IsInteger(value)) { + return false } - function Literal(schema: Types.TLiteral, references: Types.TSchema[], value: any): boolean { - return value === schema.const + if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === 0)) { + return false } - function Never(schema: Types.TNever, references: Types.TSchema[], value: any): boolean { + if (IsDefined(schema.exclusiveMinimum) && !(value > schema.exclusiveMinimum)) { return false } - function Not(schema: Types.TNot, references: Types.TSchema[], value: any): boolean { - return !Visit(schema.not, references, value) + if (IsDefined(schema.exclusiveMaximum) && !(value < schema.exclusiveMaximum)) { + return false } - function Null(schema: Types.TNull, references: Types.TSchema[], value: any): boolean { - return value === null + if (IsDefined(schema.minimum) && !(value >= schema.minimum)) { + return false } - function Number(schema: Types.TNumber, references: Types.TSchema[], value: any): boolean { - if (!IsNumber(value)) { - return false - } - if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === 0)) { - return false - } - if (IsDefined(schema.exclusiveMinimum) && !(value > schema.exclusiveMinimum)) { - return false - } - if (IsDefined(schema.exclusiveMaximum) && !(value < schema.exclusiveMaximum)) { - return false - } - if (IsDefined(schema.minimum) && !(value >= schema.minimum)) { - return false - } - if (IsDefined(schema.maximum) && !(value <= schema.maximum)) { - return false - } - return true + if (IsDefined(schema.maximum) && !(value <= schema.maximum)) { + return false } - function Object(schema: Types.TObject, references: Types.TSchema[], value: any): boolean { - if (!IsObject(value)) { - return false - } - if (IsDefined(schema.minProperties) && !(globalThis.Object.getOwnPropertyNames(value).length >= schema.minProperties)) { - return false - } - if (IsDefined(schema.maxProperties) && !(globalThis.Object.getOwnPropertyNames(value).length <= schema.maxProperties)) { - return false - } - const knownKeys = globalThis.Object.getOwnPropertyNames(schema.properties) - for (const knownKey of knownKeys) { - const property = schema.properties[knownKey] - if (schema.required && schema.required.includes(knownKey)) { - if (!Visit(property, references, value[knownKey])) { - return false - } - if ((Types.ExtendsUndefined.Check(property) || IsAnyOrUnknown(property)) && !(knownKey in value)) { - return false - } - } else { - if (IsExactOptionalProperty(value, knownKey) && !Visit(property, references, value[knownKey])) { - return false - } - } - } - if (schema.additionalProperties === false) { - const valueKeys = globalThis.Object.getOwnPropertyNames(value) - // optimization: value is valid if schemaKey length matches the valueKey length - if (schema.required && schema.required.length === knownKeys.length && valueKeys.length === knownKeys.length) { - return true - } else { - return valueKeys.every((valueKey) => knownKeys.includes(valueKey)) - } - } else if (typeof schema.additionalProperties === 'object') { - const valueKeys = globalThis.Object.getOwnPropertyNames(value) - return valueKeys.every((key) => knownKeys.includes(key) || Visit(schema.additionalProperties as Types.TSchema, references, value[key])) - } else { - return true - } + return true +} +function Intersect(schema: Types.TIntersect, references: Types.TSchema[], value: any): boolean { + const check1 = schema.allOf.every((schema) => Visit(schema, references, value)) + if (schema.unevaluatedProperties === false) { + const keyPattern = new RegExp(Types.KeyResolver.ResolvePattern(schema)) + const check2 = globalThis.Object.getOwnPropertyNames(value).every((key) => keyPattern.test(key)) + return check1 && check2 + } else if (Types.TypeGuard.TSchema(schema.unevaluatedProperties)) { + const keyCheck = new RegExp(Types.KeyResolver.ResolvePattern(schema)) + const check2 = globalThis.Object.getOwnPropertyNames(value).every((key) => keyCheck.test(key) || Visit(schema.unevaluatedProperties as Types.TSchema, references, value[key])) + return check1 && check2 + } else { + return check1 } - function Promise(schema: Types.TPromise, references: Types.TSchema[], value: any): boolean { - return typeof value === 'object' && typeof value.then === 'function' +} +function Iterator(schema: Types.TIterator, references: Types.TSchema[], value: any): boolean { + return IsObject(value) && globalThis.Symbol.iterator in value +} +function Literal(schema: Types.TLiteral, references: Types.TSchema[], value: any): boolean { + return value === schema.const +} +function Never(schema: Types.TNever, references: Types.TSchema[], value: any): boolean { + return false +} +function Not(schema: Types.TNot, references: Types.TSchema[], value: any): boolean { + return !Visit(schema.not, references, value) +} +function Null(schema: Types.TNull, references: Types.TSchema[], value: any): boolean { + return value === null +} +function Number(schema: Types.TNumber, references: Types.TSchema[], value: any): boolean { + if (!IsNumber(value)) { + return false } - function Record(schema: Types.TRecord, references: Types.TSchema[], value: any): boolean { - if (!IsRecordObject(value)) { - return false - } - if (IsDefined(schema.minProperties) && !(globalThis.Object.getOwnPropertyNames(value).length >= schema.minProperties)) { - return false - } - if (IsDefined(schema.maxProperties) && !(globalThis.Object.getOwnPropertyNames(value).length <= schema.maxProperties)) { - return false - } - const [patternKey, patternSchema] = globalThis.Object.entries(schema.patternProperties)[0] - const regex = new RegExp(patternKey) - return globalThis.Object.entries(value).every(([key, value]) => { - if (regex.test(key)) { - return Visit(patternSchema, references, value) + if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === 0)) { + return false + } + if (IsDefined(schema.exclusiveMinimum) && !(value > schema.exclusiveMinimum)) { + return false + } + if (IsDefined(schema.exclusiveMaximum) && !(value < schema.exclusiveMaximum)) { + return false + } + if (IsDefined(schema.minimum) && !(value >= schema.minimum)) { + return false + } + if (IsDefined(schema.maximum) && !(value <= schema.maximum)) { + return false + } + return true +} +function Object(schema: Types.TObject, references: Types.TSchema[], value: any): boolean { + if (!IsObject(value)) { + return false + } + if (IsDefined(schema.minProperties) && !(globalThis.Object.getOwnPropertyNames(value).length >= schema.minProperties)) { + return false + } + if (IsDefined(schema.maxProperties) && !(globalThis.Object.getOwnPropertyNames(value).length <= schema.maxProperties)) { + return false + } + const knownKeys = globalThis.Object.getOwnPropertyNames(schema.properties) + for (const knownKey of knownKeys) { + const property = schema.properties[knownKey] + if (schema.required && schema.required.includes(knownKey)) { + if (!Visit(property, references, value[knownKey])) { + return false } - if (typeof schema.additionalProperties === 'object') { - return Visit(schema.additionalProperties, references, value) + if ((Types.ExtendsUndefined.Check(property) || IsAnyOrUnknown(property)) && !(knownKey in value)) { + return false } - if (schema.additionalProperties === false) { + } else { + if (IsExactOptionalProperty(value, knownKey) && !Visit(property, references, value[knownKey])) { return false } - return true - }) - } - function Ref(schema: Types.TRef, references: Types.TSchema[], value: any): boolean { - const index = references.findIndex((foreign) => foreign.$id === schema.$ref) - if (index === -1) throw new ValueCheckDereferenceError(schema) - const target = references[index] - return Visit(target, references, value) - } - function String(schema: Types.TString, references: Types.TSchema[], value: any): boolean { - if (!IsString(value)) { - return false - } - if (IsDefined(schema.minLength)) { - if (!(value.length >= schema.minLength)) return false - } - if (IsDefined(schema.maxLength)) { - if (!(value.length <= schema.maxLength)) return false - } - if (IsDefined(schema.pattern)) { - const regex = new RegExp(schema.pattern) - if (!regex.test(value)) return false } - if (IsDefined(schema.format)) { - if (!Types.FormatRegistry.Has(schema.format)) return false - const func = Types.FormatRegistry.Get(schema.format)! - return func(value) - } - return true } - function Symbol(schema: Types.TSymbol, references: Types.TSchema[], value: any): boolean { - if (!(typeof value === 'symbol')) { - return false + if (schema.additionalProperties === false) { + const valueKeys = globalThis.Object.getOwnPropertyNames(value) + // optimization: value is valid if schemaKey length matches the valueKey length + if (schema.required && schema.required.length === knownKeys.length && valueKeys.length === knownKeys.length) { + return true + } else { + return valueKeys.every((valueKey) => knownKeys.includes(valueKey)) } + } else if (typeof schema.additionalProperties === 'object') { + const valueKeys = globalThis.Object.getOwnPropertyNames(value) + return valueKeys.every((key) => knownKeys.includes(key) || Visit(schema.additionalProperties as Types.TSchema, references, value[key])) + } else { return true } - function TemplateLiteral(schema: Types.TTemplateLiteralKind, references: Types.TSchema[], value: any): boolean { - if (!IsString(value)) { - return false - } - return new RegExp(schema.pattern).test(value) +} +function Promise(schema: Types.TPromise, references: Types.TSchema[], value: any): boolean { + return typeof value === 'object' && typeof value.then === 'function' +} +function Record(schema: Types.TRecord, references: Types.TSchema[], value: any): boolean { + if (!IsRecordObject(value)) { + return false } - function This(schema: Types.TThis, references: Types.TSchema[], value: any): boolean { - const index = references.findIndex((foreign) => foreign.$id === schema.$ref) - if (index === -1) throw new ValueCheckDereferenceError(schema) - const target = references[index] - return Visit(target, references, value) + if (IsDefined(schema.minProperties) && !(globalThis.Object.getOwnPropertyNames(value).length >= schema.minProperties)) { + return false } - function Tuple(schema: Types.TTuple, references: Types.TSchema[], value: any): boolean { - if (!globalThis.Array.isArray(value)) { - return false + if (IsDefined(schema.maxProperties) && !(globalThis.Object.getOwnPropertyNames(value).length <= schema.maxProperties)) { + return false + } + const [patternKey, patternSchema] = globalThis.Object.entries(schema.patternProperties)[0] + const regex = new RegExp(patternKey) + return globalThis.Object.entries(value).every(([key, value]) => { + if (regex.test(key)) { + return Visit(patternSchema, references, value) } - if (schema.items === undefined && !(value.length === 0)) { - return false + if (typeof schema.additionalProperties === 'object') { + return Visit(schema.additionalProperties, references, value) } - if (!(value.length === schema.maxItems)) { + if (schema.additionalProperties === false) { return false } - if (!schema.items) { - return true - } - for (let i = 0; i < schema.items.length; i++) { - if (!Visit(schema.items[i], references, value[i])) return false - } return true + }) +} +function Ref(schema: Types.TRef, references: Types.TSchema[], value: any): boolean { + const index = references.findIndex((foreign) => foreign.$id === schema.$ref) + if (index === -1) throw new ValueCheckDereferenceError(schema) + const target = references[index] + return Visit(target, references, value) +} +function String(schema: Types.TString, references: Types.TSchema[], value: any): boolean { + if (!ValueGuard.IsString(value)) { + return false } - function Undefined(schema: Types.TUndefined, references: Types.TSchema[], value: any): boolean { - return value === undefined + if (IsDefined(schema.minLength)) { + if (!(value.length >= schema.minLength)) return false } - function Union(schema: Types.TUnion, references: Types.TSchema[], value: any): boolean { - return schema.anyOf.some((inner) => Visit(inner, references, value)) + if (IsDefined(schema.maxLength)) { + if (!(value.length <= schema.maxLength)) return false } - function Uint8Array(schema: Types.TUint8Array, references: Types.TSchema[], value: any): boolean { - if (!(value instanceof globalThis.Uint8Array)) { - return false - } - if (IsDefined(schema.maxByteLength) && !(value.length <= schema.maxByteLength)) { - return false - } - if (IsDefined(schema.minByteLength) && !(value.length >= schema.minByteLength)) { - return false - } - return true + if (IsDefined(schema.pattern)) { + const regex = new RegExp(schema.pattern) + if (!regex.test(value)) return false + } + if (IsDefined(schema.format)) { + if (!Types.FormatRegistry.Has(schema.format)) return false + const func = Types.FormatRegistry.Get(schema.format)! + return func(value) + } + return true +} +function Symbol(schema: Types.TSymbol, references: Types.TSchema[], value: any): boolean { + if (!(typeof value === 'symbol')) { + return false + } + return true +} +function TemplateLiteral(schema: Types.TTemplateLiteralKind, references: Types.TSchema[], value: any): boolean { + if (!ValueGuard.IsString(value)) { + return false + } + return new RegExp(schema.pattern).test(value) +} +function This(schema: Types.TThis, references: Types.TSchema[], value: any): boolean { + const index = references.findIndex((foreign) => foreign.$id === schema.$ref) + if (index === -1) throw new ValueCheckDereferenceError(schema) + const target = references[index] + return Visit(target, references, value) +} +function Tuple(schema: Types.TTuple, references: Types.TSchema[], value: any): boolean { + if (!globalThis.Array.isArray(value)) { + return false + } + if (schema.items === undefined && !(value.length === 0)) { + return false } - function Unknown(schema: Types.TUnknown, references: Types.TSchema[], value: any): boolean { + if (!(value.length === schema.maxItems)) { + return false + } + if (!schema.items) { return true } - function Void(schema: Types.TVoid, references: Types.TSchema[], value: any): boolean { - return IsVoid(value) - } - function UserDefined(schema: Types.TSchema, references: Types.TSchema[], value: unknown): boolean { - if (!Types.TypeRegistry.Has(schema[Types.Kind])) return false - const func = Types.TypeRegistry.Get(schema[Types.Kind])! - return func(schema, value) - } - function Visit(schema: T, references: Types.TSchema[], value: any): boolean { - const references_ = IsDefined(schema.$id) ? [...references, schema] : references - const schema_ = schema as any - switch (schema_[Types.Kind]) { - case 'Any': - return Any(schema_, references_, value) - case 'Array': - return Array(schema_, references_, value) - case 'AsyncIterator': - return AsyncIterator(schema_, references_, value) - case 'BigInt': - return BigInt(schema_, references_, value) - case 'Boolean': - return Boolean(schema_, references_, value) - case 'Constructor': - return Constructor(schema_, references_, value) - case 'Date': - return Date(schema_, references_, value) - case 'Function': - return Function(schema_, references_, value) - case 'Integer': - return Integer(schema_, references_, value) - case 'Intersect': - return Intersect(schema_, references_, value) - case 'Iterator': - return Iterator(schema_, references_, value) - case 'Literal': - return Literal(schema_, references_, value) - case 'Never': - return Never(schema_, references_, value) - case 'Not': - return Not(schema_, references_, value) - case 'Null': - return Null(schema_, references_, value) - case 'Number': - return Number(schema_, references_, value) - case 'Object': - return Object(schema_, references_, value) - case 'Promise': - return Promise(schema_, references_, value) - case 'Record': - return Record(schema_, references_, value) - case 'Ref': - return Ref(schema_, references_, value) - case 'String': - return String(schema_, references_, value) - case 'Symbol': - return Symbol(schema_, references_, value) - case 'TemplateLiteral': - return TemplateLiteral(schema_, references_, value) - case 'This': - return This(schema_, references_, value) - case 'Tuple': - return Tuple(schema_, references_, value) - case 'Undefined': - return Undefined(schema_, references_, value) - case 'Union': - return Union(schema_, references_, value) - case 'Uint8Array': - return Uint8Array(schema_, references_, value) - case 'Unknown': - return Unknown(schema_, references_, value) - case 'Void': - return Void(schema_, references_, value) - default: - if (!Types.TypeRegistry.Has(schema_[Types.Kind])) throw new ValueCheckUnknownTypeError(schema_) - return UserDefined(schema_, references_, value) - } + for (let i = 0; i < schema.items.length; i++) { + if (!Visit(schema.items[i], references, value[i])) return false } - // ------------------------------------------------------------------------- - // Check - // ------------------------------------------------------------------------- - export function Check(schema: T, references: Types.TSchema[], value: any): boolean { - return Visit(schema, references, value) + return true +} +function Undefined(schema: Types.TUndefined, references: Types.TSchema[], value: any): boolean { + return value === undefined +} +function Union(schema: Types.TUnion, references: Types.TSchema[], value: any): boolean { + return schema.anyOf.some((inner) => Visit(inner, references, value)) +} +function Uint8Array(schema: Types.TUint8Array, references: Types.TSchema[], value: any): boolean { + if (!(value instanceof globalThis.Uint8Array)) { + return false + } + if (IsDefined(schema.maxByteLength) && !(value.length <= schema.maxByteLength)) { + return false + } + if (IsDefined(schema.minByteLength) && !(value.length >= schema.minByteLength)) { + return false } + return true +} +function Unknown(schema: Types.TUnknown, references: Types.TSchema[], value: any): boolean { + return true +} +function Void(schema: Types.TVoid, references: Types.TSchema[], value: any): boolean { + return IsVoid(value) +} +function UserDefined(schema: Types.TSchema, references: Types.TSchema[], value: unknown): boolean { + if (!Types.TypeRegistry.Has(schema[Types.Kind])) return false + const func = Types.TypeRegistry.Get(schema[Types.Kind])! + return func(schema, value) +} +function Visit(schema: T, references: Types.TSchema[], value: any): boolean { + const references_ = IsDefined(schema.$id) ? [...references, schema] : references + const schema_ = schema as any + switch (schema_[Types.Kind]) { + case 'Any': + return Any(schema_, references_, value) + case 'Array': + return Array(schema_, references_, value) + case 'AsyncIterator': + return AsyncIterator(schema_, references_, value) + case 'BigInt': + return BigInt(schema_, references_, value) + case 'Boolean': + return Boolean(schema_, references_, value) + case 'Constructor': + return Constructor(schema_, references_, value) + case 'Date': + return Date(schema_, references_, value) + case 'Function': + return Function(schema_, references_, value) + case 'Integer': + return Integer(schema_, references_, value) + case 'Intersect': + return Intersect(schema_, references_, value) + case 'Iterator': + return Iterator(schema_, references_, value) + case 'Literal': + return Literal(schema_, references_, value) + case 'Never': + return Never(schema_, references_, value) + case 'Not': + return Not(schema_, references_, value) + case 'Null': + return Null(schema_, references_, value) + case 'Number': + return Number(schema_, references_, value) + case 'Object': + return Object(schema_, references_, value) + case 'Promise': + return Promise(schema_, references_, value) + case 'Record': + return Record(schema_, references_, value) + case 'Ref': + return Ref(schema_, references_, value) + case 'String': + return String(schema_, references_, value) + case 'Symbol': + return Symbol(schema_, references_, value) + case 'TemplateLiteral': + return TemplateLiteral(schema_, references_, value) + case 'This': + return This(schema_, references_, value) + case 'Tuple': + return Tuple(schema_, references_, value) + case 'Undefined': + return Undefined(schema_, references_, value) + case 'Union': + return Union(schema_, references_, value) + case 'Uint8Array': + return Uint8Array(schema_, references_, value) + case 'Unknown': + return Unknown(schema_, references_, value) + case 'Void': + return Void(schema_, references_, value) + default: + if (!Types.TypeRegistry.Has(schema_[Types.Kind])) throw new ValueCheckUnknownTypeError(schema_) + return UserDefined(schema_, references_, value) + } +} +// -------------------------------------------------------------------------- +// Check +// -------------------------------------------------------------------------- +export function Check(schema: T, references: Types.TSchema[], value: any): boolean { + return Visit(schema, references, value) } diff --git a/src/value/clone.ts b/src/value/clone.ts index 060a3fd85..7d1cededd 100644 --- a/src/value/clone.ts +++ b/src/value/clone.ts @@ -26,43 +26,52 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -import { Is, ObjectType, ArrayType, TypedArrayType, ValueType } from './is' +import * as ValueGuard from './guard' -export namespace ValueClone { - function AsyncIterator(value: AsyncIterableIterator): any { - return value - } - function Iterator(value: IterableIterator): any { - return value - } - function Promise(value: Promise): any { - return value - } - function Array(value: ArrayType): any { - return value.map((element: any) => Clone(element)) - } - function Date(value: Date): any { - return new globalThis.Date(value.toISOString()) - } - function Object(value: ObjectType): any { - const keys = [...globalThis.Object.getOwnPropertyNames(value), ...globalThis.Object.getOwnPropertySymbols(value)] - return keys.reduce((acc, key) => ({ ...acc, [key]: Clone(value[key]) }), {}) - } - function TypedArray(value: TypedArrayType): any { - return value.slice() - } - function Value(value: ValueType): any { - return value - } - export function Clone(value: T): T { - if (Is.AsyncIterator(value)) return AsyncIterator(value) - if (Is.Iterator(value)) return Iterator(value) - if (Is.Promise(value)) return Promise(value) - if (Is.Date(value)) return Date(value) - if (Is.Object(value)) return Object(value) - if (Is.Array(value)) return Array(value) - if (Is.TypedArray(value)) return TypedArray(value) - if (Is.Value(value)) return Value(value) - throw new Error('ValueClone: Unable to clone value') - } +// -------------------------------------------------------------------------- +// Clonable +// -------------------------------------------------------------------------- +function Array(value: ValueGuard.ArrayType): any { + return value.map((element: any) => Clone(element)) +} +function Date(value: Date): any { + return new globalThis.Date(value.toISOString()) +} +function Object(value: ValueGuard.ObjectType): any { + const keys = [...globalThis.Object.getOwnPropertyNames(value), ...globalThis.Object.getOwnPropertySymbols(value)] + return keys.reduce((acc, key) => ({ ...acc, [key]: Clone(value[key]) }), {}) +} +function TypedArray(value: ValueGuard.TypedArrayType): any { + return value.slice() +} +function Value(value: ValueGuard.ValueType): any { + return value +} +// -------------------------------------------------------------------------- +// Non-Clonable +// -------------------------------------------------------------------------- +function AsyncIterator(value: AsyncIterableIterator): any { + return value +} +function Iterator(value: IterableIterator): any { + return value +} +function Function(value: Function): any { + return value +} +function Promise(value: Promise): any { + return value +} +/** Returns a clone of the given value */ +export function Clone(value: T): T { + if (ValueGuard.IsArray(value)) return Array(value) + if (ValueGuard.IsAsyncIterator(value)) return AsyncIterator(value) + if (ValueGuard.IsFunction(value)) return Function(value) + if (ValueGuard.IsIterator(value)) return Iterator(value) + if (ValueGuard.IsPromise(value)) return Promise(value) + if (ValueGuard.IsDate(value)) return Date(value) + if (ValueGuard.IsNonNominalObject(value)) return Object(value) + if (ValueGuard.IsTypedArray(value)) return TypedArray(value) + if (ValueGuard.IsValueType(value)) return Value(value) + throw new Error('ValueClone: Unable to clone value') } diff --git a/src/value/convert.ts b/src/value/convert.ts index 24ee27234..55604ac8e 100644 --- a/src/value/convert.ts +++ b/src/value/convert.ts @@ -27,12 +27,13 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ import * as Types from '../typebox' -import { ValueClone } from './clone' -import { ValueCheck } from './check' +import * as ValueClone from './clone' +import * as ValueCheck from './check' +import * as ValueGuard from './guard' -// ---------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------- // Errors -// ---------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------- export class ValueConvertUnknownTypeError extends Error { constructor(public readonly schema: Types.TSchema) { super('ValueConvert: Unknown type') @@ -43,327 +44,303 @@ export class ValueConvertDereferenceError extends Error { super(`ValueConvert: Unable to dereference schema with $id '${schema.$ref}'`) } } -export namespace ValueConvert { - // ---------------------------------------------------------------------------------------------- - // Guards - // ---------------------------------------------------------------------------------------------- - function IsObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !globalThis.Array.isArray(value) - } - function IsArray(value: unknown): value is unknown[] { - return typeof value === 'object' && globalThis.Array.isArray(value) - } - function IsDate(value: unknown): value is Date { - return typeof value === 'object' && value instanceof globalThis.Date - } - function IsSymbol(value: unknown): value is symbol { - return typeof value === 'symbol' - } - function IsString(value: unknown): value is string { - return typeof value === 'string' - } - function IsBoolean(value: unknown): value is boolean { - return typeof value === 'boolean' - } - function IsBigInt(value: unknown): value is bigint { - return typeof value === 'bigint' - } - function IsNumber(value: unknown): value is number { - return typeof value === 'number' && !isNaN(value) - } - function IsStringNumeric(value: unknown): value is string { - return IsString(value) && !isNaN(value as any) && !isNaN(parseFloat(value)) - } - function IsValueToString(value: unknown): value is { toString: () => string } { - return IsBigInt(value) || IsBoolean(value) || IsNumber(value) - } - function IsValueTrue(value: unknown): value is true { - return value === true || (IsNumber(value) && value === 1) || (IsBigInt(value) && value === globalThis.BigInt('1')) || (IsString(value) && (value.toLowerCase() === 'true' || value === '1')) - } - function IsValueFalse(value: unknown): value is true { - return value === false || (IsNumber(value) && value === 0) || (IsBigInt(value) && value === globalThis.BigInt('0')) || (IsString(value) && (value.toLowerCase() === 'false' || value === '0')) - } - function IsTimeStringWithTimeZone(value: unknown): value is string { - return IsString(value) && /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i.test(value) - } - function IsTimeStringWithoutTimeZone(value: unknown): value is string { - return IsString(value) && /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)?$/i.test(value) - } - function IsDateTimeStringWithTimeZone(value: unknown): value is string { - return IsString(value) && /^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i.test(value) - } - function IsDateTimeStringWithoutTimeZone(value: unknown): value is string { - return IsString(value) && /^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)?$/i.test(value) - } - function IsDateString(value: unknown): value is string { - return IsString(value) && /^\d\d\d\d-[0-1]\d-[0-3]\d$/i.test(value) - } - // ---------------------------------------------------------------------------------------------- - // Convert - // ---------------------------------------------------------------------------------------------- - function TryConvertLiteralString(value: unknown, target: string) { - const conversion = TryConvertString(value) - return conversion === target ? conversion : value - } - function TryConvertLiteralNumber(value: unknown, target: number) { - const conversion = TryConvertNumber(value) - return conversion === target ? conversion : value - } - function TryConvertLiteralBoolean(value: unknown, target: boolean) { - const conversion = TryConvertBoolean(value) - return conversion === target ? conversion : value - } - function TryConvertLiteral(schema: Types.TLiteral, value: unknown) { - if (typeof schema.const === 'string') { - return TryConvertLiteralString(value, schema.const) - } else if (typeof schema.const === 'number') { - return TryConvertLiteralNumber(value, schema.const) - } else if (typeof schema.const === 'boolean') { - return TryConvertLiteralBoolean(value, schema.const) - } else { - return ValueClone.Clone(value) - } - } - function TryConvertBoolean(value: unknown) { - return IsValueTrue(value) ? true : IsValueFalse(value) ? false : value - } - function TryConvertBigInt(value: unknown) { - return IsStringNumeric(value) ? globalThis.BigInt(parseInt(value)) : IsNumber(value) ? globalThis.BigInt(value | 0) : IsValueFalse(value) ? 0 : IsValueTrue(value) ? 1 : value - } - function TryConvertString(value: unknown) { - return IsValueToString(value) ? value.toString() : IsSymbol(value) && value.description !== undefined ? value.description.toString() : value - } - function TryConvertNumber(value: unknown) { - return IsStringNumeric(value) ? parseFloat(value) : IsValueTrue(value) ? 1 : IsValueFalse(value) ? 0 : value - } - function TryConvertInteger(value: unknown) { - return IsStringNumeric(value) ? parseInt(value) : IsNumber(value) ? value | 0 : IsValueTrue(value) ? 1 : IsValueFalse(value) ? 0 : value - } - function TryConvertNull(value: unknown) { - return IsString(value) && value.toLowerCase() === 'null' ? null : value - } - function TryConvertUndefined(value: unknown) { - return IsString(value) && value === 'undefined' ? undefined : value - } - function TryConvertDate(value: unknown) { - // note: this function may return an invalid dates for the regex tests - // above. Invalid dates will however be checked during the casting - // function and will return a epoch date if invalid. Consider better - // string parsing for the iso dates in future revisions. - return IsDate(value) - ? value - : IsNumber(value) - ? new globalThis.Date(value) - : IsValueTrue(value) - ? new globalThis.Date(1) - : IsValueFalse(value) - ? new globalThis.Date(0) - : IsStringNumeric(value) - ? new globalThis.Date(parseInt(value)) - : IsTimeStringWithoutTimeZone(value) - ? new globalThis.Date(`1970-01-01T${value}.000Z`) - : IsTimeStringWithTimeZone(value) - ? new globalThis.Date(`1970-01-01T${value}`) - : IsDateTimeStringWithoutTimeZone(value) - ? new globalThis.Date(`${value}.000Z`) - : IsDateTimeStringWithTimeZone(value) - ? new globalThis.Date(value) - : IsDateString(value) - ? new globalThis.Date(`${value}T00:00:00.000Z`) - : value - } - - // ---------------------------------------------------------------------------------------------- - // Cast - // ---------------------------------------------------------------------------------------------- - function Any(schema: Types.TAny, references: Types.TSchema[], value: any): any { - return value - } - function Array(schema: Types.TArray, references: Types.TSchema[], value: any): any { - if (IsArray(value)) { - return value.map((value) => Visit(schema.items, references, value)) - } - return value - } - function AsyncIterator(schema: Types.TAsyncIterator, references: Types.TSchema[], value: any): any { - return value - } - function BigInt(schema: Types.TBigInt, references: Types.TSchema[], value: any): unknown { - return TryConvertBigInt(value) - } - function Boolean(schema: Types.TBoolean, references: Types.TSchema[], value: any): unknown { - return TryConvertBoolean(value) - } - function Constructor(schema: Types.TConstructor, references: Types.TSchema[], value: any): unknown { +// -------------------------------------------------------------------------- +// Conversions +// -------------------------------------------------------------------------- +function IsStringNumeric(value: unknown): value is string { + return ValueGuard.IsString(value) && !isNaN(value as any) && !isNaN(parseFloat(value)) +} +function IsValueToString(value: unknown): value is { toString: () => string } { + return ValueGuard.IsBigInt(value) || ValueGuard.IsBoolean(value) || ValueGuard.IsNumber(value) +} +function IsValueTrue(value: unknown): value is true { + return value === true || (ValueGuard.IsNumber(value) && value === 1) || (ValueGuard.IsBigInt(value) && value === globalThis.BigInt('1')) || (ValueGuard.IsString(value) && (value.toLowerCase() === 'true' || value === '1')) +} +function IsValueFalse(value: unknown): value is false { + return value === false || (ValueGuard.IsNumber(value) && value === 0) || (ValueGuard.IsBigInt(value) && value === globalThis.BigInt('0')) || (ValueGuard.IsString(value) && (value.toLowerCase() === 'false' || value === '0')) +} +function IsTimeStringWithTimeZone(value: unknown): value is string { + return ValueGuard.IsString(value) && /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i.test(value) +} +function IsTimeStringWithoutTimeZone(value: unknown): value is string { + return ValueGuard.IsString(value) && /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)?$/i.test(value) +} +function IsDateTimeStringWithTimeZone(value: unknown): value is string { + return ValueGuard.IsString(value) && /^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i.test(value) +} +function IsDateTimeStringWithoutTimeZone(value: unknown): value is string { + return ValueGuard.IsString(value) && /^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)?$/i.test(value) +} +function IsDateString(value: unknown): value is string { + return ValueGuard.IsString(value) && /^\d\d\d\d-[0-1]\d-[0-3]\d$/i.test(value) +} +// -------------------------------------------------------------------------- +// Convert +// -------------------------------------------------------------------------- +function TryConvertLiteralString(value: unknown, target: string) { + const conversion = TryConvertString(value) + return conversion === target ? conversion : value +} +function TryConvertLiteralNumber(value: unknown, target: number) { + const conversion = TryConvertNumber(value) + return conversion === target ? conversion : value +} +function TryConvertLiteralBoolean(value: unknown, target: boolean) { + const conversion = TryConvertBoolean(value) + return conversion === target ? conversion : value +} +function TryConvertLiteral(schema: Types.TLiteral, value: unknown) { + if (typeof schema.const === 'string') { + return TryConvertLiteralString(value, schema.const) + } else if (typeof schema.const === 'number') { + return TryConvertLiteralNumber(value, schema.const) + } else if (typeof schema.const === 'boolean') { + return TryConvertLiteralBoolean(value, schema.const) + } else { return ValueClone.Clone(value) } - function Date(schema: Types.TDate, references: Types.TSchema[], value: any): unknown { - return TryConvertDate(value) - } - function Function(schema: Types.TFunction, references: Types.TSchema[], value: any): unknown { - return value - } - function Integer(schema: Types.TInteger, references: Types.TSchema[], value: any): unknown { - return TryConvertInteger(value) - } - function Intersect(schema: Types.TIntersect, references: Types.TSchema[], value: any): unknown { - return value - } - function Iterator(schema: Types.TIterator, references: Types.TSchema[], value: any): unknown { - return value - } - function Literal(schema: Types.TLiteral, references: Types.TSchema[], value: any): unknown { - return TryConvertLiteral(schema, value) - } - function Never(schema: Types.TNever, references: Types.TSchema[], value: any): unknown { - return value - } - function Null(schema: Types.TNull, references: Types.TSchema[], value: any): unknown { - return TryConvertNull(value) - } - function Number(schema: Types.TNumber, references: Types.TSchema[], value: any): unknown { - return TryConvertNumber(value) - } - function Object(schema: Types.TObject, references: Types.TSchema[], value: any): unknown { - if (IsObject(value)) - return globalThis.Object.keys(schema.properties).reduce((acc, key) => { - return value[key] !== undefined ? { ...acc, [key]: Visit(schema.properties[key], references, value[key]) } : { ...acc } - }, value) - return value - } - function Promise(schema: Types.TSchema, references: Types.TSchema[], value: any): unknown { - return value - } - function Record(schema: Types.TRecord, references: Types.TSchema[], value: any): unknown { - const propertyKey = globalThis.Object.getOwnPropertyNames(schema.patternProperties)[0] - const property = schema.patternProperties[propertyKey] - const result = {} as Record - for (const [propKey, propValue] of globalThis.Object.entries(value)) { - result[propKey] = Visit(property, references, propValue) - } - return result - } - function Ref(schema: Types.TRef, references: Types.TSchema[], value: any): unknown { - const index = references.findIndex((foreign) => foreign.$id === schema.$ref) - if (index === -1) throw new ValueConvertDereferenceError(schema) - const target = references[index] - return Visit(target, references, value) - } - function String(schema: Types.TString, references: Types.TSchema[], value: any): unknown { - return TryConvertString(value) - } - function Symbol(schema: Types.TSymbol, references: Types.TSchema[], value: any): unknown { - return value - } - function TemplateLiteral(schema: Types.TTemplateLiteral, references: Types.TSchema[], value: any) { - return value - } - function This(schema: Types.TThis, references: Types.TSchema[], value: any): unknown { - const index = references.findIndex((foreign) => foreign.$id === schema.$ref) - if (index === -1) throw new ValueConvertDereferenceError(schema) - const target = references[index] - return Visit(target, references, value) - } - function Tuple(schema: Types.TTuple, references: Types.TSchema[], value: any): unknown { - if (IsArray(value) && schema.items !== undefined) { - return value.map((value, index) => { - return index < schema.items!.length ? Visit(schema.items![index], references, value) : value - }) - } - return value - } - function Undefined(schema: Types.TUndefined, references: Types.TSchema[], value: any): unknown { - return TryConvertUndefined(value) - } - function Union(schema: Types.TUnion, references: Types.TSchema[], value: any): unknown { - for (const subschema of schema.anyOf) { - const converted = Visit(subschema, references, value) - if (ValueCheck.Check(subschema, references, converted)) { - return converted - } - } - return value - } - function Uint8Array(schema: Types.TUint8Array, references: Types.TSchema[], value: any): unknown { - return value - } - function Unknown(schema: Types.TUnknown, references: Types.TSchema[], value: any): unknown { - return value - } - function Void(schema: Types.TVoid, references: Types.TSchema[], value: any): unknown { - return value +} +function TryConvertBoolean(value: unknown) { + return IsValueTrue(value) ? true : IsValueFalse(value) ? false : value +} +function TryConvertBigInt(value: unknown) { + return IsStringNumeric(value) ? globalThis.BigInt(parseInt(value)) : ValueGuard.IsNumber(value) ? globalThis.BigInt(value | 0) : IsValueFalse(value) ? 0 : IsValueTrue(value) ? 1 : value +} +function TryConvertString(value: unknown) { + return IsValueToString(value) ? value.toString() : ValueGuard.IsSymbol(value) && value.description !== undefined ? value.description.toString() : value +} +function TryConvertNumber(value: unknown) { + return IsStringNumeric(value) ? parseFloat(value) : IsValueTrue(value) ? 1 : IsValueFalse(value) ? 0 : value +} +function TryConvertInteger(value: unknown) { + return IsStringNumeric(value) ? parseInt(value) : ValueGuard.IsNumber(value) ? value | 0 : IsValueTrue(value) ? 1 : IsValueFalse(value) ? 0 : value +} +function TryConvertNull(value: unknown) { + return ValueGuard.IsString(value) && value.toLowerCase() === 'null' ? null : value +} +function TryConvertUndefined(value: unknown) { + return ValueGuard.IsString(value) && value === 'undefined' ? undefined : value +} +function TryConvertDate(value: unknown) { + // -------------------------------------------------------------------------- + // note: this function may return an invalid dates for the regex tests + // above. Invalid dates will however be checked during the casting function + // and will return a epoch date if invalid. Consider better string parsing + // for the iso dates in future revisions. + // -------------------------------------------------------------------------- + return ValueGuard.IsDate(value) + ? value + : ValueGuard.IsNumber(value) + ? new globalThis.Date(value) + : IsValueTrue(value) + ? new globalThis.Date(1) + : IsValueFalse(value) + ? new globalThis.Date(0) + : IsStringNumeric(value) + ? new globalThis.Date(parseInt(value)) + : IsTimeStringWithoutTimeZone(value) + ? new globalThis.Date(`1970-01-01T${value}.000Z`) + : IsTimeStringWithTimeZone(value) + ? new globalThis.Date(`1970-01-01T${value}`) + : IsDateTimeStringWithoutTimeZone(value) + ? new globalThis.Date(`${value}.000Z`) + : IsDateTimeStringWithTimeZone(value) + ? new globalThis.Date(value) + : IsDateString(value) + ? new globalThis.Date(`${value}T00:00:00.000Z`) + : value +} +// -------------------------------------------------------------------------- +// Cast +// -------------------------------------------------------------------------- +function Any(schema: Types.TAny, references: Types.TSchema[], value: any): any { + return value +} +function Array(schema: Types.TArray, references: Types.TSchema[], value: any): any { + if (ValueGuard.IsArray(value)) { + return value.map((value) => Visit(schema.items, references, value)) } - function UserDefined(schema: Types.TSchema, references: Types.TSchema[], value: any): unknown { - return value + return value +} +function AsyncIterator(schema: Types.TAsyncIterator, references: Types.TSchema[], value: any): any { + return value +} +function BigInt(schema: Types.TBigInt, references: Types.TSchema[], value: any): unknown { + return TryConvertBigInt(value) +} +function Boolean(schema: Types.TBoolean, references: Types.TSchema[], value: any): unknown { + return TryConvertBoolean(value) +} +function Constructor(schema: Types.TConstructor, references: Types.TSchema[], value: any): unknown { + return ValueClone.Clone(value) +} +function Date(schema: Types.TDate, references: Types.TSchema[], value: any): unknown { + return TryConvertDate(value) +} +function Function(schema: Types.TFunction, references: Types.TSchema[], value: any): unknown { + return value +} +function Integer(schema: Types.TInteger, references: Types.TSchema[], value: any): unknown { + return TryConvertInteger(value) +} +function Intersect(schema: Types.TIntersect, references: Types.TSchema[], value: any): unknown { + return value +} +function Iterator(schema: Types.TIterator, references: Types.TSchema[], value: any): unknown { + return value +} +function Literal(schema: Types.TLiteral, references: Types.TSchema[], value: any): unknown { + return TryConvertLiteral(schema, value) +} +function Never(schema: Types.TNever, references: Types.TSchema[], value: any): unknown { + return value +} +function Null(schema: Types.TNull, references: Types.TSchema[], value: any): unknown { + return TryConvertNull(value) +} +function Number(schema: Types.TNumber, references: Types.TSchema[], value: any): unknown { + return TryConvertNumber(value) +} +function Object(schema: Types.TObject, references: Types.TSchema[], value: any): unknown { + if (ValueGuard.IsObject(value)) + return globalThis.Object.keys(schema.properties).reduce((acc, key) => { + return value[key] !== undefined ? { ...acc, [key]: Visit(schema.properties[key], references, value[key]) } : { ...acc } + }, value) + return value +} +function Promise(schema: Types.TSchema, references: Types.TSchema[], value: any): unknown { + return value +} +function Record(schema: Types.TRecord, references: Types.TSchema[], value: any): unknown { + const propertyKey = globalThis.Object.getOwnPropertyNames(schema.patternProperties)[0] + const property = schema.patternProperties[propertyKey] + const result = {} as Record + for (const [propKey, propValue] of globalThis.Object.entries(value)) { + result[propKey] = Visit(property, references, propValue) + } + return result +} +function Ref(schema: Types.TRef, references: Types.TSchema[], value: any): unknown { + const index = references.findIndex((foreign) => foreign.$id === schema.$ref) + if (index === -1) throw new ValueConvertDereferenceError(schema) + const target = references[index] + return Visit(target, references, value) +} +function String(schema: Types.TString, references: Types.TSchema[], value: any): unknown { + return TryConvertString(value) +} +function Symbol(schema: Types.TSymbol, references: Types.TSchema[], value: any): unknown { + return value +} +function TemplateLiteral(schema: Types.TTemplateLiteral, references: Types.TSchema[], value: any) { + return value +} +function This(schema: Types.TThis, references: Types.TSchema[], value: any): unknown { + const index = references.findIndex((foreign) => foreign.$id === schema.$ref) + if (index === -1) throw new ValueConvertDereferenceError(schema) + const target = references[index] + return Visit(target, references, value) +} +function Tuple(schema: Types.TTuple, references: Types.TSchema[], value: any): unknown { + if (ValueGuard.IsArray(value) && !ValueGuard.IsUndefined(schema.items)) { + return value.map((value, index) => { + return index < schema.items!.length ? Visit(schema.items![index], references, value) : value + }) } - export function Visit(schema: Types.TSchema, references: Types.TSchema[], value: any): unknown { - const references_ = IsString(schema.$id) ? [...references, schema] : references - const schema_ = schema as any - switch (schema[Types.Kind]) { - case 'Any': - return Any(schema_, references_, value) - case 'Array': - return Array(schema_, references_, value) - case 'AsyncIterator': - return AsyncIterator(schema_, references_, value) - case 'BigInt': - return BigInt(schema_, references_, value) - case 'Boolean': - return Boolean(schema_, references_, value) - case 'Constructor': - return Constructor(schema_, references_, value) - case 'Date': - return Date(schema_, references_, value) - case 'Function': - return Function(schema_, references_, value) - case 'Integer': - return Integer(schema_, references_, value) - case 'Intersect': - return Intersect(schema_, references_, value) - case 'Iterator': - return Iterator(schema_, references_, value) - case 'Literal': - return Literal(schema_, references_, value) - case 'Never': - return Never(schema_, references_, value) - case 'Null': - return Null(schema_, references_, value) - case 'Number': - return Number(schema_, references_, value) - case 'Object': - return Object(schema_, references_, value) - case 'Promise': - return Promise(schema_, references_, value) - case 'Record': - return Record(schema_, references_, value) - case 'Ref': - return Ref(schema_, references_, value) - case 'String': - return String(schema_, references_, value) - case 'Symbol': - return Symbol(schema_, references_, value) - case 'TemplateLiteral': - return TemplateLiteral(schema_, references_, value) - case 'This': - return This(schema_, references_, value) - case 'Tuple': - return Tuple(schema_, references_, value) - case 'Undefined': - return Undefined(schema_, references_, value) - case 'Union': - return Union(schema_, references_, value) - case 'Uint8Array': - return Uint8Array(schema_, references_, value) - case 'Unknown': - return Unknown(schema_, references_, value) - case 'Void': - return Void(schema_, references_, value) - default: - if (!Types.TypeRegistry.Has(schema_[Types.Kind])) throw new ValueConvertUnknownTypeError(schema_) - return UserDefined(schema_, references_, value) + return value +} +function Undefined(schema: Types.TUndefined, references: Types.TSchema[], value: any): unknown { + return TryConvertUndefined(value) +} +function Union(schema: Types.TUnion, references: Types.TSchema[], value: any): unknown { + for (const subschema of schema.anyOf) { + const converted = Visit(subschema, references, value) + if (ValueCheck.Check(subschema, references, converted)) { + return converted } } - export function Convert(schema: T, references: Types.TSchema[], value: any): unknown { - return Visit(schema, references, ValueClone.Clone(value)) + return value +} +function Uint8Array(schema: Types.TUint8Array, references: Types.TSchema[], value: any): unknown { + return value +} +function Unknown(schema: Types.TUnknown, references: Types.TSchema[], value: any): unknown { + return value +} +function Void(schema: Types.TVoid, references: Types.TSchema[], value: any): unknown { + return value +} +function UserDefined(schema: Types.TSchema, references: Types.TSchema[], value: any): unknown { + return value +} +export function Visit(schema: Types.TSchema, references: Types.TSchema[], value: any): unknown { + const references_ = ValueGuard.IsString(schema.$id) ? [...references, schema] : references + const schema_ = schema as any + switch (schema[Types.Kind]) { + case 'Any': + return Any(schema_, references_, value) + case 'Array': + return Array(schema_, references_, value) + case 'AsyncIterator': + return AsyncIterator(schema_, references_, value) + case 'BigInt': + return BigInt(schema_, references_, value) + case 'Boolean': + return Boolean(schema_, references_, value) + case 'Constructor': + return Constructor(schema_, references_, value) + case 'Date': + return Date(schema_, references_, value) + case 'Function': + return Function(schema_, references_, value) + case 'Integer': + return Integer(schema_, references_, value) + case 'Intersect': + return Intersect(schema_, references_, value) + case 'Iterator': + return Iterator(schema_, references_, value) + case 'Literal': + return Literal(schema_, references_, value) + case 'Never': + return Never(schema_, references_, value) + case 'Null': + return Null(schema_, references_, value) + case 'Number': + return Number(schema_, references_, value) + case 'Object': + return Object(schema_, references_, value) + case 'Promise': + return Promise(schema_, references_, value) + case 'Record': + return Record(schema_, references_, value) + case 'Ref': + return Ref(schema_, references_, value) + case 'String': + return String(schema_, references_, value) + case 'Symbol': + return Symbol(schema_, references_, value) + case 'TemplateLiteral': + return TemplateLiteral(schema_, references_, value) + case 'This': + return This(schema_, references_, value) + case 'Tuple': + return Tuple(schema_, references_, value) + case 'Undefined': + return Undefined(schema_, references_, value) + case 'Union': + return Union(schema_, references_, value) + case 'Uint8Array': + return Uint8Array(schema_, references_, value) + case 'Unknown': + return Unknown(schema_, references_, value) + case 'Void': + return Void(schema_, references_, value) + default: + if (!Types.TypeRegistry.Has(schema_[Types.Kind])) throw new ValueConvertUnknownTypeError(schema_) + return UserDefined(schema_, references_, value) } } +/** Converts any type mismatched values to their target type if a conversion is possible. */ +export function Convert(schema: T, references: Types.TSchema[], value: any): unknown { + return Visit(schema, references, ValueClone.Clone(value)) +} diff --git a/src/value/create.ts b/src/value/create.ts index db6f65d3d..72d730fe9 100644 --- a/src/value/create.ts +++ b/src/value/create.ts @@ -27,7 +27,8 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ import * as Types from '../typebox' -import { ValueCheck } from './check' +import * as ValueCheck from './check' +import * as ValueGuard from './guard' // -------------------------------------------------------------------------- // Errors @@ -68,392 +69,381 @@ export class ValueCreateRecursiveInstantiationError extends Error { } } // -------------------------------------------------------------------------- -// ValueCreate +// Types // -------------------------------------------------------------------------- -export namespace ValueCreate { - // -------------------------------------------------------- - // Guards - // -------------------------------------------------------- - function IsString(value: unknown): value is string { - return typeof value === 'string' - } - // -------------------------------------------------------- - // Types - // -------------------------------------------------------- - function Any(schema: Types.TAny, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - return {} - } +function Any(schema: Types.TAny, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + return {} } - function Array(schema: Types.TArray, references: Types.TSchema[]): any { - if (schema.uniqueItems === true && !('default' in schema)) { - throw new Error('ValueCreate.Array: Array with the uniqueItems constraint requires a default value') - } else if ('contains' in schema && !('default' in schema)) { - throw new Error('ValueCreate.Array: Array with the contains constraint requires a default value') - } else if ('default' in schema) { - return schema.default - } else if (schema.minItems !== undefined) { - return globalThis.Array.from({ length: schema.minItems }).map((item) => { - return Visit(schema.items, references) - }) - } else { - return [] - } +} +function Array(schema: Types.TArray, references: Types.TSchema[]): any { + if (schema.uniqueItems === true && !ValueGuard.HasPropertyKey(schema, 'default')) { + throw new Error('ValueCreate.Array: Array with the uniqueItems constraint requires a default value') + } else if ('contains' in schema && !ValueGuard.HasPropertyKey(schema, 'default')) { + throw new Error('ValueCreate.Array: Array with the contains constraint requires a default value') + } else if ('default' in schema) { + return schema.default + } else if (schema.minItems !== undefined) { + return globalThis.Array.from({ length: schema.minItems }).map((item) => { + return Visit(schema.items, references) + }) + } else { + return [] } - function AsyncIterator(schema: Types.TAsyncIterator, references: Types.TSchema[]) { - if ('default' in schema) { - return schema.default - } else { - return (async function* () {})() - } +} +function AsyncIterator(schema: Types.TAsyncIterator, references: Types.TSchema[]) { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + return (async function* () {})() } - function BigInt(schema: Types.TBigInt, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - return globalThis.BigInt(0) - } +} +function BigInt(schema: Types.TBigInt, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + return globalThis.BigInt(0) } - function Boolean(schema: Types.TBoolean, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - return false - } +} +function Boolean(schema: Types.TBoolean, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + return false } - function Constructor(schema: Types.TConstructor, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - const value = Visit(schema.returns, references) as any - if (typeof value === 'object' && !globalThis.Array.isArray(value)) { - return class { - constructor() { - for (const [key, val] of globalThis.Object.entries(value)) { - const self = this as any - self[key] = val - } +} +function Constructor(schema: Types.TConstructor, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + const value = Visit(schema.returns, references) as any + if (typeof value === 'object' && !globalThis.Array.isArray(value)) { + return class { + constructor() { + for (const [key, val] of globalThis.Object.entries(value)) { + const self = this as any + self[key] = val } } - } else { - return class {} } - } - } - function Date(schema: Types.TDate, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else if (schema.minimumTimestamp !== undefined) { - return new globalThis.Date(schema.minimumTimestamp) } else { - return new globalThis.Date(0) + return class {} } } - function Function(schema: Types.TFunction, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - return () => Visit(schema.returns, references) - } +} +function Date(schema: Types.TDate, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else if (schema.minimumTimestamp !== undefined) { + return new globalThis.Date(schema.minimumTimestamp) + } else { + return new globalThis.Date(0) } - function Integer(schema: Types.TInteger, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else if (schema.minimum !== undefined) { - return schema.minimum - } else { - return 0 - } +} +function Function(schema: Types.TFunction, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + return () => Visit(schema.returns, references) } - function Intersect(schema: Types.TIntersect, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - // Note: The best we can do here is attempt to instance each sub type and apply through object assign. For non-object - // sub types, we just escape the assignment and just return the value. In the latter case, this is typically going to - // be a consequence of an illogical intersection. - const value = schema.allOf.reduce((acc, schema) => { - const next = Visit(schema, references) as any - return typeof next === 'object' ? { ...acc, ...next } : next - }, {}) - if (!ValueCheck.Check(schema, references, value)) throw new ValueCreateIntersectTypeError(schema) - return value - } +} +function Integer(schema: Types.TInteger, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else if (schema.minimum !== undefined) { + return schema.minimum + } else { + return 0 } - function Iterator(schema: Types.TIterator, references: Types.TSchema[]) { - if ('default' in schema) { - return schema.default - } else { - return (function* () {})() - } +} +function Intersect(schema: Types.TIntersect, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + // Note: The best we can do here is attempt to instance each sub type and apply through object assign. For non-object + // sub types, we just escape the assignment and just return the value. In the latter case, this is typically going to + // be a consequence of an illogical intersection. + const value = schema.allOf.reduce((acc, schema) => { + const next = Visit(schema, references) as any + return typeof next === 'object' ? { ...acc, ...next } : next + }, {}) + if (!ValueCheck.Check(schema, references, value)) throw new ValueCreateIntersectTypeError(schema) + return value } - function Literal(schema: Types.TLiteral, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - return schema.const - } +} +function Iterator(schema: Types.TIterator, references: Types.TSchema[]) { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + return (function* () {})() } - function Never(schema: Types.TNever, references: Types.TSchema[]): any { - throw new ValueCreateNeverTypeError(schema) +} +function Literal(schema: Types.TLiteral, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + return schema.const } - function Not(schema: Types.TNot, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - throw new ValueCreateNotTypeError(schema) - } +} +function Never(schema: Types.TNever, references: Types.TSchema[]): any { + throw new ValueCreateNeverTypeError(schema) +} +function Not(schema: Types.TNot, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + throw new ValueCreateNotTypeError(schema) } - function Null(schema: Types.TNull, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - return null - } +} +function Null(schema: Types.TNull, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + return null } - function Number(schema: Types.TNumber, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else if (schema.minimum !== undefined) { - return schema.minimum - } else { - return 0 - } +} +function Number(schema: Types.TNumber, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else if (schema.minimum !== undefined) { + return schema.minimum + } else { + return 0 } - function Object(schema: Types.TObject, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - const required = new Set(schema.required) - return ( - schema.default || - globalThis.Object.entries(schema.properties).reduce((acc, [key, schema]) => { - return required.has(key) ? { ...acc, [key]: Visit(schema, references) } : { ...acc } - }, {}) - ) - } +} +function Object(schema: Types.TObject, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + const required = new Set(schema.required) + return ( + schema.default || + globalThis.Object.entries(schema.properties).reduce((acc, [key, schema]) => { + return required.has(key) ? { ...acc, [key]: Visit(schema, references) } : { ...acc } + }, {}) + ) } - function Promise(schema: Types.TPromise, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - return globalThis.Promise.resolve(Visit(schema.item, references)) - } +} +function Promise(schema: Types.TPromise, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + return globalThis.Promise.resolve(Visit(schema.item, references)) } - function Record(schema: Types.TRecord, references: Types.TSchema[]): any { - const [keyPattern, valueSchema] = globalThis.Object.entries(schema.patternProperties)[0] - if ('default' in schema) { - return schema.default - } else if (!(keyPattern === Types.PatternStringExact || keyPattern === Types.PatternNumberExact)) { - const propertyKeys = keyPattern.slice(1, keyPattern.length - 1).split('|') - return propertyKeys.reduce((acc, key) => { - return { ...acc, [key]: Visit(valueSchema, references) } - }, {}) - } else { - return {} - } +} +function Record(schema: Types.TRecord, references: Types.TSchema[]): any { + const [keyPattern, valueSchema] = globalThis.Object.entries(schema.patternProperties)[0] + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else if (!(keyPattern === Types.PatternStringExact || keyPattern === Types.PatternNumberExact)) { + const propertyKeys = keyPattern.slice(1, keyPattern.length - 1).split('|') + return propertyKeys.reduce((acc, key) => { + return { ...acc, [key]: Visit(valueSchema, references) } + }, {}) + } else { + return {} } - function Ref(schema: Types.TRef, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - const index = references.findIndex((foreign) => foreign.$id === schema.$ref) - if (index === -1) throw new ValueCreateDereferenceError(schema) - const target = references[index] - return Visit(target, references) - } +} +function Ref(schema: Types.TRef, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + const index = references.findIndex((foreign) => foreign.$id === schema.$ref) + if (index === -1) throw new ValueCreateDereferenceError(schema) + const target = references[index] + return Visit(target, references) } - function String(schema: Types.TString, references: Types.TSchema[]): any { - if (schema.pattern !== undefined) { - if (!('default' in schema)) { - throw new Error('ValueCreate.String: String types with patterns must specify a default value') - } else { - return schema.default - } - } else if (schema.format !== undefined) { - if (!('default' in schema)) { - throw new Error('ValueCreate.String: String types with formats must specify a default value') - } else { - return schema.default - } +} +function String(schema: Types.TString, references: Types.TSchema[]): any { + if (schema.pattern !== undefined) { + if (!ValueGuard.HasPropertyKey(schema, 'default')) { + throw new Error('ValueCreate.String: String types with patterns must specify a default value') } else { - if ('default' in schema) { - return schema.default - } else if (schema.minLength !== undefined) { - return globalThis.Array.from({ length: schema.minLength }) - .map(() => '.') - .join('') - } else { - return '' - } - } - } - function Symbol(schema: Types.TString, references: Types.TSchema[]): any { - if ('default' in schema) { return schema.default - } else if ('value' in schema) { - return globalThis.Symbol.for(schema.value) - } else { - return globalThis.Symbol() } - } - function TemplateLiteral(schema: Types.TTemplateLiteral, references: Types.TSchema[]) { - if ('default' in schema) { + } else if (schema.format !== undefined) { + if (!ValueGuard.HasPropertyKey(schema, 'default')) { + throw new Error('ValueCreate.String: String types with formats must specify a default value') + } else { return schema.default } - const expression = Types.TemplateLiteralParser.ParseExact(schema.pattern) - if (!Types.TemplateLiteralFinite.Check(expression)) throw new ValueCreateTempateLiteralTypeError(schema) - const sequence = Types.TemplateLiteralGenerator.Generate(expression) - return sequence.next().value - } - function This(schema: Types.TThis, references: Types.TSchema[]): any { - if (recursiveDepth++ > recursiveMaxDepth) throw new ValueCreateRecursiveInstantiationError(schema, recursiveMaxDepth) - if ('default' in schema) { + } else { + if (ValueGuard.HasPropertyKey(schema, 'default')) { return schema.default + } else if (schema.minLength !== undefined) { + return globalThis.Array.from({ length: schema.minLength }) + .map(() => '.') + .join('') } else { - const index = references.findIndex((foreign) => foreign.$id === schema.$ref) - if (index === -1) throw new ValueCreateDereferenceError(schema) - const target = references[index] - return Visit(target, references) + return '' } } - function Tuple(schema: Types.TTuple, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } - if (schema.items === undefined) { - return [] - } else { - return globalThis.Array.from({ length: schema.minItems }).map((_, index) => Visit((schema.items as any[])[index], references)) - } +} +function Symbol(schema: Types.TString, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else if ('value' in schema) { + return globalThis.Symbol.for(schema.value) + } else { + return globalThis.Symbol() } - function Undefined(schema: Types.TUndefined, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - return undefined - } +} +function TemplateLiteral(schema: Types.TTemplateLiteral, references: Types.TSchema[]) { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } + const expression = Types.TemplateLiteralParser.ParseExact(schema.pattern) + if (!Types.TemplateLiteralFinite.Check(expression)) throw new ValueCreateTempateLiteralTypeError(schema) + const sequence = Types.TemplateLiteralGenerator.Generate(expression) + return sequence.next().value +} +function This(schema: Types.TThis, references: Types.TSchema[]): any { + if (recursiveDepth++ > recursiveMaxDepth) throw new ValueCreateRecursiveInstantiationError(schema, recursiveMaxDepth) + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + const index = references.findIndex((foreign) => foreign.$id === schema.$ref) + if (index === -1) throw new ValueCreateDereferenceError(schema) + const target = references[index] + return Visit(target, references) } - function Union(schema: Types.TUnion, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else if (schema.anyOf.length === 0) { - throw new Error('ValueCreate.Union: Cannot create Union with zero variants') - } else { - return Visit(schema.anyOf[0], references) - } +} +function Tuple(schema: Types.TTuple, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default } - function Uint8Array(schema: Types.TUint8Array, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else if (schema.minByteLength !== undefined) { - return new globalThis.Uint8Array(schema.minByteLength) - } else { - return new globalThis.Uint8Array(0) - } + if (schema.items === undefined) { + return [] + } else { + return globalThis.Array.from({ length: schema.minItems }).map((_, index) => Visit((schema.items as any[])[index], references)) } - function Unknown(schema: Types.TUnknown, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - return {} - } +} +function Undefined(schema: Types.TUndefined, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + return undefined } - function Void(schema: Types.TVoid, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - return void 0 - } +} +function Union(schema: Types.TUnion, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else if (schema.anyOf.length === 0) { + throw new Error('ValueCreate.Union: Cannot create Union with zero variants') + } else { + return Visit(schema.anyOf[0], references) } - function UserDefined(schema: Types.TSchema, references: Types.TSchema[]): any { - if ('default' in schema) { - return schema.default - } else { - throw new Error('ValueCreate.UserDefined: User defined types must specify a default value') - } +} +function Uint8Array(schema: Types.TUint8Array, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else if (schema.minByteLength !== undefined) { + return new globalThis.Uint8Array(schema.minByteLength) + } else { + return new globalThis.Uint8Array(0) } - /** Creates a value from the given schema. If the schema specifies a default value, then that value is returned. */ - export function Visit(schema: Types.TSchema, references: Types.TSchema[]): unknown { - const references_ = IsString(schema.$id) ? [...references, schema] : references - const schema_ = schema as any - switch (schema_[Types.Kind]) { - case 'Any': - return Any(schema_, references_) - case 'Array': - return Array(schema_, references_) - case 'AsyncIterator': - return AsyncIterator(schema_, references_) - case 'BigInt': - return BigInt(schema_, references_) - case 'Boolean': - return Boolean(schema_, references_) - case 'Constructor': - return Constructor(schema_, references_) - case 'Date': - return Date(schema_, references_) - case 'Function': - return Function(schema_, references_) - case 'Integer': - return Integer(schema_, references_) - case 'Intersect': - return Intersect(schema_, references_) - case 'Iterator': - return Iterator(schema_, references_) - case 'Literal': - return Literal(schema_, references_) - case 'Never': - return Never(schema_, references_) - case 'Not': - return Not(schema_, references_) - case 'Null': - return Null(schema_, references_) - case 'Number': - return Number(schema_, references_) - case 'Object': - return Object(schema_, references_) - case 'Promise': - return Promise(schema_, references_) - case 'Record': - return Record(schema_, references_) - case 'Ref': - return Ref(schema_, references_) - case 'String': - return String(schema_, references_) - case 'Symbol': - return Symbol(schema_, references_) - case 'TemplateLiteral': - return TemplateLiteral(schema_, references_) - case 'This': - return This(schema_, references_) - case 'Tuple': - return Tuple(schema_, references_) - case 'Undefined': - return Undefined(schema_, references_) - case 'Union': - return Union(schema_, references_) - case 'Uint8Array': - return Uint8Array(schema_, references_) - case 'Unknown': - return Unknown(schema_, references_) - case 'Void': - return Void(schema_, references_) - default: - if (!Types.TypeRegistry.Has(schema_[Types.Kind])) throw new ValueCreateUnknownTypeError(schema_) - return UserDefined(schema_, references_) - } +} +function Unknown(schema: Types.TUnknown, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + return {} } - // -------------------------------------------------------- - // State - // -------------------------------------------------------- - const recursiveMaxDepth = 512 - let recursiveDepth = 0 - - /** Creates a value from the given schema and references */ - export function Create(schema: T, references: Types.TSchema[]): Types.Static { - recursiveDepth = 0 - return Visit(schema, references) +} +function Void(schema: Types.TVoid, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + return void 0 } } +function UserDefined(schema: Types.TSchema, references: Types.TSchema[]): any { + if (ValueGuard.HasPropertyKey(schema, 'default')) { + return schema.default + } else { + throw new Error('ValueCreate.UserDefined: User defined types must specify a default value') + } +} +/** Creates a value from the given schema. If the schema specifies a default value, then that value is returned. */ +export function Visit(schema: Types.TSchema, references: Types.TSchema[]): unknown { + const references_ = ValueGuard.IsString(schema.$id) ? [...references, schema] : references + const schema_ = schema as any + switch (schema_[Types.Kind]) { + case 'Any': + return Any(schema_, references_) + case 'Array': + return Array(schema_, references_) + case 'AsyncIterator': + return AsyncIterator(schema_, references_) + case 'BigInt': + return BigInt(schema_, references_) + case 'Boolean': + return Boolean(schema_, references_) + case 'Constructor': + return Constructor(schema_, references_) + case 'Date': + return Date(schema_, references_) + case 'Function': + return Function(schema_, references_) + case 'Integer': + return Integer(schema_, references_) + case 'Intersect': + return Intersect(schema_, references_) + case 'Iterator': + return Iterator(schema_, references_) + case 'Literal': + return Literal(schema_, references_) + case 'Never': + return Never(schema_, references_) + case 'Not': + return Not(schema_, references_) + case 'Null': + return Null(schema_, references_) + case 'Number': + return Number(schema_, references_) + case 'Object': + return Object(schema_, references_) + case 'Promise': + return Promise(schema_, references_) + case 'Record': + return Record(schema_, references_) + case 'Ref': + return Ref(schema_, references_) + case 'String': + return String(schema_, references_) + case 'Symbol': + return Symbol(schema_, references_) + case 'TemplateLiteral': + return TemplateLiteral(schema_, references_) + case 'This': + return This(schema_, references_) + case 'Tuple': + return Tuple(schema_, references_) + case 'Undefined': + return Undefined(schema_, references_) + case 'Union': + return Union(schema_, references_) + case 'Uint8Array': + return Uint8Array(schema_, references_) + case 'Unknown': + return Unknown(schema_, references_) + case 'Void': + return Void(schema_, references_) + default: + if (!Types.TypeRegistry.Has(schema_[Types.Kind])) throw new ValueCreateUnknownTypeError(schema_) + return UserDefined(schema_, references_) + } +} +// -------------------------------------------------------------------------- +// State +// -------------------------------------------------------------------------- +const recursiveMaxDepth = 512 +let recursiveDepth = 0 + +/** Creates a value from the given schema and references */ +export function Create(schema: T, references: Types.TSchema[]): Types.Static { + recursiveDepth = 0 + return Visit(schema, references) +} diff --git a/src/value/delta.ts b/src/value/delta.ts index 427a1d675..86306a8a8 100644 --- a/src/value/delta.ts +++ b/src/value/delta.ts @@ -27,41 +27,35 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ import { Type, Static } from '../typebox' -import { Is, ObjectType, ArrayType, TypedArrayType, ValueType } from './is' -import { ValueClone } from './clone' import { ValuePointer } from './pointer' +import * as ValueGuard from './guard' +import * as ValueClone from './clone' -// --------------------------------------------------------------------- -// Operations -// --------------------------------------------------------------------- - +// -------------------------------------------------------------------------- +// Commands +// -------------------------------------------------------------------------- export type Insert = Static export const Insert = Type.Object({ type: Type.Literal('insert'), path: Type.String(), value: Type.Unknown(), }) - export type Update = Static export const Update = Type.Object({ type: Type.Literal('update'), path: Type.String(), value: Type.Unknown(), }) - export type Delete = Static export const Delete = Type.Object({ type: Type.Literal('delete'), path: Type.String(), }) - export type Edit = Static export const Edit = Type.Union([Insert, Update, Delete]) - -// --------------------------------------------------------------------- +// -------------------------------------------------------------------------- // Errors -// --------------------------------------------------------------------- - +// -------------------------------------------------------------------------- export class ValueDeltaObjectWithSymbolKeyError extends Error { constructor(public readonly key: unknown) { super('ValueDelta: Cannot diff objects with symbol keys') @@ -72,136 +66,112 @@ export class ValueDeltaUnableToDiffUnknownValue extends Error { super('ValueDelta: Unable to create diff edits for unknown value') } } - -// --------------------------------------------------------------------- -// ValueDelta -// --------------------------------------------------------------------- - -export namespace ValueDelta { - // --------------------------------------------------------------------- - // Edits - // --------------------------------------------------------------------- - - function Update(path: string, value: unknown): Edit { - return { type: 'update', path, value } as any +// -------------------------------------------------------------------------- +// Command Factory +// -------------------------------------------------------------------------- +function CreateUpdate(path: string, value: unknown): Edit { + return { type: 'update', path, value } +} +function CreateInsert(path: string, value: unknown): Edit { + return { type: 'insert', path, value } +} +function CreateDelete(path: string): Edit { + return { type: 'delete', path } +} +// -------------------------------------------------------------------------- +// Diffing Generators +// -------------------------------------------------------------------------- +function* Object(path: string, current: ValueGuard.ObjectType, next: unknown): IterableIterator { + if (!ValueGuard.IsNonNominalObject(next)) return yield CreateUpdate(path, next) + const currentKeys = [...globalThis.Object.keys(current), ...globalThis.Object.getOwnPropertySymbols(current)] + const nextKeys = [...globalThis.Object.keys(next), ...globalThis.Object.getOwnPropertySymbols(next)] + for (const key of currentKeys) { + if (ValueGuard.IsSymbol(key)) throw new ValueDeltaObjectWithSymbolKeyError(key) + if (ValueGuard.IsUndefined(next[key]) && nextKeys.includes(key)) yield CreateUpdate(`${path}/${globalThis.String(key)}`, undefined) } - - function Insert(path: string, value: unknown): Edit { - return { type: 'insert', path, value } as any + for (const key of nextKeys) { + if (ValueGuard.IsUndefined(current[key]) || ValueGuard.IsUndefined(next[key])) continue + if (ValueGuard.IsSymbol(key)) throw new ValueDeltaObjectWithSymbolKeyError(key) + yield* Visit(`${path}/${String(key)}`, current[key], next[key]) } - - function Delete(path: string): Edit { - return { type: 'delete', path } as any + for (const key of nextKeys) { + if (ValueGuard.IsSymbol(key)) throw new ValueDeltaObjectWithSymbolKeyError(key) + if (ValueGuard.IsUndefined(current[key])) yield CreateInsert(`${path}/${globalThis.String(key)}`, next[key]) } - - // --------------------------------------------------------------------- - // Diff - // --------------------------------------------------------------------- - - function* Object(path: string, current: ObjectType, next: unknown): IterableIterator { - if (!Is.Object(next)) return yield Update(path, next) - const currentKeys = [...globalThis.Object.keys(current), ...globalThis.Object.getOwnPropertySymbols(current)] - const nextKeys = [...globalThis.Object.keys(next), ...globalThis.Object.getOwnPropertySymbols(next)] - for (const key of currentKeys) { - if (typeof key === 'symbol') throw new ValueDeltaObjectWithSymbolKeyError(key) - if (next[key] === undefined && nextKeys.includes(key)) yield Update(`${path}/${String(key)}`, undefined) - } - for (const key of nextKeys) { - if (current[key] === undefined || next[key] === undefined) continue - if (typeof key === 'symbol') throw new ValueDeltaObjectWithSymbolKeyError(key) - yield* Visit(`${path}/${String(key)}`, current[key], next[key]) - } - for (const key of nextKeys) { - if (typeof key === 'symbol') throw new ValueDeltaObjectWithSymbolKeyError(key) - if (current[key] === undefined) yield Insert(`${path}/${String(key)}`, next[key]) - } - for (const key of currentKeys.reverse()) { - if (typeof key === 'symbol') throw new ValueDeltaObjectWithSymbolKeyError(key) - if (next[key] === undefined && !nextKeys.includes(key)) yield Delete(`${path}/${String(key)}`) - } + for (const key of currentKeys.reverse()) { + if (ValueGuard.IsSymbol(key)) throw new ValueDeltaObjectWithSymbolKeyError(key) + if (ValueGuard.IsUndefined(next[key]) && !nextKeys.includes(key)) yield CreateDelete(`${path}/${globalThis.String(key)}`) } - - function* Array(path: string, current: ArrayType, next: unknown): IterableIterator { - if (!Is.Array(next)) return yield Update(path, next) - for (let i = 0; i < Math.min(current.length, next.length); i++) { - yield* Visit(`${path}/${i}`, current[i], next[i]) - } - for (let i = 0; i < next.length; i++) { - if (i < current.length) continue - yield Insert(`${path}/${i}`, next[i]) - } - for (let i = current.length - 1; i >= 0; i--) { - if (i < next.length) continue - yield Delete(`${path}/${i}`) - } - } - - function* TypedArray(path: string, current: TypedArrayType, next: unknown): IterableIterator { - if (!Is.TypedArray(next) || current.length !== next.length || globalThis.Object.getPrototypeOf(current).constructor.name !== globalThis.Object.getPrototypeOf(next).constructor.name) return yield Update(path, next) - for (let i = 0; i < Math.min(current.length, next.length); i++) { - yield* Visit(`${path}/${i}`, current[i], next[i]) - } +} +function* Array(path: string, current: ValueGuard.ArrayType, next: unknown): IterableIterator { + if (!ValueGuard.IsArray(next)) return yield CreateUpdate(path, next) + for (let i = 0; i < Math.min(current.length, next.length); i++) { + yield* Visit(`${path}/${i}`, current[i], next[i]) } - - function* Value(path: string, current: ValueType, next: unknown): IterableIterator { - if (current === next) return - yield Update(path, next) + for (let i = 0; i < next.length; i++) { + if (i < current.length) continue + yield CreateInsert(`${path}/${i}`, next[i]) } - - function* Visit(path: string, current: unknown, next: unknown): IterableIterator { - if (Is.Object(current)) { - return yield* Object(path, current, next) - } else if (Is.Array(current)) { - return yield* Array(path, current, next) - } else if (Is.TypedArray(current)) { - return yield* TypedArray(path, current, next) - } else if (Is.Value(current)) { - return yield* Value(path, current, next) - } else { - throw new ValueDeltaUnableToDiffUnknownValue(current) - } + for (let i = current.length - 1; i >= 0; i--) { + if (i < next.length) continue + yield CreateDelete(`${path}/${i}`) } - - export function Diff(current: unknown, next: unknown): Edit[] { - return [...Visit('', current, next)] +} +function* TypedArray(path: string, current: ValueGuard.TypedArrayType, next: unknown): IterableIterator { + if (!ValueGuard.IsTypedArray(next) || current.length !== next.length || globalThis.Object.getPrototypeOf(current).constructor.name !== globalThis.Object.getPrototypeOf(next).constructor.name) return yield CreateUpdate(path, next) + for (let i = 0; i < Math.min(current.length, next.length); i++) { + yield* Visit(`${path}/${i}`, current[i], next[i]) } - - // --------------------------------------------------------------------- - // Patch - // --------------------------------------------------------------------- - - function IsRootUpdate(edits: Edit[]): edits is [Update] { - return edits.length > 0 && edits[0].path === '' && edits[0].type === 'update' +} +function* ValueType(path: string, current: ValueGuard.ValueType, next: unknown): IterableIterator { + if (current === next) return + yield CreateUpdate(path, next) +} +function* Visit(path: string, current: unknown, next: unknown): IterableIterator { + if (ValueGuard.IsNonNominalObject(current)) return yield* Object(path, current, next) + if (ValueGuard.IsArray(current)) return yield* Array(path, current, next) + if (ValueGuard.IsTypedArray(current)) return yield* TypedArray(path, current, next) + if (ValueGuard.IsValueType(current)) return yield* ValueType(path, current, next) + throw new ValueDeltaUnableToDiffUnknownValue(current) +} +// --------------------------------------------------------------------- +// Diff +// --------------------------------------------------------------------- +export function Diff(current: unknown, next: unknown): Edit[] { + return [...Visit('', current, next)] +} +// --------------------------------------------------------------------- +// Patch +// --------------------------------------------------------------------- +function IsRootUpdate(edits: Edit[]): edits is [Update] { + return edits.length > 0 && edits[0].path === '' && edits[0].type === 'update' +} +function IsIdentity(edits: Edit[]) { + return edits.length === 0 +} +export function Patch(current: unknown, edits: Edit[]): T { + if (IsRootUpdate(edits)) { + return ValueClone.Clone(edits[0].value) as T } - - function IsIdentity(edits: Edit[]) { - return edits.length === 0 + if (IsIdentity(edits)) { + return ValueClone.Clone(current) as T } - - export function Patch(current: unknown, edits: Edit[]): T { - if (IsRootUpdate(edits)) { - return ValueClone.Clone(edits[0].value) as T - } - if (IsIdentity(edits)) { - return ValueClone.Clone(current) as T - } - const clone = ValueClone.Clone(current) - for (const edit of edits) { - switch (edit.type) { - case 'insert': { - ValuePointer.Set(clone, edit.path, edit.value) - break - } - case 'update': { - ValuePointer.Set(clone, edit.path, edit.value) - break - } - case 'delete': { - ValuePointer.Delete(clone, edit.path) - break - } + const clone = ValueClone.Clone(current) + for (const edit of edits) { + switch (edit.type) { + case 'insert': { + ValuePointer.Set(clone, edit.path, edit.value) + break + } + case 'update': { + ValuePointer.Set(clone, edit.path, edit.value) + break + } + case 'delete': { + ValuePointer.Delete(clone, edit.path) + break } } - return clone as T } + return clone as T } diff --git a/src/value/equal.ts b/src/value/equal.ts index ba3e2424c..6f6506111 100644 --- a/src/value/equal.ts +++ b/src/value/equal.ts @@ -26,43 +26,38 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -import { Is, ObjectType, ArrayType, TypedArrayType, ValueType } from './is' - -export namespace ValueEqual { - function Object(left: ObjectType, right: unknown): boolean { - if (!Is.Object(right)) return false - const leftKeys = [...globalThis.Object.keys(left), ...globalThis.Object.getOwnPropertySymbols(left)] - const rightKeys = [...globalThis.Object.keys(right), ...globalThis.Object.getOwnPropertySymbols(right)] - if (leftKeys.length !== rightKeys.length) return false - return leftKeys.every((key) => Equal(left[key], right[key])) - } - function Date(left: Date, right: unknown): any { - return Is.Date(right) && left.getTime() === right.getTime() - } - function Array(left: ArrayType, right: unknown): any { - if (!Is.Array(right) || left.length !== right.length) return false - return left.every((value, index) => Equal(value, right[index])) - } - function TypedArray(left: TypedArrayType, right: unknown): any { - if (!Is.TypedArray(right) || left.length !== right.length || globalThis.Object.getPrototypeOf(left).constructor.name !== globalThis.Object.getPrototypeOf(right).constructor.name) return false - return left.every((value, index) => Equal(value, right[index])) - } - function Value(left: ValueType, right: unknown): any { - return left === right - } - export function Equal(left: T, right: unknown): right is T { - if (Is.Object(left)) { - return Object(left, right) - } else if (Is.Date(left)) { - return Date(left, right) - } else if (Is.TypedArray(left)) { - return TypedArray(left, right) - } else if (Is.Array(left)) { - return Array(left, right) - } else if (Is.Value(left)) { - return Value(left, right) - } else { - throw new Error('ValueEquals: Unable to compare value') - } - } +import * as ValueGuard from './guard' + +// -------------------------------------------------------------------------- +// Equality Checks +// -------------------------------------------------------------------------- +function Object(left: ValueGuard.ObjectType, right: unknown): boolean { + if (!ValueGuard.IsNonNominalObject(right)) return false + const leftKeys = [...globalThis.Object.keys(left), ...globalThis.Object.getOwnPropertySymbols(left)] + const rightKeys = [...globalThis.Object.keys(right), ...globalThis.Object.getOwnPropertySymbols(right)] + if (leftKeys.length !== rightKeys.length) return false + return leftKeys.every((key) => Equal(left[key], right[key])) +} +function Date(left: Date, right: unknown): any { + return ValueGuard.IsDate(right) && left.getTime() === right.getTime() +} +function Array(left: ValueGuard.ArrayType, right: unknown): any { + if (!ValueGuard.IsArray(right) || left.length !== right.length) return false + return left.every((value, index) => Equal(value, right[index])) +} +function TypedArray(left: ValueGuard.TypedArrayType, right: unknown): any { + if (!ValueGuard.IsTypedArray(right) || left.length !== right.length || globalThis.Object.getPrototypeOf(left).constructor.name !== globalThis.Object.getPrototypeOf(right).constructor.name) return false + return left.every((value, index) => Equal(value, right[index])) +} +function ValueType(left: ValueGuard.ValueType, right: unknown): any { + return left === right +} +/** Returns true if the left value deep-equals the right */ +export function Equal(left: T, right: unknown): right is T { + if (ValueGuard.IsNonNominalObject(left)) return Object(left, right) + if (ValueGuard.IsDate(left)) return Date(left, right) + if (ValueGuard.IsTypedArray(left)) return TypedArray(left, right) + if (ValueGuard.IsArray(left)) return Array(left, right) + if (ValueGuard.IsValueType(left)) return ValueType(left, right) + throw new Error('ValueEquals: Unable to compare value') } diff --git a/src/value/guard.ts b/src/value/guard.ts new file mode 100644 index 000000000..58c4a4441 --- /dev/null +++ b/src/value/guard.ts @@ -0,0 +1,181 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2023 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// -------------------------------------------------------------------------- +// Types +// -------------------------------------------------------------------------- +export type ValueType = null | undefined | symbol | bigint | number | boolean | string +export type ObjectType = Record +export type ArrayType = unknown[] +// prettier-ignore +export type TypedArrayType = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | BigInt64Array + | BigUint64Array +// -------------------------------------------------------------------------- +// Iterators +// -------------------------------------------------------------------------- +/** Returns true if this value is an async iterator */ +export function IsAsyncIterator(value: unknown): value is AsyncIterableIterator { + return IsNonNominalObject(value) && Symbol.asyncIterator in value +} +/** Returns true if this value is an iterator */ +export function IsIterator(value: unknown): value is IterableIterator { + return IsNonNominalObject(value) && Symbol.iterator in value +} +// -------------------------------------------------------------------------- +// Nominal +// -------------------------------------------------------------------------- +/** 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 Promise */ +export function IsPromise(value: unknown): value is Promise { + return value instanceof globalThis.Promise +} +/** Returns true if the value is a Uint8Array */ +export function IsUint8Array(value: unknown): value is Uint8Array { + return value instanceof globalThis.Uint8Array +} +/** Returns true if this value is a Date */ +export function IsDate(value: unknown): value is Date { + return value instanceof globalThis.Date +} +// -------------------------------------------------------------------------- +// Standard +// -------------------------------------------------------------------------- +/** Returns true of this value is an object type, non-inclusive of arrays, typed-arrays, promises and dates */ +export function IsNonNominalObject(value: unknown): value is ObjectType { + // prettier-ignore + return IsObject(value) && ( + !IsTypedArray(value) && + !IsArray(value) && + !IsPromise(value) && + !IsDate(value) + ) +} +/** Returns true if this value has this property key */ +export function HasPropertyKey(value: Record, key: K): value is ObjectType & Record { + return key in value +} +/** Returns true of this value is an object type */ +export function IsObject(value: unknown): value is ObjectType { + return value !== null && typeof value === 'object' +} +/** Returns true if this value is an array, but not a typed array */ +export function IsArray(value: unknown): value is ArrayType { + return globalThis.Array.isArray(value) && !ArrayBuffer.isView(value) +} +/** Returns true if this value is an undefined */ +export function IsUndefined(value: unknown): value is undefined { + return value === undefined +} +/** Returns true if this value is an null */ +export function IsNull(value: unknown): value is null { + return value === null +} +/** Returns true if this value is an boolean */ +export function IsBoolean(value: unknown): value is boolean { + return typeof value === 'boolean' +} +/** Returns true if this value is an number */ +export function IsNumber(value: unknown): value is number { + return typeof value === 'number' +} +/** Returns true if this value is an integer */ +export function IsInteger(value: unknown): value is number { + return IsNumber(value) && globalThis.Number.isInteger(value) +} +/** Returns true if this value is bigint */ +export function IsBigInt(value: unknown): value is bigint { + return typeof value === 'bigint' +} +/** Returns true if this value is string */ +export function IsString(value: unknown): value is string { + return typeof value === 'string' +} +/** Returns true if this value is a function */ +export function IsFunction(value: unknown): value is Function { + return typeof value === 'function' +} +/** Returns true if this value is a symbol */ +export function IsSymbol(value: unknown): value is symbol { + return typeof value === 'symbol' +} +/** Returns true if this value is a value type such as number, string, boolean */ +export function IsValueType(value: unknown): value is ValueType { + // prettier-ignore + return ( + IsNull(value) || + IsUndefined(value) || + IsNumber(value) || + IsBoolean(value) || + IsString(value) || + IsBigInt(value) || + IsSymbol(value) + ) +} +// -------------------------------------------------------------------------- +// Conversions +// -------------------------------------------------------------------------- +export function IsStringNumeric(value: unknown): value is string { + return IsString(value) && !isNaN(value as any) && !isNaN(parseFloat(value)) +} +export function IsValueToString(value: unknown): value is { toString: () => string } { + return IsBigInt(value) || IsBoolean(value) || IsNumber(value) +} +export function IsValueTrue(value: unknown): value is true { + return value === true || (IsNumber(value) && value === 1) || (IsBigInt(value) && value === globalThis.BigInt('1')) || (IsString(value) && (value.toLowerCase() === 'true' || value === '1')) +} +export function IsValueFalse(value: unknown): value is false { + return value === false || (IsNumber(value) && value === 0) || (IsBigInt(value) && value === globalThis.BigInt('0')) || (IsString(value) && (value.toLowerCase() === 'false' || value === '0')) +} +export function IsTimeStringWithTimeZone(value: unknown): value is string { + return IsString(value) && /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i.test(value) +} +export function IsTimeStringWithoutTimeZone(value: unknown): value is string { + return IsString(value) && /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)?$/i.test(value) +} +export function IsDateTimeStringWithTimeZone(value: unknown): value is string { + return IsString(value) && /^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i.test(value) +} +export function IsDateTimeStringWithoutTimeZone(value: unknown): value is string { + return IsString(value) && /^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)?$/i.test(value) +} +export function IsDateString(value: unknown): value is string { + return IsString(value) && /^\d\d\d\d-[0-1]\d-[0-3]\d$/i.test(value) +} diff --git a/src/value/hash.ts b/src/value/hash.ts index 5231ecce3..3f99ba12e 100644 --- a/src/value/hash.ts +++ b/src/value/hash.ts @@ -26,166 +26,122 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ +import * as ValueGuard from './guard' + +// -------------------------------------------------------------------------- +// Errors +// -------------------------------------------------------------------------- export class ValueHashError extends Error { constructor(public readonly value: unknown) { super(`Hash: Unable to hash value`) } } -export namespace ValueHash { - enum ByteMarker { - Undefined, - Null, - Boolean, - Number, - String, - Object, - Array, - Date, - Uint8Array, - Symbol, - BigInt, - } - - // ---------------------------------------------------- - // State - // ---------------------------------------------------- - let Hash = globalThis.BigInt('14695981039346656037') - const [Prime, Size] = [globalThis.BigInt('1099511628211'), globalThis.BigInt('2') ** globalThis.BigInt('64')] - const Bytes = globalThis.Array.from({ length: 256 }).map((_, i) => globalThis.BigInt(i)) - const F64 = new globalThis.Float64Array(1) - const F64In = new globalThis.DataView(F64.buffer) - const F64Out = new globalThis.Uint8Array(F64.buffer) - // ---------------------------------------------------- - // Guards - // ---------------------------------------------------- - function IsDate(value: unknown): value is Date { - return value instanceof globalThis.Date - } - function IsUint8Array(value: unknown): value is Uint8Array { - return value instanceof globalThis.Uint8Array - } - function IsArray(value: unknown): value is Array { - return globalThis.Array.isArray(value) - } - function IsBoolean(value: unknown): value is boolean { - return typeof value === 'boolean' - } - function IsNull(value: unknown): value is null { - return value === null - } - function IsNumber(value: unknown): value is number { - return typeof value === 'number' - } - function IsSymbol(value: unknown): value is symbol { - return typeof value === 'symbol' - } - function IsBigInt(value: unknown): value is bigint { - return typeof value === 'bigint' - } - function IsObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !IsArray(value) && !IsDate(value) && !IsUint8Array(value) - } - function IsString(value: unknown): value is string { - return typeof value === 'string' - } - function IsUndefined(value: unknown): value is undefined { - return value === undefined - } - // ---------------------------------------------------- - // Encoding - // ---------------------------------------------------- - function Array(value: Array) { - FNV1A64(ByteMarker.Array) - for (const item of value) { - Visit(item) - } - } - function Boolean(value: boolean) { - FNV1A64(ByteMarker.Boolean) - FNV1A64(value ? 1 : 0) - } - function BigInt(value: bigint) { - FNV1A64(ByteMarker.BigInt) - F64In.setBigInt64(0, value) - for (const byte of F64Out) { - FNV1A64(byte) - } - } - function Date(value: Date) { - FNV1A64(ByteMarker.Date) - Visit(value.getTime()) - } - function Null(value: null) { - FNV1A64(ByteMarker.Null) - } - function Number(value: number) { - FNV1A64(ByteMarker.Number) - F64In.setFloat64(0, value) - for (const byte of F64Out) { - FNV1A64(byte) - } - } - function Object(value: Record) { - FNV1A64(ByteMarker.Object) - for (const key of globalThis.Object.keys(value).sort()) { - Visit(key) - Visit(value[key]) - } - } - function String(value: string) { - FNV1A64(ByteMarker.String) - for (let i = 0; i < value.length; i++) { - FNV1A64(value.charCodeAt(i)) - } - } - function Symbol(value: symbol) { - FNV1A64(ByteMarker.Symbol) - Visit(value.description) +// -------------------------------------------------------------------------- +// ByteMarker +// -------------------------------------------------------------------------- +export enum ByteMarker { + Undefined, + Null, + Boolean, + Number, + String, + Object, + Array, + Date, + Uint8Array, + Symbol, + BigInt, +} +// -------------------------------------------------------------------------- +// State +// -------------------------------------------------------------------------- +let Accumulator = globalThis.BigInt('14695981039346656037') +const [Prime, Size] = [globalThis.BigInt('1099511628211'), globalThis.BigInt('2') ** globalThis.BigInt('64')] +const Bytes = globalThis.Array.from({ length: 256 }).map((_, i) => globalThis.BigInt(i)) +const F64 = new globalThis.Float64Array(1) +const F64In = new globalThis.DataView(F64.buffer) +const F64Out = new globalThis.Uint8Array(F64.buffer) +// -------------------------------------------------------------------------- +// Hashing Functions +// -------------------------------------------------------------------------- +function Array(value: Array) { + FNV1A64(ByteMarker.Array) + for (const item of value) { + Visit(item) } - function Uint8Array(value: Uint8Array) { - FNV1A64(ByteMarker.Uint8Array) - for (let i = 0; i < value.length; i++) { - FNV1A64(value[i]) - } +} +function Boolean(value: boolean) { + FNV1A64(ByteMarker.Boolean) + FNV1A64(value ? 1 : 0) +} +function BigInt(value: bigint) { + FNV1A64(ByteMarker.BigInt) + F64In.setBigInt64(0, value) + for (const byte of F64Out) { + FNV1A64(byte) } - function Undefined(value: undefined) { - return FNV1A64(ByteMarker.Undefined) +} +function Date(value: Date) { + FNV1A64(ByteMarker.Date) + Visit(value.getTime()) +} +function Null(value: null) { + FNV1A64(ByteMarker.Null) +} +function Number(value: number) { + FNV1A64(ByteMarker.Number) + F64In.setFloat64(0, value) + for (const byte of F64Out) { + FNV1A64(byte) } - function Visit(value: any) { - if (IsArray(value)) { - Array(value) - } else if (IsBoolean(value)) { - Boolean(value) - } else if (IsBigInt(value)) { - BigInt(value) - } else if (IsDate(value)) { - Date(value) - } else if (IsNull(value)) { - Null(value) - } else if (IsNumber(value)) { - Number(value) - } else if (IsObject(value)) { - Object(value) - } else if (IsString(value)) { - String(value) - } else if (IsSymbol(value)) { - Symbol(value) - } else if (IsUint8Array(value)) { - Uint8Array(value) - } else if (IsUndefined(value)) { - Undefined(value) - } else { - throw new ValueHashError(value) - } +} +function Object(value: Record) { + FNV1A64(ByteMarker.Object) + for (const key of globalThis.Object.keys(value).sort()) { + Visit(key) + Visit(value[key]) } - function FNV1A64(byte: number) { - Hash = Hash ^ Bytes[byte] - Hash = (Hash * Prime) % Size +} +function String(value: string) { + FNV1A64(ByteMarker.String) + for (let i = 0; i < value.length; i++) { + FNV1A64(value.charCodeAt(i)) } - /** Creates a FNV1A-64 non cryptographic hash of the given value */ - export function Create(value: unknown) { - Hash = globalThis.BigInt('14695981039346656037') - Visit(value) - return Hash +} +function Symbol(value: symbol) { + FNV1A64(ByteMarker.Symbol) + Visit(value.description) +} +function Uint8Array(value: Uint8Array) { + FNV1A64(ByteMarker.Uint8Array) + for (let i = 0; i < value.length; i++) { + FNV1A64(value[i]) } } +function Undefined(value: undefined) { + return FNV1A64(ByteMarker.Undefined) +} +function Visit(value: any) { + if (ValueGuard.IsArray(value)) return Array(value) + if (ValueGuard.IsBoolean(value)) return Boolean(value) + if (ValueGuard.IsBigInt(value)) return BigInt(value) + if (ValueGuard.IsDate(value)) return Date(value) + if (ValueGuard.IsNull(value)) return Null(value) + if (ValueGuard.IsNumber(value)) return Number(value) + if (ValueGuard.IsNonNominalObject(value)) return Object(value) + if (ValueGuard.IsString(value)) return String(value) + if (ValueGuard.IsSymbol(value)) return Symbol(value) + if (ValueGuard.IsUint8Array(value)) return Uint8Array(value) + if (ValueGuard.IsUndefined(value)) return Undefined(value) + throw new ValueHashError(value) +} +function FNV1A64(byte: number) { + Accumulator = Accumulator ^ Bytes[byte] + Accumulator = (Accumulator * Prime) % Size +} +/** Creates a FNV1A-64 non cryptographic hash of the given value */ +export function Hash(value: unknown) { + Accumulator = globalThis.BigInt('14695981039346656037') + Visit(value) + return Accumulator +} diff --git a/src/value/index.ts b/src/value/index.ts index a2235b01f..e8beb3bbd 100644 --- a/src/value/index.ts +++ b/src/value/index.ts @@ -26,9 +26,8 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -export { ValueError, ValueErrorIterator, ValueErrorType } from '../errors/index' -export { ValueHash } from './hash' +export { ValueError, ValueErrorType, ValueErrorIterator } from '../errors/index' export { Edit, Insert, Update, Delete } from './delta' export { Mutable } from './mutate' -export * from './pointer' -export * from './value' +export { ValuePointer } from './pointer' +export { Value } from './value' diff --git a/src/value/is.ts b/src/value/is.ts deleted file mode 100644 index c107a27d3..000000000 --- a/src/value/is.ts +++ /dev/null @@ -1,59 +0,0 @@ -/*-------------------------------------------------------------------------- - -@sinclair/typebox/value - -The MIT License (MIT) - -Copyright (c) 2017-2023 Haydn Paterson (sinclair) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ----------------------------------------------------------------------------*/ - -export type ValueType = null | undefined | Function | symbol | bigint | number | boolean | string -export type ObjectType = Record -export type TypedArrayType = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array -export type ArrayType = unknown[] - -export namespace Is { - export function Array(value: unknown): value is ArrayType { - return globalThis.Array.isArray(value) && !ArrayBuffer.isView(value) - } - export function AsyncIterator(value: unknown): value is AsyncIterableIterator { - return Object(value) && Symbol.asyncIterator in value - } - export function Promise(value: unknown): value is Promise { - return value instanceof globalThis.Promise - } - export function Date(value: unknown): value is Date { - return value instanceof globalThis.Date - } - export function Iterator(value: unknown): value is IterableIterator { - return Object(value) && Symbol.iterator in value - } - export function Object(value: unknown): value is ObjectType { - return value !== null && typeof value === 'object' && !globalThis.Array.isArray(value) && !ArrayBuffer.isView(value) && !(value instanceof globalThis.Date) - } - export function Value(value: unknown): value is ValueType { - return value === null || value === undefined || typeof value === 'function' || typeof value === 'symbol' || typeof value === 'bigint' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string' - } - export function TypedArray(value: unknown): value is TypedArrayType { - return ArrayBuffer.isView(value) - } -} diff --git a/src/value/mutate.ts b/src/value/mutate.ts index 44d11a6e7..7f0681d7f 100644 --- a/src/value/mutate.ts +++ b/src/value/mutate.ts @@ -26,10 +26,13 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -import { Is, TypedArrayType } from './is' import { ValuePointer } from './pointer' -import { ValueClone } from './clone' +import * as ValueClone from './clone' +import * as ValueGuard from './guard' +// -------------------------------------------------------------------------- +// Errors +// -------------------------------------------------------------------------- export class ValueMutateTypeMismatchError extends Error { constructor() { super('ValueMutate: Cannot assign due type mismatch of assignable values') @@ -40,71 +43,76 @@ export class ValueMutateInvalidRootMutationError extends Error { super('ValueMutate: Only object and array types can be mutated at the root level') } } +// -------------------------------------------------------------------------- +// Mutators +// -------------------------------------------------------------------------- export type Mutable = { [key: string]: unknown } | unknown[] -export namespace ValueMutate { - function Object(root: Mutable, path: string, current: unknown, next: Record) { - if (!Is.Object(current)) { - ValuePointer.Set(root, path, ValueClone.Clone(next)) - } else { - const currentKeys = globalThis.Object.keys(current) - const nextKeys = globalThis.Object.keys(next) - for (const currentKey of currentKeys) { - if (!nextKeys.includes(currentKey)) { - delete current[currentKey] - } - } - for (const nextKey of nextKeys) { - if (!currentKeys.includes(nextKey)) { - current[nextKey] = null - } - } - for (const nextKey of nextKeys) { - Visit(root, `${path}/${nextKey}`, current[nextKey], next[nextKey]) +function Object(root: Mutable, path: string, current: unknown, next: Record) { + if (!ValueGuard.IsNonNominalObject(current)) { + ValuePointer.Set(root, path, ValueClone.Clone(next)) + } else { + const currentKeys = globalThis.Object.keys(current) + const nextKeys = globalThis.Object.keys(next) + for (const currentKey of currentKeys) { + if (!nextKeys.includes(currentKey)) { + delete current[currentKey] } } - } - function Array(root: Mutable, path: string, current: unknown, next: unknown[]) { - if (!Is.Array(current)) { - ValuePointer.Set(root, path, ValueClone.Clone(next)) - } else { - for (let index = 0; index < next.length; index++) { - Visit(root, `${path}/${index}`, current[index], next[index]) + for (const nextKey of nextKeys) { + if (!currentKeys.includes(nextKey)) { + current[nextKey] = null } - current.splice(next.length) } - } - function TypedArray(root: Mutable, path: string, current: unknown, next: TypedArrayType) { - if (Is.TypedArray(current) && current.length === next.length) { - for (let i = 0; i < current.length; i++) { - current[i] = next[i] - } - } else { - ValuePointer.Set(root, path, ValueClone.Clone(next)) + for (const nextKey of nextKeys) { + Visit(root, `${path}/${nextKey}`, current[nextKey], next[nextKey]) } } - function Value(root: Mutable, path: string, current: unknown, next: unknown) { - if (current === next) return - ValuePointer.Set(root, path, next) - } - function Visit(root: Mutable, path: string, current: unknown, next: unknown) { - if (Is.Array(next)) { - return Array(root, path, current, next) - } else if (Is.TypedArray(next)) { - return TypedArray(root, path, current, next) - } else if (Is.Object(next)) { - return Object(root, path, current, next) - } else if (Is.Value(next)) { - return Value(root, path, current, next) +} +function Array(root: Mutable, path: string, current: unknown, next: unknown[]) { + if (!ValueGuard.IsArray(current)) { + ValuePointer.Set(root, path, ValueClone.Clone(next)) + } else { + for (let index = 0; index < next.length; index++) { + Visit(root, `${path}/${index}`, current[index], next[index]) } + current.splice(next.length) } - /** Performs a deep mutable value assignment while retaining internal references. */ - export function Mutate(current: Mutable, next: Mutable): void { - if (Is.TypedArray(current) || Is.Value(current) || Is.TypedArray(next) || Is.Value(next)) { - throw new ValueMutateInvalidRootMutationError() - } - if ((Is.Object(current) && Is.Array(next)) || (Is.Array(current) && Is.Object(next))) { - throw new ValueMutateTypeMismatchError() +} +function TypedArray(root: Mutable, path: string, current: unknown, next: ValueGuard.TypedArrayType) { + if (ValueGuard.IsTypedArray(current) && current.length === next.length) { + for (let i = 0; i < current.length; i++) { + current[i] = next[i] } - Visit(current, '', current, next) + } else { + ValuePointer.Set(root, path, ValueClone.Clone(next)) } } +function Value(root: Mutable, path: string, current: unknown, next: unknown) { + if (current === next) return + ValuePointer.Set(root, path, next) +} +function Visit(root: Mutable, path: string, current: unknown, next: unknown) { + if (ValueGuard.IsArray(next)) return Array(root, path, current, next) + if (ValueGuard.IsTypedArray(next)) return TypedArray(root, path, current, next) + if (ValueGuard.IsNonNominalObject(next)) return Object(root, path, current, next) + if (ValueGuard.IsValueType(next)) return Value(root, path, current, next) +} +// -------------------------------------------------------------------------- +// Mutate +// -------------------------------------------------------------------------- +function IsNonMutableValue(value: unknown): value is Mutable { + return ValueGuard.IsTypedArray(value) || ValueGuard.IsValueType(value) +} +function IsMismatchedValue(current: unknown, next: unknown) { + // prettier-ignore + return ( + (ValueGuard.IsNonNominalObject(current) && ValueGuard.IsArray(next)) || + (ValueGuard.IsArray(current) && ValueGuard.IsNonNominalObject(next)) + ) +} +/** Performs a deep mutable value assignment while retaining internal references. */ +export function Mutate(current: Mutable, next: Mutable): void { + if (IsNonMutableValue(current) || IsNonMutableValue(next)) throw new ValueMutateInvalidRootMutationError() + if (IsMismatchedValue(current, next)) throw new ValueMutateTypeMismatchError() + Visit(current, '', current, next) +} diff --git a/src/value/pointer.ts b/src/value/pointer.ts index 1e7718e89..de8003cea 100644 --- a/src/value/pointer.ts +++ b/src/value/pointer.ts @@ -26,24 +26,27 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ +// -------------------------------------------------------------------------- +// Errors +// -------------------------------------------------------------------------- export class ValuePointerRootSetError extends Error { constructor(public readonly value: unknown, public readonly path: string, public readonly update: unknown) { super('ValuePointer: Cannot set root value') } } - export class ValuePointerRootDeleteError extends Error { constructor(public readonly value: unknown, public readonly path: string) { super('ValuePointer: Cannot delete root value') } } - +// -------------------------------------------------------------------------- +// ValuePointer +// -------------------------------------------------------------------------- /** Provides functionality to update values through RFC6901 string pointers */ export namespace ValuePointer { function Escape(component: string) { return component.indexOf('~') === -1 ? component : component.replace(/~1/g, '/').replace(/~0/g, '~') } - /** Formats the given pointer into navigable key components */ export function* Format(pointer: string): IterableIterator { if (pointer === '') return @@ -64,7 +67,6 @@ export namespace ValuePointer { } yield Escape(pointer.slice(start)) } - /** Sets the value at the given pointer. If the value at the pointer does not exist it is created */ export function Set(value: any, pointer: string, update: unknown): void { if (pointer === '') throw new ValuePointerRootSetError(value, pointer, update) @@ -77,7 +79,6 @@ export namespace ValuePointer { } owner[key] = update } - /** Deletes a value at the given pointer */ export function Delete(value: any, pointer: string): void { if (pointer === '') throw new ValuePointerRootDeleteError(value, pointer) @@ -95,7 +96,6 @@ export namespace ValuePointer { delete owner[key] } } - /** Returns true if a value exists at the given pointer */ export function Has(value: any, pointer: string): boolean { if (pointer === '') return true @@ -108,7 +108,6 @@ export namespace ValuePointer { } return globalThis.Object.getOwnPropertyNames(owner).includes(key) } - /** Gets the value at the given pointer */ export function Get(value: any, pointer: string): any { if (pointer === '') return value diff --git a/src/value/value.ts b/src/value/value.ts index 96bf2982f..717d09ff8 100644 --- a/src/value/value.ts +++ b/src/value/value.ts @@ -26,22 +26,22 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ +import * as ValueErrors from '../errors/index' +import * as ValueMutate from './mutate' +import * as ValueHash from './hash' +import * as ValueEqual from './equal' +import * as ValueCast from './cast' +import * as ValueClone from './clone' +import * as ValueConvert from './convert' +import * as ValueCreate from './create' +import * as ValueCheck from './check' +import * as ValueDelta from './delta' import * as Types from '../typebox' -import { ValueErrors, ValueErrorIterator } from '../errors/index' -import { ValueMutate, Mutable } from './mutate' -import { ValueHash } from './hash' -import { ValueEqual } from './equal' -import { ValueCast } from './cast' -import { ValueClone } from './clone' -import { ValueConvert } from './convert' -import { ValueCreate } from './create' -import { ValueCheck } from './check' -import { ValueDelta, Edit } from './delta' -/** Provides functions to perform structural updates to JavaScript values */ +/** Functions that perform structural operations on JavaScript values */ export namespace Value { /** Casts a value into a given type. The return value will retain as much information of the original value as possible. Cast will convert string, number, boolean and date values if a reasonable conversion is possible. */ - export function Cast(schema: T, references: [...R], value: unknown): Types.Static + export function Cast(schema: T, references: Types.TSchema[], value: unknown): Types.Static /** Casts a value into a given type. The return value will retain as much information of the original value as possible. Cast will convert string, number, boolean and date values if a reasonable conversion is possible. */ export function Cast(schema: T, value: unknown): Types.Static export function Cast(...args: any[]) { @@ -49,7 +49,7 @@ export namespace Value { return ValueCast.Cast(schema, references, value) } /** Creates a value from the given type */ - export function Create(schema: T, references: [...R]): Types.Static + export function Create(schema: T, references: Types.TSchema[]): Types.Static /** Creates a value from the given type */ export function Create(schema: T): Types.Static export function Create(...args: any[]) { @@ -57,7 +57,7 @@ export namespace Value { return ValueCreate.Create(schema, references) } /** Returns true if the value matches the given type. */ - export function Check(schema: T, references: [...R], value: unknown): value is Types.Static + export function Check(schema: T, references: Types.TSchema[], value: unknown): value is Types.Static /** Returns true if the value matches the given type. */ export function Check(schema: T, value: unknown): value is Types.Static export function Check(...args: any[]) { @@ -65,7 +65,7 @@ export namespace Value { return ValueCheck.Check(schema, references, value) } /** Converts any type mismatched values to their target type if a conversion is possible. */ - export function Convert(schema: T, references: [...R], value: unknown): unknown + export function Convert(schema: T, references: Types.TSchema[], value: unknown): unknown /** Converts any type mismatched values to their target type if a conversion is possible. */ export function Convert(schema: T, value: unknown): unknown export function Convert(...args: any[]) { @@ -77,31 +77,31 @@ export namespace Value { return ValueClone.Clone(value) } /** Returns an iterator for each error in this value. */ - export function Errors(schema: T, references: [...R], value: unknown): ValueErrorIterator + export function Errors(schema: T, references: Types.TSchema[], value: unknown): ValueErrors.ValueErrorIterator /** Returns an iterator for each error in this value. */ - export function Errors(schema: T, value: unknown): ValueErrorIterator + export function Errors(schema: T, value: unknown): ValueErrors.ValueErrorIterator export function Errors(...args: any[]) { const [schema, references, value] = args.length === 3 ? [args[0], args[1], args[2]] : [args[0], [], args[1]] - return ValueErrors.Errors(schema, references, value) as ValueErrorIterator + return ValueErrors.ValueErrors.Errors(schema, references, value) as ValueErrors.ValueErrorIterator } /** Returns true if left and right values are structurally equal */ export function Equal(left: T, right: unknown): right is T { return ValueEqual.Equal(left, right) } /** Returns edits to transform the current value into the next value */ - export function Diff(current: unknown, next: unknown): Edit[] { + export function Diff(current: unknown, next: unknown): ValueDelta.Edit[] { return ValueDelta.Diff(current, next) } /** Returns a FNV1A-64 non cryptographic hash of the given value */ export function Hash(value: unknown): bigint { - return ValueHash.Create(value) + return ValueHash.Hash(value) } /** Returns a new value with edits applied to the given value */ - export function Patch(current: unknown, edits: Edit[]): T { + export function Patch(current: unknown, edits: ValueDelta.Edit[]): T { return ValueDelta.Patch(current, edits) as T } /** Performs a deep mutable value assignment while retaining internal references. */ - export function Mutate(current: Mutable, next: Mutable): void { + export function Mutate(current: ValueMutate.Mutable, next: ValueMutate.Mutable): void { ValueMutate.Mutate(current, next) } } diff --git a/test/runtime/value/hash/hash.ts b/test/runtime/value/hash/hash.ts index eac4765e8..7e0a77d5c 100644 --- a/test/runtime/value/hash/hash.ts +++ b/test/runtime/value/hash/hash.ts @@ -1,111 +1,111 @@ -import { ValueHash } from '@sinclair/typebox/value' +import * as ValueHash from '@sinclair/typebox/value/hash' import { Assert } from '../../assert/index' describe('value/hash/Hash', () => { it('Should hash number', () => { - Assert.isEqual('bigint', typeof ValueHash.Create(1)) - const A = ValueHash.Create(1) - const B = ValueHash.Create(2) + Assert.isEqual('bigint', typeof ValueHash.Hash(1)) + const A = ValueHash.Hash(1) + const B = ValueHash.Hash(2) Assert.notEqual(A, B) }) it('Should hash string', () => { - Assert.isEqual('bigint', typeof ValueHash.Create('hello')) - const A = ValueHash.Create('hello') - const B = ValueHash.Create('world') + Assert.isEqual('bigint', typeof ValueHash.Hash('hello')) + const A = ValueHash.Hash('hello') + const B = ValueHash.Hash('world') Assert.notEqual(A, B) }) it('Should hash boolean', () => { - Assert.isEqual('bigint', typeof ValueHash.Create(true)) - Assert.isEqual('bigint', typeof ValueHash.Create(false)) - const A = ValueHash.Create(true) - const B = ValueHash.Create(false) + Assert.isEqual('bigint', typeof ValueHash.Hash(true)) + Assert.isEqual('bigint', typeof ValueHash.Hash(false)) + const A = ValueHash.Hash(true) + const B = ValueHash.Hash(false) Assert.notEqual(A, B) }) it('Should hash null', () => { - Assert.isEqual('bigint', typeof ValueHash.Create(null)) - const A = ValueHash.Create(null) - const B = ValueHash.Create(undefined) + Assert.isEqual('bigint', typeof ValueHash.Hash(null)) + const A = ValueHash.Hash(null) + const B = ValueHash.Hash(undefined) Assert.notEqual(A, B) }) it('Should hash array', () => { - Assert.isEqual('bigint', typeof ValueHash.Create([0, 1, 2, 3])) - const A = ValueHash.Create([0, 1, 2, 3]) - const B = ValueHash.Create([0, 2, 2, 3]) + Assert.isEqual('bigint', typeof ValueHash.Hash([0, 1, 2, 3])) + const A = ValueHash.Hash([0, 1, 2, 3]) + const B = ValueHash.Hash([0, 2, 2, 3]) Assert.notEqual(A, B) }) it('Should hash object 1', () => { // prettier-ignore - Assert.isEqual('bigint', typeof ValueHash.Create({ x: 1, y: 2 })) - const A = ValueHash.Create({ x: 1, y: 2 }) - const B = ValueHash.Create({ x: 2, y: 2 }) + Assert.isEqual('bigint', typeof ValueHash.Hash({ x: 1, y: 2 })) + const A = ValueHash.Hash({ x: 1, y: 2 }) + const B = ValueHash.Hash({ x: 2, y: 2 }) Assert.notEqual(A, B) }) it('Should hash object 2', () => { - const A = ValueHash.Create({ x: 1, y: [1, 2] }) - const B = ValueHash.Create({ x: 1, y: [1, 3] }) + const A = ValueHash.Hash({ x: 1, y: [1, 2] }) + const B = ValueHash.Hash({ x: 1, y: [1, 3] }) Assert.notEqual(A, B) }) it('Should hash object 3', () => { - const A = ValueHash.Create({ x: 1, y: undefined }) - const B = ValueHash.Create({ x: 1 }) + const A = ValueHash.Hash({ x: 1, y: undefined }) + const B = ValueHash.Hash({ x: 1 }) Assert.notEqual(A, B) }) it('Should hash object 4', () => { - const A = ValueHash.Create({ x: 1, y: new Uint8Array([0, 1, 2]) }) - const B = ValueHash.Create({ x: 1, y: [0, 1, 2] }) + const A = ValueHash.Hash({ x: 1, y: new Uint8Array([0, 1, 2]) }) + const B = ValueHash.Hash({ x: 1, y: [0, 1, 2] }) Assert.notEqual(A, B) }) it('Should hash object 5', () => { - const A = ValueHash.Create({ x: 1, y: undefined }) - const B = ValueHash.Create({ x: 2, y: undefined }) + const A = ValueHash.Hash({ x: 1, y: undefined }) + const B = ValueHash.Hash({ x: 2, y: undefined }) Assert.notEqual(A, B) }) it('Should hash Date', () => { - Assert.isEqual('bigint', typeof ValueHash.Create(new Date())) - const A = ValueHash.Create(new Date(1)) - const B = ValueHash.Create(new Date(2)) + Assert.isEqual('bigint', typeof ValueHash.Hash(new Date())) + const A = ValueHash.Hash(new Date(1)) + const B = ValueHash.Hash(new Date(2)) Assert.notEqual(A, B) }) it('Should hash Uint8Array', () => { - Assert.isEqual('bigint', typeof ValueHash.Create(new Uint8Array([0, 1, 2, 3]))) - const A = ValueHash.Create(new Uint8Array([0, 1, 2, 3])) - const B = ValueHash.Create(new Uint8Array([0, 2, 2, 3])) + Assert.isEqual('bigint', typeof ValueHash.Hash(new Uint8Array([0, 1, 2, 3]))) + const A = ValueHash.Hash(new Uint8Array([0, 1, 2, 3])) + const B = ValueHash.Hash(new Uint8Array([0, 2, 2, 3])) Assert.notEqual(A, B) }) it('Should hash undefined', () => { - Assert.isEqual('bigint', typeof ValueHash.Create(undefined)) - const A = ValueHash.Create(undefined) - const B = ValueHash.Create(null) + Assert.isEqual('bigint', typeof ValueHash.Hash(undefined)) + const A = ValueHash.Hash(undefined) + const B = ValueHash.Hash(null) Assert.notEqual(A, B) }) it('Should hash symbol 1', () => { - Assert.isEqual('bigint', typeof ValueHash.Create(Symbol())) - const A = ValueHash.Create(Symbol(1)) - const B = ValueHash.Create(Symbol()) + Assert.isEqual('bigint', typeof ValueHash.Hash(Symbol())) + const A = ValueHash.Hash(Symbol(1)) + const B = ValueHash.Hash(Symbol()) Assert.notEqual(A, B) }) it('Should hash symbol 2', () => { - Assert.isEqual('bigint', typeof ValueHash.Create(Symbol())) - const A = ValueHash.Create(Symbol(1)) - const B = ValueHash.Create(Symbol(2)) + Assert.isEqual('bigint', typeof ValueHash.Hash(Symbol())) + const A = ValueHash.Hash(Symbol(1)) + const B = ValueHash.Hash(Symbol(2)) Assert.notEqual(A, B) }) it('Should hash symbol 2', () => { - Assert.isEqual('bigint', typeof ValueHash.Create(Symbol())) - const A = ValueHash.Create(Symbol(1)) - const B = ValueHash.Create(Symbol(1)) + Assert.isEqual('bigint', typeof ValueHash.Hash(Symbol())) + const A = ValueHash.Hash(Symbol(1)) + const B = ValueHash.Hash(Symbol(1)) Assert.isEqual(A, B) }) it('Should hash bigint 1', () => { - Assert.isEqual('bigint', typeof ValueHash.Create(BigInt(1))) - const A = ValueHash.Create(BigInt(1)) - const B = ValueHash.Create(BigInt(2)) + Assert.isEqual('bigint', typeof ValueHash.Hash(BigInt(1))) + const A = ValueHash.Hash(BigInt(1)) + const B = ValueHash.Hash(BigInt(2)) Assert.notEqual(A, B) }) it('Should hash bigint 2', () => { - Assert.isEqual('bigint', typeof ValueHash.Create(BigInt(1))) - const A = ValueHash.Create(BigInt(1)) - const B = ValueHash.Create(BigInt(1)) + Assert.isEqual('bigint', typeof ValueHash.Hash(BigInt(1))) + const A = ValueHash.Hash(BigInt(1)) + const B = ValueHash.Hash(BigInt(1)) Assert.isEqual(A, B) }) }) diff --git a/tsconfig.json b/tsconfig.json index 0932a6a80..41a696de3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,39 @@ "@sinclair/typebox/system": [ "src/system/index.ts" ], + "@sinclair/typebox/value/cast": [ + "src/value/cast.ts" + ], + "@sinclair/typebox/value/check": [ + "src/value/check.ts" + ], + "@sinclair/typebox/value/clone": [ + "src/value/clone.ts" + ], + "@sinclair/typebox/value/convert": [ + "src/value/convert.ts" + ], + "@sinclair/typebox/value/create": [ + "src/value/create.ts" + ], + "@sinclair/typebox/value/delta": [ + "src/value/delta.ts" + ], + "@sinclair/typebox/value/equal": [ + "src/value/equal.ts" + ], + "@sinclair/typebox/value/guard": [ + "src/value/guard.ts" + ], + "@sinclair/typebox/value/hash": [ + "src/value/hash.ts" + ], + "@sinclair/typebox/value/mutate": [ + "src/value/mutate.ts" + ], + "@sinclair/typebox/value/pointer": [ + "src/value/pointer.ts" + ], "@sinclair/typebox/value": [ "src/value/index.ts" ],