Allow sync sentinel short-circuits in Sium

active-uix
dev 5 months ago
parent fbb1e0f645
commit c4eafd02f2

@ -104,11 +104,13 @@ Actualizacion 2026-05-16:
5. Validado: `npx vitest run src/arts/sium/test` -> 18 archivos, 366 tests OK;
`npm run check` -> 0 errores, 0 warnings; `npm run test` -> 233 archivos,
2262 tests OK.
6. Pendiente deliberado: revisar `optional/nullable/defaulted` con schema interno async. El problema
es real, pero la solucion no es mover un `if`: `createSchema.decodeSync`
lanza por `spec.async === true` antes de entrar en `decodeImpl`. Hace falta
una decision de runtime (`syncShortCircuit`, decodeSync custom por wrapper,
o semantica documentada de "schema async siempre async").
6. Cerrado: `optional/nullable/defaulted` con inner async conservan el
short-circuit sync del sentinel (`undefined`, `null` o default). Si el
valor definido necesita entrar al inner async, `decodeSync` /
`validateSync` siguen lanzando `SiumAsyncSchemaError`.
7. Validado tras wrappers async: `npx vitest run src/arts/sium/test` -> 18
archivos, 368 tests OK; `npm run check` -> 0 errores, 0 warnings;
`npm run test` -> 233 archivos, 2264 tests OK.
- Sium P1 coordinado con consumidores:
1. No cambiar `lazy()` a `kind: 'lazy'` hasta que
`src/uix/soma/components/form/components/form-auto-fields.svelte` sepa

@ -252,6 +252,11 @@ defaulted(number(), () => 18);
`defaulted` aplica el valor por defecto en decode cuando el input es `undefined`.
Si el schema interno es async, los wrappers conservan el short-circuit sincronico del sentinel:
`optional(asyncSchema).decodeSync(undefined)`, `nullable(asyncSchema).decodeSync(null)` y
`defaulted(asyncSchema).decodeSync(undefined)` no entran al schema interno. Cualquier valor que
si necesite validar el inner async lanza `SiumAsyncSchemaError` en APIs sync.
### Combinadores
```ts
@ -730,7 +735,9 @@ if (error instanceof SiumValidationError) {
### `SiumAsyncSchemaError`
Lanzado por `decodeSync` o `validateSync` cuando el schema contiene 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
(`optional(undefined)`, `nullable(null)`, `defaulted(undefined)`).
Solucion: usa `await schema.decode(...)` o `await schema.validate(...)`.

@ -8,9 +8,9 @@ import type { Schema } from './types';
* - `encode(undefined)` returns `undefined` directly.
* - `encode(value)` delegates to the inner schema.
*
* Sync APIs inherit the async contract of the wrapped schema. If the inner
* schema is async, `decodeSync` / `validateSync` throw `SiumAsyncSchemaError`
* even when the input is `undefined` and could be short-circuited.
* Sync APIs preserve the sentinel short-circuit even when the inner schema is
* async. `decodeSync(undefined)` returns `undefined`; defined values delegate
* to the inner schema and throw `SiumAsyncSchemaError` if that path is async.
*/
export function optional<I, O>(schema: Schema<I, O>): Schema<I | undefined, O | undefined> {
const innerIsAsync = schema['~sium'].async;
@ -32,6 +32,13 @@ export function optional<I, O>(schema: Schema<I, O>): Schema<I | undefined, O |
return innerIsAsync ? schema.decode(input, ctx) : schema.decodeSync(input, ctx);
},
decodeSyncImpl: (input, ctx) => {
if (input === undefined) {
return undefined;
}
return schema.decodeSync(input, ctx);
},
encode: (value) => {
if (value === undefined) {
return undefined;
@ -49,9 +56,9 @@ export function optional<I, O>(schema: Schema<I, O>): Schema<I | undefined, O |
* - `encode(null)` returns `null` directly.
* - `encode(value)` delegates to the inner schema.
*
* Sync APIs inherit the async contract of the wrapped schema. If the inner
* schema is async, `decodeSync` / `validateSync` throw `SiumAsyncSchemaError`
* before any null short-circuit happens.
* Sync APIs preserve the sentinel short-circuit even when the inner schema is
* async. `decodeSync(null)` returns `null`; non-null values delegate to the
* inner schema and throw `SiumAsyncSchemaError` if that path is async.
*/
export function nullable<I, O>(schema: Schema<I, O>): Schema<I | null, O | null> {
const innerIsAsync = schema['~sium'].async;
@ -73,6 +80,13 @@ export function nullable<I, O>(schema: Schema<I, O>): Schema<I | null, O | null>
return innerIsAsync ? schema.decode(input, ctx) : schema.decodeSync(input, ctx);
},
decodeSyncImpl: (input, ctx) => {
if (input === null) {
return null;
}
return schema.decodeSync(input, ctx);
},
encode: (value) => {
if (value === null) {
return null;
@ -92,12 +106,14 @@ export function nullable<I, O>(schema: Schema<I, O>): Schema<I | null, O | null>
* - `encode(value)` delegates directly to the inner schema and never
* re-injects the default.
*
* Sync APIs inherit the async contract of the wrapped schema. If the inner
* schema is async, `decodeSync` / `validateSync` throw `SiumAsyncSchemaError`
* even when the input is `undefined` and the default could be returned
* without consulting the inner decoder.
* Sync APIs preserve the default short-circuit even when the inner schema is
* async. `decodeSync(undefined)` returns `getDefault()`; defined values delegate
* to the inner schema and throw `SiumAsyncSchemaError` if that path is async.
*/
export function defaulted<I, O>(schema: Schema<I, O>, getDefault: () => O): Schema<I | undefined, O> {
export function defaulted<I, O>(
schema: Schema<I, O>,
getDefault: () => O
): Schema<I | undefined, O> {
const innerIsAsync = schema['~sium'].async;
return createSchema({
@ -118,6 +134,13 @@ export function defaulted<I, O>(schema: Schema<I, O>, getDefault: () => O): Sche
return innerIsAsync ? schema.decode(input, ctx) : schema.decodeSync(input, ctx);
},
decodeSyncImpl: (input, ctx) => {
if (input === undefined) {
return getDefault();
}
return schema.decodeSync(input, ctx);
},
encode: (value) => schema.encode(value)
});
}

@ -17,6 +17,7 @@ import type {
type CreateSchemaSpec<I, O> = {
kind: SchemaKind;
decodeImpl: (input: I, ctx: Ctx) => O | Promise<O>;
decodeSyncImpl?: (input: I, ctx: Ctx) => O;
encode: (value: O) => I;
wrappers?: ReadonlyArray<SchemaWrapper>;
effects?: ReadonlyArray<SchemaEffect>;
@ -65,6 +66,10 @@ export function createSchema<I, O>(spec: CreateSchemaSpec<I, O>): Schema<I, O> {
};
const decodeSync = (input: I, ctx?: Partial<Ctx>): O => {
if (spec.decodeSyncImpl) {
return spec.decodeSyncImpl(input, normalizeCtx(input, ctx));
}
if (spec.async === true) {
throw new SiumAsyncSchemaError(spec.kind);
}

@ -14,7 +14,9 @@
* reports `kind='color'`, `wrappers=['optional']`, `effects=[]` so UI
* generators can peel the wrappers and resolve the semantic type.
* 3. Async is the default API surface. `decodeSync` / `validateSync` throw
* `SiumAsyncSchemaError` if the schema contains async refinements.
* `SiumAsyncSchemaError` when the requested path needs async work. Wrapper
* sentinels (`optional(undefined)`, `nullable(null)`, `defaulted(undefined)`)
* may short-circuit synchronously even if their inner schema is async.
* 4. Errors are structured data: `{ path, code, message, params }`.
* `message` uses the framework idlangref format
* (`#?sium.errors.X|fallback`) so the adapter can resolve or fall back

