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.
85 lines
3.0 KiB
85 lines
3.0 KiB
/**
|
|
* Standard Schema v1 — minimal inline port of the specification.
|
|
*
|
|
* <https://standardschema.dev/>
|
|
*
|
|
* A thin type + helper layer that lets `soma/Form` accept any validation
|
|
* library that implements the `"~standard"` interface: Zod, Valibot, ArkType,
|
|
* Effect/Schema, Yup (via adapter), etc. No runtime dependency on any specific
|
|
* schema library.
|
|
*
|
|
* The consumer imports their schema from their library of choice and passes
|
|
* it to `createForm({ schema })`. Validation is invoked via
|
|
* `schema["~standard"].validate(values)`.
|
|
*/
|
|
|
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
readonly '~standard': StandardSchemaV1.Props<Input, Output>;
|
|
}
|
|
|
|
export namespace StandardSchemaV1 {
|
|
export interface Props<Input = unknown, Output = Input> {
|
|
/** Version number of the standard. */
|
|
readonly version: 1;
|
|
/** Vendor name (e.g. `"zod"`, `"valibot"`, `"arktype"`). */
|
|
readonly vendor: string;
|
|
/** Validates a value. May return a `Promise<Result>` for async schemas. */
|
|
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
|
|
/** Type metadata — compile-time only, undefined at runtime. */
|
|
readonly types?: Types<Input, Output>;
|
|
}
|
|
|
|
export type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
|
|
export interface SuccessResult<Output> {
|
|
readonly value: Output;
|
|
readonly issues?: undefined;
|
|
}
|
|
|
|
export interface FailureResult {
|
|
readonly issues: ReadonlyArray<Issue>;
|
|
}
|
|
|
|
export interface Issue {
|
|
readonly message: string;
|
|
readonly path?: ReadonlyArray<PropertyKey | PathSegment>;
|
|
}
|
|
|
|
export interface PathSegment {
|
|
readonly key: PropertyKey;
|
|
}
|
|
|
|
export interface Types<Input = unknown, Output = Input> {
|
|
readonly input: Input;
|
|
readonly output: Output;
|
|
}
|
|
|
|
export type InferInput<S extends StandardSchemaV1> = NonNullable<
|
|
S['~standard']['types']
|
|
>['input'];
|
|
|
|
export type InferOutput<S extends StandardSchemaV1> = NonNullable<
|
|
S['~standard']['types']
|
|
>['output'];
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
/** `true` when the value's first segment is a `PathSegment` object. */
|
|
function isPathSegment(v: PropertyKey | StandardSchemaV1.PathSegment): v is StandardSchemaV1.PathSegment {
|
|
return typeof v === 'object' && v !== null && 'key' in v;
|
|
}
|
|
|
|
/** Normalize an issue's `path` to a string array of keys. */
|
|
export function pathKeys(issue: StandardSchemaV1.Issue): string[] {
|
|
if (!issue.path) return [];
|
|
return issue.path.map((p) => String(isPathSegment(p) ? p.key : p));
|
|
}
|
|
|
|
/** `true` when the validate call returned a Promise. */
|
|
export function isPromiseLike<T>(v: T | Promise<T>): v is Promise<T> {
|
|
return typeof (v as { then?: unknown })?.then === 'function';
|
|
}
|