pi-mono/packages/schema/test/bench/corpus.ts
Mario Zechner fab2c5353e feat(schema): add pi-schema, JSON Schema builders and validation without eval
@earendil-works/pi-schema compiles JSON Schema (drafts 2020-12 and 7) once into
plain closures: no eval or new Function, so it runs under a strict CSP. It
passes 2,222 of the 2,223 core tests of the official JSON-Schema-Test-Suite,
checks Chord-style argument schemas within 1.2x of TypeBox's JIT, and keeps
3,200 compiled schemas in 7.9 MB (TypeBox: 54.7 MB).

- compile(schema, options) returns a Validator: check() is the boolean check
  (a type guard for builder schemas), errors() reports each failure with a
  JSON Pointer path, missing and unknown fields at the field.
- Builders (str, int, obj, opt, union, ...) return plain JSON Schema typed with
  the values it accepts (Static<typeof schema>).
- Formats are an optional entry point (./formats), adapted from TypeBox (MIT).
- Tests run the vendored suite (drafts 2020-12 and 7, with optional and format
  tests); npm run bench compares conformance, speed, setup cost, memory and
  bundle size with TypeBox, @cfworker/json-schema and omptype.
2026-10-31 21:39:34 +00:00

122 lines
4.1 KiB
TypeScript

// A deterministic corpus of distinct tool-like JSON Schemas with one valid value each: random property names,
// descriptions, leaf constraints, enums, nested objects, arrays and some $defs/$ref. No two schemas are copies.
/** A schema and a value it accepts. */
export interface CorpusItem {
readonly schema: Record<string, unknown>;
readonly value: unknown;
}
const SYLLABLES = [
"ka",
"lo",
"mi",
"re",
"su",
"ta",
"ne",
"vo",
"pi",
"da",
"ru",
"xe",
"bo",
"fi",
"gu",
"ha",
"jo",
"ze",
];
const PATTERNS: readonly (readonly [string, string])[] = [
["^[a-z0-9-]+$", "a-1"],
["^[A-Za-z0-9._-]+$", "A.b_c"],
["^\\d{4}-\\d{2}-\\d{2}$", "2026-10-31"],
["^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "01890a5d-ac96-774b-bcce-b302099a8057"],
["^#[0-9a-f]{6}$", "#00ff00"],
];
/** `count` distinct schemas, the same for the same seed. */
export function corpus(count: number, seed = 42): CorpusItem[] {
const random = mulberry32(seed);
const int = (lo: number, hi: number) => lo + Math.floor(random() * (hi - lo + 1));
const pick = <T>(list: readonly T[]): T => list[Math.floor(random() * list.length)];
const word = () => Array.from({ length: int(2, 4) }, () => pick(SYLLABLES)).join("");
const sentence = () => `${Array.from({ length: int(4, 12) }, word).join(" ")}.`;
function leaf(depth: number): [Record<string, unknown>, unknown] {
const r = random();
if (r < 0.3) return [{ type: "string", description: sentence() }, word()];
if (r < 0.4) {
const min = int(0, 3);
return [
{ type: "string", minLength: min, maxLength: min + int(5, 200), description: sentence() },
"x".repeat(min + 1),
];
}
if (r < 0.47) {
const [pattern, value] = pick(PATTERNS);
return [{ type: "string", pattern, description: sentence() }, value];
}
if (r < 0.55) {
const values = Array.from({ length: int(2, 5) }, word);
return [{ type: "string", enum: values, description: sentence() }, values[0]];
}
if (r < 0.65) {
const min = int(0, 10);
return [{ type: "integer", minimum: min, maximum: min + int(10, 10000), description: sentence() }, min + 1];
}
if (r < 0.72) return [{ type: "number", description: sentence() }, random() * 100];
if (r < 0.8) return [{ type: "boolean", description: sentence() }, random() < 0.5];
if (r < 0.88) {
return [{ type: "array", items: { type: "string" }, maxItems: int(1, 50), description: sentence() }, [word()]];
}
if (depth < 2) return object(depth + 1, int(2, 5));
return [{ type: "string", description: sentence() }, word()];
}
function object(depth: number, n: number): [Record<string, unknown>, Record<string, unknown>] {
const properties: Record<string, Record<string, unknown>> = {};
const value: Record<string, unknown> = {};
const required: string[] = [];
for (let i = 0; i < n; i++) {
const name = word() + (i > 0 ? i : "");
const [schema, v] = leaf(depth);
properties[name] = schema;
if (random() < 0.6) {
required.push(name);
value[name] = v;
} else if (random() < 0.5) value[name] = v;
}
const schema: Record<string, unknown> = { type: "object", properties, required };
if (random() < 0.5) schema.additionalProperties = false;
return [schema, value];
}
const items: CorpusItem[] = [];
for (let i = 0; i < count; i++) {
const [schema, value] = object(0, int(2, 10));
const properties = schema.properties as Record<string, Record<string, unknown>>;
if (random() < 0.2) {
// Move one nested object into $defs and reference it, as MCP servers built with zod often do.
const key = Object.keys(properties).find((k) => properties[k].type === "object");
if (key !== undefined) {
const name = `${word()}Def`;
schema.$defs = { [name]: properties[key] };
properties[key] = { $ref: `#/$defs/${name}` };
}
}
schema.description = sentence();
items.push({ schema, value });
}
return items;
}
function mulberry32(seed: number): () => number {
let state = seed;
return () => {
state = (state + 0x6d2b79f5) | 0;
let t = Math.imul(state ^ (state >>> 15), 1 | state);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}