|
|
5 months ago | |
|---|---|---|
| .. | ||
| _examples | 5 months ago | |
| core | 5 months ago | |
| langs | 5 months ago | |
| test | 5 months ago | |
| types | 5 months ago | |
| README.md | 5 months ago | |
| consts.ts | 5 months ago | |
| diagnostics.ts | 5 months ago | |
| engine-resolver.ts | 5 months ago | |
| engine-sium.ts | 5 months ago | |
| engine-validation.ts | 5 months ago | |
| errors.ts | 5 months ago | |
| index.ts | 5 months ago | |
README.md
Sium
Sium es la capa de contratos, validacion e introspeccion de Active.
Su objetivo no es competir con Zod, Valibot o ArkType como libreria generalista. Sium existe para que Active tenga un contrato nativo entre datos, validacion, errores traducibles, tipos de dominio y UI auto-generada.
Este archivo es la unica documentacion Markdown del modulo. Si cambia el comportamiento de Sium, actualiza este README en lugar de crear documentos paralelos.
Sium no forma parte de active-app core
createActiveApp(...) deliberadamente no construye Sium. La validacion es
page-scoped: solo las paginas con formularios la necesitan. Cargar todo
el core de schemas/types/issues en paginas sin formularios serìa coste
innecesario.
El patron estandar es una linea al inicio del modulo de la pagina:
import { createEngineSium } from '$sium';
const sium = createEngineSium({ langs: App.langs, logger: App.logger });
Asi cada formulario obtiene un engine afinado a sus necesidades (logger category, contexto de validacion, etc.) y comparte la traduccion de issues + el bus de logs con la app.
Estado
Core, tipos de dominio, lenguajes, Standard Schema e introspeccion estan activos.
El adapter svelte/ todavia no esta portado en este arbol. Por eso la suite recomendada para Sium omite la integracion Svelte hasta que esa capa se migre.
Importaciones
Sium expone una fachada publica y barrels tematicos.
import { createEngineSium } from '$sium';
const sium = createEngineSium();
Si quieres traducciones activas, inyecta el engine de traducciones:
import { createEngineSium, siumLangs } from '$sium';
import { createEngineLangs } from '$langs';
const langs = createEngineLangs({ sium: siumLangs }, 'es');
const sium = createEngineSium({ langs });
import { object, pipe, string, email, min } from '$sium/core';
import { colorValue, dateValue, timeValue } from '$sium/types';
import { siumLangs, ISSUE_CODES, resolve } from '$sium/langs';
createSchema() es interno. La API publica debe entrar por createEngineSium() o por los barrels core, types y langs.
El directorio _examples/ contiene esquemas de referencia (formularios de
login, signup, settings, etc.) usados por /test/sium y citados en este
README. No es API publica — el guion bajo inicial es la convencion que
señala material interno/dev. No esta re-exportado por sium/index.ts y
ningun consumidor lo importa. Si encuentras un caso real para uno de esos
esquemas, copialo al consumidor; promoverlo a sium publico significaria
soporte indefinido para una decision de UX que no pertenece al runtime.
Idea Central
Un schema Sium es un codec bidireccional con validacion, introspeccion y compatibilidad Standard Schema.
interface Schema<I = unknown, O = I> {
readonly '~standard': StandardSchemaV1.Props<I, O>;
readonly '~sium': SiumIntrospection;
decode(input: I, ctx?: Partial<Ctx>): Promise<O>;
decodeSync(input: I, ctx?: Partial<Ctx>): O;
encode(value: O): I;
validate(input: I, ctx?: Partial<Ctx>): Promise<Result<O>>;
validateSync(input: I, ctx?: Partial<Ctx>): Result<O>;
}
decode valida y transforma. Si falla, lanza SiumValidationError.
validate envuelve decode y devuelve { ok: true, value } o { ok: false, issues }.
createEngineSium({ logger }).validate(schema, input) hace lo mismo y, si falla, registra un evento debug usando LOGGER_CATEGORY y SIUM_ERRORS.VALIDATION_FAILED, con issueCount e issues.
encode invierte la salida hacia la forma de entrada. En transforms de una sola direccion actua como identidad para esa parte; usa codec() cuando necesites reversibilidad real.
~standard permite usar schemas Sium en cualquier consumidor de Standard Schema v1.
~sium contiene metadata para UI, debug, docs y herramientas.
Por Que Existe
Sium cubre cuatro huecos que las librerias externas no resuelven de forma nativa dentro de Active.
- Tipos de dominio:
ColorValue,DateValue,TimeValuey sus segmentos ya existen en Active. - UI metadata:
meta.widget,meta.channel, labels y opciones viajan dentro del schema. - Errores traducibles: los issues llevan
code,messageidlangref,paramsypath. - Interop: cada schema tambien habla Standard Schema v1.
Arquitectura
src/arts/sium/
index.ts fachada publica
engine-sium.ts createEngineSium()
core/ kernel puro TypeScript
types/ tipos de dominio sobre $libs/color y $libs/days
langs/ catalogo de mensajes y fallback de interpolacion
svelte/ adapter Svelte pendiente de port, no presente ahora
examples/ schemas de ejemplo ejecutables/importables
test/ suite Vitest
Reglas de dependencia:
core/no importa Svelte, UI,days,colornilangs.types/puede importarcore/,$libs/colory$libs/days.langs/define el catalogosiumLangsy un fallback minimo de interpolacion.svelte/es adapter opcional y no debe contaminar el core.engine-sium.tscompone la fachada publica.
Guia Rapida
Usar La Fachada
import { createEngineSium } from '$sium';
const s = createEngineSium();
export const User = s.object({
name: s.pipe(s.string(), s.min(2), s.max(40)),
email: s.pipe(s.string(), s.email()),
age: s.optional(s.pipe(s.number(), s.integer(), s.min(0)))
});
const result = await User.validate({
name: 'Ada',
email: 'ada@example.com',
age: 37
});
Con integracion:
const s = createEngineSium({ langs, logger });
s.resolveIssue({
path: ['email'],
code: 'type',
message: '#?sium.errors.type|Expected {{expected}} but received {{actual}}',
params: { expected: 'string', actual: 'number' }
});
Si langs existe, resolveIssue() delega en EngineLangs. Si no existe, usa el fallback local de $sium/langs.resolve.
En desarrollo tambien puedes validar a traves del engine para obtener logs debug cuando hay issues:
const result = await s.validate(User, input);
const syncResult = s.validateSync(User, input);
Estos wrappers no cambian el resultado de schema.validate(). Solo emiten un log debug en la categoria exportada LOGGER_CATEGORY cuando la validacion falla y has inyectado logger.
Usar Imports Granulares
import { object, optional, pipe, string, number, integer, min, email } from '$sium/core';
export const User = object({
name: pipe(string(), min(2)),
email: pipe(string(), email()),
age: optional(pipe(number(), integer(), min(0)))
});
Validar
const result = await User.validate(input);
if (result.ok) {
result.value;
} else {
result.issues;
}
Decodificar Con Excepcion
import { SiumValidationError } from '$sium/core';
try {
const value = await User.decode(input);
} catch (error) {
if (error instanceof SiumValidationError) {
console.log(error.issues);
}
}
Codificar
const QueryNumber = s.pipe(
s.string(),
s.codec(
(value) => Number(value),
(value) => String(value)
)
);
await QueryNumber.decode('42'); // 42
QueryNumber.encode(42); // '42'
API De Core
Primitivos
string();
number();
boolean();
literal('admin');
enumOf(['admin', 'user', 'guest'] as const);
Modificadores
optional(string());
nullable(string());
defaulted(number(), () => 18);
optional permite undefined.
nullable permite null.
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
object({ name: string() });
array(string());
union(string(), number());
discriminated('kind', [
object({ kind: literal('text'), value: string() }),
object({ kind: literal('count'), value: number() })
]);
object() usa unknownKeys: 'strip' por defecto.
object({ name: string() }, { unknownKeys: 'strict' });
object({ name: string() }, { unknownKeys: 'passthrough' });
Pipe Y Steps
pipe(string(), min(3), max(40), meta({ label: 'Name', widget: 'text-field' }));
Steps disponibles:
refine(check, fail, opts?)transform(fn, opts?)codec(decode, encode, opts?)meta(annotations)
Si una funcion normal devuelve Promise, declara el step como async.
const UniqueEmail = pipe(
string(),
refine(
(value) => api.emailIsFree(value),
{
code: 'custom',
message: '#?sium.errors.custom|Email is already in use'
},
{ async: true }
)
);
Refines Incluidos
min(2);
max(40);
length(8);
regex(/^[a-z0-9-]+$/);
email();
url();
integer();
min y max funcionan con numeros, strings y arrays. Emiten codigos distintos para valor numerico y longitud.
Tipos De Dominio
Los tipos de dominio viven en $sium/types y envuelven las librerias canonicas $libs/color y $libs/days.
Color
import {
colorValue,
red,
green,
blue,
hue,
saturation,
lightness,
brightness,
alpha
} from '$sium/types';
const Color = colorValue();
const Red = red();
colorValue() valida un ColorValue normalizado con hex, rgb, hsl y hsv.
Los segmentos RGB son enteros 0..255.
Los segmentos HSL/HSV son numeros en sus rangos naturales.
alpha() valida 0..1.
Fecha
import { dateValue, dateRange, coerceDate, year, month, day } from '$sium/types';
const DateOnly = dateValue();
const Range = dateRange();
const FromNativeDate = coerceDate();
dateValue() acepta DateValue de $libs/days.
coerceDate() acepta Date | DateValue y devuelve DateValue.
dateRange() valida { start, end } y rechaza rangos donde start > end.
Hora
import { timeValue, timeRange, hour, minute, second, dayPeriod } from '$sium/types';
const TimeOnly = timeValue();
const Range = timeRange();
timeRange() valida { start, end } y rechaza rangos donde start > end.
hour({ cycle: 12 }) usa rango 1..12.
hour() usa rango 0..23.
Introspeccion
Cada schema expone schema['~sium'].
const schema = pipe(string(), email(), meta({ label: 'Email', widget: 'email-field' }));
schema['~sium'];
Forma simplificada:
{
kind: 'string',
wrappers: [],
effects: ['refine'],
async: false,
meta: { label: 'Email', widget: 'email-field' },
shape: undefined
}
Herramientas incluidas:
serializeSchema(schema);
countLeafFields(schema);
walkSchema(schema, (node, path) => {});
walkSchema() recorre objects, arrays y wrappers. Las variantes de union/discriminated no se expanden automaticamente para evitar UI ambigua.
Errores E i18n
Un issue tiene esta forma:
type Issue = {
path: ReadonlyArray<string | number>;
code: string;
message: string;
params?: Record<string, unknown>;
};
message usa idlangref:
#?sium.errors.type|Expected {{expected}} but received {{actual}}
Los mensajes base viven en siumLangs.
import { siumLangs, resolve } from '$sium/langs';
resolve('#?sium.errors.type|Invalid', {
expected: 'string',
actual: 'number'
});
resolve() de $sium/langs no traduce por locale. Es solo el fallback local de interpolacion cuando no hay EngineLangs.
Para traduccion real:
const sium = createEngineSium({ langs });
const text = sium.resolveIssue(issue, 'es');
El engine expone langSchema como nombre historico para acceder al catalogo
estatico si lo necesitas:
sium.langSchema === siumLangs;
Codigos estables actuales:
type
literal
enum
min_value
max_value
min_length
max_length
length
regex
email
url
integer
custom
range_order
unknown_keys
union_no_match
discriminated_missing_key
discriminated_unknown_value
Contexto De Validacion
Los callbacks de refine, transform y codec reciben ctx.
type Ctx = {
path: ReadonlyArray<string | number>;
rootInput: unknown;
rootValue?: unknown;
locale?: string;
meta?: Record<string, unknown>;
};
ctx.path es absoluto. En object({ user: object({ name }) }), un refine de name recibe ['user', 'name'].
ctx.rootInput es el input original.
ctx.rootValue es passthrough opcional. Sium lo conserva si el caller o un adapter lo
pasan en el contexto, pero el core no lo sintetiza automaticamente.
Para validaciones cross-field sobre input de usuario, usa ctx.rootInput. Para validaciones
cross-field sobre datos ya decodificados, coloca un refine() sobre el schema object() y usa
el objeto decodificado que recibe ese refine.
Ejemplos
Login
import { object, pipe, string, email, min, meta } from '$sium/core';
export const Login = object({
email: pipe(string(), email(), meta({ label: 'Email' })),
password: pipe(string(), min(8), meta({ label: 'Password' }))
});
Signup Con Confirmacion
import { boolean, object, optional, pipe, refine, string, meta } from '$sium/core';
export const Signup = pipe(
object({
email: pipe(string(), meta({ label: 'Email' })),
password: pipe(string(), meta({ label: 'Password' })),
confirmPassword: pipe(string(), meta({ label: 'Confirm password' })),
acceptTerms: pipe(
boolean(),
refine((value) => value === true, {
code: 'custom',
message: '#?sium.errors.custom|You must accept the terms'
}),
meta({ label: 'Accept terms' })
),
newsletter: pipe(optional(boolean()), meta({ label: 'Newsletter' }))
}),
refine((value) => value.password === value.confirmPassword, {
code: 'custom',
message: '#?sium.errors.custom|Passwords do not match'
})
);
Perfil Con Fecha Y Color
import { object, pipe, string, email, min, max, meta } from '$sium/core';
import { colorValue, dateValue } from '$sium/types';
export const Profile = object({
name: pipe(string(), min(2), max(40), meta({ label: 'Name' })),
email: pipe(string(), email(), meta({ label: 'Email' })),
birthDate: pipe(dateValue(), meta({ label: 'Birth date', widget: 'date-field' })),
favoriteColor: pipe(colorValue(), meta({ label: 'Favorite color', widget: 'color-picker' }))
});
Reserva Con Rango
import { integer, max, meta, min, number, object, optional, pipe, string } from '$sium/core';
import { dateRange, timeValue } from '$sium/types';
export const Booking = object({
range: pipe(dateRange(), meta({ label: 'Date range' })),
checkInTime: pipe(timeValue(), meta({ label: 'Check-in time' })),
guests: pipe(number(), integer(), min(1), max(10), meta({ label: 'Guests' })),
notes: pipe(optional(string()), meta({ label: 'Notes' }))
});
Discriminated Union
import { discriminated, literal, number, object, string } from '$sium/core';
export const Event = discriminated('kind', [
object({
kind: literal('text'),
message: string()
}),
object({
kind: literal('score'),
value: number()
})
]);
Recursion Con Lazy
import { array, lazy, object, optional, string } from '$sium/core';
import type { Schema } from '$sium/core';
type TreeNode = {
label: string;
children?: TreeNode[];
};
export const Tree: Schema<TreeNode> = lazy(() =>
object({
label: string(),
children: optional(array(Tree))
})
);
lazy() protege errores de construccion del schema: si el thunk intenta usar el propio proxy
mientras resuelve, o devuelve ese mismo proxy como schema final, lanza SiumLazyResolvingError.
Esto no es deteccion de ciclos en datos de usuario. Si pasas un objeto runtime autociclico a un
schema recursivo, el decode/encode puede seguir desbordando la pila; valida esos datos antes o
anade una capa de normalizacion especifica del dominio.
Transform Y Codec
import { codec, pipe, string, transform } from '$sium/core';
export const TrimmedLower = pipe(
string(),
transform((value) => value.trim().toLowerCase())
);
export const NumberFromString = pipe(
string(),
codec(
(value) => Number(value),
(value) => String(value)
)
);
Validacion Async
import { pipe, refine, string } from '$sium/core';
export const UniqueUsername = pipe(
string(),
refine(
(value) => fetch(`/api/users/${value}`).then((res) => res.status === 404),
{
code: 'custom',
message: '#?sium.errors.custom|Username is already taken'
},
{ async: true }
)
);
Integracion Svelte
La futura carpeta svelte/ representara el adapter para conectar Sium con providers, formularios y resolucion reactiva de errores.
Estado actual:
- El core puede usarse sin Svelte.
- Los schemas exponen
~standard, asi que pueden conectarse a consumidores Standard Schema. - El adapter
svelte/no esta presente en este arbol porque depende de piezas UI pendientes de port. - Hasta completar esa migracion, las pruebas y auditorias de Sium deben omitir
src/arts/sium/svelte.
Recetas Rapidas
UUID v4
const UUID = pipe(
string(),
regex(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
);
Slug
const Slug = pipe(string(), min(1), max(80), regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/));
Password Fuerte
const StrongPassword = pipe(
string(),
min(8),
refine((value) => /[a-z]/.test(value), {
code: 'custom',
message: '#?sium.errors.custom|Password needs a lowercase letter'
}),
refine((value) => /[A-Z]/.test(value), {
code: 'custom',
message: '#?sium.errors.custom|Password needs an uppercase letter'
}),
refine((value) => /\d/.test(value), {
code: 'custom',
message: '#?sium.errors.custom|Password needs a number'
})
);
HTTPS URL
const HttpsUrl = pipe(
string(),
url(),
refine((value) => new URL(value).protocol === 'https:', {
code: 'custom',
message: '#?sium.errors.custom|URL must use HTTPS'
})
);
File Upload
const ImageFile = refine<File>((file) => file.size <= 2_000_000 && file.type.startsWith('image/'), {
code: 'custom',
message: '#?sium.errors.custom|File must be an image under 2 MB'
});
Troubleshooting
SiumValidationError
Lanzado por decode cuando hay errores de validacion.
if (error instanceof SiumValidationError) {
error.issues;
}
SiumAsyncSchemaError
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(...).
Un callback devuelve Promise pero ~sium.async es false
JavaScript no permite detectar una funcion normal que devuelve Promise sin ejecutarla. Declara el step con { async: true }.
refine((value) => Promise.resolve(Boolean(value)), { code: 'custom' }, { async: true });
El path de un error no coincide
Los errores emitidos dentro de schemas hijos deben usar ctx.path. Sium pasa paths absolutos a hijos de object y array.
npm run check falla en sium/svelte
Es esperado mientras el provider UI no este portado. El core de Sium se valida con la suite sin Svelte.
Verificacion Recomendada
npx vitest run src/arts/sium/test/barrel.test.ts src/arts/sium/test/engine-sium.test.ts src/arts/sium/test/combinators.test.ts src/arts/sium/test/date.test.ts src/arts/sium/test/time.test.ts src/arts/sium/test/color.test.ts src/arts/sium/test/pipe.test.ts src/arts/sium/test/primitives.test.ts src/arts/sium/test/refines.test.ts src/arts/sium/test/schema.test.ts src/arts/sium/test/standard-schema.test.ts src/arts/sium/test/modifiers.test.ts src/arts/sium/test/issue.test.ts src/arts/sium/test/lazy.test.ts src/arts/sium/test/introspect.test.ts src/arts/sium/test/langs.test.ts src/arts/sium/test/resolver.test.ts
npx vitest run src/libs/days/test/parse.test.ts src/libs/days/test/queries.test.ts src/libs/color/test/guards.test.ts
Criterio Para Cambios Futuros
Antes de ampliar Sium, comprueba que el cambio cumple estas reglas.
- El core sigue siendo TypeScript puro.
- La API publica no expone
createSchema(). - Cada schema mantiene
decode,encode,validate,~standardy~sium. - Los errores son datos estructurados, no strings finales.
- Los tipos de dominio reutilizan
$libs/colory$libs/days. - La documentacion se actualiza en este unico README.