// Single responsibility: the Standard Schema v1 interface plus library-agnostic helpers. // This is the swap point: every Ultimate primitive accepts any conforming schema, so ArkType, // Zod and Valibot are all first-class without a line of adapter code. import { SchemaUnsupportedError, ValidationFailedError, type ValidationIssue } from './errors'; export interface StandardSchemaV1 { readonly '~standard': StandardSchemaProps; } export interface StandardSchemaProps { readonly version: 1; /** Library name, e.g. `ultimate`, `arktype`, `zod`. */ readonly vendor: string; readonly validate: (value: unknown) => StandardResult | Promise>; readonly types?: StandardTypes | undefined; } export interface StandardTypes { readonly input: Input; readonly output: Output; } export interface StandardSuccessResult { readonly value: Output; readonly issues?: undefined; } export interface StandardFailureResult { readonly issues: readonly StandardIssue[]; } export type StandardResult = StandardSuccessResult | StandardFailureResult; export interface StandardPathSegment { readonly key: PropertyKey; } export interface StandardIssue { readonly message: string; readonly path?: readonly (PropertyKey | StandardPathSegment)[] | undefined; } export type InferInput = NonNullable['input']; export type InferOutput = NonNullable< S['~standard']['types'] >['output']; /** `['items', 0, 'price']` -> `items[0].price` */ export function formatPath( path: readonly (PropertyKey | StandardPathSegment)[] | undefined, ): string { if (path === undefined || path.length === 0) return ''; let out = ''; for (const segment of path) { const key = typeof segment === 'object' ? segment.key : segment; if (typeof key === 'number') { out += `[${key}]`; } else { out += out === '' ? String(key) : `.${String(key)}`; } } return out; } /** * Any issue list this package or its consumers produce, whatever shape the path takes — * a `StandardIssue`'s segment array, or an already-flattened dotted string. */ export interface FormattableIssue { readonly message: string; readonly path?: string | readonly (PropertyKey | StandardPathSegment)[] | undefined; } /** * The one issue-to-text renderer for the whole framework: `x_validation_failed`, * MCP arg errors and action input/output errors all read the same `path: message` * line so a human or an agent parses one format everywhere. Callers join or index * the array however their surface needs it. */ export function formatIssues(issues: readonly FormattableIssue[]): readonly string[] { return issues.map((issue) => { const path = typeof issue.path === 'string' ? issue.path : formatPath(issue.path); return path === '' ? issue.message : `${path}: ${issue.message}`; }); } /** Normalise any conforming library's issues into Ultimate's path-annotated shape. */ export function toValidationIssues(issues: readonly StandardIssue[]): readonly ValidationIssue[] { return issues.map((issue) => ({ path: formatPath(issue.path), expected: issue.message, received: '', message: issue.message, })); } /** * Async by the rule `await` itself uses — a callable `then` — never `instanceof Promise`. A * library's own promise class, a polyfill or a Promise from another realm fails that test, and was * then read as a RESULT: no `issues` key, so a success, with `value` undefined. `parse` returned * `undefined` for input nothing had validated. */ export function isThenable(value: unknown): value is PromiseLike { if ((typeof value !== 'object' && typeof value !== 'function') || value === null) return false; return typeof (value as { then?: unknown }).then === 'function'; } function assertSync( result: StandardResult | Promise>, vendor: string, ): StandardResult { if (isThenable(result)) { throw new SchemaUnsupportedError({ cause: `${vendor} validated asynchronously in a synchronous call site`, fix: 'call validateAsync()/parseAsync() instead of validate()/parse()', meta: { vendor }, }); } return result; } /** Validate against ANY conforming schema. Never throws on invalid input. */ export function validate( schema: S, value: unknown, ): StandardResult> { const props = schema['~standard']; return assertSync(props.validate(value), props.vendor) as StandardResult>; } export async function validateAsync( schema: S, value: unknown, ): Promise>> { const result = await schema['~standard'].validate(value); return result as StandardResult>; } /** Validate or throw `X_VALIDATION_FAILED` with a path-annotated issue list. */ export function parse( schema: S, value: unknown, root = 'value', ): InferOutput { const result = validate(schema, value); if (result.issues === undefined) return result.value; throw new ValidationFailedError(toValidationIssues(result.issues), root); } export async function parseAsync( schema: S, value: unknown, root = 'value', ): Promise> { const result = await validateAsync(schema, value); if (result.issues === undefined) return result.value; throw new ValidationFailedError(toValidationIssues(result.issues), root); } export function isStandardSchema(value: unknown): value is StandardSchemaV1 { if (typeof value !== 'object' || value === null || !('~standard' in value)) return false; const props = (value as { '~standard': unknown })['~standard']; return ( typeof props === 'object' && props !== null && (props as { version?: unknown }).version === 1 && typeof (props as { validate?: unknown }).validate === 'function' ); }