Skip to content

Commit 37983ce

Browse files
authored
Version 1.1.20 (#1568)
* Union Priority Sort * ChangeLog * Version
1 parent 7a5369d commit 37983ce

15 files changed

Lines changed: 600 additions & 102 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.20](https://github.com/sinclairzx81/typebox/pull/1568)
7+
- Union Priority Sort for Clean, Encode and Decode
68
- [Revision 1.1.19](https://github.com/sinclairzx81/typebox/pull/1566)
79
- Improved Intersect Encode and Decode Handling
810
- [Revision 1.1.18](https://github.com/sinclairzx81/typebox/pull/1565)

src/value/clean/from-union.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ import { Check } from '../check/index.ts'
3333
import { Clone } from '../clone/index.ts'
3434
import { FromType } from './from-type.ts'
3535

36+
import { UnionPrioritySort } from '../shared/union-priority-sort.ts'
37+
3638
export function FromUnion(context: TProperties, type: TUnion, value: unknown): unknown {
37-
for (const schema of type.anyOf) {
39+
for (const schema of UnionPrioritySort(type.anyOf)) {
3840
const clean = FromType(context, schema, Clone(value))
3941
if(Check(context, schema, clean)) return clean
4042
}

src/value/codec/from-object.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ import { Guard } from '../../guard/index.ts'
3333
import { type TObject, type TProperties } from '../../type/index.ts'
3434
import { FromType } from './from-type.ts'
3535
import { Callback } from './callback.ts'
36-
import { IsOptionalUndefined } from '../shared/index.ts'
36+
37+
import { IsOptionalUndefined } from '../shared/optional-undefined.ts'
3738

3839
// ------------------------------------------------------------------
3940
// Decode

src/value/codec/from-union.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,34 +28,32 @@ THE SOFTWARE.
2828

2929
// deno-fmt-ignore-file
3030

31-
import { Unreachable } from '../../system/unreachable/index.ts'
3231
import { Guard } from '../../guard/index.ts'
3332
import { type TProperties, type TUnion } from '../../type/index.ts'
3433
import { Callback } from './callback.ts'
3534
import { FromType } from './from-type.ts'
3635
import { Clone } from '../clone/index.ts'
3736
import { Check } from '../check/index.ts'
3837

38+
import { UnionPrioritySort } from '../shared/union-priority-sort.ts'
39+
3940
// ------------------------------------------------------------------
4041
// Decode
4142
// ------------------------------------------------------------------
42-
// deno-coverage-ignore-start - unreachable | checked
4343
function Decode(direction: string, context: TProperties, type: TUnion, value: unknown): unknown {
44-
for (const schema of type.anyOf) {
44+
for (const schema of UnionPrioritySort(type.anyOf, 1)) {
4545
if(!Check(context, schema, value)) continue
4646
const variant = FromType(direction, context, schema, value)
4747
return Callback(direction, context, type, variant)
4848
}
49-
return Unreachable()
49+
return value
5050
}
51-
// deno-coverage-ignore-stop
52-
5351
// ------------------------------------------------------------------
5452
// Encode
5553
// ------------------------------------------------------------------
5654
function Encode(direction: string, context: TProperties, type: TUnion, value: unknown): unknown {
5755
let exterior = Callback(direction, context, type, value)
58-
for (const schema of type.anyOf) {
56+
for (const schema of UnionPrioritySort(type.anyOf, -1)) {
5957
const variant = FromType(direction, context, schema, Clone(exterior))
6058
if(!Check(context, schema, variant)) continue
6159
return variant

src/value/convert/from-object.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import { type TSchema, type TObject, type TProperties } from '../../type/index.t
3232
import { Guard } from '../../guard/index.ts'
3333
import { FromType } from './from-type.ts'
3434
import { FromAdditionalProperties } from './from-additional.ts'
35-
import { IsOptionalUndefined } from '../shared/index.ts'
35+
36+
import { IsOptionalUndefined } from '../shared/optional-undefined.ts'
3637

3738
// ------------------------------------------------------------------
3839
// FromProperties

src/value/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ export * from './delta/index.ts'
4646
export * from './pipeline/index.ts'
4747
export * from './pointer/index.ts'
4848
export * from './repair/index.ts'
49-
49+
// ------------------------------------------------------------------
50+
// Shared
51+
// ------------------------------------------------------------------
52+
export * from './shared/index.ts'
5053
// ------------------------------------------------------------------
5154
// Default
5255
// ------------------------------------------------------------------

src/value/repair/from-union.ts

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -30,71 +30,23 @@ THE SOFTWARE.
3030

3131
import { IsDefault } from '../../schema/types/index.ts'
3232

33-
import { type TProperties, type TSchema, type TUnion, Union, IsLiteral, IsObject, IsRef } from '../../type/index.ts'
33+
import { type TProperties, type TUnion, Union } from '../../type/index.ts'
3434
import { Flatten } from '../../type/engine/evaluate/index.ts'
35-
import { Guard } from '../../guard/index.ts'
3635
import { Check } from '../check/index.ts'
3736
import { Clone } from '../clone/index.ts'
3837
import { Create } from '../create/index.ts'
39-
import { RepairError } from './error.ts'
4038
import { FromType } from './from-type.ts'
4139

42-
// ------------------------------------------------------------------
43-
// Deref
44-
// ------------------------------------------------------------------
45-
function Deref(context: TProperties, type: TSchema, value: unknown): TSchema {
46-
return IsRef(type)
47-
? Guard.HasPropertyKey(context, type.$ref)
48-
? Deref(context, context[type.$ref], value)
49-
: (() => { throw new RepairError(context, type, value, 'Unable to Deref target on Union repair') })()
50-
: type
51-
}
40+
import { UnionScoreSelect } from '../shared/union-score-select.ts'
5241

53-
// ------------------------------------------------------------------
54-
// The following will score a schema against a value. For objects,
55-
// the score is the tally of points awarded for each property of
56-
// the value. Property points are (1.0 / propertyCount) to prevent
57-
// large property counts biasing results. Properties that match
58-
// literal values are maximally awarded as literals are typically
59-
// used as union discriminator fields.
60-
// ------------------------------------------------------------------
61-
function ScoreVariant(context: TProperties, type: TSchema, value: unknown): number {
62-
// scoring is only possible for object types.
63-
if(!(IsObject(type) && Guard.IsObject(value))) return 0
64-
65-
const keys = Guard.Keys(value)
66-
const entries = Guard.Entries(type.properties)
67-
return entries.reduce((result, [key, schema]) => {
68-
const literal = IsLiteral(schema) && Guard.IsEqual(schema.const, value[key]) ? 100 : 0
69-
const checks = Check(context, schema, value[key]) ? 10 : 0
70-
const exists = keys.includes(key) ? 1 : 0
71-
return result + (literal + checks + exists)
72-
}, 0)
73-
}
74-
// ------------------------------------------------------------------
75-
// SelectVariant
76-
// ------------------------------------------------------------------
77-
function SelectVariant(context: TProperties, type: TUnion, value: unknown): TSchema {
78-
const schemas = type.anyOf.map((schema) => Deref(context, schema, value))
79-
let [select, best] = [schemas[0], 0]
80-
for (const schema of schemas) {
81-
const score = ScoreVariant(context, schema, value)
82-
if (score > best) {
83-
select = schema
84-
best = score
85-
}
86-
}
87-
return select
88-
}
8942
// ------------------------------------------------------------------
9043
// RepairUnion
9144
// ------------------------------------------------------------------
9245
function RepairUnion(context: TProperties, type: TUnion, value: unknown): unknown {
9346
const union = Union(Flatten(type.anyOf))
94-
const schema = SelectVariant(context, union, value)
47+
const schema = UnionScoreSelect(context, union, value)
9548
return FromType(context, schema, value)
9649
}
97-
9850
export function FromUnion(context: TProperties, type: TUnion, value: unknown): unknown {
9951
if (Check(context, type, value)) return Clone(value)
10052
if (IsDefault(type)) return Create(context, type)

src/value/shared/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@ THE SOFTWARE.
2828

2929
// deno-fmt-ignore-file
3030

31-
export * from './optional-undefined.ts'
31+
export * from './optional-undefined.ts'
32+
export * from './union-priority-sort.ts'
33+
export * from './union-score-select.ts'

src/value/shared/optional-undefined.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ THE SOFTWARE.
3131
import { Guard } from '../../guard/index.ts'
3232
import { type TSchema, IsOptional } from '../../type/index.ts'
3333

34-
3534
// ------------------------------------------------------------------
3635
// IsOptionalUndefined
3736
//
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*--------------------------------------------------------------------------
2+
3+
TypeBox
4+
5+
The MIT License (MIT)
6+
7+
Copyright (c) 2017-2026 Haydn Paterson
8+
9+
Permission is hereby granted, free of charge, to any person obtaining a copy
10+
of this software and associated documentation files (the "Software"), to deal
11+
in the Software without restriction, including without limitation the rights
12+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
copies of the Software, and to permit persons to whom the Software is
14+
furnished to do so, subject to the following conditions:
15+
16+
The above copyright notice and this permission notice shall be included in
17+
all copies or substantial portions of the Software.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
27+
---------------------------------------------------------------------------*/
28+
29+
// deno-fmt-ignore-file
30+
31+
import { Guard } from '../../guard/index.ts'
32+
import { Compare, type TSchema } from '../../type/index.ts'
33+
34+
// ------------------------------------------------------------------
35+
// DeterministicCompare
36+
//
37+
// Provides a deterministic tie-break for schemas. This is used when
38+
// schemas are structurally disjoint or mutually inclusive. While
39+
// JSON serialization incurs a performance overhead, it serves as a
40+
// reliable mechanism to ensure stable ordering and preserves the
41+
// alphabetical alignment of named constants.
42+
//
43+
// ------------------------------------------------------------------
44+
function DeterministicCompare(left: TSchema, right: TSchema): number {
45+
return JSON.stringify(left).localeCompare(JSON.stringify(right))
46+
}
47+
// ------------------------------------------------------------------
48+
// UnionPrioritySort
49+
//
50+
// Performs a deterministic sort on Union members. By default, this
51+
// function ensures that narrow (more specific) types precede broader
52+
// types in the resulting array. The order can be reversed by setting
53+
// the order property to -1 which will reverse unions from broader
54+
// to more narrow.
55+
//
56+
// ------------------------------------------------------------------
57+
58+
/** Deterministically sorts schemas by structural relationship (narrow to broad) */
59+
export function UnionPrioritySort(types: TSchema[], order: number = 1): TSchema[] {
60+
return types.sort((left, right) => {
61+
const result = Compare(left, right) as string
62+
return (
63+
Guard.IsEqual(result, 'disjoint') ? DeterministicCompare(left, right) :
64+
Guard.IsEqual(result, 'right-inside') ? 1 :
65+
Guard.IsEqual(result, 'left-inside') ? -1 :
66+
DeterministicCompare(left, right)
67+
) * order
68+
})
69+
}

0 commit comments

Comments
 (0)