import { checkField, type ObjectSchema, type ParseResult, type RuleDescriptor, type SchemaDescriptor, } from "./index.ts"; export interface JsonSchemaDocument { $schema: "https://json-schema.org/draft/2020-12/schema"; $id?: string; title?: string; type: "object"; properties: Record>; required?: string[]; additionalProperties: false; } function descriptorOf(schema: ObjectSchema | SchemaDescriptor): SchemaDescriptor { return "describe" in schema ? schema.describe() : schema; } function fieldJsonSchema(field: SchemaDescriptor["fields"][string]): Record { const output: Record = field.type === "unknown" ? {} : { type: field.type }; if (field.label) output.title = field.label; if (field.trim) output["x-wrn-trim"] = true; if (field.typeMessage) output["x-wrn-type-message"] = field.typeMessage; for (const rule of field.rules) { switch (rule.kind) { case "min": output[field.type === "string" ? "minLength" : "minimum"] = rule.n; break; case "max": output[field.type === "string" ? "maxLength" : "maximum"] = rule.n; break; case "length": output.minLength = rule.n; output.maxLength = rule.n; break; case "email": case "uuid": case "date": output.format = rule.kind; break; case "url": output.format = "uri"; break; case "oneOf": output.enum = rule.values; break; case "pattern": output.pattern = rule.source; if (rule.flags) output["x-wrn-pattern-flags"] = rule.flags; break; case "integer": output.type = "integer"; break; } if (rule.message) { const messages = (output["x-wrn-messages"] ??= []) as string[]; messages.push(rule.message); } } return output; } export function toJsonSchema( schema: ObjectSchema | SchemaDescriptor, options: { id?: string; title?: string } = {}, ): JsonSchemaDocument { const descriptor = descriptorOf(schema); const properties: Record> = {}; const required: string[] = []; for (const [name, field] of Object.entries(descriptor.fields)) { properties[name] = fieldJsonSchema(field); if (!field.optional) required.push(name); } return { $schema: "https://json-schema.org/draft/2020-12/schema", ...(options.id ? { $id: options.id } : {}), ...(options.title ? { title: options.title } : {}), type: "object", properties, ...(required.length ? { required } : {}), additionalProperties: false, }; } export function openApiRequestBody( schema: ObjectSchema | SchemaDescriptor, options: { description?: string; required?: boolean; contentTypes?: string[] } = {}, ) { const document = toJsonSchema(schema); const { $schema: _, ...openApiSchema } = document; const content = Object.fromEntries( (options.contentTypes ?? ["application/json"]).map((contentType) => [ contentType, { schema: openApiSchema }, ]), ); return { ...(options.description ? { description: options.description } : {}), required: options.required ?? true, content, }; } export type ValidationMessageKey = "required" | "number" | `rule.${RuleDescriptor["kind"]}`; export type ValidationMessageTranslator = ( key: ValidationMessageKey, params: Record, ) => string; export function localizeDescriptor( schema: ObjectSchema | SchemaDescriptor, translate: ValidationMessageTranslator, ): SchemaDescriptor { const descriptor = structuredClone(descriptorOf(schema)); for (const [field, value] of Object.entries(descriptor.fields)) { value.requiredMessage ??= translate("required", { field, label: value.label ?? field }); if (value.type === "number") { value.typeMessage ??= translate("number", { field, label: value.label ?? field }); } value.rules = value.rules.map((rule) => ({ ...rule, message: rule.message ?? translate(`rule.${rule.kind}`, { field, label: value.label ?? field, ...(rule as unknown as Record), }), })); } return descriptor; } export function parseDescriptor>( descriptor: SchemaDescriptor, source: Record, ): ParseResult { const value: Record = {}; const errors: Record = {}; for (const [name, field] of Object.entries(descriptor.fields)) { const result = checkField(field, source[name]); value[name] = result.value; if (result.error) errors[name] = result.error; } return { ok: Object.keys(errors).length === 0, value: value as T, errors }; }