import { OmpTypeError } from "./errors"; import type { Def, EmbeddableSchema } from "./ir"; import { type NarrowContext, type Type as OmpType, type ToJsonSchemaOptions, type } from "./type"; export interface Meta { title?: string; description?: string; default?: unknown; examples?: unknown[]; [key: string]: unknown; } export interface StringOpts extends Meta { minLength?: number; maxLength?: number; pattern?: string; format?: string; } export interface NumberOpts extends Meta { minimum?: number; maximum?: number; exclusiveMinimum?: number; exclusiveMaximum?: number; multipleOf?: number; } export interface ArrayOpts extends Meta { minItems?: number; maxItems?: number; uniqueItems?: boolean; } export interface ObjectOpts extends Meta { additionalProperties?: boolean | AnySchema; } const OPTIONAL_INNER = Symbol("omptype.typebox.optionalInner"); const OBJECT_INFO = Symbol("omptype.typebox.objectInfo"); export interface TypeBoxValidationFailure { message: string; } export type TypeBoxSafeParseResult = | { success: true; data: T } | { success: false; error: TypeBoxValidationFailure }; interface LegacyTypeBoxCompat { /** TypeBox compatibility validator used by legacy extension loaders. */ __validator(data: unknown): T | TypeBoxValidationFailure; /** Zod-style compatibility parser used by legacy extensions. */ safeParse(input: unknown): TypeBoxSafeParseResult; } /** * Erased schema surface accepted anywhere this facade takes a schema, native * omptype schemas included (those carry no legacy compat members). * * Members use method syntax so parameter positions stay bivariant: the typed * {@link TTyped} form is invariant in its static type (its `in`/`out` * validators are), so a concrete `TString` is not assignable to * `TTyped`. Erasing here is what keeps `TString`, `TObject<…>` and * friends assignable to a plain schema annotation. */ export interface AnySchema extends EmbeddableSchema { (data: unknown): unknown; readonly infer: unknown; readonly hasSteps: boolean; toJsonSchema(options?: ToJsonSchemaOptions): Record; } /** * Every schema this facade returns: the TypeBox `TSchema` analog. Erased like * {@link AnySchema}, plus the legacy compat members builder results carry. */ export interface TSchema extends AnySchema { /** TypeBox compatibility validator used by legacy extension loaders. */ __validator(data: unknown): unknown; /** Zod-style compatibility parser used by legacy extensions. */ safeParse(input: unknown): TypeBoxSafeParseResult; } /** Schema carrying a statically known type; every `TXxx` alias resolves here. */ export type TTyped = OmpType & LegacyTypeBoxCompat; export type Static = T["infer"]; export type TAny = TTyped; export type TUnknown = TTyped; export type TNever = TTyped; export type TNull = TTyped; export type TString = TTyped; export type TNumber = TTyped; export type TInteger = TTyped; export type TBoolean = TTyped; export type TLiteral = TTyped; export type TArray = TTyped[]>; export type TTuple = TTyped<{ -readonly [K in keyof E]: Static; }>; export type TOptional = TTyped | undefined> & { readonly [OPTIONAL_INNER]: E }; export type TUnion = TTyped>; export type TIntersect = TTyped< UnionToIntersection> >; export type TEnum = TTyped; export type TRecord = TTyped< Record, PropertyKey>, Static> >; export type TNullable = TTyped | null>; export type TReadonly = TTyped>>; export type TUnsafe = TTyped; type OptionalKeys

> = { [K in keyof P]-?: P[K] extends { readonly [OPTIONAL_INNER]: AnySchema } ? K : never; }[keyof P]; type RequiredKeys

> = Exclude>; type ObjectStatic

> = { [K in RequiredKeys

]: Static; } & { [K in OptionalKeys

]?: Exclude, undefined>; }; export type TObject

= Record> = TTyped>; type RequiredProps

