Skip to content

Commit f29b798

Browse files
committed
fix: Intersect codec multirun
1 parent b14eb6b commit f29b798

File tree

2 files changed

+78
-7
lines changed

2 files changed

+78
-7
lines changed

src/value/codec/from-intersect.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,58 @@ THE SOFTWARE.
2929
// deno-fmt-ignore-file
3030

3131
import { Guard } from '../../guard/index.ts'
32-
import { type TIntersect, type TProperties } from '../../type/index.ts'
32+
import { type TIntersect, type TProperties, type TSchema, IsObject } from '../../type/index.ts'
3333
import { FromType } from './from-type.ts'
3434
import { Callback } from './callback.ts'
3535

36+
// ------------------------------------------------------------------
37+
// ProcessIntersectSchemas
38+
// ------------------------------------------------------------------
39+
// Processes intersection schemas while tracking which properties have been
40+
// handled to prevent duplicate codec execution. When the same property appears
41+
// in multiple schemas within an intersection, only the first occurrence is
42+
// processed to avoid running codecs multiple times.
43+
function ProcessIntersectSchemas(
44+
direction: string,
45+
context: TProperties,
46+
schemas: TSchema[],
47+
value: unknown
48+
): unknown {
49+
const processedKeys = new Set<string>()
50+
51+
for (const schema of schemas) {
52+
if (IsObject(schema)) {
53+
// For object schemas, manually process properties that haven't been seen yet
54+
if (!Guard.IsObjectNotArray(value)) continue
55+
56+
for (const key of Guard.Keys(schema.properties)) {
57+
if (!processedKeys.has(key) && Guard.HasPropertyKey(value, key)) {
58+
// Process this property through its codec
59+
value[key] = FromType(direction, context, schema.properties[key], value[key])
60+
processedKeys.add(key)
61+
}
62+
}
63+
} else {
64+
// For non-object schemas, process the entire value
65+
value = FromType(direction, context, schema, value)
66+
}
67+
}
68+
69+
return value
70+
}
3671
// ------------------------------------------------------------------
3772
// Decode
3873
// ------------------------------------------------------------------
3974
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-
}
75+
value = ProcessIntersectSchemas(direction, context, type.allOf, value)
4376
return Callback(direction, context, type, value)
4477
}
4578
// ------------------------------------------------------------------
4679
// Encode
4780
// ------------------------------------------------------------------
4881
function Encode(direction: string, context: TProperties, type: TIntersect, value: unknown): unknown {
4982
let exterior = Callback(direction, context, type, value)
50-
for (const schema of type.allOf) {
51-
exterior = FromType(direction, context, schema, exterior)
52-
}
83+
exterior = ProcessIntersectSchemas(direction, context, type.allOf, exterior)
5384
return exterior
5485
}
5586
export function FromIntersect(direction: string, context: TProperties, type: TIntersect, value: unknown): unknown {

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,43 @@ Test('Should Intersect 4', () => {
7272
Assert.Throws(() => Value.Decode(T, 1))
7373
Assert.Throws(() => Value.Encode(T, [null]))
7474
})
75+
// ------------------------------------------------------------------
76+
// Duplicate Properties
77+
// ------------------------------------------------------------------
78+
Test('Should Intersect 5: Duplicate properties should not run codec twice', () => {
79+
let decodeCount = 0
80+
let encodeCount = 0
81+
82+
const IdCodec = Type.Codec(
83+
Type.Object({
84+
table: Type.String(),
85+
id: Type.String()
86+
})
87+
)
88+
.Decode((encoded) => {
89+
decodeCount++
90+
return `${encoded.table}:${encoded.id}`
91+
})
92+
.Encode((decoded) => {
93+
encodeCount++
94+
const [table, id] = decoded.split(':') as [string, string]
95+
return { table, id }
96+
})
97+
98+
const T = Type.Intersect([
99+
Type.Object({ id: IdCodec }),
100+
Type.Object({ id: IdCodec }),
101+
])
102+
103+
// Reset counts
104+
decodeCount = 0
105+
encodeCount = 0
106+
107+
const D = Value.Decode(T, { id: { table: 't', id: 'x' } })
108+
Assert.IsEqual(D, { id: 't:x' })
109+
Assert.IsEqual(decodeCount, 1) // Should only decode once, not twice
110+
111+
const E = Value.Encode(T, D)
112+
Assert.IsEqual(E, { id: { table: 't', id: 'x' } })
113+
Assert.IsEqual(encodeCount, 1) // Should only encode once, not twice
114+
})

0 commit comments

Comments
 (0)