Files
plainleaf/client/plugos/syscalls/jsonschema.ts
T
2026-03-12 13:40:20 +01:00

110 lines
3.1 KiB
TypeScript

import type { SysCallMapping } from "../system.ts";
import { type OutputUnit, Validator, format } from "@cfworker/json-schema";
// Register custom formats
format.email = (data: string) => data.includes("@");
format["page-ref"] = (data: string) =>
data.startsWith("[[") && data.endsWith("]]");
const schemaCache = new Map<string, Validator>();
/**
* Deep-clone a value, replacing any functions with null.
* JSON schema can't validate functions, so we strip them before validation.
*/
function stripFunctions(value: any): any {
if (typeof value === "function") return null;
if (value === null || value === undefined || typeof value !== "object") {
return value;
}
if (Array.isArray(value)) {
return value.map(stripFunctions);
}
const result: Record<string, any> = {};
for (const key of Object.keys(value)) {
result[key] = stripFunctions(value[key]);
}
return result;
}
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);
},
};
}