> = { [K in keyof P]: P[K] extends TOptional ? E : P[K]; }; interface RuntimeType extends OmpType { [OPTIONAL_INNER]?: AnySchema; [OBJECT_INFO]?: ObjectInfo; describe(description: string): RuntimeType; default(value: T | (() => T)): RuntimeType; or(schema: schema): RuntimeType>; and(schema: schema): RuntimeType>; array(): RuntimeType; atLeastLength(bound: number): RuntimeType; atMostLength(bound: number): RuntimeType; atLeast(bound: number): RuntimeType; atMost(bound: number): RuntimeType; narrow(predicate: (value: T, ctx: NarrowContext) => value is N): RuntimeType; narrow(predicate: (value: T, ctx: NarrowContext) => boolean): RuntimeType; } type CompatRuntime = RuntimeType & LegacyTypeBoxCompat; type ObjectInfo = { props: Record; additionalProperties?: boolean | AnySchema; }; function asRuntime(schema: AnySchema): RuntimeType { return schema as unknown as RuntimeType; } function asSchema(schema: AnySchema): TTyped { return schema as unknown as TTyped; } function validationFailure(message: string): TypeBoxValidationFailure { return { message }; } function withLegacyCompat(schema: OmpType): CompatRuntime { const compatSchema = schema as unknown as CompatRuntime; if (!Object.hasOwn(compatSchema, "__validator")) { Object.defineProperty(compatSchema, "__validator", { value: (data: unknown): T | TypeBoxValidationFailure => { const result = schema(data); return result instanceof type.errors ? validationFailure(result.summary) : result; }, configurable: true, }); } if (!Object.hasOwn(compatSchema, "safeParse")) { Object.defineProperty(compatSchema, "safeParse", { value: (input: unknown): TypeBoxSafeParseResult => { const result = schema(input); return result instanceof type.errors ? { success: false, error: validationFailure(result.summary) } : { success: true, data: result }; }, configurable: true, }); } return compatSchema; } function applyMeta(schema: RuntimeType, opts?: Meta): CompatRuntime { let result = schema; const description = opts?.description ?? opts?.title; if (description !== undefined) result = result.describe(description); if (opts && Object.hasOwn(opts, "default")) result = result.default(opts.default as T); return withLegacyCompat(result); } function withJsonSchemaKeywords(schema: CompatRuntime, keywords: Record): CompatRuntime { const emitBase = schema.toJsonSchema.bind(schema); schema.toJsonSchema = options => ({ ...emitBase(options), ...keywords }); return schema; } function checkFiniteOption(name: string, value: number | undefined): void { if (value !== undefined && !Number.isFinite(value)) throw new OmpTypeError(`${name} must be finite`); } function tString(opts?: StringOpts): TString { checkFiniteOption("minLength", opts?.minLength); checkFiniteOption("maxLength", opts?.maxLength); let schema = asRuntime(type.raw(opts?.format === "url" || opts?.format === "uri" ? "string.url" : "string")); if (opts?.minLength !== undefined) schema = schema.atLeastLength(opts.minLength); if (opts?.maxLength !== undefined) schema = schema.atMostLength(opts.maxLength); if (opts?.pattern !== undefined) { let regex: RegExp; try { regex = new RegExp(opts.pattern); } catch { throw new OmpTypeError(`invalid regular expression pattern ${JSON.stringify(opts.pattern)}`); } schema = schema.narrow((value, ctx) => regex.test(value) || ctx.mustBe(`a string matching ${opts.pattern}`)); } if (opts?.format !== undefined && opts.format !== "url" && opts.format !== "uri") { const format = opts.format; const valid = formatPredicate(format); schema = schema.narrow((value, ctx) => valid(value) || ctx.mustBe(`a string in ${format} format`)); } const result = applyMeta(schema, opts); const keywords: Record = {}; if (opts?.pattern !== undefined) keywords.pattern = opts.pattern; if (opts?.format !== undefined) keywords.format = opts.format === "url" ? "uri" : opts.format; return opts?.pattern !== undefined || opts?.format !== undefined ? withJsonSchemaKeywords(result, keywords) : result; } function formatPredicate(format: string): (value: string) => boolean { switch (format) { case "url": case "uri": return value => { try { new URL(value); return true; } catch { return false; } }; case "email": return value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); case "uuid": return value => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); case "date-time": return value => /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(value) && !Number.isNaN(Date.parse(value)); case "date": return value => /^\d{4}-\d\d-\d\d$/.test(value) && !Number.isNaN(Date.parse(`${value}T00:00:00Z`)); default: return () => true; } } function tNumber(opts?: NumberOpts, integer = false): TNumber { for (const key of ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"] as const) { checkFiniteOption(key, opts?.[key]); } if (opts?.multipleOf !== undefined && opts.multipleOf <= 0) throw new OmpTypeError("multipleOf must be greater than zero"); let lower: { value: number; exclusive: boolean } | undefined; if (opts?.minimum !== undefined) lower = { value: opts.minimum, exclusive: false }; if (opts?.exclusiveMinimum !== undefined && (!lower || opts.exclusiveMinimum >= lower.value)) { lower = { value: opts.exclusiveMinimum, exclusive: true }; } let upper: { value: number; exclusive: boolean } | undefined; if (opts?.maximum !== undefined) upper = { value: opts.maximum, exclusive: false }; if (opts?.exclusiveMaximum !== undefined && (!upper || opts.exclusiveMaximum <= upper.value)) { upper = { value: opts.exclusiveMaximum, exclusive: true }; } const keyword = integer ? "number.integer" : "number"; // The `LO <= TYPE <= HI` range spelling requires both bounds; a min-only // bound must use the postfix `TYPE >= LO` form (see parseBounded in ir.ts). let src: string; if (lower && upper) { src = `${lower.value} ${lower.exclusive ? "<" : "<="} ${keyword} ${upper.exclusive ? "<" : "<="} ${upper.value}`; } else if (lower) { src = `${keyword} ${lower.exclusive ? ">" : ">="} ${lower.value}`; } else if (upper) { src = `${keyword} ${upper.exclusive ? "<" : "<="} ${upper.value}`; } else { src = keyword; } let schema = asRuntime(type.raw(src)); if (opts?.multipleOf !== undefined) { const divisor = opts.multipleOf; schema = schema.narrow((value, ctx) => { const quotient = value / divisor; return ( Math.abs(quotient - Math.round(quotient)) <= Number.EPSILON * Math.max(1, Math.abs(quotient)) || ctx.mustBe(`a multiple of ${divisor}`) ); }); } const result = applyMeta(schema, opts); return opts?.multipleOf !== undefined ? withJsonSchemaKeywords(result, { multipleOf: opts.multipleOf }) : result; } function tLiteral(value: V, opts?: Meta): TLiteral { return applyMeta(asRuntime(type.enumerated(value)), opts); } function tNever(opts?: Meta): TNever { return applyMeta( asRuntime(type.raw("unknown")).narrow((_value, ctx): _value is never => ctx.mustBe("never")), opts, ); } function tUnion(schemas: E, opts?: Meta): TUnion { if (schemas.length === 0) return asSchema>(tNever(opts)); let result = asRuntime(schemas[0]); for (let i = 1; i < schemas.length; i++) result = result.or(schemas[i]); return asSchema>(applyMeta(result, opts)); } function tIntersect( schemas: E, opts?: Meta, ): TTyped>> { if (schemas.length === 0) { return applyMeta(asRuntime>>(type.raw("unknown")), opts); } const validateAll = (): RuntimeType>> => { const base = asRuntime(type.raw("unknown")); return base.narrow((value, ctx): value is UnionToIntersection> => { for (const schema of schemas) { if (schema(value) instanceof type.errors) return ctx.mustBe("a value satisfying every intersection member"); } return true; }); }; if (schemas.some(schema => schema.hasSteps)) return applyMeta(validateAll(), opts); let result = asRuntime(schemas[0]); try { for (let i = 1; i < schemas.length; i++) result = result.and(schemas[i]); } catch (error) { if (error instanceof OmpTypeError) return applyMeta(validateAll(), opts); throw error; } return asSchema>>(applyMeta(result, opts)); } type UnionToIntersection = (U extends unknown ? (value: U) => void : never) extends (value: infer I) => void ? I : never; function enumValues(values: Record | readonly (string | number)[]): (string | number)[] { if (Array.isArray(values)) return [...values]; const result: (string | number)[] = []; const record = values as Record; for (const key in record) { const value = record[key]; if (!(/^\d+$/.test(key) && typeof value === "string") && !result.includes(value)) result.push(value); } return result; } function tEnum | readonly (string | number)[]>( values: E, opts?: Meta, ): TTyped { return applyMeta( asRuntime(type.enumerated(...enumValues(values))), opts, ); } function tArray(item: E, opts?: ArrayOpts): TArray { checkFiniteOption("minItems", opts?.minItems); checkFiniteOption("maxItems", opts?.maxItems); let schema = asRuntime>(item).array(); if (opts?.minItems !== undefined) schema = schema.atLeastLength(opts.minItems); if (opts?.maxItems !== undefined) schema = schema.atMostLength(opts.maxItems); if (opts?.uniqueItems) { schema = schema.narrow((values, ctx) => { for (let i = 0; i < values.length; i++) { for (let j = i + 1; j < values.length; j++) { if (jsonEqual(values[i], values[j])) return ctx.mustBe("an array with unique items"); } } return true; }); } const result = applyMeta(schema, opts); return opts?.uniqueItems ? withJsonSchemaKeywords(result, { uniqueItems: true }) : result; } function jsonEqual(left: unknown, right: unknown): boolean { if (Object.is(left, right)) return true; if (typeof left !== "object" || left === null || typeof right !== "object" || right === null) return false; try { return JSON.stringify(left) === JSON.stringify(right); } catch { return false; } } function tTuple(items: E, opts?: Meta): TTuple { const schema = asRuntime(type.raw("unknown")).narrow( (value, ctx): value is { -readonly [K in keyof E]: Static } => { if (!Array.isArray(value) || value.length !== items.length) return ctx.mustBe(`a tuple of length ${items.length}`); for (let i = 0; i < items.length; i++) if (items[i](value[i]) instanceof type.errors) return ctx.mustBe(`a valid item at index ${i}`); return true; }, ); return applyMeta(schema, opts); } function tObject>(properties: P, opts?: ObjectOpts): TObject

{ const def: Record = {}; const props: Record = {}; for (const key in properties) { const schema = properties[key]; const inner = asRuntime(schema)[OPTIONAL_INNER]; // A defaulted `Type.Optional(...)` maps to a plain defaulted key: // omptype (like ArkType) rejects `key?` with a default, and a default // already makes the key omittable on input. const optionalKey = inner !== undefined && !asRuntime(inner).hasDefault; def[optionalKey ? `${key}?` : key] = inner ?? schema; props[key] = schema; } if (opts?.additionalProperties === false) def["+"] = "reject"; else if (opts?.additionalProperties && opts.additionalProperties !== true) def["[string]"] = opts.additionalProperties; const schema = applyMeta(asRuntime>(type.raw(def)), opts); schema[OBJECT_INFO] = { props, additionalProperties: opts?.additionalProperties }; return schema; } function tRecord(key: K, value: V, opts?: Meta): TRecord { const base = asRuntime>>(type.raw({ "[string]": value })).narrow((record, ctx) => { for (const name in record) if (key(name) instanceof type.errors) return ctx.mustBe("an object with valid record keys"); return true; }); return applyMeta(base, opts) as TRecord; } function tOptional(schema: E, opts?: Meta): TOptional { const marker = applyMeta( asRuntime>(schema).or(asRuntime(type.raw("undefined"))), opts, ) as RuntimeType | undefined>; marker[OPTIONAL_INNER] = schema; return marker as unknown as TOptional; } function tNullable(schema: E, opts?: Meta): TTyped | null> { return applyMeta(asRuntime>(schema).or(asRuntime(type.raw("null"))), opts); } function requireObject(schema: AnySchema, operation: string): ObjectInfo { const info = asRuntime(schema)[OBJECT_INFO]; if (!info) throw new OmpTypeError(`Type.${operation} requires a schema created by Type.Object`); return info; } function tPartial

>(schema: TObject

): TTyped>> { const info = requireObject(schema, "Partial"); const props: Record = {}; for (const key in info.props) props[key] = asRuntime(info.props[key])[OPTIONAL_INNER] ? info.props[key] : tOptional(info.props[key]); return tObject(props, { additionalProperties: info.additionalProperties }) as TTyped>>; } function tRequired

>(schema: TObject

): TObject> { const info = requireObject(schema, "Required"); const props: Record = {}; for (const key in info.props) { props[key] = asRuntime(info.props[key])[OPTIONAL_INNER] ?? info.props[key]; } return tObject(props, { additionalProperties: info.additionalProperties }) as TObject>; } function tPick

, const K extends readonly (keyof P)[]>( schema: TObject

, keys: K, ): TObject> { const info = requireObject(schema, "Pick"); const props: Record = {}; for (const key of keys) if (typeof key === "string" && info.props[key]) props[key] = info.props[key]; return tObject(props, { additionalProperties: info.additionalProperties }) as TObject>; } function tOmit

, const K extends readonly (keyof P)[]>( schema: TObject

, keys: K, ): TObject> { const info = requireObject(schema, "Omit"); const omitted = new Set(keys); const props: Record = {}; for (const key in info.props) if (!omitted.has(key)) props[key] = info.props[key]; return tObject(props, { additionalProperties: info.additionalProperties }) as TObject>; } function tComposite>[]>( schemas: E, opts?: ObjectOpts, ): TTyped>> { const props: Record = {}; for (const schema of schemas) Object.assign(props, requireObject(schema, "Composite").props); return asSchema>>(tObject(props, opts)); } function tUnsafe(_jsonSchema: Record = {}): TUnsafe { // Raw JSON Schema is accepted for source compatibility but is not retained or validated: // omptype cannot honestly implement that contract without importing a second validator. return withLegacyCompat(type.unknown as OmpType); } export const Type = { String: tString, Number: (opts?: NumberOpts) => tNumber(opts), Integer: (opts?: NumberOpts) => tNumber(opts, true), Boolean: (opts?: Meta) => applyMeta(asRuntime(type.raw("boolean")), opts), Null: (opts?: Meta) => applyMeta(asRuntime(type.raw("null")), opts), Any: (opts?: Meta) => applyMeta(asRuntime(type.raw("unknown")), opts), Unknown: (opts?: Meta) => applyMeta(asRuntime(type.raw("unknown")), opts), Never: tNever, Literal: tLiteral, Union: tUnion, Intersect: tIntersect, Enum: tEnum, Array: tArray, Tuple: tTuple, Object: tObject, Record: tRecord, Optional: tOptional, Nullable: tNullable, Readonly: (schema: E): TReadonly => asSchema>>(withLegacyCompat(asRuntime>>(schema))), Partial: tPartial, Required: tRequired, Pick: tPick, Omit: tOmit, Composite: tComposite, Unsafe: tUnsafe, } as const; export type TypeBuilder = typeof Type; export default { Type };