Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 49 additions & 7 deletions src/value/codec/from-intersect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,69 @@ THE SOFTWARE.
// deno-fmt-ignore-file

import { Guard } from '../../guard/index.ts'
import { type TIntersect, type TProperties } from '../../type/index.ts'
import { type TIntersect, type TProperties, type TSchema, IsObject, IsOptional } from '../../type/index.ts'
import { FromType } from './from-type.ts'
import { Callback } from './callback.ts'

// ------------------------------------------------------------------
// ProcessIntersectSchemas
// ------------------------------------------------------------------
// Processes intersection schemas while tracking which properties have been
// handled to prevent duplicate codec execution. When the same property appears
// in multiple schemas within an intersection, only the first occurrence is
// processed to avoid running codecs multiple times.
function ProcessIntersectSchemas(
direction: string,
context: TProperties,
schemas: TSchema[],
value: unknown
): unknown {
const nonProcessedKeys = new Set<string>()

// make sure we don't process optional undefined values
for (const schema of schemas) {
if (!IsObject(schema) || !Guard.IsObjectNotArray(value)) continue;
for (const key of Guard.Keys(schema.properties)) {
if (nonProcessedKeys.has(key)) continue
if (!Guard.IsUndefined(value[key]) || !IsOptional(schema.properties[key])) {
nonProcessedKeys.add(key)
}
}
}

for (const schema of schemas) {
if (IsObject(schema)) {
// For object schemas, manually process properties that haven't been seen yet
if (!Guard.IsObjectNotArray(value)) continue

for (const key of Guard.Keys(schema.properties)) {
if (nonProcessedKeys.has(key) && Guard.HasPropertyKey(value, key)) {
// Process this property through its codec
value[key] = FromType(direction, context, schema.properties[key], value[key])
nonProcessedKeys.delete(key)
}
}
} else {
// For non-object schemas, process the entire value
value = FromType(direction, context, schema, value)
}
}

return value
}
// ------------------------------------------------------------------
// Decode
// ------------------------------------------------------------------
function Decode(direction: string, context: TProperties, type: TIntersect, value: unknown): unknown {
for (const schema of type.allOf) {
value = FromType(direction, context, schema, value)
}
value = ProcessIntersectSchemas(direction, context, type.allOf, value)
return Callback(direction, context, type, value)
}
// ------------------------------------------------------------------
// Encode
// ------------------------------------------------------------------
function Encode(direction: string, context: TProperties, type: TIntersect, value: unknown): unknown {
let exterior = Callback(direction, context, type, value)
for (const schema of type.allOf) {
exterior = FromType(direction, context, schema, exterior)
}
exterior = ProcessIntersectSchemas(direction, context, type.allOf, exterior)
return exterior
}
export function FromIntersect(direction: string, context: TProperties, type: TIntersect, value: unknown): unknown {
Expand Down
40 changes: 40 additions & 0 deletions test/typebox/runtime/value/codec/intersect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,43 @@ Test('Should Intersect 4', () => {
Assert.Throws(() => Value.Decode(T, 1))
Assert.Throws(() => Value.Encode(T, [null]))
})
// ------------------------------------------------------------------
// Duplicate Properties
// ------------------------------------------------------------------
Test('Should Intersect 5: Duplicate properties should not run codec twice', () => {
let decodeCount = 0
let encodeCount = 0

const IdCodec = Type.Codec(
Type.Object({
table: Type.String(),
id: Type.String()
})
)
.Decode((encoded) => {
decodeCount++
return `${encoded.table}:${encoded.id}`
})
.Encode((decoded) => {
encodeCount++
const [table, id] = decoded.split(':') as [string, string]
return { table, id }
})

const T = Type.Intersect([
Type.Object({ id: IdCodec }),
Type.Object({ id: IdCodec }),
])

// Reset counts
decodeCount = 0
encodeCount = 0

const D = Value.Decode(T, { id: { table: 't', id: 'x' } })
Assert.IsEqual(D, { id: 't:x' })
Assert.IsEqual(decodeCount, 1) // Should only decode once, not twice

const E = Value.Encode(T, D)
Assert.IsEqual(E, { id: { table: 't', id: 'x' } })
Assert.IsEqual(encodeCount, 1) // Should only encode once, not twice
})