Files
plainleaf/client/plugos/syscalls/jsonschema.ts
T
Zef Hemel 0c9c07b3e6 Config: schema defaults, UI categories and annotations
`config.define` now propagates schema `default` values, so features like smart quotes no longer need their own fallback constants.

Adds `defineCategory` / `getCategories` for the upcoming configuration  manager UI.
2026-04-21 10:03:40 +02:00

92 lines
2.6 KiB
TypeScript

import type { SysCallMapping } from "../system.ts";
import { type OutputUnit, Validator, format } from "@cfworker/json-schema";
import { stripFunctions } from "../util.ts";
// Register custom formats
format.email = (data: string) => data.includes("@");
format["page-ref"] = (data: string) =>
data.startsWith("[[") && data.endsWith("]]");
const schemaCache = new Map<string, Validator>();
function formatErrors(errors: OutputUnit[]): string {
// Filter out "properties" wrapper errors, keep only the specific leaf errors
const leafErrors = errors.filter((e) => e.keyword !== "properties");
const errorsToUse = leafErrors.length > 0 ? leafErrors : errors;
return errorsToUse
.map((e) => {
// Convert instanceLocation from "#/foo/bar" to "foo.bar"
const path =
e.instanceLocation === "#"
? ""
: e.instanceLocation.slice(2).replaceAll("/", ".");
return path ? `${path}: ${e.error}` : e.error;
})
.join(", ");
}
export function validateObject(schema: any, object: any): undefined | string {
try {
const schemaKey = JSON.stringify(schema);
if (!schemaCache.has(schemaKey)) {
const validator = new Validator(schema, "7");
schemaCache.set(schemaKey, validator);
}
const validator = schemaCache.get(schemaKey)!;
const result = validator.validate(stripFunctions(object));
if (result.valid) {
return;
} else {
return formatErrors(result.errors);
}
} catch (e: any) {
return e.message;
}
}
export function validateSchema(schema: any): undefined | string {
if (schema === null || schema === undefined) {
return "schema must not be null or undefined";
}
if (typeof schema === "boolean") {
return;
}
if (typeof schema !== "object" || Array.isArray(schema)) {
return "schema must be an object or boolean";
}
if (schema.type !== undefined) {
const validTypes = [
"string",
"number",
"integer",
"boolean",
"object",
"array",
"null",
];
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
for (const t of types) {
if (!validTypes.includes(t)) {
return `schema.type must be one of ${validTypes.join(", ")}`;
}
}
}
return;
}
export function jsonschemaSyscalls(): SysCallMapping {
return {
"jsonschema.validateObject": (
_ctx,
schema: any,
object: any,
): undefined | string => {
return validateObject(schema, object);
},
"jsonschema.validateSchema": (_ctx, schema: any): undefined | string => {
return validateSchema(schema);
},
};
}