Revision 0.33.5 (#959)

* Use ExactOptionalProperty Policy on Decode | Encode

* Version
This commit is contained in:
sinclairzx81
2024-08-14 14:56:09 +09:00
committed by GitHub
parent e36f5658e3
commit cc5e6c7aca
8 changed files with 162 additions and 21 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@sinclair/typebox",
"version": "0.33.4",
"version": "0.33.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@sinclair/typebox",
"version": "0.33.4",
"version": "0.33.5",
"license": "MIT",
"devDependencies": {
"@arethetypeswrong/cli": "^0.13.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@sinclair/typebox",
"version": "0.33.4",
"version": "0.33.5",
"description": "Json Schema Type Builder with Static Type Resolution for TypeScript",
"keywords": [
"typescript",

View File

@@ -52,24 +52,24 @@ export namespace TypeSystemPolicy {
export let AllowNaN: boolean = false
/** Sets whether `null` should validate for void types. The default is `false` */
export let AllowNullVoid: boolean = false
/** Asserts this value using the ExactOptionalPropertyTypes policy */
/** Checks this value using the ExactOptionalPropertyTypes policy */
export function IsExactOptionalProperty(value: Record<keyof any, unknown>, key: string) {
return ExactOptionalPropertyTypes ? key in value : value[key] !== undefined
}
/** Asserts this value using the AllowArrayObjects policy */
/** Checks this value using the AllowArrayObjects policy */
export function IsObjectLike(value: unknown): value is Record<keyof any, unknown> {
const isObject = IsObject(value)
return AllowArrayObject ? isObject : isObject && !IsArray(value)
}
/** Asserts this value as a record using the AllowArrayObjects policy */
/** Checks this value as a record using the AllowArrayObjects policy */
export function IsRecordLike(value: unknown): value is Record<keyof any, unknown> {
return IsObjectLike(value) && !(value instanceof Date) && !(value instanceof Uint8Array)
}
/** Asserts this value using the AllowNaN policy */
/** Checks this value using the AllowNaN policy */
export function IsNumberLike(value: unknown): value is number {
return AllowNaN ? IsNumber(value) : Number.isFinite(value)
}
/** Asserts this value using the AllowVoidNull policy */
/** Checks this value using the AllowVoidNull policy */
export function IsVoidLike(value: unknown): value is void {
const isUndefined = IsUndefined(value)
return AllowNullVoid ? isUndefined || value === null : isUndefined

View File

@@ -55,6 +55,12 @@ import type { TUndefined } from '../../type/undefined/index'
// ValueGuard
// ------------------------------------------------------------------
import { IsArray, IsObject, IsDate, IsUndefined, IsString, IsNumber, IsBoolean, IsBigInt, IsSymbol, HasPropertyKey } from '../guard/index'
// ------------------------------------------------------------------
// TypeGuard
// ------------------------------------------------------------------
import { IsOptional } from '../../type/guard/kind'
// ------------------------------------------------------------------
// Conversions
// ------------------------------------------------------------------
@@ -193,8 +199,9 @@ function FromNumber(schema: TNumber, references: TSchema[], value: any): unknown
// prettier-ignore
function FromObject(schema: TObject, references: TSchema[], value: any): unknown {
if(!IsObject(value)) return value
for(const key of Object.getOwnPropertyNames(schema.properties)) {
value[key] = Visit(schema.properties[key], references, value[key])
for(const propertyKey of Object.getOwnPropertyNames(schema.properties)) {
if(!HasPropertyKey(value, propertyKey)) continue
value[propertyKey] = Visit(schema.properties[propertyKey], references, value[propertyKey])
}
return value
}

View File

@@ -26,11 +26,11 @@ THE SOFTWARE.
---------------------------------------------------------------------------*/
import { TypeSystemPolicy } from '../../system/policy'
import { Kind, TransformKind } from '../../type/symbols/index'
import { TypeBoxError } from '../../type/error/index'
import { ValueError } from '../../errors/index'
import { KeyOfPropertyKeys, KeyOfPropertyEntries } from '../../type/keyof/index'
import { Index } from '../../type/indexed/index'
import { Deref } from '../deref/index'
import { Check } from '../check/index'
@@ -48,11 +48,11 @@ import type { TUnion } from '../../type/union/index'
// ------------------------------------------------------------------
// ValueGuard
// ------------------------------------------------------------------
import { IsObject, IsArray, IsValueType } from '../guard/index'
import { HasPropertyKey, IsObject, IsArray, IsValueType, IsUndefined as IsUndefinedValue } from '../guard/index'
// ------------------------------------------------------------------
// TypeGuard
// ------------------------------------------------------------------
import { IsTransform, IsSchema } from '../../type/guard/type'
import { IsTransform, IsSchema, IsUndefined } from '../../type/guard/type'
// ------------------------------------------------------------------
// Errors
// ------------------------------------------------------------------
@@ -121,9 +121,18 @@ function FromNot(schema: TNot, references: TSchema[], path: string, value: any)
// prettier-ignore
function FromObject(schema: TObject, references: TSchema[], path: string, value: any) {
if (!IsObject(value)) return Default(schema, path, value)
const knownKeys = KeyOfPropertyKeys(schema)
const knownKeys = KeyOfPropertyKeys(schema) as string[]
const knownProperties = { ...value } as Record<PropertyKey, unknown>
for(const key of knownKeys) if(key in knownProperties) {
for(const key of knownKeys) {
if(!HasPropertyKey(knownProperties, key)) continue
// if the property value is undefined, but the target is not, nor does it satisfy exact optional
// property policy, then we need to continue. This is a special case for optional property handling
// where a transforms wrapped in a optional modifiers should not run.
if(IsUndefinedValue(knownProperties[key]) && (
!IsUndefined(schema.properties[key]) ||
TypeSystemPolicy.IsExactOptionalProperty(knownProperties, key)
)) continue
// decode property
knownProperties[key] = Visit(schema.properties[key], references, `${path}/${key}`, knownProperties[key])
}
if (!IsSchema(schema.additionalProperties)) {

View File

@@ -26,11 +26,11 @@ THE SOFTWARE.
---------------------------------------------------------------------------*/
import { TypeSystemPolicy } from '../../system/policy'
import { Kind, TransformKind } from '../../type/symbols/index'
import { TypeBoxError } from '../../type/error/index'
import { ValueError } from '../../errors/index'
import { KeyOfPropertyKeys, KeyOfPropertyEntries } from '../../type/keyof/index'
import { Index } from '../../type/indexed/index'
import { Deref } from '../deref/index'
import { Check } from '../check/index'
@@ -48,11 +48,11 @@ import type { TUnion } from '../../type/union/index'
// ------------------------------------------------------------------
// ValueGuard
// ------------------------------------------------------------------
import { IsObject, IsArray, IsValueType } from '../guard/index'
import { HasPropertyKey, IsObject, IsArray, IsValueType, IsUndefined as IsUndefinedValue } from '../guard/index'
// ------------------------------------------------------------------
// TypeGuard
// ------------------------------------------------------------------
import { IsTransform, IsSchema } from '../../type/guard/type'
import { IsTransform, IsSchema, IsUndefined } from '../../type/guard/type'
// ------------------------------------------------------------------
// Errors
// ------------------------------------------------------------------
@@ -126,7 +126,16 @@ function FromObject(schema: TObject, references: TSchema[], path: string, value:
if (!IsObject(defaulted)) return defaulted
const knownKeys = KeyOfPropertyKeys(schema) as string[]
const knownProperties = { ...defaulted } as Record<PropertyKey, unknown>
for(const key of knownKeys) if(key in knownProperties) {
for(const key of knownKeys) {
if(!HasPropertyKey(knownProperties, key)) continue
// if the property value is undefined, but the target is not, nor does it satisfy exact optional
// property policy, then we need to continue. This is a special case for optional property handling
// where a transforms wrapped in a optional modifiers should not run.
if(IsUndefinedValue(knownProperties[key]) && (
!IsUndefined(schema.properties[key]) ||
TypeSystemPolicy.IsExactOptionalProperty(knownProperties, key)
)) continue
// encode property
knownProperties[key] = Visit(schema.properties[key], references, `${path}/${key}`, knownProperties[key])
}
if (!IsSchema(schema.additionalProperties)) {

View File

@@ -21,4 +21,9 @@ describe('value/convert/Object', () => {
const R = Value.Convert(T, { x: '42', y: 'true', z: 'hello' })
Assert.IsEqual(R, { x: 42, y: true, z: 'hello' })
})
it('Should not convert missing properties', () => {
const T = Type.Object({ x: Type.Number() })
const R = Value.Convert(T, { })
Assert.IsEqual(R, { })
})
})

View File

@@ -1,5 +1,7 @@
import * as Encoder from './_encoder'
import { Assert } from '../../assert'
import { TypeSystemPolicy } from '@sinclair/typebox/system'
import { Value } from '@sinclair/typebox/value'
import { Type } from '@sinclair/typebox'
@@ -156,7 +158,7 @@ describe('value/transform/Object', () => {
// https://github.com/sinclairzx81/typebox/issues/859
// ----------------------------------------------------------------
it('Should decode for nested transform with renamed property', () => {
class User { constructor(public name: string, public createdAt: Date) {} }
class User { constructor(public name: string, public createdAt: Date) { } }
const TDate = Type.Transform(Type.Number())
.Decode(v => new Date(v))
.Encode(v => v.getTime())
@@ -185,7 +187,116 @@ describe('value/transform/Object', () => {
const B = Object.create(null); B.x = '1'
const D = Value.Decode(T, A)
const E = Value.Encode(T, B)
Assert.IsEqual(D, { x: '1' })
Assert.IsEqual(D, { x: '1' })
Assert.IsEqual(E, { x: 1 })
})
// ----------------------------------------------------------------
// https://github.com/sinclairzx81/typebox/issues/958
// ----------------------------------------------------------------
it('Should not decode missing optional properties 0', () => {
let Invoked = false
const S = Type.Transform(Type.RegExp(/foo/))
.Decode((value) => { Invoked = true; return value })
.Encode((value) => value)
const T = Type.Object({ value: Type.Optional(S) })
const D = Value.Decode(T, { value: 'foo' })
Assert.IsEqual(D, { value: 'foo' })
Assert.IsTrue(Invoked)
})
it('Should not decode missing optional properties 1', () => {
let Invoked = false
const S = Type.Transform(Type.RegExp(/foo/))
.Decode((value) => { Invoked = true; return value })
.Encode((value) => value)
const T = Type.Object({ value: Type.Optional(S) })
const D = Value.Decode(T, {})
Assert.IsEqual(D, {})
Assert.IsFalse(Invoked)
})
it('Should not decode missing optional properties 2', () => {
let Invoked = false
const S = Type.Transform(Type.RegExp(/foo/))
.Decode((value) => { Invoked = true; return value })
.Encode((value) => value)
const T = Type.Object({ value: Type.Optional(S) })
const D = Value.Decode(T, { value: undefined })
Assert.IsEqual(D, { value: undefined })
Assert.IsFalse(Invoked)
})
it('Should not decode missing optional properties 3 (ExactOptionalPropertyTypes)', () => {
let [Invoked, Revert] = [false, TypeSystemPolicy.ExactOptionalPropertyTypes]
TypeSystemPolicy.ExactOptionalPropertyTypes = true
const S = Type.Transform(Type.RegExp(/foo/))
.Decode((value) => { Invoked = true; return value })
.Encode((value) => value)
const T = Type.Object({ value: Type.Optional(S) })
const D = Value.Decode(T, {})
Assert.IsEqual(D, {})
Assert.IsFalse(Invoked)
TypeSystemPolicy.ExactOptionalPropertyTypes = Revert
})
it('Should not decode missing optional properties 4 (ExactOptionalPropertyTypes)', () => {
let [Invoked, Revert] = [false, TypeSystemPolicy.ExactOptionalPropertyTypes]
TypeSystemPolicy.ExactOptionalPropertyTypes = true
const S = Type.Transform(Type.RegExp(/foo/))
.Decode((value) => { Invoked = true; return value })
.Encode((value) => value)
const T = Type.Object({ value: Type.Optional(S) })
Assert.Throws(() => Value.Decode(T, { value: undefined }))
Assert.IsFalse(Invoked)
TypeSystemPolicy.ExactOptionalPropertyTypes = Revert
})
it('Should not encode missing optional properties 0', () => {
let Invoked = false
const S = Type.Transform(Type.RegExp(/foo/))
.Decode((value) => value)
.Encode((value) => { Invoked = true; return value })
const T = Type.Object({ value: Type.Optional(S) })
const D = Value.Encode(T, { value: 'foo' })
Assert.IsEqual(D, { value: 'foo' })
Assert.IsTrue(Invoked)
})
it('Should not encode missing optional properties 1', () => {
let Invoked = false
const S = Type.Transform(Type.RegExp(/foo/))
.Decode((value) => value)
.Encode((value) => { Invoked = true; return value })
const T = Type.Object({ value: Type.Optional(S) })
const D = Value.Encode(T, {})
Assert.IsEqual(D, {})
Assert.IsFalse(Invoked)
})
it('Should not encode missing optional properties 2', () => {
let Invoked = false
const S = Type.Transform(Type.RegExp(/foo/))
.Decode((value) => value)
.Encode((value) => { Invoked = true; return value })
const T = Type.Object({ value: Type.Optional(S) })
const D = Value.Encode(T, { value: undefined })
Assert.IsEqual(D, { value: undefined })
Assert.IsFalse(Invoked)
})
it('Should not encode missing optional properties 3 (ExactOptionalPropertyTypes)', () => {
let [Invoked, Revert] = [false, TypeSystemPolicy.ExactOptionalPropertyTypes]
TypeSystemPolicy.ExactOptionalPropertyTypes = true
const S = Type.Transform(Type.RegExp(/foo/))
.Decode((value) => value)
.Encode((value) => { Invoked = true; return value })
const T = Type.Object({ value: Type.Optional(S) })
const D = Value.Encode(T, {})
Assert.IsEqual(D, {})
Assert.IsFalse(Invoked)
TypeSystemPolicy.ExactOptionalPropertyTypes = Revert
})
it('Should not encode missing optional properties 4 (ExactOptionalPropertyTypes)', () => {
let [Invoked, Revert] = [false, TypeSystemPolicy.ExactOptionalPropertyTypes]
TypeSystemPolicy.ExactOptionalPropertyTypes = true
const S = Type.Transform(Type.RegExp(/foo/))
.Decode((value) => value)
.Encode((value) => { Invoked = true; return value })
const T = Type.Object({ value: Type.Optional(S) })
Assert.Throws(() => Value.Encode(T, { value: undefined }))
Assert.IsFalse(Invoked)
TypeSystemPolicy.ExactOptionalPropertyTypes = Revert
})
})