// 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'
);
}