import { describe, expect, it } from 'vitest'; import { array, lazy, object, optional, string } from '../core'; import { createSchema } from '../core/schema'; import type { Schema } from '../core/types'; import { SiumAsyncSchemaError, SiumLazyResolvingError } from '../errors'; type TreeNode = { value: string; children?: TreeNode[]; }; type Alpha = { name: string; beta?: Beta; }; type Beta = { label: string; alpha?: Alpha; }; describe('lazy()', () => { it('delegates primitive decode/encode through the cached schema', () => { const schema = lazy(() => string()); expect(schema.decodeSync('x')).toBe('x'); expect(schema.encode('x')).toBe('x'); }); it('decodes self-referential trees to arbitrary input depth', () => { const TreeSchema: Schema = lazy(() => object({ value: string(), children: optional(array(TreeSchema)) }) ); const tree = TreeSchema.decodeSync({ value: 'root', children: [ { value: 'branch', children: [{ value: 'leaf' }] } ] }); expect(tree).toEqual({ value: 'root', children: [ { value: 'branch', children: [{ value: 'leaf', children: undefined }] } ] }); }); it('supports mutual recursion through lazy forward references', () => { const AlphaSchema: Schema = lazy(() => object({ name: string(), beta: optional(BetaSchema) }) ); const BetaSchema: Schema = lazy(() => object({ label: string(), alpha: optional(AlphaSchema) }) ); expect( AlphaSchema.decodeSync({ name: 'a1', beta: { label: 'b1', alpha: { name: 'a2' } } }) ).toEqual({ name: 'a1', beta: { label: 'b1', alpha: { name: 'a2', beta: undefined } } }); }); it('round-trips recursive trees through decode + encode', () => { const TreeSchema: Schema = lazy(() => object({ value: string(), children: optional(array(TreeSchema)) }) ); const input = { value: 'root', children: [{ value: 'leaf' }] }; expect(TreeSchema.encode(TreeSchema.decodeSync(input))).toEqual({ value: 'root', children: [ { value: 'leaf', children: undefined } ] }); }); it('inherits async from the resolved inner schema', () => { const schema = lazy(() => createSchema({ kind: 'string', async: true, decodeImpl: async (input: string) => input, encode: (value: string) => value }) ); expect(schema['~sium'].async).toBe(true); }); it('reports the inner kind transparently after resolution', () => { const schema = lazy(() => string()); expect(schema['~sium'].kind).toBe('string'); }); it('passes through the inner shape transparently after resolution', () => { const inner = object({ value: string() }); const schema = lazy(() => inner); expect(schema['~sium'].shape).toBe(inner['~sium'].shape); }); it('invokes the factory thunk once and caches the resolved schema', async () => { let calls = 0; const schema = lazy(() => { calls += 1; return string(); }); expect(schema.decodeSync('a')).toBe('a'); expect(schema.encode('b')).toBe('b'); await expect(schema.validate('c')).resolves.toEqual({ ok: true, value: 'c' }); expect(schema['~sium'].kind).toBe('string'); expect(calls).toBe(1); }); it('delegates the Standard Schema contract to the resolved inner schema', async () => { const schema = lazy(() => string()); expect(schema['~standard'].version).toBe(1); expect(schema['~standard'].vendor).toBe('sium'); await expect(schema['~standard'].validate('ok')).resolves.toEqual({ value: 'ok' }); }); it('preserves validation issue paths from recursive objects', async () => { const TreeSchema: Schema = lazy(() => object({ value: string(), children: optional(array(TreeSchema)) }) ); await expect( TreeSchema.validate({ value: 42 as never, children: [{ value: 'leaf' }] }) ).resolves.toEqual({ ok: false, issues: [ { path: ['value'], code: 'type', message: '#?sium.errors.type|Expected {{expected}} but received {{actual}}', params: { expected: 'string', actual: 'number' } } ] }); }); it('resolves recursion based on the input shape rather than schema depth', () => { const TreeSchema: Schema = lazy(() => object({ value: string(), children: optional(array(TreeSchema)) }) ); expect(TreeSchema.decodeSync({ value: 'leaf' })).toEqual({ value: 'leaf', children: undefined }); }); it('rethrows SiumAsyncSchemaError from decodeSync when the inner schema is async', () => { const schema = lazy(() => createSchema({ kind: 'string', async: true, decodeImpl: async (input: string) => input, encode: (value: string) => value }) ); expect(() => schema.decodeSync('x')).toThrow(SiumAsyncSchemaError); }); it('throws SiumLazyResolvingError when the thunk uses the proxy while resolving', () => { const schema: Schema = lazy(() => { schema.decodeSync('x'); return string(); }); expect(() => schema.decodeSync('x')).toThrow(SiumLazyResolvingError); }); it('throws SiumLazyResolvingError when the thunk returns the same lazy proxy', () => { const schema: Schema = lazy(() => schema); expect(() => schema.decodeSync('x')).toThrow(SiumLazyResolvingError); }); it('is re-exported by the core barrel', () => { expect(lazy).toBeTypeOf('function'); }); });