From 6fd7dac6a4e0aa9229c37d76ec2c4238c968ce8b Mon Sep 17 00:00:00 2001 From: sinclairzx81 Date: Mon, 20 Nov 2023 21:53:12 +0900 Subject: [PATCH] Revision 0.31.28 (#674) * Update Intersect Transform Logic * Tests * Version --- package-lock.json | 4 +- package.json | 2 +- readme.md | 10 +- src/value/transform.ts | 308 ++++++++++------------ test/runtime/value/transform/intersect.ts | 57 ++++ 5 files changed, 200 insertions(+), 181 deletions(-) diff --git a/package-lock.json b/package-lock.json index b7f256f22..a9ffcd256 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.31.27", + "version": "0.31.28", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.31.27", + "version": "0.31.28", "license": "MIT", "devDependencies": { "@sinclair/hammer": "^0.18.0", diff --git a/package.json b/package.json index e5fc6ea76..04d98e936 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.31.27", + "version": "0.31.28", "description": "JSONSchema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", diff --git a/readme.md b/readme.md index d4868e0d7..8ba350169 100644 --- a/readme.md +++ b/readme.md @@ -1719,11 +1719,11 @@ The following table lists esbuild compiled and minified sizes for each TypeBox m ┌──────────────────────┬────────────┬────────────┬─────────────┐ │ (index) │ Compiled │ Minified │ Compression │ ├──────────────────────┼────────────┼────────────┼─────────────┤ -│ typebox/compiler │ '148.9 kb' │ ' 65.8 kb' │ '2.26 x' │ -│ typebox/errors │ '111.5 kb' │ ' 49.1 kb' │ '2.27 x' │ -│ typebox/system │ ' 82.6 kb' │ ' 36.8 kb' │ '2.24 x' │ -│ typebox/value │ '190.5 kb' │ ' 82.4 kb' │ '2.31 x' │ -│ typebox │ ' 72.4 kb' │ ' 31.6 kb' │ '2.29 x' │ +│ typebox/compiler │ '163.6 kb' │ ' 71.6 kb' │ '2.28 x' │ +│ typebox/errors │ '113.3 kb' │ ' 50.1 kb' │ '2.26 x' │ +│ typebox/system │ ' 83.9 kb' │ ' 37.5 kb' │ '2.24 x' │ +│ typebox/value │ '191.1 kb' │ ' 82.3 kb' │ '2.32 x' │ +│ typebox │ ' 73.8 kb' │ ' 32.3 kb' │ '2.29 x' │ └──────────────────────┴────────────┴────────────┴─────────────┘ ``` diff --git a/src/value/transform.ts b/src/value/transform.ts index d0c7daba5..bb377f9a1 100644 --- a/src/value/transform.ts +++ b/src/value/transform.ts @@ -32,19 +32,9 @@ import { Deref } from './deref' import { Check } from './check' import * as Types from '../typebox' -// ------------------------------------------------------------------------- -// CheckFunction -// ------------------------------------------------------------------------- -export type CheckFunction = (schema: Types.TSchema, references: Types.TSchema[], value: unknown) => boolean - // ------------------------------------------------------------------------- // Errors // ------------------------------------------------------------------------- -export class TransformUnknownTypeError extends Types.TypeBoxError { - constructor(public readonly schema: Types.TRef | Types.TThis) { - super(`Unknown type`) - } -} export class TransformDecodeCheckError extends Types.TypeBoxError { constructor(public readonly schema: Types.TSchema, public readonly value: unknown, public readonly error: ValueError) { super(`Unable to decode due to invalid value`) @@ -65,9 +55,9 @@ export class TransformEncodeError extends Types.TypeBoxError { 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 { @@ -124,9 +114,6 @@ export namespace HasTransform { if (schema.$id && visited.has(schema.$id)) return false if (schema.$id) visited.add(schema.$id) switch (schema[Types.Kind]) { - // ------------------------------------------------------ - // Structural - // ------------------------------------------------------ case 'Array': return TArray(schema_, references_) case 'AsyncIterator': @@ -155,28 +142,7 @@ export namespace HasTransform { return TTuple(schema_, references_) case 'Union': return TUnion(schema_, references_) - // ------------------------------------------------------ - // Default - // ------------------------------------------------------ - case 'Any': - case 'BigInt': - case 'Boolean': - case 'Date': - case 'Integer': - case 'Literal': - case 'Never': - case 'Null': - case 'Number': - case 'String': - case 'Symbol': - case 'TemplateLiteral': - case 'Undefined': - case 'Uint8Array': - case 'Unknown': - case 'Void': - return Types.TypeGuard.TTransform(schema) default: - if (!Types.TypeRegistry.Has(schema_[Types.Kind])) throw new TransformUnknownTypeError(schema_) return Types.TypeGuard.TTransform(schema) } } @@ -187,9 +153,9 @@ export namespace HasTransform { 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) { @@ -199,82 +165,105 @@ export namespace DecodeTransform { throw new TransformDecodeError(schema, value, error) } } + // prettier-ignore function TArray(schema: Types.TArray, references: Types.TSchema[], value: any): any { - const elements1 = value.map((value: any) => Visit(schema.items, references, value)) as unknown[] - return Default(schema, elements1) + 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 keys = Types.KeyResolver.ResolveKeys(schema, { includePatterns: false }) - const properties1 = Object.entries(value).reduce((acc, [key, value]) => { - return !keys.includes(key) ? { ...acc, [key]: value } : { ...acc, [key]: Default(Types.IndexedAccessor.Resolve(schema, [key]), value) } - }, {} as Record) - if (!Types.TypeGuard.TTransform(schema.unevaluatedProperties)) return Default(schema, properties1) - const properties2 = Object.entries(properties1).reduce((acc, [key, value]) => { - return keys.includes(key) ? { ...acc, [key]: value } : { ...acc, [key]: Default(schema.unevaluatedProperties as Types.TSchema, value) } - }, {} as Record) - return Default(schema, properties2) + const knownKeys = Types.KeyResolver.ResolveKeys(schema, { includePatterns: false }) + const knownProperties = knownKeys.reduce((value, key) => { + return (key in value) + ? { ...value, [key]: Visit(Types.IndexedAccessor.Resolve(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) { - const value1 = Visit(schema.not, references, value) - return Default(schema, value1) + 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 properties1 = Object.entries(value).reduce((acc, [key, value]) => { - return !(key in schema.properties) ? { ...acc, [key]: value } : { ...acc, [key]: Visit(schema.properties[key], references, value) } - }, {} as Record) - if (!Types.TypeGuard.TSchema(schema.additionalProperties)) return Default(schema, properties1) + const knownKeys = Types.KeyResolver.ResolveKeys(schema, { includePatterns: false }) + 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 properties2 = Object.entries(properties1).reduce((acc, [key, value]) => { - return key in schema.properties ? { ...acc, [key]: value } : { ...acc, [key]: Visit(additionalProperties, references, value) } - }, {} as Record) - return Default(schema, properties2) - } + 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 property = schema.patternProperties[pattern] - const regex = new RegExp(pattern) - const properties1 = Object.entries(value).reduce((acc, [key, value]) => { - return !regex.test(key) ? { ...acc, [key]: value } : { ...acc, [key]: Visit(property, references, value) } - }, {} as Record) - if (!Types.TypeGuard.TSchema(schema.additionalProperties)) return Default(schema, properties1) + 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 properties2 = Object.entries(properties1).reduce((acc, [key, value]) => { - return regex.test(key) ? { ...acc, [key]: value } : { ...acc, [key]: Visit(additionalProperties, references, value) } - }, {} as Record) - return Default(schema, properties2) + 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) - const resolved = Visit(target, references, value) - return Default(schema, resolved) + return Default(schema, Visit(target, references, value)) } 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) + return Default(schema, Visit(target, references, value)) } + // prettier-ignore function TTuple(schema: Types.TTuple, references: Types.TSchema[], value: any) { - const value1 = IsArray(schema.items) ? schema.items.map((schema, index) => Visit(schema, references, value[index])) : [] - return Default(schema, value1) + 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 value1 = Default(schema, value) + const defaulted = Default(schema, value) for (const subschema of schema.anyOf) { - if (!Check(subschema, references, value1)) continue - return Visit(subschema, references, value1) + if (!Check(subschema, references, defaulted)) continue + return Visit(subschema, references, defaulted) } - return value1 + 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]) { - // ------------------------------------------------------ - // Structural - // ------------------------------------------------------ case 'Array': return TArray(schema_, references_, value) case 'Intersect': @@ -295,32 +284,7 @@ export namespace DecodeTransform { return TTuple(schema_, references_, value) case 'Union': return TUnion(schema_, references_, value) - // ------------------------------------------------------ - // Default - // ------------------------------------------------------ - case 'Any': - case 'AsyncIterator': - case 'BigInt': - case 'Boolean': - case 'Constructor': - case 'Date': - case 'Function': - case 'Integer': - case 'Iterator': - case 'Literal': - case 'Never': - case 'Null': - case 'Number': - case 'Promise': - case 'String': - case 'TemplateLiteral': - case 'Undefined': - case 'Uint8Array': - case 'Unknown': - case 'Void': - return Default(schema_, value) default: - if (!Types.TypeRegistry.Has(schema_[Types.Kind])) throw new TransformUnknownTypeError(schema_) return Default(schema_, value) } } @@ -328,9 +292,9 @@ export namespace DecodeTransform { 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) { @@ -340,52 +304,79 @@ export namespace EncodeTransform { throw new TransformEncodeError(schema, value, error) } } + // prettier-ignore function TArray(schema: Types.TArray, references: Types.TSchema[], value: any): any { - const elements1 = Default(schema, value) - return elements1.map((value: any) => Visit(schema.items, references, value)) as unknown[] + 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 properties1 = Default(schema, value) - if (!IsPlainObject(value) || IsValueType(value)) return properties1 - const keys = Types.KeyResolver.ResolveKeys(schema, { includePatterns: false }) - const properties2 = Object.entries(properties1).reduce((acc, [key, value]) => { - return !keys.includes(key) ? { ...acc, [key]: value } : { ...acc, [key]: Default(Types.IndexedAccessor.Resolve(schema, [key]), value) } - }, {} as Record) - if (!Types.TypeGuard.TTransform(schema.unevaluatedProperties)) return Default(schema, properties2) - return Object.entries(properties2).reduce((acc, [key, value]) => { - return keys.includes(key) ? { ...acc, [key]: value } : { ...acc, [key]: Default(schema.unevaluatedProperties as Types.TSchema, value) } - }, {} as Record) + const defaulted = Default(schema, value) + if (!IsPlainObject(value) || IsValueType(value)) return defaulted + const knownKeys = Types.KeyResolver.ResolveKeys(schema, { includePatterns: false }) + const knownProperties = knownKeys.reduce((value, key) => { + return key in defaulted + ? { ...value, [key]: Visit(Types.IndexedAccessor.Resolve(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) { - const value1 = Default(schema, value) - return Default(schema.not, value1) + return Default(schema.not, Default(schema, value)) } + // prettier-ignore function TObject(schema: Types.TObject, references: Types.TSchema[], value: any) { - const properties1 = Default(schema, value) as Record - if (!IsPlainObject(value)) return properties1 - const properties2 = Object.entries(properties1).reduce((acc, [key, value]) => { - return !(key in schema.properties) ? { ...acc, [key]: value } : { ...acc, [key]: Visit(schema.properties[key], references, value) } - }, {} as Record) - if (!Types.TypeGuard.TSchema(schema.additionalProperties)) return properties2 + const defaulted = Default(schema, value) + if (!IsPlainObject(value)) return defaulted + const knownKeys = Types.KeyResolver.ResolveKeys(schema, { includePatterns: false }) + 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 Object.entries(properties2).reduce((acc, [key, value]) => { - return key in schema.properties ? { ...acc, [key]: value } : { ...acc, [key]: Visit(additionalProperties, references, value) } - }, {} as Record) + 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 properties1 = Default(schema, value) as Record - if (!IsPlainObject(value)) return properties1 + const defaulted = Default(schema, value) as Record + if (!IsPlainObject(value)) return defaulted const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0] - const property = schema.patternProperties[pattern] - const regex = new RegExp(pattern) - const properties2 = Object.entries(properties1).reduce((acc, [key, value]) => { - return !regex.test(key) ? { ...acc, [key]: value } : { ...acc, [key]: Visit(property, references, value) } - }, {} as Record) - if (!Types.TypeGuard.TSchema(schema.additionalProperties)) return Default(schema, properties2) + 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 Object.entries(properties2).reduce((acc, [key, value]) => { - return regex.test(key) ? { ...acc, [key]: value } : { ...acc, [key]: Visit(additionalProperties, references, value) } - }, {} as Record) + 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) @@ -420,9 +411,6 @@ export namespace EncodeTransform { const references_ = typeof schema.$id === 'string' ? [...references, schema] : references const schema_ = schema as any switch (schema[Types.Kind]) { - // ------------------------------------------------------ - // Structural - // ------------------------------------------------------ case 'Array': return TArray(schema_, references_, value) case 'Intersect': @@ -441,33 +429,7 @@ export namespace EncodeTransform { return TTuple(schema_, references_, value) case 'Union': return TUnion(schema_, references_, value) - // ------------------------------------------------------ - // Apply - // ------------------------------------------------------ - case 'Any': - case 'AsyncIterator': - case 'BigInt': - case 'Boolean': - case 'Constructor': - case 'Date': - case 'Function': - case 'Integer': - case 'Iterator': - case 'Literal': - case 'Never': - case 'Null': - case 'Number': - case 'Promise': - case 'String': - case 'Symbol': - case 'TemplateLiteral': - case 'Undefined': - case 'Uint8Array': - case 'Unknown': - case 'Void': - return Default(schema_, value) default: - if (!Types.TypeRegistry.Has(schema_[Types.Kind])) throw new TransformUnknownTypeError(schema_) return Default(schema_, value) } } diff --git a/test/runtime/value/transform/intersect.ts b/test/runtime/value/transform/intersect.ts index 0d7f39043..7e0bcc825 100644 --- a/test/runtime/value/transform/intersect.ts +++ b/test/runtime/value/transform/intersect.ts @@ -120,4 +120,61 @@ describe('value/transform/Intersect', () => { it('Should throw on exterior value type decode', () => { Assert.Throws(() => Encoder.Decode(T4, null)) }) + // -------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/discussions/672 + // -------------------------------------------------------- + // prettier-ignore + { + const A = Type.Object({ isHybrid: Type.Boolean() }) + const T = Type.Transform(A) + .Decode((value) => ({ isHybrid: value.isHybrid ? 1 : 0 })) + .Encode((value) => ({ isHybrid: value.isHybrid === 1 ? true : false })) + const I = Type.Intersect([ + Type.Object({ model: Type.String() }), + Type.Object({ features: Type.Array(T) }), + ]) + it('Should decode nested 1', () => { + const value = Value.Decode(T, { isHybrid: true }) + Assert.IsEqual(value, { isHybrid: 1 }) + }) + // prettier-ignore + it('Should decode nested 2', () => { + const value = Value.Decode(I, { + model: 'Prius', + features: [ + { isHybrid: true }, + { isHybrid: false } + ], + }) + Assert.IsEqual(value, { + model: 'Prius', + features: [ + { isHybrid: 1 }, + { isHybrid: 0 } + ], + }) + }) + it('should encode nested 1', () => { + let value = Value.Encode(T, { isHybrid: 1 }) + Assert.IsEqual(value, { isHybrid: true }) + }) + // prettier-ignore + it('Should encode nested 2', () => { + const value = Value.Encode(I, { + model: 'Prius', + features: [ + { isHybrid: 1 }, + { isHybrid: 0 } + ], + }) + Assert.IsEqual(value, { + model: 'Prius', + features: [ + { isHybrid: true }, + { isHybrid: false } + + ], + }) + }) + } })