@ -171,7 +171,7 @@ describe('wrapper stacking and async inheritance', () => {
expect(schema['~sium'].wrappers).toEqual(['nullable', 'optional']);
});
it('throws SiumAsyncSchemaError for optional(asyncSchema).decodeSync(undefined)', () => {
it('short-circuits optional(asyncSchema).decodeSync(undefined)', () => {
const decodeImpl = vi.fn(async (input: string) => input);
const asyncSchema = createSchema({
kind: 'string',
@ -180,11 +180,16 @@ describe('wrapper stacking and async inheritance', () => {
encode: (value: string) => value
});
expect(() => optional(asyncSchema).decodeSync(undefined)).toThrow(SiumAsyncSchemaError);
expect(optional(asyncSchema).decodeSync(undefined)).toBeUndefined();
expect(optional(asyncSchema).validateSync(undefined)).toEqual({
ok: true,
value: undefined
});
expect(() => optional(asyncSchema).decodeSync('x')).toThrow(SiumAsyncSchemaError);
expect(decodeImpl).not.toHaveBeenCalled();
});
it('throws SiumAsyncSchemaError for defaulted(asyncSchema).decodeSync(undefined)', () => {
it('short-circuits nullable(asyncSchema).decodeSync(null)', () => {
const decodeImpl = vi.fn(async (input: string) => input);
const asyncSchema = createSchema({
kind: 'string',
@ -193,9 +198,48 @@ describe('wrapper stacking and async inheritance', () => {
encode: (value: string) => value
});
expect(() => defaulted(asyncSchema, () => 'fallback').decodeSync(undefined)).toThrow(
SiumAsyncSchemaError
);
expect(nullable(asyncSchema).decodeSync(null)).toBeNull();
expect(nullable(asyncSchema).validateSync(null)).toEqual({
ok: true,
value: null
});
expect(() => nullable(asyncSchema).decodeSync('x')).toThrow(SiumAsyncSchemaError);
expect(decodeImpl).not.toHaveBeenCalled();
});
it('short-circuits nested wrapper sentinels before async inner work', () => {
const decodeImpl = vi.fn(async (input: string) => input);
const asyncSchema = createSchema({
kind: 'string',
async: true,
decodeImpl,
encode: (value: string) => value
});
const schema = optional(nullable(asyncSchema));
expect(schema.decodeSync(undefined)).toBeUndefined();
expect(schema.decodeSync(null)).toBeNull();
expect(() => schema.decodeSync('x')).toThrow(SiumAsyncSchemaError);
expect(decodeImpl).not.toHaveBeenCalled();
});
it('short-circuits defaulted(asyncSchema).decodeSync(undefined)', () => {
const decodeImpl = vi.fn(async (input: string) => input);
const asyncSchema = createSchema({
kind: 'string',
async: true,
decodeImpl,
encode: (value: string) => value
});
const schema = defaulted(asyncSchema, () => 'fallback');
expect(schema.decodeSync(undefined)).toBe('fallback');
expect(schema.validateSync(undefined)).toEqual({
ok: true,
value: 'fallback'
});
expect(() => schema.decodeSync('x')).toThrow(SiumAsyncSchemaError);
expect(decodeImpl).not.toHaveBeenCalled();
});
@ -218,4 +262,3 @@ describe('wrapper stacking and async inheritance', () => {
expect(schema['~sium'].meta.widgetOptions).toBe(inner['~sium'].meta.widgetOptions);
});
});

Loading…
Cancel
Save

Powered by TurnKey Linux.