Allow path-based sync Sium combinators

active-uix
dev 5 months ago
parent ac5fdca4e7
commit 848259e225

@ -127,8 +127,11 @@ Actualizacion 2026-05-16:
`createDomainRangeSchema(...)`. `colorValue`, `dateValue`, `timeValue`, `createDomainRangeSchema(...)`. `colorValue`, `dateValue`, `timeValue`,
`dateRange` y `timeRange` comparten ahora el patron identity codec + `dateRange` y `timeRange` comparten ahora el patron identity codec +
guard + widget + range order sin mover esa logica a `core`. guard + widget + range order sin mover esa logica a `core`.
2. Unificar paths sync/async de combinadores solo con tests amplios; es zona 2. Cerrado: combinadores (`object`, `array`, `union`, `discriminated`) usan
sensible por `SiumAsyncSchemaError`. la misma semantica por ruta que wrappers. Un schema marcado `async` puede
completar `decodeSync` si el input no necesita tocar trabajo async (por
ejemplo field optional async ausente, array vacio o union sync-first). Si
la ruta seleccionada necesita async, sigue lanzando `SiumAsyncSchemaError`.
3. Cerrado: `lazy()` documenta limites de datos runtime autociclicos y lanza 3. Cerrado: `lazy()` documenta limites de datos runtime autociclicos y lanza
`SiumLazyResolvingError` cuando el thunk usa el proxy mientras resuelve o `SiumLazyResolvingError` cuando el thunk usa el proxy mientras resuelve o
devuelve el mismo proxy como schema final. No se anade `WeakSet` global devuelve el mismo proxy como schema final. No se anade `WeakSet` global
@ -137,6 +140,9 @@ Actualizacion 2026-05-16:
4. Validado tras `lazy()`: `npx vitest run src/arts/sium/test` -> 18 4. Validado tras `lazy()`: `npx vitest run src/arts/sium/test` -> 18
archivos, 370 tests OK; `npm run check` -> 0 errores, 0 warnings; archivos, 370 tests OK; `npm run check` -> 0 errores, 0 warnings;
`npm run test` -> 233 archivos, 2266 tests OK. `npm run test` -> 233 archivos, 2266 tests OK.
5. Validado tras paths sync/async de combinadores: `npx vitest run
src/arts/sium/test` -> 18 archivos, 374 tests OK; `npm run check` -> 0
errores, 0 warnings; `npm run test` -> 233 archivos, 2270 tests OK.
- Validacion Sium domain factory: - Validacion Sium domain factory:
- `npx vitest run src/arts/sium/test/color.test.ts src/arts/sium/test/date.test.ts src/arts/sium/test/time.test.ts src/arts/sium/test/standard-schema.test.ts` -> 4 archivos, 122 tests OK. - `npx vitest run src/arts/sium/test/color.test.ts src/arts/sium/test/date.test.ts src/arts/sium/test/time.test.ts src/arts/sium/test/standard-schema.test.ts` -> 4 archivos, 122 tests OK.
- `npm run check` -> 0 errores, 0 warnings. - `npm run check` -> 0 errores, 0 warnings.

