Skip to content

Commit

Permalink
Fix Compile For Deep Referential Module Types
Browse files Browse the repository at this point in the history
  • Loading branch information
sinclairzx81 committed Dec 5, 2024
1 parent 3976541 commit 4f02292
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 18 deletions.
38 changes: 20 additions & 18 deletions src/compiler/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,6 @@ export class TypeCheck<T extends TSchema> {
public Code(): string {
return this.code
}
/** Returns the schema type used to validate */
public Schema(): T {
return this.schema
}
/** Returns reference types used to validate */
public References(): TSchema[] {
return this.references
}
/** Returns an iterator for each error in this value. */
public Errors(value: unknown): ValueErrorIterator {
return Errors(this.schema, this.references, value)
Expand All @@ -113,13 +105,21 @@ export class TypeCheck<T extends TSchema> {
public Check(value: unknown): value is Static<T> {
return this.checkFunc(value)
}
/** Returns the schema type used to validate */
public Schema(): T {
return this.schema
}
/** Returns reference types used to validate */
public References(): TSchema[] {
return this.references
}
/** Decodes a value or throws if error */
public Decode<Static = StaticDecode<T>, Result extends Static = Static>(value: unknown): Result {
if (!this.checkFunc(value)) throw new TransformDecodeCheckError(this.schema, value, this.Errors(value).First()!)
return (this.hasTransform ? TransformDecode(this.schema, this.references, value) : value) as never
}
/** Encodes a value or throws if error */
public Encode<Static = StaticEncode<T>, Result extends Static = Static>(value: unknown): Result {
public Encode<Static = StaticDecode<T>, Result extends Static = Static>(value: unknown): Result {
const encoded = this.hasTransform ? TransformEncode(this.schema, this.references, value) : value
if (!this.checkFunc(encoded)) throw new TransformEncodeCheckError(this.schema, value, this.Errors(value).First()!)
return encoded as never
Expand Down Expand Up @@ -298,9 +298,11 @@ export namespace TypeCompiler {
yield `(typeof ${value} === 'function')`
}
function* FromImport(schema: TImport, references: TSchema[], value: string): IterableIterator<string> {
const definitions = globalThis.Object.values(schema.$defs) as TSchema[]
const target = schema.$defs[schema.$ref] as TSchema
yield* Visit(target, [...references, ...definitions], value)
const interior = globalThis.Object.getOwnPropertyNames(schema.$defs).reduce((result, key) => {
return [...result, schema.$defs[key as never] as TSchema]
}, [] as TSchema[])
const target = { [Kind]: 'Ref', $ref: schema.$ref } as never
yield* Visit(target, [...references, ...interior], value)
}
function* FromInteger(schema: TInteger, references: TSchema[], value: string): IterableIterator<string> {
yield `Number.isInteger(${value})`
Expand Down Expand Up @@ -399,12 +401,8 @@ export namespace TypeCompiler {
function* FromRef(schema: TRef, references: TSchema[], value: string): IterableIterator<string> {
const target = Deref(schema, references)
// Reference: If we have seen this reference before we can just yield and return the function call.
// If this isn't the case we defer to visit to generate and set the _recursion_end_for_ for subsequent
// passes. This operation is very awkward as we are using the functions state to store values to
// enable self referential types to terminate. This needs to be refactored.
const recursiveEnd = `_recursion_end_for_${schema.$ref}`
if (state.functions.has(recursiveEnd)) return yield `${CreateFunctionName(schema.$ref)}(${value})`
state.functions.set(recursiveEnd, '') // terminate recursion here by setting the name.
// If this isn't the case we defer to visit to generate and set the function for subsequent passes.
if (state.functions.has(schema.$ref)) return yield `${CreateFunctionName(schema.$ref)}(${value})`
yield* Visit(target, references, value)
}
function* FromRegExp(schema: TRegExp, references: TSchema[], value: string): IterableIterator<string> {
Expand Down Expand Up @@ -481,6 +479,10 @@ export namespace TypeCompiler {
if (state.functions.has(functionName)) {
return yield `${functionName}(${value})`
} else {
// Note: In the case of cyclic types, we need to create a 'functions' record
// to prevent infinitely re-visiting the CreateFunction. Subsequent attempts
// to visit will be caught by the above condition.
state.functions.set(functionName, '<deferred>')
const functionCode = CreateFunction(functionName, schema, references, 'value', false)
state.functions.set(functionName, functionCode)
return yield `${functionName}(${value})`
Expand Down
35 changes: 35 additions & 0 deletions test/runtime/compiler-ajv/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,39 @@ describe('compiler-ajv/Module', () => {
Ok(T, { y: [null], w: [null] })
Fail(T, { x: [1], y: [null], w: [null] })
})
// ----------------------------------------------------------------
// https://github.com/sinclairzx81/typebox/issues/1109
// ----------------------------------------------------------------
it('Should validate deep referential 1', () => {
const Module = Type.Module({
A: Type.Union([Type.Literal('Foo'), Type.Literal('Bar')]),
B: Type.Ref('A'),
C: Type.Object({ ref: Type.Ref('B') }),
D: Type.Union([Type.Ref('B'), Type.Ref('C')]),
})
Ok(Module.Import('A') as never, 'Foo')
Ok(Module.Import('A') as never, 'Bar')
Ok(Module.Import('B') as never, 'Foo')
Ok(Module.Import('B') as never, 'Bar')
Ok(Module.Import('C') as never, { ref: 'Foo' })
Ok(Module.Import('C') as never, { ref: 'Bar' })
Ok(Module.Import('D') as never, 'Foo')
Ok(Module.Import('D') as never, 'Bar')
Ok(Module.Import('D') as never, { ref: 'Foo' })
Ok(Module.Import('D') as never, { ref: 'Bar' })
})
it('Should validate deep referential 2', () => {
const Module = Type.Module({
A: Type.Literal('Foo'),
B: Type.Ref('A'),
C: Type.Ref('B'),
D: Type.Ref('C'),
E: Type.Ref('D'),
})
Ok(Module.Import('A'), 'Foo')
Ok(Module.Import('B'), 'Foo')
Ok(Module.Import('C'), 'Foo')
Ok(Module.Import('D'), 'Foo')
Ok(Module.Import('E'), 'Foo')
})
})
35 changes: 35 additions & 0 deletions test/runtime/compiler/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,39 @@ describe('compiler/Module', () => {
Ok(T, { y: [null], w: [null] })
Fail(T, { x: [1], y: [null], w: [null] })
})
// ----------------------------------------------------------------
// https://github.com/sinclairzx81/typebox/issues/1109
// ----------------------------------------------------------------
it('Should validate deep referential 1', () => {
const Module = Type.Module({
A: Type.Union([Type.Literal('Foo'), Type.Literal('Bar')]),
B: Type.Ref('A'),
C: Type.Object({ ref: Type.Ref('B') }),
D: Type.Union([Type.Ref('B'), Type.Ref('C')]),
})
Ok(Module.Import('A') as never, 'Foo')
Ok(Module.Import('A') as never, 'Bar')
Ok(Module.Import('B') as never, 'Foo')
Ok(Module.Import('B') as never, 'Bar')
Ok(Module.Import('C') as never, { ref: 'Foo' })
Ok(Module.Import('C') as never, { ref: 'Bar' })
Ok(Module.Import('D') as never, 'Foo')
Ok(Module.Import('D') as never, 'Bar')
Ok(Module.Import('D') as never, { ref: 'Foo' })
Ok(Module.Import('D') as never, { ref: 'Bar' })
})
it('Should validate deep referential 2', () => {
const Module = Type.Module({
A: Type.Literal('Foo'),
B: Type.Ref('A'),
C: Type.Ref('B'),
D: Type.Ref('C'),
E: Type.Ref('D'),
})
Ok(Module.Import('A'), 'Foo')
Ok(Module.Import('B'), 'Foo')
Ok(Module.Import('C'), 'Foo')
Ok(Module.Import('D'), 'Foo')
Ok(Module.Import('E'), 'Foo')
})
})
35 changes: 35 additions & 0 deletions test/runtime/value/check/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,39 @@ describe('value/check/Module', () => {
Assert.IsTrue(Value.Check(T, { y: [null], w: [null] }))
Assert.IsFalse(Value.Check(T, { x: [1], y: [null], w: [null] }))
})
// ----------------------------------------------------------------
// https://github.com/sinclairzx81/typebox/issues/1109
// ----------------------------------------------------------------
it('Should validate deep referential 1', () => {
const Module = Type.Module({
A: Type.Union([Type.Literal('Foo'), Type.Literal('Bar')]),
B: Type.Ref('A'),
C: Type.Object({ ref: Type.Ref('B') }),
D: Type.Union([Type.Ref('B'), Type.Ref('C')]),
})
Assert.IsTrue(Value.Check(Module.Import('A') as never, 'Foo'))
Assert.IsTrue(Value.Check(Module.Import('A') as never, 'Bar'))
Assert.IsTrue(Value.Check(Module.Import('B') as never, 'Foo'))
Assert.IsTrue(Value.Check(Module.Import('B') as never, 'Bar'))
Assert.IsTrue(Value.Check(Module.Import('C') as never, { ref: 'Foo' }))
Assert.IsTrue(Value.Check(Module.Import('C') as never, { ref: 'Bar' }))
Assert.IsTrue(Value.Check(Module.Import('D') as never, 'Foo'))
Assert.IsTrue(Value.Check(Module.Import('D') as never, 'Bar'))
Assert.IsTrue(Value.Check(Module.Import('D') as never, { ref: 'Foo' }))
Assert.IsTrue(Value.Check(Module.Import('D') as never, { ref: 'Bar' }))
})
it('Should validate deep referential 2', () => {
const Module = Type.Module({
A: Type.Literal('Foo'),
B: Type.Ref('A'),
C: Type.Ref('B'),
D: Type.Ref('C'),
E: Type.Ref('D'),
})
Assert.IsTrue(Value.Check(Module.Import('A'), 'Foo'))
Assert.IsTrue(Value.Check(Module.Import('B'), 'Foo'))
Assert.IsTrue(Value.Check(Module.Import('C'), 'Foo'))
Assert.IsTrue(Value.Check(Module.Import('D'), 'Foo'))
Assert.IsTrue(Value.Check(Module.Import('E'), 'Foo'))
})
})

0 comments on commit 4f02292

Please sign in to comment.