Skip to content

Commit

Permalink
Revision 0.32.16 (#791)
Browse files Browse the repository at this point in the history
* Update Indexed Union Evaluation

* Use Reduce Strategy for Union and Intersect Value Conversion

* Add Path to Transform Codec Errors

* Support Control Character Escape in Template Literal Syntax

* Intersect Union Convert Test Case

* Revision 0.32.16
  • Loading branch information
sinclairzx81 committed Mar 19, 2024
1 parent abe71c6 commit 9fa2e28
Show file tree
Hide file tree
Showing 18 changed files with 333 additions and 150 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sinclair/typebox",
"version": "0.32.15",
"version": "0.32.16",
"description": "Json Schema Type Builder with Static Type Resolution for TypeScript",
"keywords": [
"typescript",
Expand Down
33 changes: 31 additions & 2 deletions src/type/indexed/indexed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,41 @@ function FromIntersect<T extends TSchema[], K extends PropertyKey>(T: [...T], K:
}
// ------------------------------------------------------------------
// FromUnionRest
//
// The following accept a tuple of indexed key results. When evaluating
// these results, we check if any result evaluated to TNever. For key
// indexed unions, a TNever result indicates that the key was not
// present on the variant. In these cases, we must evaluate the indexed
// union to TNever (as given by a [] result). This logic aligns to the
// following behaviour.
//
// Non-Overlapping Union
//
// type A = { a: string }
// type B = { b: string }
// type C = (A | B) & { a: number } // C is { a: number }
//
// Overlapping Union
//
// type A = { a: string }
// type B = { a: string }
// type C = (A | B) & { a: number } // C is { a: never }
//
// ------------------------------------------------------------------
// prettier-ignore
type TFromUnionRest<T extends TSchema[]> = T
type TFromUnionRest<T extends TSchema[], Acc extends TSchema[] = []> =
T extends [infer L extends TSchema, ...infer R extends TSchema[]]
? L extends TNever
? []
: TFromUnionRest<R, [L, ...Acc]>
: Acc
// prettier-ignore
function FromUnionRest<T extends TSchema[]>(T: [...T]): TFromUnionRest<T> {
return T as never // review this
return (
T.some(L => IsNever(L))
? []
: T
) as never
}
// ------------------------------------------------------------------
// FromUnion
Expand Down
29 changes: 24 additions & 5 deletions src/type/template-literal/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,42 @@ export type Expression = ExpressionAnd | ExpressionOr | ExpressionConst
export type ExpressionConst = { type: 'const'; const: string }
export type ExpressionAnd = { type: 'and'; expr: Expression[] }
export type ExpressionOr = { type: 'or'; expr: Expression[] }
// -------------------------------------------------------------------
// Unescape
//
// Unescape for these control characters specifically. Note that this
// function is only called on non union group content, and where we
// still want to allow the user to embed control characters in that
// content. For review.
// -------------------------------------------------------------------
// prettier-ignore
function Unescape(pattern: string) {
return pattern
.replace(/\\\$/g, '$')
.replace(/\\\*/g, '*')
.replace(/\\\^/g, '^')
.replace(/\\\|/g, '|')
.replace(/\\\(/g, '(')
.replace(/\\\)/g, ')')
}
// -------------------------------------------------------------------
// Control Characters
// -------------------------------------------------------------------
function IsNonEscaped(pattern: string, index: number, char: string) {
return pattern[index] === char && pattern.charCodeAt(index - 1) !== 92
}
// prettier-ignore
function IsOpenParen(pattern: string, index: number) {
return IsNonEscaped(pattern, index, '(')
}
// prettier-ignore
function IsCloseParen(pattern: string, index: number) {
return IsNonEscaped(pattern, index, ')')
}
// prettier-ignore
function IsSeparator(pattern: string, index: number) {
return IsNonEscaped(pattern, index, '|')
}
// prettier-ignore
// -------------------------------------------------------------------
// Control Groups
// -------------------------------------------------------------------
function IsGroup(pattern: string) {
if (!(IsOpenParen(pattern, 0) && IsCloseParen(pattern, pattern.length - 1))) return false
let count = 0
Expand Down Expand Up @@ -155,7 +174,7 @@ export function TemplateLiteralParse(pattern: string): Expression {
IsGroup(pattern) ? TemplateLiteralParse(InGroup(pattern)) :
IsPrecedenceOr(pattern) ? Or(pattern) :
IsPrecedenceAnd(pattern) ? And(pattern) :
{ type: 'const', const: pattern }
{ type: 'const', const: Unescape(pattern) }
)
}
// ------------------------------------------------------------------
Expand Down
5 changes: 4 additions & 1 deletion src/type/template-literal/syntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ type FromTerminal<T extends string> =
// prettier-ignore
type FromString<T extends string> =
T extends `{${infer L}}${infer R}` ? [FromTerminal<L>, ...FromString<R>] :
T extends `${infer L}$${infer R}` ? [TLiteral<L>, ...FromString<R>] :
// note: to correctly handle $ characters encoded in the sequence, we need to
// lookahead and test against opening and closing union groups.
T extends `${infer L}$\{${infer R1}\}${infer R2}` ? [TLiteral<L>, ...FromString<`{${R1}}`>, ...FromString<R2>] :
T extends `${infer L}$\{${infer R1}\}` ? [TLiteral<L>, ...FromString<`{${R1}}`>] :
T extends `${infer L}` ? [TLiteral<L>] :
[]

Expand Down
13 changes: 2 additions & 11 deletions src/value/convert/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,8 @@ function FromDate(schema: TDate, references: TSchema[], value: any): unknown {
function FromInteger(schema: TInteger, references: TSchema[], value: any): unknown {
return TryConvertInteger(value)
}
// prettier-ignore
function FromIntersect(schema: TIntersect, references: TSchema[], value: any): unknown {
const allObjects = schema.allOf.every(schema => IsObjectType(schema))
if(allObjects) return Visit(Composite(schema.allOf as TObject[]), references, value)
return Visit(schema.allOf[0], references, value) // todo: fix this
return schema.allOf.reduce((value, schema) => Visit(schema, references, value), value)
}
function FromLiteral(schema: TLiteral, references: TSchema[], value: any): unknown {
return TryConvertLiteral(schema, value)
Expand Down Expand Up @@ -243,13 +240,7 @@ function FromUndefined(schema: TUndefined, references: TSchema[], value: any): u
return TryConvertUndefined(value)
}
function FromUnion(schema: TUnion, references: TSchema[], value: any): unknown {
for (const subschema of schema.anyOf) {
const converted = Visit(subschema, references, value)
if (Check(subschema, references, converted)) {
return converted
}
}
return value
return schema.anyOf.reduce((value, schema) => Visit(schema, references, value), value)
}
function Visit(schema: TSchema, references: TSchema[], value: any): unknown {
const references_ = IsString(schema.$id) ? [...references, schema] : references
Expand Down
117 changes: 64 additions & 53 deletions src/value/transform/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,153 +57,164 @@ import { IsTransform, IsSchema } from '../../type/guard/type'
// Errors
// ------------------------------------------------------------------
// thrown externally
// prettier-ignore
export class TransformDecodeCheckError extends TypeBoxError {
constructor(public readonly schema: TSchema, public readonly value: unknown, public readonly error: ValueError) {
super(`Unable to decode due to invalid value`)
constructor(
public readonly schema: TSchema,
public readonly value: unknown,
public readonly error: ValueError
) {
super(`Unable to decode value as it does not match the expected schema`)
}
}
// prettier-ignore
export class TransformDecodeError extends TypeBoxError {
constructor(public readonly schema: TSchema, public readonly value: unknown, error: any) {
super(`${error instanceof Error ? error.message : 'Unknown error'}`)
constructor(
public readonly schema: TSchema,
public readonly path: string,
public readonly value: unknown,
public readonly error: Error,
) {
super(error instanceof Error ? error.message : 'Unknown error')
}
}
// ------------------------------------------------------------------
// Decode
// ------------------------------------------------------------------
// prettier-ignore
function Default(schema: TSchema, value: any) {
function Default(schema: TSchema, path: string, value: any) {
try {
return IsTransform(schema) ? schema[TransformKind].Decode(value) : value
} catch (error) {
throw new TransformDecodeError(schema, value, error)
throw new TransformDecodeError(schema, path, value, error as Error)
}
}
// prettier-ignore
function FromArray(schema: TArray, references: TSchema[], value: any): any {
function FromArray(schema: TArray, references: TSchema[], path: string, value: any): any {
return (IsArray(value))
? Default(schema, value.map((value: any) => Visit(schema.items, references, value)))
: Default(schema, value)
? Default(schema, path, value.map((value: any, index) => Visit(schema.items, references, `${path}/${index}`, value)))
: Default(schema, path, value)
}
// prettier-ignore
function FromIntersect(schema: TIntersect, references: TSchema[], value: any) {
if (!IsStandardObject(value) || IsValueType(value)) return Default(schema, value)
function FromIntersect(schema: TIntersect, references: TSchema[], path: string, value: any) {
if (!IsStandardObject(value) || IsValueType(value)) return Default(schema, path, value)
const knownKeys = KeyOfPropertyKeys(schema) as string[]
const knownProperties = knownKeys.reduce((value, key) => {
return (key in value)
? { ...value, [key]: Visit(Index(schema, [key]), references, value[key]) }
? { ...value, [key]: Visit(Index(schema, [key]), references, `${path}/${key}`, value[key]) }
: value
}, value)
if (!IsTransform(schema.unevaluatedProperties)) {
return Default(schema, knownProperties)
return Default(schema, path, knownProperties)
}
const unknownKeys = Object.getOwnPropertyNames(knownProperties)
const unevaluatedProperties = schema.unevaluatedProperties as TSchema
const unknownProperties = unknownKeys.reduce((value, key) => {
return !knownKeys.includes(key)
? { ...value, [key]: Default(unevaluatedProperties, value[key]) }
? { ...value, [key]: Default(unevaluatedProperties, `${path}/${key}`, value[key]) }
: value
}, knownProperties)
return Default(schema, unknownProperties)
return Default(schema, path, unknownProperties)
}
function FromNot(schema: TNot, references: TSchema[], value: any) {
return Default(schema, Visit(schema.not, references, value))
function FromNot(schema: TNot, references: TSchema[], path: string, value: any) {
return Default(schema, path, Visit(schema.not, references, path, value))
}
// prettier-ignore
function FromObject(schema: TObject, references: TSchema[], value: any) {
if (!IsStandardObject(value)) return Default(schema, value)
function FromObject(schema: TObject, references: TSchema[], path: string, value: any) {
if (!IsStandardObject(value)) return Default(schema, path, value)
const knownKeys = KeyOfPropertyKeys(schema)
const knownProperties = knownKeys.reduce((value, key) => {
return (key in value)
? { ...value, [key]: Visit(schema.properties[key], references, value[key]) }
? { ...value, [key]: Visit(schema.properties[key], references, `${path}/${key}`, value[key]) }
: value
}, value)
if (!IsSchema(schema.additionalProperties)) {
return Default(schema, knownProperties)
return Default(schema, path, knownProperties)
}
const unknownKeys = Object.getOwnPropertyNames(knownProperties)
const additionalProperties = schema.additionalProperties as TSchema
const unknownProperties = unknownKeys.reduce((value, key) => {
return !knownKeys.includes(key)
? { ...value, [key]: Default(additionalProperties, value[key]) }
? { ...value, [key]: Default(additionalProperties, `${path}/${key}`, value[key]) }
: value
}, knownProperties)
return Default(schema, unknownProperties)
return Default(schema, path, unknownProperties)
}
// prettier-ignore
function FromRecord(schema: TRecord, references: TSchema[], value: any) {
if (!IsStandardObject(value)) return Default(schema, value)
function FromRecord(schema: TRecord, references: TSchema[], path: string, value: any) {
if (!IsStandardObject(value)) return Default(schema, path, value)
const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0]
const knownKeys = new RegExp(pattern)
const knownProperties = Object.getOwnPropertyNames(value).reduce((value, key) => {
return knownKeys.test(key)
? { ...value, [key]: Visit(schema.patternProperties[pattern], references, value[key]) }
? { ...value, [key]: Visit(schema.patternProperties[pattern], references, `${path}/${key}`, value[key]) }
: value
}, value)
if (!IsSchema(schema.additionalProperties)) {
return Default(schema, knownProperties)
return Default(schema, path, knownProperties)
}
const unknownKeys = Object.getOwnPropertyNames(knownProperties)
const additionalProperties = schema.additionalProperties as TSchema
const unknownProperties = unknownKeys.reduce((value, key) => {
return !knownKeys.test(key)
? { ...value, [key]: Default(additionalProperties, value[key]) }
? { ...value, [key]: Default(additionalProperties, `${path}/${key}`, value[key]) }
: value
}, knownProperties)
return Default(schema, unknownProperties)
return Default(schema, path, unknownProperties)
}
// prettier-ignore
function FromRef(schema: TRef, references: TSchema[], value: any) {
function FromRef(schema: TRef, references: TSchema[], path: string, value: any) {
const target = Deref(schema, references)
return Default(schema, Visit(target, references, value))
return Default(schema, path, Visit(target, references, path, value))
}
// prettier-ignore
function FromThis(schema: TThis, references: TSchema[], value: any) {
function FromThis(schema: TThis, references: TSchema[], path: string, value: any) {
const target = Deref(schema, references)
return Default(schema, Visit(target, references, value))
return Default(schema, path, Visit(target, references, path, value))
}
// prettier-ignore
function FromTuple(schema: TTuple, references: TSchema[], value: any) {
function FromTuple(schema: TTuple, references: TSchema[], path: string, value: any) {
return (IsArray(value) && IsArray(schema.items))
? Default(schema, schema.items.map((schema, index) => Visit(schema, references, value[index])))
: Default(schema, value)
? Default(schema, path, schema.items.map((schema, index) => Visit(schema, references, `${path}/${index}`, value[index])))
: Default(schema, path, value)
}
// prettier-ignore
function FromUnion(schema: TUnion, references: TSchema[], value: any) {
function FromUnion(schema: TUnion, references: TSchema[], path: string, value: any) {
for (const subschema of schema.anyOf) {
if (!Check(subschema, references, value)) continue
// note: ensure interior is decoded first
const decoded = Visit(subschema, references, value)
return Default(schema, decoded)
const decoded = Visit(subschema, references, path, value)
return Default(schema, path, decoded)
}
return Default(schema, value)
return Default(schema, path, value)
}
// prettier-ignore
function Visit(schema: TSchema, references: TSchema[], value: any): any {
function Visit(schema: TSchema, references: TSchema[], path: string, value: any): any {
const references_ = typeof schema.$id === 'string' ? [...references, schema] : references
const schema_ = schema as any
switch (schema[Kind]) {
case 'Array':
return FromArray(schema_, references_, value)
return FromArray(schema_, references_, path, value)
case 'Intersect':
return FromIntersect(schema_, references_, value)
return FromIntersect(schema_, references_, path, value)
case 'Not':
return FromNot(schema_, references_, value)
return FromNot(schema_, references_, path, value)
case 'Object':
return FromObject(schema_, references_, value)
return FromObject(schema_, references_, path, value)
case 'Record':
return FromRecord(schema_, references_, value)
return FromRecord(schema_, references_, path, value)
case 'Ref':
return FromRef(schema_, references_, value)
return FromRef(schema_, references_, path, value)
case 'Symbol':
return Default(schema_, value)
return Default(schema_, path, value)
case 'This':
return FromThis(schema_, references_, value)
return FromThis(schema_, references_, path, value)
case 'Tuple':
return FromTuple(schema_, references_, value)
return FromTuple(schema_, references_, path, value)
case 'Union':
return FromUnion(schema_, references_, value)
return FromUnion(schema_, references_, path, value)
default:
return Default(schema_, value)
return Default(schema_, path, value)
}
}
/**
Expand All @@ -212,5 +223,5 @@ function Visit(schema: TSchema, references: TSchema[], value: any): any {
* undefined behavior. Refer to the `Value.Decode()` for implementation details.
*/
export function TransformDecode(schema: TSchema, references: TSchema[], value: unknown): unknown {
return Visit(schema, references, value)
return Visit(schema, references, '', value)
}

0 comments on commit 9fa2e28

Please sign in to comment.