import { type OmpErrors, OmpTypeError } from "./errors"; import { type EmbeddableSchema, embed, type IR, IR_BRAND, type PropIR } from "./ir"; import { type NarrowContext, type Type, type } from "./type"; interface OptionalSchemaMarker { readonly _optional: true; } interface RefineOptions { message?: string; error?: string; } interface Decoratable extends EmbeddableSchema { (value: unknown): Out | OmpErrors; narrow(predicate: (value: Out, context: NarrowContext) => unknown): Decoratable; pipe(transform: (value: Out, context: NarrowContext) => Next): Decoratable>; or(def: unknown): Decoratable; describe(description: string): Decoratable; default(value: Out | (() => Out)): Decoratable; } export interface SuperRefineIssue { code?: string; path?: PropertyKey[]; message: string; actual?: unknown; } export interface SuperRefineContext { addIssue(issue: SuperRefineIssue): void; } export interface ZodLikeIssue { path: PropertyKey[]; message: string; } export type ZodLikeSafeParseResult = | { success: true; data: Out } | { success: false; error: { message: string; issues: ZodLikeIssue[] } }; /** A callable omptype schema carrying the Zod-v4-style fluent surface. */ export interface ZodLikeSchema extends Type { readonly _output: Out; /** @internal Used while composing object property IR. */ readonly isOptional: boolean; parse(value: unknown): Out; safeParse(value: unknown): ZodLikeSafeParseResult; min(bound: number): ZodLikeSchema; max(bound: number): ZodLikeSchema; int(): ZodLikeSchema; positive(): ZodLikeSchema; nonnegative(): ZodLikeSchema; regex(expression: RegExp, message?: string): ZodLikeSchema; url(): ZodLikeSchema; optional(): ZodLikeSchema & OptionalSchemaMarker; nullable(): ZodLikeSchema; default(value: Exclude | (() => Exclude)): ZodLikeSchema>; describe(description: string): ZodLikeSchema; refine(predicate: (value: Out) => unknown, messageOrOptions?: string | RefineOptions): ZodLikeSchema; superRefine(refinement: (value: Out, ctx: SuperRefineContext) => void): ZodLikeSchema; trim(): ZodLikeSchema; transform(transformer: (value: Out) => Next): ZodLikeSchema; catch(fallback: Out | (() => Out)): ZodLikeSchema; strict(): ZodLikeSchema; passthrough(): ZodLikeSchema>; strip(): ZodLikeSchema; partial(): Out extends object ? ZodLikeSchema> : ZodLikeSchema; } function schemaFromIR(ir: IR): Decoratable { const embedded: EmbeddableSchema = { [IR_BRAND]: true, ir, hasSteps: false, hasDefault: false, run: value => value, }; return type.raw(embedded) as unknown as Decoratable; } function restrictBase(source: Decoratable, ir: IR): Decoratable { let next = source.hasSteps ? schemaFromIR({ k: "morph", input: ir, fn: value => source(value) }) : schemaFromIR(ir); if (source.ir.desc !== undefined) next = next.describe(source.ir.desc); if (source.hasDefault) next = next.default(source.defaultValue as Out | (() => Out)); return next; } function lengthBound(kind: "min" | "max", schema: Decoratable, bound: number): void { if (schema.ir.k !== "string" && schema.ir.k !== "array") return; if (!Number.isSafeInteger(bound) || bound < 0) { throw new OmpTypeError(`${kind} length must be a nonnegative safe integer`); } } function refinementMessage(messageOrOptions: string | RefineOptions | undefined): string { if (typeof messageOrOptions === "string") return messageOrOptions; return messageOrOptions?.message ?? messageOrOptions?.error ?? "valid (refinement failed)"; } function isStringKeyIR(ir: IR): boolean { switch (ir.k) { case "string": return true; case "lit": return typeof ir.v === "string"; case "union": return ir.members.length > 0 && ir.members.every(isStringKeyIR); case "sub": return isStringKeyIR(ir.schema.ir); default: return false; } } function decorate(schema: Decoratable, optional = false): ZodLikeSchema { const next = (inner: Decoratable, nextOptional = optional): ZodLikeSchema => decorate(inner, nextOptional); const withObjectExtras = (extras: "keep" | "reject" | "delete"): ZodLikeSchema => { if (schema.ir.k !== "object") throw new OmpTypeError("object mode requires an object schema"); return next(restrictBase(schema, { ...schema.ir, extras })); }; Object.defineProperty(schema, "isOptional", { value: optional, enumerable: false }); return Object.assign(schema, { parse(value: unknown): Out { const result = schema(value); if (result instanceof type.errors) throw new Error(result.summary); return result; }, safeParse(value: unknown): ZodLikeSafeParseResult { const result = schema(value); if (!(result instanceof type.errors)) return { success: true, data: result }; return { success: false, error: { message: result.summary, issues: result.map(issue => ({ path: [...issue.path], message: issue.problem })), }, }; }, min(bound: number): ZodLikeSchema { const ir = schema.ir; if (ir.k === "string" || ir.k === "array") { lengthBound("min", schema, bound); const min = ir.min === undefined ? bound : Math.max(ir.min, bound); return next(restrictBase(schema, { ...ir, min })); } if (ir.k === "number") { if (Number.isNaN(bound)) throw new OmpTypeError("number min must not be NaN"); if (ir.min !== undefined && ir.min >= bound) return next(restrictBase(schema, ir)); return next(restrictBase(schema, { ...ir, min: bound, xmin: false })); } if ( ir.k === "morph" && !schema.hasSteps && ir.out !== undefined && (ir.out.k === "string" || ir.out.k === "array") ) { if (!Number.isSafeInteger(bound) || bound < 0) throw new OmpTypeError("min length must be a nonnegative safe integer"); const out = { ...ir.out, min: ir.out.min === undefined ? bound : Math.max(ir.out.min, bound) }; return next(restrictBase(schema, { ...ir, out })); } if (ir.k === "morph" && (schema.hasSteps || ir.out === undefined)) { if (!Number.isSafeInteger(bound) || bound < 0) throw new OmpTypeError("min length must be a nonnegative safe integer"); return next( schema.narrow((value, ctx) => { if (typeof value === "string" || Array.isArray(value)) { return value.length >= bound || ctx.mustBe(`at least ${bound} characters`); } return ctx.mustBe("a string or array"); }), ); } throw new OmpTypeError(`cannot apply min to ${ir.k}`); }, max(bound: number): ZodLikeSchema { const ir = schema.ir; if (ir.k === "string" || ir.k === "array") { lengthBound("max", schema, bound); const max = ir.max === undefined ? bound : Math.min(ir.max, bound); return next(restrictBase(schema, { ...ir, max })); } if (ir.k === "number") { if (Number.isNaN(bound)) throw new OmpTypeError("number max must not be NaN"); if (ir.max !== undefined && ir.max <= bound) return next(restrictBase(schema, ir)); return next(restrictBase(schema, { ...ir, max: bound, xmax: false })); } if ( ir.k === "morph" && !schema.hasSteps && ir.out !== undefined && (ir.out.k === "string" || ir.out.k === "array") ) { if (!Number.isSafeInteger(bound) || bound < 0) throw new OmpTypeError("max length must be a nonnegative safe integer"); const out = { ...ir.out, max: ir.out.max === undefined ? bound : Math.min(ir.out.max, bound) }; return next(restrictBase(schema, { ...ir, out })); } if (ir.k === "morph" && (schema.hasSteps || ir.out === undefined)) { if (!Number.isSafeInteger(bound) || bound < 0) throw new OmpTypeError("max length must be a nonnegative safe integer"); return next( schema.narrow((value, ctx) => { if (typeof value === "string" || Array.isArray(value)) { return value.length <= bound || ctx.mustBe(`at most ${bound} characters`); } return ctx.mustBe("a string or array"); }), ); } throw new OmpTypeError(`cannot apply max to ${ir.k}`); }, int(): ZodLikeSchema { if (schema.ir.k !== "number") throw new OmpTypeError(`cannot apply int to ${schema.ir.k}`); return next(restrictBase(schema, { ...schema.ir, int: true })); }, positive(): ZodLikeSchema { if (schema.ir.k !== "number") throw new OmpTypeError(`cannot apply positive to ${schema.ir.k}`); const ir = schema.ir; if (ir.min !== undefined && ir.min > 0) return next(restrictBase(schema, ir)); return next(restrictBase(schema, { ...ir, min: 0, xmin: true })); }, nonnegative(): ZodLikeSchema { if (schema.ir.k !== "number") throw new OmpTypeError(`cannot apply nonnegative to ${schema.ir.k}`); return this.min(0); }, regex(expression: RegExp, message?: string): ZodLikeSchema { const ir = schema.ir; const isStringLike = ir.k === "string" || (ir.k === "morph" && (ir.out?.k === "string" || (ir.out === undefined && ir.input.k === "string"))); if (!isStringLike) throw new OmpTypeError(`cannot apply regex to ${ir.k}`); const expectation = message ?? `matching ${expression}`; const narrowed = schema.narrow((value, ctx) => { if (typeof value !== "string") return ctx.mustBe("a string"); expression.lastIndex = 0; const matches = expression.test(value); expression.lastIndex = 0; return matches || ctx.mustBe(expectation); }); return next(narrowed); }, url(): ZodLikeSchema { const ir = schema.ir; if (ir.k === "string") return next(restrictBase(schema, { ...ir, url: true })); if (ir.k === "morph" && !schema.hasSteps && ir.out !== undefined && ir.out.k === "string") { return next(restrictBase(schema, { ...ir, out: { ...ir.out, url: true } })); } if (ir.k === "morph" && (schema.hasSteps || ir.out === undefined)) { return next( schema.narrow((value, ctx) => { if (typeof value !== "string") return ctx.mustBe("a string"); try { new URL(value); return true; } catch { return ctx.mustBe("a valid URL"); } }), ); } throw new OmpTypeError(`cannot apply url to ${ir.k}`); }, optional(): ZodLikeSchema & OptionalSchemaMarker { const widened = schema.or(type.raw("undefined")) as Decoratable; return decorate(widened, true) as ZodLikeSchema & OptionalSchemaMarker; }, nullable(): ZodLikeSchema { return decorate(schema.or(type.raw("null")) as Decoratable, optional); }, default( value: Exclude | (() => Exclude), ): ZodLikeSchema> { type DefaultOut = Exclude; const widened = schema.or(type.raw("undefined")) as Decoratable; const piped = widened.pipe(output => { if (output !== undefined) return output as DefaultOut; return typeof value === "function" ? (value as () => DefaultOut)() : value; }) as Decoratable; return decorate(piped.default(value as DefaultOut | (() => DefaultOut))); }, describe(description: string): ZodLikeSchema { return next(restrictBase(schema, { ...schema.ir, desc: description }).describe(description)); }, refine(predicate: (value: Out) => unknown, messageOrOptions?: string | RefineOptions): ZodLikeSchema { const expectation = refinementMessage(messageOrOptions); return next(schema.narrow((value, ctx) => Boolean(predicate(value)) || ctx.mustBe(expectation))); }, superRefine(refinement: (value: Out, ctx: SuperRefineContext) => void): ZodLikeSchema { return next( schema.narrow((value, ctx) => { const proxy: SuperRefineContext = { addIssue(issue) { ctx.error({ expected: issue.message, path: issue.path ?? [], ...(issue.actual !== undefined ? { actual: issue.actual } : {}), }); }, }; refinement(value, proxy); return true; }), ); }, trim(): ZodLikeSchema { const ir = schema.ir; if (ir.k === "string" && !schema.hasSteps) { let trimmed = schemaFromIR({ k: "morph", input: { k: "string" }, fn: v => (v as string).trim(), out: ir, }); if (schema.hasDefault) trimmed = trimmed.default(schema.defaultValue as Out | (() => Out)); return next(trimmed); } if (ir.k === "morph" || schema.hasSteps) { let trimmed = schemaFromIR({ k: "morph", input: { k: "unknown" }, fn: v => { const r = schema(v); if (r instanceof type.errors) return r; if (typeof r !== "string") throw new OmpTypeError("trim requires a string output"); return r.trim(); }, out: { k: "string" }, }); if (schema.hasDefault) trimmed = trimmed.default(schema.defaultValue as Out | (() => Out)); return next(trimmed); } throw new OmpTypeError(`cannot apply trim to ${ir.k}`); }, transform(transformer: (value: Out) => Next): ZodLikeSchema { return decorate( schema.pipe(value => transformer(value)), optional, ); }, catch(fallback: Out | (() => Out)): ZodLikeSchema { const caught = type.unknown.pipe(input => { try { const result = schema(input); if (!(result instanceof type.errors)) return result; } catch { // A caught schema is deliberately total, including user refinement/transform exceptions. } return typeof fallback === "function" ? (fallback as () => Out)() : fallback; }); return decorate(caught as Decoratable, optional); }, strict(): ZodLikeSchema { return withObjectExtras("reject"); }, passthrough(): ZodLikeSchema> { return withObjectExtras("keep") as ZodLikeSchema>; }, strip(): ZodLikeSchema { return withObjectExtras("delete"); }, partial(): Out extends object ? ZodLikeSchema> : ZodLikeSchema { if (schema.ir.k !== "object") throw new OmpTypeError(`cannot apply partial to ${schema.ir.k}`); const props = schema.ir.props.map(prop => ({ ...prop, opt: true })); return next(restrictBase(schema, { ...schema.ir, props })) as Out extends object ? ZodLikeSchema> : ZodLikeSchema; }, }) as unknown as ZodLikeSchema; } function decorateUnknown(schema: Decoratable): ZodLikeSchema { return decorate(schema); } export type infer = T extends { readonly _output: infer Out } ? Out : never; type SchemaOutput = Schema extends { readonly _output: infer Out } ? Out : never; type Shape = Readonly>>; type ObjectOutput = { -readonly [K in keyof S as S[K] extends OptionalSchemaMarker ? never : K]: SchemaOutput; } & { -readonly [K in keyof S as S[K] extends OptionalSchemaMarker ? K : never]?: SchemaOutput; }; type Simplify = { [K in keyof T]: T[K] }; type UnionOutput[]> = SchemaOutput; function objectSchema(shape: S): ZodLikeSchema> { const props: PropIR[] = []; for (const key in shape) { const member = shape[key]; const prop: PropIR = { key, opt: member.isOptional, val: embed(member) }; if (member.hasDefault) { prop.hasDefault = true; prop.def = member.defaultValue; prop.defFactory = typeof member.defaultValue === "function"; } props.push(prop); } return decorateUnknown(schemaFromIR({ k: "object", props, extras: "delete" })) as unknown as ZodLikeSchema< ObjectOutput >; } export const string = (): ZodLikeSchema => decorate(schemaFromIR(type.string.ir)); export const number = (): ZodLikeSchema => decorate(schemaFromIR(type.number.ir)); export const boolean = (): ZodLikeSchema => decorate(schemaFromIR(type.boolean.ir)); export const literal = (value: Value): ZodLikeSchema => decorate(schemaFromIR(type.enumerated(value).ir)); const enumSchema = ( values: Values, ): ZodLikeSchema => { if (values.length === 0) throw new OmpTypeError("enum requires at least one value"); return decorate(schemaFromIR(type.enumerated(...values).ir)); }; export { enumSchema as enum }; export const union = < const Schemas extends readonly [ZodLikeSchema, ZodLikeSchema, ...ZodLikeSchema[]], >( schemas: Schemas, ): ZodLikeSchema> => decorate(schemaFromIR({ k: "union", members: schemas.map(schema => embed(schema)) })); export const array = (element: ZodLikeSchema): ZodLikeSchema => decorate(schemaFromIR({ k: "array", el: embed(element) })); export const object = (shape: S): ZodLikeSchema>> => objectSchema(shape); export function record( keySchema: ZodLikeSchema, valueSchema: ZodLikeSchema, ): ZodLikeSchema>; export function record(valueSchema: ZodLikeSchema): ZodLikeSchema>; export function record( keyOrValueSchema: ZodLikeSchema | ZodLikeSchema, valueSchema?: ZodLikeSchema, ): ZodLikeSchema> { const keySchema = (valueSchema === undefined ? string() : keyOrValueSchema) as ZodLikeSchema; const valSchema = (valueSchema === undefined ? keyOrValueSchema : valueSchema) as ZodLikeSchema; if (!isStringKeyIR(keySchema.ir)) throw new OmpTypeError("record keys must use a string schema"); const base = schemaFromIR>({ k: "object", props: [], index: embed(valSchema), extras: "keep", }); const checked = base.narrow((value, ctx: NarrowContext) => { for (const key in value) { if (keySchema(key) instanceof type.errors) return ctx.mustBe("a record with valid string keys"); } return true; }); return decorate(checked); } export const unknown = (): ZodLikeSchema => decorate(schemaFromIR(type.unknown.ir)); export const any = (): ZodLikeSchema => decorate(schemaFromIR(type.unknown.ir)); const nullSchema = (): ZodLikeSchema => decorate(type.raw("null") as unknown as Decoratable); const undefinedSchema = (): ZodLikeSchema => decorate(type.raw("undefined") as unknown as Decoratable); export { nullSchema as null, undefinedSchema as undefined }; /** Runtime `z.*` facade, merged with the `z.infer` type namespace below. */ export const z = { string, number, boolean, literal, enum: enumSchema, union, array, object, record, unknown, any, null: nullSchema, undefined: undefinedSchema, }; export namespace z { export type infer = Schema extends { readonly _output: infer Out } ? Out : never; }