diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 357f598db..9796bd41b 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -26,7 +26,7 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -import { EncodeTransform, DecodeTransform, HasTransform, TransformDecodeCheckError, TransformEncodeCheckError } from '../value/transform/transform' +import { Encode as TransformEncode, Decode as TransformDecode, Has as HasTransform, TransformDecodeCheckError, TransformEncodeCheckError } from '../value/transform/index' import { IsArray, IsString, IsNumber, IsBigInt } from '../value/guard/guard' import { Errors, ValueErrorIterator } from '../errors/errors' import { TypeSystemPolicy } from '../system/index' @@ -44,7 +44,7 @@ export type CheckFunction = (value: unknown) => boolean export class TypeCheck { private readonly hasTransform: boolean constructor(private readonly schema: T, private readonly references: Types.TSchema[], private readonly checkFunc: CheckFunction, private readonly code: string) { - this.hasTransform = HasTransform.Has(schema, references) + this.hasTransform = HasTransform(schema, references) } /** Returns the generated assertion code used to validate this type. */ public Code(): string { @@ -61,11 +61,11 @@ export class TypeCheck { /** Decodes a value or throws if error */ public Decode(value: unknown): Types.StaticDecode { if (!this.checkFunc(value)) throw new TransformDecodeCheckError(this.schema, value, this.Errors(value).First()!) - return this.hasTransform ? DecodeTransform.Decode(this.schema, this.references, value) : value + return this.hasTransform ? TransformDecode(this.schema, this.references, value) : value } /** Encodes a value or throws if error */ public Encode(value: unknown): Types.StaticEncode { - const encoded = this.hasTransform ? EncodeTransform.Encode(this.schema, this.references, value) : value + const encoded = this.hasTransform ? TransformEncode(this.schema, this.references, value) : value if (!this.checkFunc(encoded)) throw new TransformEncodeCheckError(this.schema, value, this.Errors(value).First()!) return encoded } diff --git a/src/value/transform/decode.ts b/src/value/transform/decode.ts new file mode 100644 index 000000000..7ffe3d899 --- /dev/null +++ b/src/value/transform/decode.ts @@ -0,0 +1,204 @@ +/*-------------------------------------------------------------------------- + +@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. + +---------------------------------------------------------------------------*/ + +import { TTransform as IsTransformType, TSchema as IsSchemaType } from '../../type/guard/type' +import { Kind, TransformKind } from '../../type/symbols/index' +import { IsPlainObject, IsArray, IsValueType } from '../guard/index' +import { ValueError } from '../../errors/index' +import { KeyOfStringResolve } from '../../type/keyof/index' +import { IndexedTypeResolve } from '../../type/indexed/index' +import { Deref } from '../deref/index' +import { Check } from '../check/index' + +import type { TSchema } from '../../type/schema/index' +import type { TArray } from '../../type/array/index' +import type { TIntersect } from '../../type/intersect/index' +import type { TNot } from '../../type/not/index' +import type { TObject } from '../../type/object/index' +import type { TRecord } from '../../type/record/index' +import type { TRef } from '../../type/ref/index' +import type { TThis } from '../../type/recursive/index' +import type { TTuple } from '../../type/tuple/index' +import type { TUnion } from '../../type/union/index' + +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +// thrown externally +export class TransformDecodeCheckError extends Error { + constructor(public readonly schema: TSchema, public readonly value: unknown, public readonly error: ValueError) { + super(`Unable to decode due to invalid value`) + } +} +export class TransformDecodeError extends Error { + constructor(public readonly schema: TSchema, public readonly value: unknown, error: any) { + super(`${error instanceof Error ? error.message : 'Unknown error'}`) + } +} +// ------------------------------------------------------------------ +// Decode +// ------------------------------------------------------------------ +// prettier-ignore +function Default(schema: TSchema, value: any) { + try { + return IsTransformType(schema) ? schema[TransformKind].Decode(value) : value + } catch (error) { + throw new TransformDecodeError(schema, value, error) + } +} +// prettier-ignore +function TArray(schema: TArray, references: TSchema[], value: any): any { + return (IsArray(value)) + ? Default(schema, value.map((value: any) => Visit(schema.items, references, value))) + : Default(schema, value) +} +// prettier-ignore +function TIntersect(schema: TIntersect, references: TSchema[], value: any) { + if (!IsPlainObject(value) || IsValueType(value)) return Default(schema, value) + const knownKeys = KeyOfStringResolve(schema) as string[] + const knownProperties = knownKeys.reduce((value, key) => { + return (key in value) + ? { ...value, [key]: Visit(IndexedTypeResolve(schema, [key]), references, value[key]) } + : value + }, value) + if (!IsTransformType(schema.unevaluatedProperties)) { + return Default(schema, knownProperties) + } + const unknownKeys = Object.getOwnPropertyNames(knownProperties) + const unevaluatedProperties = schema.unevaluatedProperties as TSchema + const unknownProperties = unknownKeys.reduce((value, key) => { + return !knownKeys.includes(key) + ? { ...value, [key]: Default(unevaluatedProperties, value[key]) } + : value + }, knownProperties) + return Default(schema, unknownProperties) +} +function TNot(schema: TNot, references: TSchema[], value: any) { + return Default(schema, Visit(schema.not, references, value)) +} +// prettier-ignore +function TObject(schema: TObject, references: TSchema[], value: any) { + if (!IsPlainObject(value)) return Default(schema, value) + const knownKeys = KeyOfStringResolve(schema) + const knownProperties = knownKeys.reduce((value, key) => { + return (key in value) + ? { ...value, [key]: Visit(schema.properties[key], references, value[key]) } + : value + }, value) + if (!IsSchemaType(schema.additionalProperties)) { + return Default(schema, knownProperties) + } + const unknownKeys = Object.getOwnPropertyNames(knownProperties) + const additionalProperties = schema.additionalProperties as TSchema + const unknownProperties = unknownKeys.reduce((value, key) => { + return !knownKeys.includes(key) + ? { ...value, [key]: Default(additionalProperties, value[key]) } + : value + }, knownProperties) + return Default(schema, unknownProperties) +} +// prettier-ignore +function TRecord(schema: TRecord, references: TSchema[], value: any) { + if (!IsPlainObject(value)) return Default(schema, value) + const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0] + const knownKeys = new RegExp(pattern) + const knownProperties = Object.getOwnPropertyNames(value).reduce((value, key) => { + return knownKeys.test(key) + ? { ...value, [key]: Visit(schema.patternProperties[pattern], references, value[key]) } + : value + }, value) + if (!IsSchemaType(schema.additionalProperties)) { + return Default(schema, knownProperties) + } + const unknownKeys = Object.getOwnPropertyNames(knownProperties) + const additionalProperties = schema.additionalProperties as TSchema + const unknownProperties = unknownKeys.reduce((value, key) => { + return !knownKeys.test(key) + ? { ...value, [key]: Default(additionalProperties, value[key]) } + : value + }, knownProperties) + return Default(schema, unknownProperties) +} +// prettier-ignore +function TRef(schema: TRef, references: TSchema[], value: any) { + const target = Deref(schema, references) + return Default(schema, Visit(target, references, value)) +} +// prettier-ignore +function TThis(schema: TThis, references: TSchema[], value: any) { + const target = Deref(schema, references) + return Default(schema, Visit(target, references, value)) +} +// prettier-ignore +function TTuple(schema: TTuple, references: TSchema[], value: any) { + return (IsArray(value) && IsArray(schema.items)) + ? Default(schema, schema.items.map((schema, index) => Visit(schema, references, value[index]))) + : Default(schema, value) +} +// prettier-ignore +function TUnion(schema: TUnion, references: TSchema[], value: any) { + const defaulted = Default(schema, value) + for (const subschema of schema.anyOf) { + if (!Check(subschema, references, defaulted)) continue + return Visit(subschema, references, defaulted) + } + return defaulted +} +// prettier-ignore +function Visit(schema: TSchema, references: TSchema[], value: any): any { + const references_ = typeof schema.$id === 'string' ? [...references, schema] : references + const schema_ = schema as any + switch (schema[Kind]) { + case 'Array': + return TArray(schema_, references_, value) + case 'Intersect': + return TIntersect(schema_, references_, value) + case 'Not': + return TNot(schema_, references_, value) + case 'Object': + return TObject(schema_, references_, value) + case 'Record': + return TRecord(schema_, references_, value) + case 'Ref': + return TRef(schema_, references_, value) + case 'Symbol': + return Default(schema_, value) + case 'This': + return TThis(schema_, references_, value) + case 'Tuple': + return TTuple(schema_, references_, value) + case 'Union': + return TUnion(schema_, references_, value) + default: + return Default(schema_, value) + } +} +// prettier-ignore +export function Decode(schema: TSchema, references: TSchema[], value: unknown): unknown { + return Visit(schema, references, value) +} diff --git a/src/value/transform/encode.ts b/src/value/transform/encode.ts new file mode 100644 index 000000000..87e5c1144 --- /dev/null +++ b/src/value/transform/encode.ts @@ -0,0 +1,211 @@ +/*-------------------------------------------------------------------------- + +@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. + +---------------------------------------------------------------------------*/ + +import { TTransform as IsTransformType, TSchema as IsSchemaType } from '../../type/guard/type' +import { Kind, TransformKind } from '../../type/symbols/index' +import { IsPlainObject, IsArray, IsValueType } from '../guard/index' +import { ValueError } from '../../errors/index' +import { KeyOfStringResolve } from '../../type/keyof/index' +import { IndexedTypeResolve } from '../../type/indexed/index' +import { Deref } from '../deref/index' +import { Check } from '../check/index' + +import type { TSchema } from '../../type/schema/index' +import type { TArray } from '../../type/array/index' +import type { TIntersect } from '../../type/intersect/index' +import type { TNot } from '../../type/not/index' +import type { TObject } from '../../type/object/index' +import type { TRecord } from '../../type/record/index' +import type { TRef } from '../../type/ref/index' +import type { TThis } from '../../type/recursive/index' +import type { TTuple } from '../../type/tuple/index' +import type { TUnion } from '../../type/union/index' + +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +export class TransformEncodeCheckError extends Error { + constructor(public readonly schema: TSchema, public readonly value: unknown, public readonly error: ValueError) { + super(`Unable to encode due to invalid value`) + } +} +export class TransformEncodeError extends Error { + constructor(public readonly schema: TSchema, public readonly value: unknown, error: any) { + super(`${error instanceof Error ? error.message : 'Unknown error'}`) + } +} +// ------------------------------------------------------------------ +// Encode +// ------------------------------------------------------------------ +// prettier-ignore +function Default(schema: TSchema, value: any) { + try { + return IsTransformType(schema) ? schema[TransformKind].Encode(value) : value + } catch (error) { + throw new TransformEncodeError(schema, value, error) + } +} +// prettier-ignore +function TArray(schema: TArray, references: TSchema[], value: any): any { + const defaulted = Default(schema, value) + return IsArray(defaulted) + ? defaulted.map((value: any) => Visit(schema.items, references, value)) + : defaulted +} +// prettier-ignore +function TIntersect(schema: TIntersect, references: TSchema[], value: any) { + const defaulted = Default(schema, value) + if (!IsPlainObject(value) || IsValueType(value)) return defaulted + const knownKeys = KeyOfStringResolve(schema) as string[] + const knownProperties = knownKeys.reduce((value, key) => { + return key in defaulted + ? { ...value, [key]: Visit(IndexedTypeResolve(schema, [key]), references, value[key]) } + : value + }, defaulted) + if (!IsTransformType(schema.unevaluatedProperties)) { + return Default(schema, knownProperties) + } + const unknownKeys = Object.getOwnPropertyNames(knownProperties) + const unevaluatedProperties = schema.unevaluatedProperties as TSchema + return unknownKeys.reduce((value, key) => { + return !knownKeys.includes(key) + ? { ...value, [key]: Default(unevaluatedProperties, value[key]) } + : value + }, knownProperties) +} +// prettier-ignore +function TNot(schema: TNot, references: TSchema[], value: any) { + return Default(schema.not, Default(schema, value)) +} +// prettier-ignore +function TObject(schema: TObject, references: TSchema[], value: any) { + const defaulted = Default(schema, value) + if (!IsPlainObject(value)) return defaulted + const knownKeys = KeyOfStringResolve(schema) as string[] + const knownProperties = knownKeys.reduce((value, key) => { + return key in value + ? { ...value, [key]: Visit(schema.properties[key], references, value[key]) } + : value + }, defaulted) + if (!IsSchemaType(schema.additionalProperties)) { + return knownProperties + } + const unknownKeys = Object.getOwnPropertyNames(knownProperties) + const additionalProperties = schema.additionalProperties as TSchema + return unknownKeys.reduce((value, key) => { + return !knownKeys.includes(key) + ? { ...value, [key]: Default(additionalProperties, value[key]) } + : value + }, knownProperties) +} +// prettier-ignore +function TRecord(schema: TRecord, references: TSchema[], value: any) { + const defaulted = Default(schema, value) as Record + if (!IsPlainObject(value)) return defaulted + const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0] + const knownKeys = new RegExp(pattern) + const knownProperties = Object.getOwnPropertyNames(value).reduce((value, key) => { + return knownKeys.test(key) + ? { ...value, [key]: Visit(schema.patternProperties[pattern], references, value[key]) } + : value + }, defaulted) + if (!IsSchemaType(schema.additionalProperties)) { + return Default(schema, knownProperties) + } + const unknownKeys = Object.getOwnPropertyNames(knownProperties) + const additionalProperties = schema.additionalProperties as TSchema + return unknownKeys.reduce((value, key) => { + return !knownKeys.test(key) + ? { ...value, [key]: Default(additionalProperties, value[key]) } + : value + }, knownProperties) +} +// prettier-ignore +function TRef(schema: TRef, references: TSchema[], value: any) { + const target = Deref(schema, references) + const resolved = Visit(target, references, value) + return Default(schema, resolved) +} +// prettier-ignore +function TThis(schema: TThis, references: TSchema[], value: any) { + const target = Deref(schema, references) + const resolved = Visit(target, references, value) + return Default(schema, resolved) +} +// prettier-ignore +function TTuple(schema: TTuple, references: TSchema[], value: any) { + const value1 = Default(schema, value) + return IsArray(schema.items) ? schema.items.map((schema, index) => Visit(schema, references, value1[index])) : [] +} +// prettier-ignore +function TUnion(schema: TUnion, references: TSchema[], value: any) { + // test value against union variants + for (const subschema of schema.anyOf) { + if (!Check(subschema, references, value)) continue + const value1 = Visit(subschema, references, value) + return Default(schema, value1) + } + // test transformed value against union variants + for (const subschema of schema.anyOf) { + const value1 = Visit(subschema, references, value) + if (!Check(schema, references, value1)) continue + return Default(schema, value1) + } + return Default(schema, value) +} +// prettier-ignore +function Visit(schema: TSchema, references: TSchema[], value: any): any { + const references_ = typeof schema.$id === 'string' ? [...references, schema] : references + const schema_ = schema as any + switch (schema[Kind]) { + case 'Array': + return TArray(schema_, references_, value) + case 'Intersect': + return TIntersect(schema_, references_, value) + case 'Not': + return TNot(schema_, references_, value) + case 'Object': + return TObject(schema_, references_, value) + case 'Record': + return TRecord(schema_, references_, value) + case 'Ref': + return TRef(schema_, references_, value) + case 'This': + return TThis(schema_, references_, value) + case 'Tuple': + return TTuple(schema_, references_, value) + case 'Union': + return TUnion(schema_, references_, value) + default: + return Default(schema_, value) + } +} +// prettier-ignore +export function Encode(schema: TSchema, references: TSchema[], value: unknown): unknown { + return Visit(schema, references, value) +} diff --git a/src/value/transform/has.ts b/src/value/transform/has.ts new file mode 100644 index 000000000..21e09eb75 --- /dev/null +++ b/src/value/transform/has.ts @@ -0,0 +1,160 @@ +/*-------------------------------------------------------------------------- + +@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. + +---------------------------------------------------------------------------*/ + +import { TTransform as IsTransformType, TSchema as IsSchemaType } from '../../type/guard/type' +import { Kind } from '../../type/symbols/index' +import { IsString, IsUndefined } from '../guard/index' +import { Deref } from '../deref/index' + +import type { TSchema } from '../../type/schema/index' +import type { TArray } from '../../type/array/index' +import type { TAsyncIterator } from '../../type/async-iterator/index' +import type { TConstructor } from '../../type/constructor/index' +import type { TFunction } from '../../type/function/index' +import type { TIntersect } from '../../type/intersect/index' +import type { TIterator } from '../../type/iterator/index' +import type { TNot } from '../../type/not/index' +import type { TObject } from '../../type/object/index' +import type { TPromise } from '../../type/promise/index' +import type { TRecord } from '../../type/record/index' +import type { TRef } from '../../type/ref/index' +import type { TThis } from '../../type/recursive/index' +import type { TTuple } from '../../type/tuple/index' +import type { TUnion } from '../../type/union/index' + +// prettier-ignore +function TArray(schema: TArray, references: TSchema[]): boolean { + return IsTransformType(schema) || Visit(schema.items, references) +} +// prettier-ignore +function TAsyncIterator(schema: TAsyncIterator, references: TSchema[]): boolean { + return IsTransformType(schema) || Visit(schema.items, references) +} +// prettier-ignore +function TConstructor(schema: TConstructor, references: TSchema[]) { + return IsTransformType(schema) || Visit(schema.returns, references) || schema.parameters.some((schema) => Visit(schema, references)) +} +// prettier-ignore +function TFunction(schema: TFunction, references: TSchema[]) { + return IsTransformType(schema) || Visit(schema.returns, references) || schema.parameters.some((schema) => Visit(schema, references)) +} +// prettier-ignore +function TIntersect(schema: TIntersect, references: TSchema[]) { + return IsTransformType(schema) || IsTransformType(schema.unevaluatedProperties) || schema.allOf.some((schema) => Visit(schema, references)) +} +// prettier-ignore +function TIterator(schema: TIterator, references: TSchema[]) { + return IsTransformType(schema) || Visit(schema.items, references) +} +// prettier-ignore +function TNot(schema: TNot, references: TSchema[]) { + return IsTransformType(schema) || Visit(schema.not, references) +} +// prettier-ignore +function TObject(schema: TObject, references: TSchema[]) { + return ( + IsTransformType(schema) || + Object.values(schema.properties).some((schema) => Visit(schema, references)) || + ( + IsSchemaType(schema.additionalProperties) && Visit(schema.additionalProperties, references) + ) + ) +} +// prettier-ignore +function TPromise(schema: TPromise, references: TSchema[]) { + return IsTransformType(schema) || Visit(schema.item, references) +} +// prettier-ignore +function TRecord(schema: TRecord, references: TSchema[]) { + const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0] + const property = schema.patternProperties[pattern] + return IsTransformType(schema) || Visit(property, references) || (IsSchemaType(schema.additionalProperties) && IsTransformType(schema.additionalProperties)) +} +// prettier-ignore +function TRef(schema: TRef, references: TSchema[]) { + if (IsTransformType(schema)) return true + return Visit(Deref(schema, references), references) +} +// prettier-ignore +function TThis(schema: TThis, references: TSchema[]) { + if (IsTransformType(schema)) return true + return Visit(Deref(schema, references), references) +} +// prettier-ignore +function TTuple(schema: TTuple, references: TSchema[]) { + return IsTransformType(schema) || (!IsUndefined(schema.items) && schema.items.some((schema) => Visit(schema, references))) +} +// prettier-ignore +function TUnion(schema: TUnion, references: TSchema[]) { + return IsTransformType(schema) || schema.anyOf.some((schema) => Visit(schema, references)) +} +// prettier-ignore +function Visit(schema: TSchema, references: TSchema[]): boolean { + const references_ = IsString(schema.$id) ? [...references, schema] : references + const schema_ = schema as any + if (schema.$id && visited.has(schema.$id)) return false + if (schema.$id) visited.add(schema.$id) + switch (schema[Kind]) { + case 'Array': + return TArray(schema_, references_) + case 'AsyncIterator': + return TAsyncIterator(schema_, references_) + case 'Constructor': + return TConstructor(schema_, references_) + case 'Function': + return TFunction(schema_, references_) + case 'Intersect': + return TIntersect(schema_, references_) + case 'Iterator': + return TIterator(schema_, references_) + case 'Not': + return TNot(schema_, references_) + case 'Object': + return TObject(schema_, references_) + case 'Promise': + return TPromise(schema_, references_) + case 'Record': + return TRecord(schema_, references_) + case 'Ref': + return TRef(schema_, references_) + case 'This': + return TThis(schema_, references_) + case 'Tuple': + return TTuple(schema_, references_) + case 'Union': + return TUnion(schema_, references_) + default: + return IsTransformType(schema) + } +} +const visited = new Set() +/** Returns true if this schema contains a transform codec */ +export function Has(schema: TSchema, references: TSchema[]): boolean { + visited.clear() + return Visit(schema, references) +} diff --git a/src/value/transform/index.ts b/src/value/transform/index.ts index 69177dec9..f821f6d29 100644 --- a/src/value/transform/index.ts +++ b/src/value/transform/index.ts @@ -26,4 +26,6 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -export * from './transform' +export * from './decode' +export * from './encode' +export * from './has' diff --git a/src/value/transform/transform.ts b/src/value/transform/transform.ts deleted file mode 100644 index d2e9e6d68..000000000 --- a/src/value/transform/transform.ts +++ /dev/null @@ -1,439 +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. - ----------------------------------------------------------------------------*/ - -import { IsString, IsPlainObject, IsArray, IsValueType, IsUndefined } from '../guard/guard' -import { ValueError } from '../../errors/errors' -import { Deref } from '../deref/deref' -import { Check } from '../check/check' -import * as Types from '../../type/index' -Types.TypeGuard -// ------------------------------------------------------------------------- -// Errors -// ------------------------------------------------------------------------- -export class TransformDecodeCheckError extends Error { - constructor(public readonly schema: Types.TSchema, public readonly value: unknown, public readonly error: ValueError) { - super(`Unable to decode due to invalid value`) - } -} -export class TransformEncodeCheckError extends Error { - constructor(public readonly schema: Types.TSchema, public readonly value: unknown, public readonly error: ValueError) { - super(`Unable to encode due to invalid value`) - } -} -export class TransformDecodeError extends Error { - constructor(public readonly schema: Types.TSchema, public readonly value: unknown, error: any) { - super(`${error instanceof Error ? error.message : 'Unknown error'}`) - } -} -export class TransformEncodeError extends Error { - constructor(public readonly schema: Types.TSchema, public readonly value: unknown, error: any) { - super(`${error instanceof Error ? error.message : 'Unknown error'}`) - } -} -// ------------------------------------------------------------------ -// HasTransform -// ------------------------------------------------------------------ -/** Recursively checks a schema for transform codecs */ -export namespace HasTransform { - function TArray(schema: Types.TArray, references: Types.TSchema[]): boolean { - return Types.TypeGuard.TTransform(schema) || Visit(schema.items, references) - } - function TAsyncIterator(schema: Types.TAsyncIterator, references: Types.TSchema[]): boolean { - return Types.TypeGuard.TTransform(schema) || Visit(schema.items, references) - } - function TConstructor(schema: Types.TConstructor, references: Types.TSchema[]) { - return Types.TypeGuard.TTransform(schema) || Visit(schema.returns, references) || schema.parameters.some((schema) => Visit(schema, references)) - } - function TFunction(schema: Types.TFunction, references: Types.TSchema[]) { - return Types.TypeGuard.TTransform(schema) || Visit(schema.returns, references) || schema.parameters.some((schema) => Visit(schema, references)) - } - function TIntersect(schema: Types.TIntersect, references: Types.TSchema[]) { - return Types.TypeGuard.TTransform(schema) || Types.TypeGuard.TTransform(schema.unevaluatedProperties) || schema.allOf.some((schema) => Visit(schema, references)) - } - function TIterator(schema: Types.TIterator, references: Types.TSchema[]) { - return Types.TypeGuard.TTransform(schema) || Visit(schema.items, references) - } - function TNot(schema: Types.TNot, references: Types.TSchema[]) { - return Types.TypeGuard.TTransform(schema) || Visit(schema.not, references) - } - function TObject(schema: Types.TObject, references: Types.TSchema[]) { - // prettier-ignore - return (Types.TypeGuard.TTransform(schema) || Object.values(schema.properties).some((schema) => Visit(schema, references)) || Types.TypeGuard.TSchema(schema.additionalProperties) && Visit(schema.additionalProperties, references) - ) - } - function TPromise(schema: Types.TPromise, references: Types.TSchema[]) { - return Types.TypeGuard.TTransform(schema) || Visit(schema.item, references) - } - function TRecord(schema: Types.TRecord, references: Types.TSchema[]) { - const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0] - const property = schema.patternProperties[pattern] - return Types.TypeGuard.TTransform(schema) || Visit(property, references) || (Types.TypeGuard.TSchema(schema.additionalProperties) && Types.TypeGuard.TTransform(schema.additionalProperties)) - } - function TRef(schema: Types.TRef, references: Types.TSchema[]) { - if (Types.TypeGuard.TTransform(schema)) return true - return Visit(Deref(schema, references), references) - } - function TThis(schema: Types.TThis, references: Types.TSchema[]) { - if (Types.TypeGuard.TTransform(schema)) return true - return Visit(Deref(schema, references), references) - } - function TTuple(schema: Types.TTuple, references: Types.TSchema[]) { - return Types.TypeGuard.TTransform(schema) || (!IsUndefined(schema.items) && schema.items.some((schema) => Visit(schema, references))) - } - function TUnion(schema: Types.TUnion, references: Types.TSchema[]) { - return Types.TypeGuard.TTransform(schema) || schema.anyOf.some((schema) => Visit(schema, references)) - } - function Visit(schema: Types.TSchema, references: Types.TSchema[]): boolean { - const references_ = IsString(schema.$id) ? [...references, schema] : references - const schema_ = schema as any - if (schema.$id && visited.has(schema.$id)) return false - if (schema.$id) visited.add(schema.$id) - switch (schema[Types.Kind]) { - case 'Array': - return TArray(schema_, references_) - case 'AsyncIterator': - return TAsyncIterator(schema_, references_) - case 'Constructor': - return TConstructor(schema_, references_) - case 'Function': - return TFunction(schema_, references_) - case 'Intersect': - return TIntersect(schema_, references_) - case 'Iterator': - return TIterator(schema_, references_) - case 'Not': - return TNot(schema_, references_) - case 'Object': - return TObject(schema_, references_) - case 'Promise': - return TPromise(schema_, references_) - case 'Record': - return TRecord(schema_, references_) - case 'Ref': - return TRef(schema_, references_) - case 'This': - return TThis(schema_, references_) - case 'Tuple': - return TTuple(schema_, references_) - case 'Union': - return TUnion(schema_, references_) - default: - return Types.TypeGuard.TTransform(schema) - } - } - const visited = new Set() - /** Returns true if this schema contains a transform codec */ - export function Has(schema: Types.TSchema, references: Types.TSchema[]): boolean { - visited.clear() - return Visit(schema, references) - } -} -// ------------------------------------------------------------------ -// DecodeTransform -// ------------------------------------------------------------------ -/** Decodes a value using transform decoders if available. Does not ensure correct results. */ -export namespace DecodeTransform { - function Default(schema: Types.TSchema, value: any) { - try { - return Types.TypeGuard.TTransform(schema) ? schema[Types.TransformKind].Decode(value) : value - } catch (error) { - throw new TransformDecodeError(schema, value, error) - } - } - // prettier-ignore - function TArray(schema: Types.TArray, references: Types.TSchema[], value: any): any { - return (IsArray(value)) - ? Default(schema, value.map((value: any) => Visit(schema.items, references, value))) - : Default(schema, value) - } - // prettier-ignore - function TIntersect(schema: Types.TIntersect, references: Types.TSchema[], value: any) { - if (!IsPlainObject(value) || IsValueType(value)) return Default(schema, value) - const knownKeys = Types.KeyOfStringResolve(schema) as string[] - const knownProperties = knownKeys.reduce((value, key) => { - return (key in value) - ? { ...value, [key]: Visit(Types.IndexedTypeResolve(schema, [key]), references, value[key]) } - : value - }, value) - if (!Types.TypeGuard.TTransform(schema.unevaluatedProperties)) { - return Default(schema, knownProperties) - } - const unknownKeys = Object.getOwnPropertyNames(knownProperties) - const unevaluatedProperties = schema.unevaluatedProperties as Types.TSchema - const unknownProperties = unknownKeys.reduce((value, key) => { - return !knownKeys.includes(key) - ? { ...value, [key]: Default(unevaluatedProperties, value[key]) } - : value - }, knownProperties) - return Default(schema, unknownProperties) - } - function TNot(schema: Types.TNot, references: Types.TSchema[], value: any) { - return Default(schema, Visit(schema.not, references, value)) - } - // prettier-ignore - function TObject(schema: Types.TObject, references: Types.TSchema[], value: any) { - if (!IsPlainObject(value)) return Default(schema, value) - const knownKeys = Types.KeyOfStringResolve(schema) - const knownProperties = knownKeys.reduce((value, key) => { - return (key in value) - ? { ...value, [key]: Visit(schema.properties[key], references, value[key]) } - : value - }, value) - if (!Types.TypeGuard.TSchema(schema.additionalProperties)) { - return Default(schema, knownProperties) - } - const unknownKeys = Object.getOwnPropertyNames(knownProperties) - const additionalProperties = schema.additionalProperties as Types.TSchema - const unknownProperties = unknownKeys.reduce((value, key) => { - return !knownKeys.includes(key) - ? { ...value, [key]: Default(additionalProperties, value[key]) } - : value - }, knownProperties) - return Default(schema, unknownProperties) - } - // prettier-ignore - function TRecord(schema: Types.TRecord, references: Types.TSchema[], value: any) { - if (!IsPlainObject(value)) return Default(schema, value) - const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0] - const knownKeys = new RegExp(pattern) - const knownProperties = Object.getOwnPropertyNames(value).reduce((value, key) => { - return knownKeys.test(key) - ? { ...value, [key]: Visit(schema.patternProperties[pattern], references, value[key]) } - : value - }, value) - if (!Types.TypeGuard.TSchema(schema.additionalProperties)) { - return Default(schema, knownProperties) - } - const unknownKeys = Object.getOwnPropertyNames(knownProperties) - const additionalProperties = schema.additionalProperties as Types.TSchema - const unknownProperties = unknownKeys.reduce((value, key) => { - return !knownKeys.test(key) - ? { ...value, [key]: Default(additionalProperties, value[key]) } - : value - }, knownProperties) - return Default(schema, unknownProperties) - } - function TRef(schema: Types.TRef, references: Types.TSchema[], value: any) { - const target = Deref(schema, references) - return Default(schema, Visit(target, references, value)) - } - function TThis(schema: Types.TThis, references: Types.TSchema[], value: any) { - const target = Deref(schema, references) - return Default(schema, Visit(target, references, value)) - } - // prettier-ignore - function TTuple(schema: Types.TTuple, references: Types.TSchema[], value: any) { - return (IsArray(value) && IsArray(schema.items)) - ? Default(schema, schema.items.map((schema, index) => Visit(schema, references, value[index]))) - : Default(schema, value) - } - function TUnion(schema: Types.TUnion, references: Types.TSchema[], value: any) { - const defaulted = Default(schema, value) - for (const subschema of schema.anyOf) { - if (!Check(subschema, references, defaulted)) continue - return Visit(subschema, references, defaulted) - } - return defaulted - } - function Visit(schema: Types.TSchema, references: Types.TSchema[], value: any): any { - const references_ = typeof schema.$id === 'string' ? [...references, schema] : references - const schema_ = schema as any - switch (schema[Types.Kind]) { - case 'Array': - return TArray(schema_, references_, value) - case 'Intersect': - return TIntersect(schema_, references_, value) - case 'Not': - return TNot(schema_, references_, value) - case 'Object': - return TObject(schema_, references_, value) - case 'Record': - return TRecord(schema_, references_, value) - case 'Ref': - return TRef(schema_, references_, value) - case 'Symbol': - return Default(schema_, value) - case 'This': - return TThis(schema_, references_, value) - case 'Tuple': - return TTuple(schema_, references_, value) - case 'Union': - return TUnion(schema_, references_, value) - default: - return Default(schema_, value) - } - } - export function Decode(schema: Types.TSchema, references: Types.TSchema[], value: unknown): unknown { - return Visit(schema, references, value) - } -} -// ------------------------------------------------------------------ -// DecodeTransform -// ------------------------------------------------------------------ -/** Encodes a value using transform encoders if available. Does not ensure correct results. */ -export namespace EncodeTransform { - function Default(schema: Types.TSchema, value: any) { - try { - return Types.TypeGuard.TTransform(schema) ? schema[Types.TransformKind].Encode(value) : value - } catch (error) { - throw new TransformEncodeError(schema, value, error) - } - } - // prettier-ignore - function TArray(schema: Types.TArray, references: Types.TSchema[], value: any): any { - const defaulted = Default(schema, value) - return IsArray(defaulted) - ? defaulted.map((value: any) => Visit(schema.items, references, value)) - : defaulted - } - // prettier-ignore - function TIntersect(schema: Types.TIntersect, references: Types.TSchema[], value: any) { - const defaulted = Default(schema, value) - if (!IsPlainObject(value) || IsValueType(value)) return defaulted - const knownKeys = Types.KeyOfStringResolve(schema) as string[] - const knownProperties = knownKeys.reduce((value, key) => { - return key in defaulted - ? { ...value, [key]: Visit(Types.IndexedTypeResolve(schema, [key]), references, value[key]) } - : value - }, defaulted) - if (!Types.TypeGuard.TTransform(schema.unevaluatedProperties)) { - return Default(schema, knownProperties) - } - const unknownKeys = Object.getOwnPropertyNames(knownProperties) - const unevaluatedProperties = schema.unevaluatedProperties as Types.TSchema - return unknownKeys.reduce((value, key) => { - return !knownKeys.includes(key) - ? { ...value, [key]: Default(unevaluatedProperties, value[key]) } - : value - }, knownProperties) - } - function TNot(schema: Types.TNot, references: Types.TSchema[], value: any) { - return Default(schema.not, Default(schema, value)) - } - // prettier-ignore - function TObject(schema: Types.TObject, references: Types.TSchema[], value: any) { - const defaulted = Default(schema, value) - if (!IsPlainObject(value)) return defaulted - const knownKeys = Types.KeyOfStringResolve(schema) as string[] - const knownProperties = knownKeys.reduce((value, key) => { - return key in value - ? { ...value, [key]: Visit(schema.properties[key], references, value[key]) } - : value - }, defaulted) - if (!Types.TypeGuard.TSchema(schema.additionalProperties)) { - return knownProperties - } - const unknownKeys = Object.getOwnPropertyNames(knownProperties) - const additionalProperties = schema.additionalProperties as Types.TSchema - return unknownKeys.reduce((value, key) => { - return !knownKeys.includes(key) - ? { ...value, [key]: Default(additionalProperties, value[key]) } - : value - }, knownProperties) - } - // prettier-ignore - function TRecord(schema: Types.TRecord, references: Types.TSchema[], value: any) { - const defaulted = Default(schema, value) as Record - if (!IsPlainObject(value)) return defaulted - const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0] - const knownKeys = new RegExp(pattern) - const knownProperties = Object.getOwnPropertyNames(value).reduce((value, key) => { - return knownKeys.test(key) - ? { ...value, [key]: Visit(schema.patternProperties[pattern], references, value[key]) } - : value - }, defaulted) - if (!Types.TypeGuard.TSchema(schema.additionalProperties)) { - return Default(schema, knownProperties) - } - const unknownKeys = Object.getOwnPropertyNames(knownProperties) - const additionalProperties = schema.additionalProperties as Types.TSchema - return unknownKeys.reduce((value, key) => { - return !knownKeys.test(key) - ? { ...value, [key]: Default(additionalProperties, value[key]) } - : value - }, knownProperties) - } - function TRef(schema: Types.TRef, references: Types.TSchema[], value: any) { - const target = Deref(schema, references) - const resolved = Visit(target, references, value) - return Default(schema, resolved) - } - function TThis(schema: Types.TThis, references: Types.TSchema[], value: any) { - const target = Deref(schema, references) - const resolved = Visit(target, references, value) - return Default(schema, resolved) - } - function TTuple(schema: Types.TTuple, references: Types.TSchema[], value: any) { - const value1 = Default(schema, value) - return IsArray(schema.items) ? schema.items.map((schema, index) => Visit(schema, references, value1[index])) : [] - } - function TUnion(schema: Types.TUnion, references: Types.TSchema[], value: any) { - // test value against union variants - for (const subschema of schema.anyOf) { - if (!Check(subschema, references, value)) continue - const value1 = Visit(subschema, references, value) - return Default(schema, value1) - } - // test transformed value against union variants - for (const subschema of schema.anyOf) { - const value1 = Visit(subschema, references, value) - if (!Check(schema, references, value1)) continue - return Default(schema, value1) - } - return Default(schema, value) - } - function Visit(schema: Types.TSchema, references: Types.TSchema[], value: any): any { - const references_ = typeof schema.$id === 'string' ? [...references, schema] : references - const schema_ = schema as any - switch (schema[Types.Kind]) { - case 'Array': - return TArray(schema_, references_, value) - case 'Intersect': - return TIntersect(schema_, references_, value) - case 'Not': - return TNot(schema_, references_, value) - case 'Object': - return TObject(schema_, references_, value) - case 'Record': - return TRecord(schema_, references_, value) - case 'Ref': - return TRef(schema_, references_, value) - case 'This': - return TThis(schema_, references_, value) - case 'Tuple': - return TTuple(schema_, references_, value) - case 'Union': - return TUnion(schema_, references_, value) - default: - return Default(schema_, value) - } - } - export function Encode(schema: Types.TSchema, references: Types.TSchema[], value: unknown): unknown { - return Visit(schema, references, value) - } -} diff --git a/src/value/value.ts b/src/value/value.ts index 43c3899dd..6be038022 100644 --- a/src/value/value.ts +++ b/src/value/value.ts @@ -85,7 +85,7 @@ export namespace Value { export function Decode(...args: any[]) { const [schema, references, value] = args.length === 3 ? [args[0], args[1], args[2]] : [args[0], [], args[1]] if (!Check(schema, references, value)) throw new ValueTransform.TransformDecodeCheckError(schema, value, Errors(schema, references, value).First()!) - return ValueTransform.DecodeTransform.Decode(schema, references, value) + return ValueTransform.Decode(schema, references, value) } /** Encodes a value or throws if error */ export function Encode>(schema: T, references: Types.TSchema[], value: unknown): R @@ -94,7 +94,7 @@ export namespace Value { /** Encodes a value or throws if error */ export function Encode(...args: any[]) { const [schema, references, value] = args.length === 3 ? [args[0], args[1], args[2]] : [args[0], [], args[1]] - const encoded = ValueTransform.EncodeTransform.Encode(schema, references, value) + const encoded = ValueTransform.Encode(schema, references, value) if (!Check(schema, references, encoded)) throw new ValueTransform.TransformEncodeCheckError(schema, value, Errors(schema, references, value).First()!) return encoded }