Files
WRNexusJS/packages/validation/src/interop.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

151 lines
4.7 KiB
TypeScript

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<string, Record<string, unknown>>;
required?: string[];
additionalProperties: false;
}
function descriptorOf(schema: ObjectSchema | SchemaDescriptor): SchemaDescriptor {
return "describe" in schema ? schema.describe() : schema;
}
function fieldJsonSchema(field: SchemaDescriptor["fields"][string]): Record<string, unknown> {
const output: Record<string, unknown> = 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<string, Record<string, unknown>> = {};
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, unknown>,
) => 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<string, unknown>),
}),
}));
}
return descriptor;
}
export function parseDescriptor<T = Record<string, unknown>>(
descriptor: SchemaDescriptor,
source: Record<string, unknown>,
): ParseResult<T> {
const value: Record<string, unknown> = {};
const errors: Record<string, string> = {};
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 };
}