You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
241 lines
5.4 KiB
241 lines
5.4 KiB
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<TreeNode, TreeNode> = 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<Alpha, Alpha> = lazy(() =>
|
|
object({
|
|
name: string(),
|
|
beta: optional(BetaSchema)
|
|
})
|
|
);
|
|
const BetaSchema: Schema<Beta, Beta> = 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<TreeNode, TreeNode> = 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<TreeNode, TreeNode> = 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<TreeNode, TreeNode> = 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<string, string> = 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<string, string> = lazy(() => schema);
|
|
|
|
expect(() => schema.decodeSync('x')).toThrow(SiumLazyResolvingError);
|
|
});
|
|
|
|
it('is re-exported by the core barrel', () => {
|
|
expect(lazy).toBeTypeOf('function');
|
|
});
|
|
});
|