Skip to content

Commit b8face8

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

File tree

2 files changed

+90
-8
lines changed

2 files changed

+90
-8
lines changed

src/value/codec/from-intersect.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,70 @@ THE SOFTWARE.
2828

2929
// deno-fmt-ignore-file
3030

31-
import { Guard } from '../../guard/index.ts'
32-
import { type TIntersect, type TProperties } from '../../type/index.ts'
31+
import { Guard, } from '../../guard/index.ts'
32+
import { type TIntersect, type TProperties, type TSchema, IsObject, IsOptional } 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 nonProcessedKeys = new Set<string>()
50+
51+
// make sure we don't process optional undefined values
52+
for (const schema of schemas) {
53+
if (!IsObject(schema) || !Guard.IsObjectNotArray(value)) continue;
54+
for (const key of Guard.Keys(schema.properties)) {
55+
if (nonProcessedKeys.has(key)) continue
56+
if (!Guard.IsUndefined(value[key]) || !IsOptional(schema.properties[key])) {
57+
nonProcessedKeys.add(key)
58+
}
59+
}
60+
}
61+
62+
for (const schema of schemas) {
63+
if (IsObject(schema)) {
64+
// For object schemas, manually process properties that haven't been seen yet
65+
if (!Guard.IsObjectNotArray(value)) continue
66+
67+
for (const key of Guard.Keys(schema.properties)) {
68+
if (nonProcessedKeys.has(key) && Guard.HasPropertyKey(value, key)) {
69+
// Process this property through its codec
70+
value[key] = FromType(direction, context, schema.properties[key], value[key])
71+
nonProcessedKeys.delete(key)
72+
}
73+
}
74+
} else {
75+
// For non-object schemas, process the entire value
76+
value = FromType(direction, context, schema, value)
77+
}
78+
}
79+
80+
return value
81+
}
3682
// ------------------------------------------------------------------
3783
// Decode
3884
// ------------------------------------------------------------------
3985
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-
}
86+
value = ProcessIntersectSchemas(direction, context, type.allOf, value)
4387
return Callback(direction, context, type, value)
4488
}
4589
// ------------------------------------------------------------------
4690
// Encode
4791
// ------------------------------------------------------------------
4892
function Encode(direction: string, context: TProperties, type: TIntersect, value: unknown): unknown {
4993
let exterior = Callback(direction, context, type, value)
50-
for (const schema of type.allOf) {
51-
exterior = FromType(direction, context, schema, exterior)
52-
}
94+
exterior = ProcessIntersectSchemas(direction, context, type.allOf, exterior)
5395
return exterior
5496
}
5597
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)