From 4f0229295d6b10ce25bc4879619ed0c337159a7d Mon Sep 17 00:00:00 2001 From: sinclair Date: Thu, 5 Dec 2024 13:35:21 +0900 Subject: [PATCH] Fix Compile For Deep Referential Module Types --- src/compiler/compiler.ts | 38 +++++++++++++++-------------- test/runtime/compiler-ajv/module.ts | 35 ++++++++++++++++++++++++++ test/runtime/compiler/module.ts | 35 ++++++++++++++++++++++++++ test/runtime/value/check/module.ts | 35 ++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 18 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 7450c8aa..dfdb5b85 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -97,14 +97,6 @@ export class TypeCheck { 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) @@ -113,13 +105,21 @@ export class TypeCheck { public Check(value: unknown): value is Static { 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, 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, Result extends Static = Static>(value: unknown): Result { + public Encode, 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 @@ -298,9 +298,11 @@ export namespace TypeCompiler { yield `(typeof ${value} === 'function')` } function* FromImport(schema: TImport, references: TSchema[], value: string): IterableIterator { - 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 { yield `Number.isInteger(${value})` @@ -399,12 +401,8 @@ export namespace TypeCompiler { function* FromRef(schema: TRef, references: TSchema[], value: string): IterableIterator { 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 { @@ -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, '') const functionCode = CreateFunction(functionName, schema, references, 'value', false) state.functions.set(functionName, functionCode) return yield `${functionName}(${value})` diff --git a/test/runtime/compiler-ajv/module.ts b/test/runtime/compiler-ajv/module.ts index 756c09f3..0797dd45 100644 --- a/test/runtime/compiler-ajv/module.ts +++ b/test/runtime/compiler-ajv/module.ts @@ -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') + }) }) diff --git a/test/runtime/compiler/module.ts b/test/runtime/compiler/module.ts index 32b45560..2121a9d5 100644 --- a/test/runtime/compiler/module.ts +++ b/test/runtime/compiler/module.ts @@ -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') + }) }) diff --git a/test/runtime/value/check/module.ts b/test/runtime/value/check/module.ts index 371b302d..af80b0e6 100644 --- a/test/runtime/value/check/module.ts +++ b/test/runtime/value/check/module.ts @@ -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')) + }) })