diff --git a/src/type/registry/base.ts b/src/type/registry/base.ts new file mode 100644 index 00000000..dab00a19 --- /dev/null +++ b/src/type/registry/base.ts @@ -0,0 +1,29 @@ +/** A base registry class */ +export class BaseRegistry { + private readonly map = new Map; + + /** Returns the entries in this registry */ + Entries = () => { + return new Map(this.map) + } + /** Clears all user defined string formats */ + Clear = () => { + return this.map.clear() + } + /** Deletes a registered format */ + Delete = (format: string) => { + return this.map.delete(format) + } + /** Returns true if the user defined string format exists */ + Has = (format: string) => { + return this.map.has(format) + } + /** Sets a validation function for a user defined string format */ + Set = (format: string, func: Fn) => { + this.map.set(format, func) + } + /** Gets a validation function for a user defined string format */ + Get = (format: string) => { + return this.map.get(format) + } +} \ No newline at end of file diff --git a/src/type/registry/convert.ts b/src/type/registry/convert.ts new file mode 100644 index 00000000..00a18097 --- /dev/null +++ b/src/type/registry/convert.ts @@ -0,0 +1,39 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 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. + +---------------------------------------------------------------------------*/ +import { BaseRegistry } from './base' + +export type ConvertRegistryFunction = (schema: TSchema, value: unknown, data?: any) => unknown + +const ConvertRegistry = new BaseRegistry>() + +export const Entries = ConvertRegistry.Entries +export const Clear = ConvertRegistry.Clear +export const Delete = ConvertRegistry.Delete +export const Has = ConvertRegistry.Has +export const Set = ConvertRegistry.Set +export const Get = ConvertRegistry.Get \ No newline at end of file diff --git a/src/type/registry/format.ts b/src/type/registry/format.ts index af818ae1..58d3bae9 100644 --- a/src/type/registry/format.ts +++ b/src/type/registry/format.ts @@ -25,31 +25,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------*/ +import { BaseRegistry } from './base' export type FormatRegistryValidationFunction = (value: string) => boolean -/** A registry for user defined string formats */ -const map = new Map() -/** Returns the entries in this registry */ -export function Entries() { - return new Map(map) -} -/** Clears all user defined string formats */ -export function Clear() { - return map.clear() -} -/** Deletes a registered format */ -export function Delete(format: string) { - return map.delete(format) -} -/** Returns true if the user defined string format exists */ -export function Has(format: string) { - return map.has(format) -} -/** Sets a validation function for a user defined string format */ -export function Set(format: string, func: FormatRegistryValidationFunction) { - map.set(format, func) -} -/** Gets a validation function for a user defined string format */ -export function Get(format: string) { - return map.get(format) -} + +const FormatRegistry = new BaseRegistry() + +export const Entries = FormatRegistry.Entries +export const Clear = FormatRegistry.Clear +export const Delete = FormatRegistry.Delete +export const Has = FormatRegistry.Has +export const Set = FormatRegistry.Set +export const Get = FormatRegistry.Get \ No newline at end of file diff --git a/src/type/registry/index.ts b/src/type/registry/index.ts index cdb2ac47..c81db71b 100644 --- a/src/type/registry/index.ts +++ b/src/type/registry/index.ts @@ -26,5 +26,6 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ +export * as ConvertRegistry from './convert' export * as FormatRegistry from './format' export * as TypeRegistry from './type' diff --git a/src/type/registry/type.ts b/src/type/registry/type.ts index 955357a5..8b79c09e 100644 --- a/src/type/registry/type.ts +++ b/src/type/registry/type.ts @@ -25,32 +25,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------*/ +import { BaseRegistry } from './base' export type TypeRegistryValidationFunction = (schema: TSchema, value: unknown) => boolean -/** A registry for user defined types */ - -const map = new Map>() -/** Returns the entries in this registry */ -export function Entries() { - return new Map(map) -} -/** Clears all user defined types */ -export function Clear() { - return map.clear() -} -/** Deletes a registered type */ -export function Delete(kind: string) { - return map.delete(kind) -} -/** Returns true if this registry contains this kind */ -export function Has(kind: string) { - return map.has(kind) -} -/** Sets a validation function for a user defined type */ -export function Set(kind: string, func: TypeRegistryValidationFunction) { - map.set(kind, func) -} -/** Gets a custom validation function for a user defined type */ -export function Get(kind: string) { - return map.get(kind) -} + +const TypeRegistry = new BaseRegistry>() + +export const Entries = TypeRegistry.Entries +export const Clear = TypeRegistry.Clear +export const Delete = TypeRegistry.Delete +export const Has = TypeRegistry.Has +export const Set = TypeRegistry.Set +export const Get = TypeRegistry.Get \ No newline at end of file diff --git a/src/value/convert/convert.ts b/src/value/convert/convert.ts index 752ba37a..d84b7ffe 100644 --- a/src/value/convert/convert.ts +++ b/src/value/convert/convert.ts @@ -31,6 +31,7 @@ import { Check } from '../check/index' import { Deref, Pushref } from '../deref/index' import { Kind } from '../../type/symbols/index' +import { ConvertRegistry } from '../../type/registry/index' import type { TSchema } from '../../type/schema/index' import type { TArray } from '../../type/array/index' import type { TBigInt } from '../../type/bigint/index' @@ -165,146 +166,153 @@ function Default(value: unknown): unknown { // ------------------------------------------------------------------ // Convert // ------------------------------------------------------------------ -function FromArray(schema: TArray, references: TSchema[], value: any): any { +function FromArray(schema: TArray, references: TSchema[], value: any, data?: Map): any { const elements = IsArray(value) ? value : [value] - return elements.map((element) => Visit(schema.items, references, element)) + return elements.map((element) => Visit(schema.items, references, element, data)) } -function FromBigInt(schema: TBigInt, references: TSchema[], value: any): unknown { +function FromBigInt(schema: TBigInt, references: TSchema[], value: any, data?: Map): unknown { return TryConvertBigInt(value) } -function FromBoolean(schema: TBoolean, references: TSchema[], value: any): unknown { +function FromBoolean(schema: TBoolean, references: TSchema[], value: any, data?: Map): unknown { return TryConvertBoolean(value) } -function FromDate(schema: TDate, references: TSchema[], value: any): unknown { +function FromDate(schema: TDate, references: TSchema[], value: any, data?: Map): unknown { return TryConvertDate(value) } -function FromImport(schema: TImport, references: TSchema[], value: unknown): unknown { +function FromImport(schema: TImport, references: TSchema[], value: unknown, data?: Map): unknown { const definitions = globalThis.Object.values(schema.$defs) as TSchema[] const target = schema.$defs[schema.$ref] as TSchema - return Visit(target, [...references, ...definitions], value) + return Visit(target, [...references, ...definitions], value, data) } -function FromInteger(schema: TInteger, references: TSchema[], value: any): unknown { +function FromInteger(schema: TInteger, references: TSchema[], value: any, data?: Map): unknown { return TryConvertInteger(value) } -function FromIntersect(schema: TIntersect, references: TSchema[], value: any): unknown { - return schema.allOf.reduce((value, schema) => Visit(schema, references, value), value) +function FromIntersect(schema: TIntersect, references: TSchema[], value: any, data?: Map): unknown { + return schema.allOf.reduce((value, schema) => Visit(schema, references, value, data), value) } -function FromLiteral(schema: TLiteral, references: TSchema[], value: any): unknown { +function FromLiteral(schema: TLiteral, references: TSchema[], value: any, data?: Map): unknown { return TryConvertLiteral(schema, value) } -function FromNull(schema: TNull, references: TSchema[], value: any): unknown { +function FromNull(schema: TNull, references: TSchema[], value: any, data?: Map): unknown { return TryConvertNull(value) } -function FromNumber(schema: TNumber, references: TSchema[], value: any): unknown { +function FromNumber(schema: TNumber, references: TSchema[], value: any, data?: Map): unknown { return TryConvertNumber(value) } // prettier-ignore -function FromObject(schema: TObject, references: TSchema[], value: any): unknown { +function FromObject(schema: TObject, references: TSchema[], value: any, data?: Map): unknown { if(!IsObject(value)) return value for(const propertyKey of Object.getOwnPropertyNames(schema.properties)) { if(!HasPropertyKey(value, propertyKey)) continue - value[propertyKey] = Visit(schema.properties[propertyKey], references, value[propertyKey]) + value[propertyKey] = Visit(schema.properties[propertyKey], references, value[propertyKey], data) } return value } -function FromRecord(schema: TRecord, references: TSchema[], value: any): unknown { +function FromRecord(schema: TRecord, references: TSchema[], value: any, data?: Map): unknown { const isConvertable = IsObject(value) if (!isConvertable) return value const propertyKey = Object.getOwnPropertyNames(schema.patternProperties)[0] const property = schema.patternProperties[propertyKey] for (const [propKey, propValue] of Object.entries(value)) { - value[propKey] = Visit(property, references, propValue) + value[propKey] = Visit(property, references, propValue, data) } return value } -function FromRef(schema: TRef, references: TSchema[], value: any): unknown { - return Visit(Deref(schema, references), references, value) +function FromRef(schema: TRef, references: TSchema[], value: any, data?: Map): unknown { + return Visit(Deref(schema, references), references, value, data) } -function FromString(schema: TString, references: TSchema[], value: any): unknown { +function FromString(schema: TString, references: TSchema[], value: any, data?: Map): unknown { return TryConvertString(value) } -function FromSymbol(schema: TSymbol, references: TSchema[], value: any): unknown { +function FromSymbol(schema: TSymbol, references: TSchema[], value: any, data?: Map): unknown { return IsString(value) || IsNumber(value) ? Symbol(value) : value } -function FromThis(schema: TThis, references: TSchema[], value: any): unknown { - return Visit(Deref(schema, references), references, value) +function FromThis(schema: TThis, references: TSchema[], value: any, data?: Map): unknown { + return Visit(Deref(schema, references), references, value, data) } // prettier-ignore -function FromTuple(schema: TTuple, references: TSchema[], value: any): unknown { +function FromTuple(schema: TTuple, references: TSchema[], value: any, data?: Map): unknown { const isConvertable = IsArray(value) && !IsUndefined(schema.items) if(!isConvertable) return value return value.map((value, index) => { return (index < schema.items!.length) - ? Visit(schema.items![index], references, value) + ? Visit(schema.items![index], references, value, data) : value }) } -function FromUndefined(schema: TUndefined, references: TSchema[], value: any): unknown { +function FromUndefined(schema: TUndefined, references: TSchema[], value: any, data?: Map): unknown { return TryConvertUndefined(value) } -function FromUnion(schema: TUnion, references: TSchema[], value: any): unknown { +function FromUnion(schema: TUnion, references: TSchema[], value: any, data?: Map): unknown { for (const subschema of schema.anyOf) { - const converted = Visit(subschema, references, Clone(value)) + const converted = Visit(subschema, references, Clone(value), data) if (!Check(subschema, references, converted)) continue return converted } return value } -function Visit(schema: TSchema, references: TSchema[], value: any): unknown { +function FromKind(schema: TSchema, references: TSchema[], value: unknown, data?: Map): unknown { + if (!ConvertRegistry.Has(schema[Kind])) return Default(value) + const func = ConvertRegistry.Get(schema[Kind])! + return func(schema, value, data && data.get(schema[Kind])) +} +function Visit(schema: TSchema, references: TSchema[], value: any, data?: Map): unknown { const references_ = Pushref(schema, references) const schema_ = schema as any switch (schema[Kind]) { case 'Array': - return FromArray(schema_, references_, value) + return FromArray(schema_, references_, value, data) case 'BigInt': - return FromBigInt(schema_, references_, value) + return FromBigInt(schema_, references_, value, data) case 'Boolean': - return FromBoolean(schema_, references_, value) + return FromBoolean(schema_, references_, value, data) case 'Date': - return FromDate(schema_, references_, value) + return FromDate(schema_, references_, value, data) case 'Import': - return FromImport(schema_, references_, value) + return FromImport(schema_, references_, value, data) case 'Integer': - return FromInteger(schema_, references_, value) + return FromInteger(schema_, references_, value, data) case 'Intersect': - return FromIntersect(schema_, references_, value) + return FromIntersect(schema_, references_, value, data) case 'Literal': - return FromLiteral(schema_, references_, value) + return FromLiteral(schema_, references_, value, data) case 'Null': - return FromNull(schema_, references_, value) + return FromNull(schema_, references_, value, data) case 'Number': - return FromNumber(schema_, references_, value) + return FromNumber(schema_, references_, value, data) case 'Object': - return FromObject(schema_, references_, value) + return FromObject(schema_, references_, value, data) case 'Record': - return FromRecord(schema_, references_, value) + return FromRecord(schema_, references_, value, data) case 'Ref': - return FromRef(schema_, references_, value) + return FromRef(schema_, references_, value, data) case 'String': - return FromString(schema_, references_, value) + return FromString(schema_, references_, value, data) case 'Symbol': - return FromSymbol(schema_, references_, value) + return FromSymbol(schema_, references_, value, data) case 'This': - return FromThis(schema_, references_, value) + return FromThis(schema_, references_, value, data) case 'Tuple': - return FromTuple(schema_, references_, value) + return FromTuple(schema_, references_, value, data) case 'Undefined': - return FromUndefined(schema_, references_, value) + return FromUndefined(schema_, references_, value, data) case 'Union': - return FromUnion(schema_, references_, value) + return FromUnion(schema_, references_, value, data) default: - return Default(value) + return FromKind(schema, references_, value, data) } } // ------------------------------------------------------------------ // Convert // ------------------------------------------------------------------ /** `[Mutable]` Converts any type mismatched values to their target type if a reasonable conversion is possible. */ +export function Convert(schema: TSchema, references: TSchema[], value: unknown, data?: Map): unknown +/** `[Mutable]` Converts any type mismatched values to their target type if a reasonable conversion is possible. */ export function Convert(schema: TSchema, references: TSchema[], value: unknown): unknown /** `[Mutable]` Converts any type mismatched values to their target type if a reasonable conversion is possible. */ export function Convert(schema: TSchema, value: unknown): unknown /** `[Mutable]` Converts any type mismatched values to their target type if a reasonable conversion is possible. */ // prettier-ignore export function Convert(...args: any[]) { - return args.length === 3 ? Visit(args[0], args[1], args[2]) : Visit(args[0], [], args[1]) + return args.length >= 3 ? Visit(args[0], args[1], args[2], args[3]) : Visit(args[0], [], args[1]) } diff --git a/test/runtime/value/convert/custom.ts b/test/runtime/value/convert/custom.ts new file mode 100644 index 00000000..81ea292b --- /dev/null +++ b/test/runtime/value/convert/custom.ts @@ -0,0 +1,41 @@ +import { Value } from '@sinclair/typebox/value' +import { Kind, TSchema, ConvertRegistry, Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Custom', () => { + // --------------------------------------------------------- + // Fixtures + // --------------------------------------------------------- + afterEach(() => ConvertRegistry.Clear()) + // --------------------------------------------------------- + // Test + // --------------------------------------------------------- + it('Should convert value with a function', () => { + const T = { [Kind]: 'Custom' } as TSchema + ConvertRegistry.Set('Custom', (_, t) => !t) + const R = Value.Convert(T, true) + Assert.IsEqual(R, false) + }) + it('Should convert value using params', () => { + const T = { [Kind]: 'Custom', params: ['a'] } as TSchema + ConvertRegistry.Set('Custom', (s, t) => `${s.params[0]}_${t}`) + const R = Value.Convert(T, "test") + Assert.IsEqual(R, "a_test") + }) + + it('Should convert value using injected data', () => { + const T = { [Kind]: 'Custom' } as TSchema + ConvertRegistry.Set('Custom', (_s, t, v) => `${v}_${t}`) + const R = Value.Convert(T, [], "test", new Map([['Custom', 'b']])) + Assert.IsEqual(R, "b_test") + }) + + it('Should convert nested value using injected data', () => { + const T = Type.Object({ + t: { [Kind]: 'Custom' } as TSchema + }) + ConvertRegistry.Set('Custom', (_s, t, v) => `${v}_${t}`) + const R = Value.Convert(T, [], { t: "test" }, new Map([['Custom', 'b']])) + Assert.IsEqual(R, { t: "b_test" }) + }) +}) diff --git a/test/runtime/value/convert/index.ts b/test/runtime/value/convert/index.ts index fe5f47fd..b7ebc726 100644 --- a/test/runtime/value/convert/index.ts +++ b/test/runtime/value/convert/index.ts @@ -5,6 +5,7 @@ import './bigint' import './boolean' import './composite' import './constructor' +import './custom' import './kind' import './date' import './enum'