import { invalid, type ObjectSchema, type ParseResult, type SchemaDescriptor } from "./index.ts"; export interface AsyncValidationContext { value: T; addIssue(field: keyof T | string, message: string): void; signal?: AbortSignal; } export type AsyncRefinement = (context: AsyncValidationContext) => void | Promise; export class AsyncObjectSchema> { readonly #refinements: AsyncRefinement[] = []; constructor(readonly base: ObjectSchema) {} refine(refinement: AsyncRefinement): this { this.#refinements.push(refinement); return this; } describe(): SchemaDescriptor { return this.base.describe(); } async parse(input: unknown, signal?: AbortSignal): Promise> { const result = this.base.parse(input) as ParseResult; if (!result.ok) return result; const errors: Record = {}; const context: AsyncValidationContext = { value: result.value, signal, addIssue(field, message) { if (!(String(field) in errors)) errors[String(field)] = message; }, }; for (const refinement of this.#refinements) { if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError"); await refinement(context); } return { ok: Object.keys(errors).length === 0, value: result.value, errors }; } } export function asyncSchema(schema: ObjectSchema): AsyncObjectSchema { return new AsyncObjectSchema(schema); } export async function parseBodyAsync( schema: AsyncObjectSchema, request: Request, signal?: AbortSignal, ): Promise<{ ok: true; value: T } | { ok: false; response: Response }> { let body: Record = {}; const contentType = request.headers.get("content-type") ?? ""; try { if (contentType.includes("json")) body = (await request.json()) as Record; else { const data = await request.formData(); for (const [key, value] of data) body[key] = value; } } catch { /* handled by schema */ } const result = await schema.parse(body, signal); return result.ok ? { ok: true, value: result.value } : { ok: false, response: invalid(result.errors) }; } export interface OpenApiSchema { type: "object"; properties: Record>; required?: string[]; } export function schemaToOpenApi(schema: ObjectSchema | AsyncObjectSchema): OpenApiSchema { const descriptor = schema.describe(); const required: string[] = []; const properties: Record> = {}; for (const [name, field] of Object.entries(descriptor.fields)) { const property: Record = { type: field.type === "boolean" ? "boolean" : field.type, }; if (field.label) property.title = field.label; for (const rule of field.rules) { if (rule.kind === "min") property[field.type === "string" ? "minLength" : "minimum"] = rule.n; else if (rule.kind === "max") property[field.type === "string" ? "maxLength" : "maximum"] = rule.n; else if (rule.kind === "length") property.minLength = property.maxLength = rule.n; else if (["email", "url", "uuid", "date"].includes(rule.kind)) property.format = rule.kind === "url" ? "uri" : rule.kind; else if (rule.kind === "oneOf") property.enum = rule.values; else if (rule.kind === "pattern") property.pattern = rule.source; else if (rule.kind === "integer") property.type = "integer"; } properties[name] = property; if (!field.optional) required.push(name); } return { type: "object", properties, ...(required.length ? { required } : {}) }; } export function mergeValidationResults(...results: ParseResult[]): ParseResult { return { ok: results.every((result) => result.ok), value: Object.assign({}, ...results.map((result) => result.value)) as T, errors: Object.assign({}, ...results.map((result) => result.errors)), }; }