Skip to content

Commit 7a5369d

Browse files
authored
Version 1.1.19 (#1566)
* Improved Intersect Encode and Decode Handling * ChangeLog * Version
1 parent e54b5e1 commit 7a5369d

4 files changed

Lines changed: 171 additions & 15 deletions

File tree

changelog/1.1.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
---
44

55
### Version Updates
6+
- [Revision 1.1.19](https://github.com/sinclairzx81/typebox/pull/1566)
7+
- Improved Intersect Encode and Decode Handling
68
- [Revision 1.1.18](https://github.com/sinclairzx81/typebox/pull/1565)
79
- Use Explicit Static Return Type Annotation for Value Functions
810
- [Revision 1.1.17](https://github.com/sinclairzx81/typebox/pull/1564)

src/value/codec/from-intersect.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,53 @@ import { Guard } from '../../guard/index.ts'
3232
import { type TIntersect, type TProperties } from '../../type/index.ts'
3333
import { FromType } from './from-type.ts'
3434
import { Callback } from './callback.ts'
35+
import { Clone } from '../clone/index.ts'
36+
import { Clean } from '../clean/index.ts'
3537

38+
// ------------------------------------------------------------------
39+
// MergeInteriors
40+
//
41+
// Merges all interior operand results into a single object. Each
42+
// subsequent operand's properties override those of prior operands.
43+
//
44+
// ------------------------------------------------------------------
45+
function MergeInteriors(interiors: Record<PropertyKey, unknown>[]): unknown {
46+
return interiors.reduce((results, interior) => ({ ...results, ...interior }), {})
47+
}
48+
// ------------------------------------------------------------------
49+
// NonMatchingInterior
50+
//
51+
// Used when Intersect operands do not all produce Objects. Returns
52+
// the first interior result that differs from the original value,
53+
// indicating a Codec has transformed the data. If no operand
54+
// produced a change, defaults to the first interior result.
55+
//
56+
// ------------------------------------------------------------------
57+
function NonMatchingInterior(value: unknown, interiors: unknown[]) {
58+
for (const interior of interiors) if (!Guard.IsDeepEqual(value, interior)) return interior
59+
return value // value-unchanged
60+
}
3661
// ------------------------------------------------------------------
3762
// Decode
3863
// ------------------------------------------------------------------
3964
function Decode(direction: string, context: TProperties, type: TIntersect, value: unknown): unknown {
40-
for (const schema of type.allOf) {
41-
value = FromType(direction, context, schema, value)
42-
}
43-
return Callback(direction, context, type, value)
65+
if (Guard.IsEqual(type.allOf.length, 0)) return Callback(direction, context, type, value)
66+
const interiors = type.allOf.map((schema) => FromType(direction, context, schema, Clean(schema, Clone(value))))
67+
const structural = interiors.every((result) => Guard.IsObject(result))
68+
const exterior = structural ? MergeInteriors(interiors) : NonMatchingInterior(value, interiors)
69+
return Callback(direction, context, type, exterior)
4470
}
4571
// ------------------------------------------------------------------
4672
// Encode
4773
// ------------------------------------------------------------------
4874
function Encode(direction: string, context: TProperties, type: TIntersect, value: unknown): unknown {
49-
let exterior = Callback(direction, context, type, value)
50-
for (const schema of type.allOf) {
51-
exterior = FromType(direction, context, schema, exterior)
52-
}
53-
return exterior
75+
if (Guard.IsEqual(type.allOf.length, 0)) return Callback(direction, context, type, value)
76+
const exterior = Callback(direction, context, type, value)
77+
const interiors = type.allOf.map((schema) => FromType(direction, context, schema, Clean(schema, Clone(exterior))))
78+
const structural = interiors.every((result) => Guard.IsObject(result))
79+
if (structural) return MergeInteriors(interiors)
80+
return NonMatchingInterior(exterior, interiors)
5481
}
5582
export function FromIntersect(direction: string, context: TProperties, type: TIntersect, value: unknown): unknown {
56-
return Guard.IsEqual(direction, 'Decode')
57-
? Decode(direction, context, type, value)
58-
: Encode(direction, context, type, value)
83+
return Guard.IsEqual(direction, 'Decode') ? Decode(direction, context, type, value) : Encode(direction, context, type, value)
5984
}

tasks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Metrics } from './task/metrics/index.ts'
99
import { Spec } from './task/spec/index.ts'
1010
import { Task } from 'tasksmith'
1111

12-
const Version = '1.1.18'
12+
const Version = '1.1.19'
1313

1414
// ------------------------------------------------------------------
1515
// Build

test/typebox/runtime/value/codec/intersect.ts

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Test('Should Intersect 3', () => {
5353
Assert.IsEqual(D, '1')
5454
Assert.IsEqual(E, 1)
5555
})
56-
Test('Should Intersect 3', () => {
56+
Test('Should Intersect 4', () => {
5757
const NumberToString = Type.Codec(Type.Number())
5858
.Decode((value) => value.toString())
5959
.Encode((value) => parseFloat(value))
@@ -67,8 +67,137 @@ Test('Should Intersect 3', () => {
6767
// ------------------------------------------------------------------
6868
// Illogical
6969
// ------------------------------------------------------------------
70-
Test('Should Intersect 4', () => {
70+
Test('Should Intersect 5', () => {
7171
const T = Type.Intersect([Type.Array(Type.Null()), Type.Number()])
7272
Assert.Throws(() => Value.Decode(T, 1))
7373
Assert.Throws(() => Value.Encode(T, [null]))
7474
})
75+
// ------------------------------------------------------------------
76+
// Intersect Operand Should Not Break Subsequent Operands
77+
//
78+
// https://github.com/sinclairzx81/typebox/issues/1466
79+
// ------------------------------------------------------------------
80+
Test('Should Intersect 6', () => {
81+
let C = 0
82+
const T = Type.Codec(Type.Object({
83+
L: Type.String(),
84+
R: Type.String()
85+
}))
86+
.Decode((encoded) => (`${encoded.L}:${encoded.R}`))
87+
.Encode((decoded) => {
88+
C = C + 1
89+
const [L, R] = decoded.split(':') as [string, string]
90+
return { L, R }
91+
})
92+
const S = Type.Intersect([
93+
Type.Object({ id: T }),
94+
Type.Object({ id: T })
95+
])
96+
const D = Value.Decode(S, { id: { L: 'L', R: 'R' } })
97+
const E = Value.Encode(S, D)
98+
// Expect Multiple Calls Per Operand
99+
Assert.IsEqual(C, 2)
100+
Assert.IsEqual(D, { id: 'L:R' })
101+
Assert.IsEqual(E, { id: { L: 'L', R: 'R' } })
102+
})
103+
// ------------------------------------------------------------------
104+
// Intersect With Outer Codec
105+
//
106+
// Verifies the outer Intersect-level Codec is applied after merging
107+
// interiors on Decode, and before on Encode.
108+
// ------------------------------------------------------------------
109+
Test('Should Intersect 7', () => {
110+
const T = Type.Codec(Type.Intersect([
111+
Type.Object({ x: Type.Number() }),
112+
Type.Object({ y: Type.Number() })
113+
]))
114+
.Decode((value) => ({ ...value, decoded: true }))
115+
.Encode(({ decoded: _, ...rest }) => rest)
116+
117+
const D = Value.Decode(T, { x: 1, y: 2 })
118+
const E = Value.Encode(T, D)
119+
Assert.IsEqual(D, { x: 1, y: 2, decoded: true })
120+
Assert.IsEqual(E, { x: 1, y: 2 })
121+
})
122+
// ------------------------------------------------------------------
123+
// Intersect Key Override
124+
//
125+
// Verifies that when two operands produce the same key, the latter
126+
// operand's value wins on merge.
127+
// ------------------------------------------------------------------
128+
Test('Should Intersect 8', () => {
129+
const Increment = Type.Codec(Type.Number())
130+
.Decode((value) => value + 1)
131+
.Encode((value) => value - 1)
132+
133+
const T = Type.Intersect([
134+
Type.Object({ n: Type.Number() }),
135+
Type.Object({ n: Increment })
136+
])
137+
const D = Value.Decode(T, { n: 1 })
138+
const E = Value.Encode(T, D)
139+
Assert.IsEqual(D, { n: 2 }) // latter operand wins
140+
Assert.IsEqual(E, { n: 1 })
141+
})
142+
// ------------------------------------------------------------------
143+
// Empty Intersect
144+
//
145+
// Verifies that an Intersect with no operands decodes and encodes
146+
// without error, returning an empty object.
147+
// ------------------------------------------------------------------
148+
Test('Should Intersect 9', () => {
149+
const T = Type.Intersect([])
150+
const D = Value.Decode(T, {})
151+
const E = Value.Encode(T, D)
152+
Assert.IsEqual(D, {})
153+
Assert.IsEqual(E, {})
154+
})
155+
// ------------------------------------------------------------------
156+
// Empty Intersect
157+
//
158+
// Verifies that an empty Intersect returns the original value
159+
// unchanged for both Decode and Encode.
160+
// ------------------------------------------------------------------
161+
Test('Should Intersect 10', () => {
162+
const T = Type.Intersect([])
163+
const D = Value.Decode(T, 42)
164+
const E = Value.Encode(T, 42)
165+
Assert.IsEqual(D, 42)
166+
Assert.IsEqual(E, 42)
167+
})
168+
// ------------------------------------------------------------------
169+
// Primitive With No Transformation
170+
//
171+
// Verifies that when no operand transforms the value,
172+
// NonMatchingInterior correctly falls back to the first result.
173+
// ------------------------------------------------------------------
174+
Test('Should Intersect 11', () => {
175+
const T = Type.Intersect([Type.Number(), Type.Number()])
176+
const D = Value.Decode(T, 42)
177+
const E = Value.Encode(T, 42)
178+
Assert.IsEqual(D, 42)
179+
Assert.IsEqual(E, 42)
180+
})
181+
// ------------------------------------------------------------------
182+
// Nested Intersect
183+
//
184+
// Verifies that codec transformation composes correctly when
185+
// Intersect types are nested inside one another.
186+
// ------------------------------------------------------------------
187+
Test('Should Intersect 12', () => {
188+
const NumberToString = Type.Codec(Type.Number())
189+
.Decode((value) => value.toString())
190+
.Encode((value) => parseFloat(value))
191+
192+
const Inner = Type.Intersect([
193+
Type.Object({ x: NumberToString })
194+
])
195+
const Outer = Type.Intersect([
196+
Inner,
197+
Type.Object({ y: NumberToString })
198+
])
199+
const D = Value.Decode(Outer, { x: 1, y: 2 })
200+
const E = Value.Encode(Outer, D)
201+
Assert.IsEqual(D, { x: '1', y: '2' })
202+
Assert.IsEqual(E, { x: 1, y: 2 })
203+
})

0 commit comments

Comments
 (0)