From 959fc8e5d2f64c27a0e0004f913e3d369972a58a Mon Sep 17 00:00:00 2001 From: sinclair Date: Fri, 27 Oct 2023 15:59:01 +0900 Subject: [PATCH] Clean Tests | Updates to Default Handling --- examples/index.ts | 8 ++++ src/value/clean.ts | 31 ++++++++++----- src/value/default.ts | 46 +++++++++++++--------- test/runtime/value/clean/any.ts | 11 ++++++ test/runtime/value/clean/array.ts | 21 ++++++++++ test/runtime/value/clean/async-iterator.ts | 11 ++++++ test/runtime/value/clean/bigint.ts | 11 ++++++ test/runtime/value/clean/boolean.ts | 11 ++++++ test/runtime/value/clean/composite.ts | 11 ++++++ test/runtime/value/clean/constructor.ts | 11 ++++++ test/runtime/value/clean/enum.ts | 11 ++++++ test/runtime/value/clean/function.ts | 11 ++++++ test/runtime/value/clean/index.ts | 10 +++++ test/runtime/value/clean/integer.ts | 11 ++++++ test/runtime/value/clean/intersect.ts | 26 ++++++++++++ test/runtime/value/create/date.ts | 14 +++++++ test/runtime/value/create/index.ts | 1 + test/runtime/value/default/date.ts | 16 ++++++++ test/runtime/value/default/index.ts | 2 + test/runtime/value/default/promise.ts | 16 ++++++++ test/runtime/value/default/record.ts | 16 ++++++-- test/runtime/value/index.ts | 1 + 22 files changed, 275 insertions(+), 32 deletions(-) create mode 100644 test/runtime/value/clean/any.ts create mode 100644 test/runtime/value/clean/array.ts create mode 100644 test/runtime/value/clean/async-iterator.ts create mode 100644 test/runtime/value/clean/bigint.ts create mode 100644 test/runtime/value/clean/boolean.ts create mode 100644 test/runtime/value/clean/composite.ts create mode 100644 test/runtime/value/clean/constructor.ts create mode 100644 test/runtime/value/clean/enum.ts create mode 100644 test/runtime/value/clean/function.ts create mode 100644 test/runtime/value/clean/index.ts create mode 100644 test/runtime/value/clean/integer.ts create mode 100644 test/runtime/value/clean/intersect.ts create mode 100644 test/runtime/value/create/date.ts create mode 100644 test/runtime/value/default/date.ts create mode 100644 test/runtime/value/default/promise.ts diff --git a/examples/index.ts b/examples/index.ts index ef6373543..ff7ed51ac 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -5,3 +5,11 @@ import { Type, TypeGuard, Kind, Static, TSchema } from '@sinclair/typebox' // Todo: Investigate Union Default (initialize interior types) // Todo: Implement Value.Clean() Tests + +const T = Type.Record(Type.Number(), Type.String({ default: 1 }), { + additionalProperties: Type.Any({ default: 1000 }), +}) + +const R = Value.Default(T, { y: 2, z: undefined }) + +console.log(R) diff --git a/src/value/clean.ts b/src/value/clean.ts index ee6c1c9d2..1ed9ecd66 100644 --- a/src/value/clean.ts +++ b/src/value/clean.ts @@ -39,6 +39,12 @@ function Default(value: unknown) { return value } // -------------------------------------------------------------------------- +// IsSchema +// -------------------------------------------------------------------------- +function IsSchema(value: unknown): value is Types.TSchema { + return Types.TypeGuard.TKind(value) && Types.TypeRegistry.Has(value[Types.Kind]) +} +// -------------------------------------------------------------------------- // Structural // -------------------------------------------------------------------------- function TArray(schema: Types.TArray, references: Types.TSchema[], value: unknown): any { @@ -53,23 +59,28 @@ function TIntersect(schema: Types.TIntersect, references: Types.TSchema[], value } function TObject(schema: Types.TObject, references: Types.TSchema[], value: unknown): any { if (!IsObject(value)) return Default(value) - return Object.keys(schema.properties).reduce((acc, key) => { - // prettier-ignore - return key in value - ? { ...acc, [key]: Visit(schema.properties[key], references, value[key]) } - : acc + const properties = Object.keys(schema.properties).reduce((acc, key) => { + return key in value ? { ...acc, [key]: Visit(schema.properties[key], references, value[key]) } : acc }, {}) + if (!IsSchema(schema.additionalProperties)) { + return properties + } + const additionalProperties = Object.keys(value).reduce((acc, key) => { + if (key in schema.properties) return acc + return Check(schema.additionalProperties as Types.TSchema, value[key]) ? { ...acc, [key]: value[key] } : acc + }, {}) + return { ...properties, ...additionalProperties } } function TRecord(schema: Types.TRecord, references: Types.TSchema[], value: unknown): any { if (!IsObject(value)) return Default(value) const [patternKey, patternSchema] = Object.entries(schema.patternProperties)[0] const patternRegExp = new RegExp(patternKey) - return Object.keys(value).reduce((acc, key) => { - // prettier-ignore - return patternRegExp.test(key) - ? { ...acc, [key]: Visit(patternSchema, references, value[key]) } - : acc + const properties = Object.keys(value).reduce((acc, key) => { + return patternRegExp.test(key) ? { ...acc, [key]: Visit(patternSchema, references, value[key]) } : acc }, {}) + if (!Types.TypeGuard.TKind(schema.additionalProperties)) { + return properties + } } function TRef(schema: Types.TRef, references: Types.TSchema[], value: unknown): any { const target = Deref(schema, references) diff --git a/src/value/default.ts b/src/value/default.ts index 962501340..25e8033c7 100644 --- a/src/value/default.ts +++ b/src/value/default.ts @@ -26,7 +26,7 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -import { IsString, IsObject, IsArray, IsUndefined } from './guard' +import { HasPropertyKey, IsString, IsObject, IsArray, IsUndefined } from './guard' import { Deref } from './deref' import { Clone } from './clone' import * as Types from '../typebox' @@ -60,31 +60,39 @@ function TIntersect(schema: Types.TIntersect, references: Types.TSchema[], value function TObject(schema: Types.TObject, references: Types.TSchema[], value: unknown): any { const object = ValueOrDefault(schema, value) if (!IsObject(object)) return object - const knownObject = Object.getOwnPropertyNames(schema.properties).reduce((acc, key) => { + const knownPropertyKeys = Object.getOwnPropertyNames(schema.properties) + // Reduce on object using known keys and only map for property types that have + // default values specified. The returned object should be the complete object + // with additional properties unmapped. + const properties = knownPropertyKeys.reduce((acc, key) => { return HasDefault(schema.properties[key]) ? { ...acc, [key]: Visit(schema.properties[key], references, object[key]) } : acc }, object) - if (!Types.TypeGuard.TSchema(schema.additionalProperties) || !HasDefault(schema.additionalProperties as Types.TSchema)) { - return knownObject - } - const knownKeys = Object.getOwnPropertyNames(schema.properties) - return Object.getOwnPropertyNames(knownObject).reduce((acc, key) => { - return knownKeys.includes(key) ? acc : { ...acc, [key]: Visit(schema.additionalProperties as Types.TSchema, references, knownObject[key]) } - }, knownObject) + // If additionalProperties not is schema-like with a default property, we exit with properties. + const additionalPropertiesSchema = schema.additionalProperties as Types.TSchema + if (!(IsObject(additionalPropertiesSchema) && HasDefault(additionalPropertiesSchema))) return properties + // Reduce on properties using object key. Only map properties outside the known key set + return Object.getOwnPropertyNames(object).reduce((acc, key) => { + return !knownPropertyKeys.includes(key) ? { ...acc, [key]: Visit(additionalPropertiesSchema, references, object[key]) } : acc + }, properties) } function TRecord(schema: Types.TRecord, references: Types.TSchema[], value: unknown): any { const object = ValueOrDefault(schema, value) if (!IsObject(object)) return object - const [patternKey, patternSchema] = Object.entries(schema.patternProperties)[0] - const patternRegExp = new RegExp(patternKey) - const knownObject = Object.getOwnPropertyNames(value).reduce((acc, key) => { - return HasDefault(patternSchema) && patternRegExp.test(key) ? { ...acc, [key]: Visit(patternSchema, references, object[key]) } : acc + const [propertyKeyPattern, propertySchema] = Object.entries(schema.patternProperties)[0] + const knownPropertyKey = new RegExp(propertyKeyPattern) + // Reduce on object keys using object keys and only map for property types that have + // default values specified. The returned object should be the complete object + // with additional properties unmapped. + const properties = Object.getOwnPropertyNames(object).reduce((acc, key) => { + return knownPropertyKey.test(key) && HasDefault(propertySchema) ? { ...acc, [key]: Visit(propertySchema, references, object[key]) } : { ...acc, [key]: object[key] } }, object) - if (!Types.TypeGuard.TSchema(schema.additionalProperties) || !HasDefault(schema.additionalProperties as Types.TSchema)) { - return knownObject - } - return Object.getOwnPropertyNames(knownObject).reduce((acc, key) => { - return !patternRegExp.test(key) ? { ...acc, [key]: Visit(schema.additionalProperties as Types.TSchema, references, knownObject[key]) } : acc - }, knownObject) + // If additionalProperties not is schema-like with a default property, we exit with properties. + const additionalPropertiesSchema = schema.additionalProperties as Types.TSchema + if (!(IsObject(additionalPropertiesSchema) && HasDefault(additionalPropertiesSchema))) return properties + // Reduce on properties using object key. Only map properties outside the known key set + return Object.getOwnPropertyNames(object).reduce((acc, key) => { + return !knownPropertyKey.test(key) ? { ...acc, [key]: Visit(additionalPropertiesSchema, references, object[key]) } : acc + }, properties) } function TRef(schema: Types.TRef, references: Types.TSchema[], value: unknown): any { return Visit(Deref(schema, references), references, ValueOrDefault(schema, value)) diff --git a/test/runtime/value/clean/any.ts b/test/runtime/value/clean/any.ts new file mode 100644 index 000000000..9a49a2562 --- /dev/null +++ b/test/runtime/value/clean/any.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Any', () => { + it('Should clean 1', () => { + const T = Type.Any() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/array.ts b/test/runtime/value/clean/array.ts new file mode 100644 index 000000000..6e8a21844 --- /dev/null +++ b/test/runtime/value/clean/array.ts @@ -0,0 +1,21 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Array', () => { + it('Should clean 1', () => { + const T = Type.Any() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean 2', () => { + const T = Type.Array( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ) + const R = Value.Clean(T, [undefined, null, { x: 1 }, { x: 1, y: 2 }, { x: 1, y: 2, z: 3 }]) + Assert.IsEqual(R, [undefined, null, { x: 1 }, { x: 1, y: 2 }, { x: 1, y: 2 }]) + }) +}) diff --git a/test/runtime/value/clean/async-iterator.ts b/test/runtime/value/clean/async-iterator.ts new file mode 100644 index 000000000..a067233b3 --- /dev/null +++ b/test/runtime/value/clean/async-iterator.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/AsyncIterator', () => { + it('Should clean 1', () => { + const T = Type.AsyncIterator(Type.Number()) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/bigint.ts b/test/runtime/value/clean/bigint.ts new file mode 100644 index 000000000..cdd77d4de --- /dev/null +++ b/test/runtime/value/clean/bigint.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/BigInt', () => { + it('Should clean 1', () => { + const T = Type.BigInt() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/boolean.ts b/test/runtime/value/clean/boolean.ts new file mode 100644 index 000000000..7697d25c3 --- /dev/null +++ b/test/runtime/value/clean/boolean.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Boolean', () => { + it('Should clean 1', () => { + const T = Type.Boolean() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/composite.ts b/test/runtime/value/clean/composite.ts new file mode 100644 index 000000000..035f335a9 --- /dev/null +++ b/test/runtime/value/clean/composite.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Composite', () => { + it('Should clean 1', () => { + const T = Type.Composite([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/constructor.ts b/test/runtime/value/clean/constructor.ts new file mode 100644 index 000000000..4a4be492c --- /dev/null +++ b/test/runtime/value/clean/constructor.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Constructor', () => { + it('Should clean 1', () => { + const T = Type.Constructor([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })], Type.Object({ z: Type.Number() })) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/enum.ts b/test/runtime/value/clean/enum.ts new file mode 100644 index 000000000..88902047b --- /dev/null +++ b/test/runtime/value/clean/enum.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Enum', () => { + it('Should clean 1', () => { + const T = Type.Enum({ x: 1, y: 2 }) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/function.ts b/test/runtime/value/clean/function.ts new file mode 100644 index 000000000..2bc5a4ee5 --- /dev/null +++ b/test/runtime/value/clean/function.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Function', () => { + it('Should clean 1', () => { + const T = Type.Function([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })], Type.Object({ z: Type.Number() })) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/index.ts b/test/runtime/value/clean/index.ts new file mode 100644 index 000000000..04a20cdf3 --- /dev/null +++ b/test/runtime/value/clean/index.ts @@ -0,0 +1,10 @@ +import './any' +import './array' +import './async-iterator' +import './bigint' +import './boolean' +import './composite' +import './enum' +import './function' +import './integer' +import './intersect' diff --git a/test/runtime/value/clean/integer.ts b/test/runtime/value/clean/integer.ts new file mode 100644 index 000000000..ff377049c --- /dev/null +++ b/test/runtime/value/clean/integer.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Integer', () => { + it('Should clean 1', () => { + const T = Type.Integer() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/intersect.ts b/test/runtime/value/clean/intersect.ts new file mode 100644 index 000000000..6ab61cb0b --- /dev/null +++ b/test/runtime/value/clean/intersect.ts @@ -0,0 +1,26 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Intersect', () => { + it('Should clean 1', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean 2', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clean 3', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + const R = Value.Clean(T, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should clean 4', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + const R = Value.Clean(T, { x: 1, y: 2, z: 3 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) +}) diff --git a/test/runtime/value/create/date.ts b/test/runtime/value/create/date.ts new file mode 100644 index 000000000..122ecb68e --- /dev/null +++ b/test/runtime/value/create/date.ts @@ -0,0 +1,14 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Date', () => { + it('Should create value', () => { + const T = Type.Date() + Assert.IsInstanceOf(Value.Create(T), Date) + }) + it('Should create default', () => { + const T = Type.Date({ default: 1 }) + Assert.IsEqual(Value.Create(T), 1) + }) +}) diff --git a/test/runtime/value/create/index.ts b/test/runtime/value/create/index.ts index 9bb482979..7b51cf3bf 100644 --- a/test/runtime/value/create/index.ts +++ b/test/runtime/value/create/index.ts @@ -5,6 +5,7 @@ import './bigint' import './boolean' import './composite' import './constructor' +import './date' import './enum' import './function' import './integer' diff --git a/test/runtime/value/default/date.ts b/test/runtime/value/default/date.ts new file mode 100644 index 000000000..2522cf20d --- /dev/null +++ b/test/runtime/value/default/date.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Date', () => { + it('Should use default', () => { + const T = Type.Date({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Date({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/index.ts b/test/runtime/value/default/index.ts index 8df41b03f..7395f9fce 100644 --- a/test/runtime/value/default/index.ts +++ b/test/runtime/value/default/index.ts @@ -5,6 +5,7 @@ import './bigint' import './boolean' import './composite' import './constructor' +import './date' import './enum' import './function' import './integer' @@ -18,6 +19,7 @@ import './not' import './null' import './number' import './object' +import './promise' import './record' import './recursive' import './ref' diff --git a/test/runtime/value/default/promise.ts b/test/runtime/value/default/promise.ts new file mode 100644 index 000000000..659e052cb --- /dev/null +++ b/test/runtime/value/default/promise.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Promise', () => { + it('Should use default', () => { + const T = Type.Any({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Any({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/record.ts b/test/runtime/value/default/record.ts index 6f788584b..4b300ddc2 100644 --- a/test/runtime/value/default/record.ts +++ b/test/runtime/value/default/record.ts @@ -13,9 +13,9 @@ describe('value/default/Record', () => { const R = Value.Default(T, null) Assert.IsEqual(R, null) }) - // ---------------------------------------------------------------- - // Properties - // ---------------------------------------------------------------- + // // ---------------------------------------------------------------- + // // Properties + // // ---------------------------------------------------------------- it('Should use property defaults 1', () => { const T = Type.Record(Type.Number(), Type.Number({ default: 1 })) const R = Value.Default(T, { 0: undefined }) @@ -32,6 +32,16 @@ describe('value/default/Record', () => { Assert.IsEqual(R, { a: undefined }) }) it('Should use property defaults 4', () => { + const T = Type.Record(Type.Number(), Type.Number({ default: 1 })) + const R = Value.Default(T, { 0: undefined }) + Assert.IsEqual(R, { 0: 1 }) + }) + it('Should use property defaults 5', () => { + const T = Type.Record(Type.Number(), Type.Number()) + const R = Value.Default(T, { 0: undefined }) + Assert.IsEqual(R, { 0: undefined }) + }) + it('Should use property defaults 6', () => { const T = Type.Record(Type.Number(), Type.Number({ default: 1 })) const R = Value.Default(T, {}) Assert.IsEqual(R, {}) diff --git a/test/runtime/value/index.ts b/test/runtime/value/index.ts index b795b7a55..4e6ce996e 100644 --- a/test/runtime/value/index.ts +++ b/test/runtime/value/index.ts @@ -1,5 +1,6 @@ import './cast' import './check' +import './clean' import './clone' import './convert' import './create'