@ -744,6 +744,9 @@ if (error instanceof SiumValidationError) {
Lanzado por `decodeSync` o `validateSync` cuando la ruta de validacion necesita steps async. Lanzado por `decodeSync` o `validateSync` cuando la ruta de validacion necesita steps async.
Los wrappers de sentinel pueden devolver sincronamente si no consultan el inner async Los wrappers de sentinel pueden devolver sincronamente si no consultan el inner async
(`optional(undefined)`, `nullable(null)`, `defaulted(undefined)`). (`optional(undefined)`, `nullable(null)`, `defaulted(undefined)`).
Los combinadores siguen la misma regla por ruta: por ejemplo, `object({ a:
optional(asyncSchema) }).decodeSync({ a: undefined })` puede completarse; `{ a: 'x' }`
lanza porque ya necesita validar el inner async.
Solucion: usa `await schema.decode(...)` o `await schema.validate(...)`. Solucion: usa `await schema.decode(...)` o `await schema.validate(...)`.

@ -80,6 +80,7 @@ export function array<I, O>(item: Schema<I, O>): Schema<I[], O[]> {
item item
}, },
decodeImpl: item['~sium'].async ? decodeAsyncArray : decodeSyncArray, decodeImpl: item['~sium'].async ? decodeAsyncArray : decodeSyncArray,
decodeSyncImpl: decodeSyncArray,
encode: (value) => { encode: (value) => {
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
throw new SiumEncodeExpectsArrayError(); throw new SiumEncodeExpectsArrayError();

@ -118,6 +118,7 @@ export function object<T extends Record<string, Schema>>(
unknownKeys unknownKeys
}, },
decodeImpl: isAsync ? decodeAsyncShape : decodeSyncShape, decodeImpl: isAsync ? decodeAsyncShape : decodeSyncShape,
decodeSyncImpl: decodeSyncShape,
encode: (value) => { encode: (value) => {
if (!isPlainObjectRecord(value)) { if (!isPlainObjectRecord(value)) {
throw new SiumEncodeExpectsObjectError('object'); throw new SiumEncodeExpectsObjectError('object');

@ -68,6 +68,7 @@ export function union<M extends readonly [Schema, ...Schema[]]>(
members members
}, },
decodeImpl: isAsync ? decodeAsyncUnion : decodeSyncUnion, decodeImpl: isAsync ? decodeAsyncUnion : decodeSyncUnion,
decodeSyncImpl: decodeSyncUnion,
encode: (value) => value as InferInput<M[number]> encode: (value) => value as InferInput<M[number]>
}); });
} }
@ -160,6 +161,7 @@ export function discriminated<K extends string, M extends readonly [Schema, ...S
members members
}, },
decodeImpl: isAsync ? decodeAsyncDiscriminated : decodeSyncDiscriminated, decodeImpl: isAsync ? decodeAsyncDiscriminated : decodeSyncDiscriminated,
decodeSyncImpl: decodeSyncDiscriminated,
encode: (value) => { encode: (value) => {
if (!isPlainObjectRecord(value)) { if (!isPlainObjectRecord(value)) {
throw new SiumEncodeExpectsObjectError('discriminated'); throw new SiumEncodeExpectsObjectError('discriminated');

@ -1,5 +1,6 @@
import { describe, expect, expectTypeOf, it, vi } from 'vitest'; import { describe, expect, expectTypeOf, it, vi } from 'vitest';
import { array, discriminated, object, union } from '../core/combinators'; import { array, discriminated, object, union } from '../core/combinators';
import { optional } from '../core/modifiers';
import { pipe, refine } from '../core/pipe'; import { pipe, refine } from '../core/pipe';
import { literal, number, string } from '../core/primitives'; import { literal, number, string } from '../core/primitives';
import { createSchema } from '../core/schema'; import { createSchema } from '../core/schema';
@ -297,6 +298,19 @@ describe('object()', () => {
expect(() => schema.decodeSync({ a: 'x', b: 1 })).toThrow(SiumAsyncSchemaError); expect(() => schema.decodeSync({ a: 'x', b: 1 })).toThrow(SiumAsyncSchemaError);
}); });
it('short-circuits async optional fields in sync object paths', () => {
const asyncField = createSchema({
kind: 'string',
async: true,
decodeImpl: async (input: string) => input,
encode: (value: string) => value
});
const schema = object({ a: optional(asyncField), b: number() });
expect(schema.decodeSync({ a: undefined, b: 1 })).toEqual({ a: undefined, b: 1 });
expect(() => schema.decodeSync({ a: 'x', b: 1 })).toThrow(SiumAsyncSchemaError);
});
}); });
describe('array()', () => { describe('array()', () => {
@ -429,6 +443,19 @@ describe('array()', () => {
expect(() => schema.decodeSync(['a'])).toThrow(SiumAsyncSchemaError); expect(() => schema.decodeSync(['a'])).toThrow(SiumAsyncSchemaError);
}); });
it('short-circuits sync array paths that do not need async item work', () => {
const asyncItem = createSchema({
kind: 'string',
async: true,
decodeImpl: async (input: string) => input,
encode: (value: string) => value
});
expect(array(asyncItem).decodeSync([])).toEqual([]);
expect(array(optional(asyncItem)).decodeSync([undefined])).toEqual([undefined]);
expect(() => array(asyncItem).decodeSync(['a'])).toThrow(SiumAsyncSchemaError);
});
}); });
describe('union()', () => { describe('union()', () => {
@ -496,6 +523,18 @@ describe('union()', () => {
expect(() => schema.decodeSync('x')).toThrow(SiumAsyncSchemaError); expect(() => schema.decodeSync('x')).toThrow(SiumAsyncSchemaError);
}); });
it('can decode sync union members before an async member is needed', () => {
const asyncMember = createSchema({
kind: 'string',
async: true,
decodeImpl: async (input: string) => input,
encode: (value: string) => value
});
expect(union(number(), asyncMember).decodeSync(1)).toBe(1);
expect(() => union(asyncMember, number()).decodeSync(1 as never)).toThrow(SiumAsyncSchemaError);
});
}); });
describe('discriminated()', () => { describe('discriminated()', () => {
@ -640,4 +679,24 @@ describe('discriminated()', () => {
expect(() => schema.decodeSync({ kind: 'a', val: 'x' })).toThrow(SiumAsyncSchemaError); expect(() => schema.decodeSync({ kind: 'a', val: 'x' })).toThrow(SiumAsyncSchemaError);
}); });
it('short-circuits selected discriminated member paths when async work is not needed', () => {
const asyncValue = createSchema({
kind: 'string',
async: true,
decodeImpl: async (input: string) => input,
encode: (value: string) => value
});
const schema = discriminated('kind', [
object({ kind: literal('a'), val: optional(asyncValue) }),
object({ kind: literal('b'), val: number() })
]);
expect(schema.decodeSync({ kind: 'a', val: undefined })).toEqual({
kind: 'a',
val: undefined
});
expect(schema.decodeSync({ kind: 'b', val: 1 })).toEqual({ kind: 'b', val: 1 });
expect(() => schema.decodeSync({ kind: 'a', val: 'x' })).toThrow(SiumAsyncSchemaError);
});
}); });

Loading…
Cancel
Save

Powered by TurnKey Linux.