151 lines
4.7 KiB
TypeScript
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 };
|
|
}
|