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.
svelte-kit-vice/src/arts/sium/core/steps.ts

161 lines
3.8 KiB

import { issue } from './issue';
import { SiumAsyncSchemaError, SiumValidationError } from '../errors';
import type { Ctx, MetaAnnotations, RefineFailure, Step } from './types';
import { isPromiseLike } from './internals';
type StepOptions = {
/**
* Mark callbacks as async when they are regular functions that return a
* Promise. JavaScript cannot detect that shape before executing the callback.
*/
async?: boolean;
};
function isAsyncFunction(fn: Function): boolean {
return fn.constructor.name === 'AsyncFunction';
}
function createAsyncStepError() {
return new SiumAsyncSchemaError(
undefined,
'Step contains async logic — use decode() instead of decodeSync().'
);
}
function createRefineIssue(path: ReadonlyArray<string | number>, fail: RefineFailure) {
return issue({
path,
code: fail.code ?? 'custom',
message: fail.message ?? '#?sium.errors.custom|Invalid value',
...(fail.params === undefined ? {} : { params: fail.params })
});
}
/**
* Creates a refinement step that validates the current value without changing it.
*
* Failed checks emit a single `Issue` built from `fail`, defaulting to
* `code='custom'` and `message='#?sium.errors.custom|Invalid value'`.
*/
export function refine<T>(
check: (value: T, ctx: Ctx) => boolean | Promise<boolean>,
fail: RefineFailure,
opts?: StepOptions
): Step<T, T> {
const async = opts?.async ?? isAsyncFunction(check);
return {
kind: 'refine',
async,
meta: {},
decode: async (input, ctx) => {
const result = await check(input, ctx);
if (result) {
return input;
}
throw new SiumValidationError([createRefineIssue(ctx.path, fail)]);
},
decodeSync: (input, ctx) => {
if (async) {
throw createAsyncStepError();
}
const result = check(input, ctx);
if (isPromiseLike(result)) {
throw createAsyncStepError();
}
if (result) {
return input;
}
throw new SiumValidationError([createRefineIssue(ctx.path, fail)]);
}
};
}
/**
* Creates a one-way transformation step.
*
* `transform` affects decode only. It intentionally does not expose `encode`,
* so `pipe(...).encode(...)` treats it as identity in reverse composition.
*/
export function transform<I, O>(
fn: (value: I, ctx: Ctx) => O | Promise<O>,
opts?: StepOptions
): Step<I, O> {
const async = opts?.async ?? isAsyncFunction(fn);
return {
kind: 'transform',
async,
meta: {},
decode: async (input, ctx) => await fn(input, ctx),
decodeSync: (input, ctx) => {
if (async) {
throw createAsyncStepError();
}
const result = fn(input, ctx);
if (isPromiseLike(result)) {
throw createAsyncStepError();
}
return result;
}
};
}
/**
* Creates a bidirectional step with explicit decode and encode sides.
*
* Unlike `transform`, `codec` participates in reverse composition during
* `pipe(...).encode(...)` by applying the provided `encode` function.
*/
export function codec<I, O>(
decode: (value: I, ctx: Ctx) => O | Promise<O>,
encode: (value: O) => I,
opts?: StepOptions
): Step<I, O> {
const async = opts?.async ?? isAsyncFunction(decode);
return {
kind: 'codec',
async,
meta: {},
decode: async (input, ctx) => await decode(input, ctx),
decodeSync: (input, ctx) => {
if (async) {
throw createAsyncStepError();
}
const result = decode(input, ctx);
if (isPromiseLike(result)) {
throw createAsyncStepError();
}
return result;
},
encode
};
}
/**
* Creates a pure annotation step that leaves the value untouched.
*
* `meta` never changes runtime data and never participates in encode; it only
* contributes shallow-copied annotations to the final schema introspection.
*/
export function meta<T>(annotations: MetaAnnotations): Step<T, T> {
const copiedMeta = { ...annotations };
return {
kind: 'meta',
async: false,
meta: copiedMeta,
decode: async (input) => input,
decodeSync: (input) => input
};
}

Powered by TurnKey Linux.