mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
@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.
104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
// The builders: the JSON Schema they produce, the static types they carry, and how they compile.
|
|
|
|
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
import {
|
|
arr,
|
|
bool,
|
|
bytes,
|
|
compile,
|
|
int,
|
|
lit,
|
|
map,
|
|
nul,
|
|
nullable,
|
|
num,
|
|
obj,
|
|
oneOf,
|
|
opt,
|
|
type Static,
|
|
str,
|
|
tuple,
|
|
union,
|
|
unknown,
|
|
} from "../src/index.ts";
|
|
|
|
describe("builders", () => {
|
|
it("produce plain JSON Schema", () => {
|
|
const schema = obj({
|
|
path: str({ description: "File path", minLength: 1 }),
|
|
offset: opt(int({ minimum: 1 })),
|
|
mode: oneOf(["read", "write"]),
|
|
});
|
|
expect(JSON.parse(JSON.stringify(schema))).toEqual({
|
|
type: "object",
|
|
properties: {
|
|
path: { type: "string", description: "File path", minLength: 1 },
|
|
offset: { type: "integer", minimum: 1 },
|
|
mode: { enum: ["read", "write"] },
|
|
},
|
|
required: ["path", "mode"],
|
|
additionalProperties: false,
|
|
});
|
|
});
|
|
|
|
it("produce every leaf", () => {
|
|
expect([str(), num(), int(), bool(), nul(), unknown(), lit(3), bytes({ maxLength: 8 })]).toEqual([
|
|
{ type: "string" },
|
|
{ type: "number" },
|
|
{ type: "integer" },
|
|
{ type: "boolean" },
|
|
{ type: "null" },
|
|
{},
|
|
{ const: 3 },
|
|
{ type: "bytes", maxLength: 8 },
|
|
]);
|
|
expect(arr(str(), { maxItems: 2 })).toEqual({ type: "array", items: { type: "string" }, maxItems: 2 });
|
|
expect(map(num())).toEqual({ type: "object", additionalProperties: { type: "number" } });
|
|
expect(tuple([str(), num()])).toEqual({
|
|
type: "array",
|
|
prefixItems: [{ type: "string" }, { type: "number" }],
|
|
items: false,
|
|
minItems: 2,
|
|
});
|
|
expect(nullable(str())).toEqual({ anyOf: [{ type: "string" }, { type: "null" }] });
|
|
expect(obj({ a: str() }, { open: true })).toEqual({
|
|
type: "object",
|
|
properties: { a: { type: "string" } },
|
|
required: ["a"],
|
|
});
|
|
});
|
|
|
|
it("carry the static type of valid values", () => {
|
|
const message = union([
|
|
obj({ kind: lit("text"), text: str() }),
|
|
obj({ kind: lit("image"), url: str(), alt: opt(str()) }),
|
|
]);
|
|
expectTypeOf<Static<typeof message>>().toEqualTypeOf<
|
|
{ kind: "text"; text: string } | { kind: "image"; url: string; alt?: string }
|
|
>();
|
|
const pair = tuple([str(), arr(int())]);
|
|
expectTypeOf<Static<typeof pair>>().toEqualTypeOf<[string, number[]]>();
|
|
expectTypeOf<Static<ReturnType<typeof nullable<string>>>>().toEqualTypeOf<string | null>();
|
|
expectTypeOf<Static<{ type: "string" }>>().toEqualTypeOf<unknown>();
|
|
});
|
|
|
|
it("narrow values through a compiled check", () => {
|
|
const point = obj({ x: num(), y: num(), label: opt(str()) });
|
|
const value: unknown = { x: 1, y: 2 };
|
|
const { check } = compile(point);
|
|
if (!check(value)) throw new Error("expected a point");
|
|
expectTypeOf(value).toEqualTypeOf<{ x: number; y: number; label?: string }>();
|
|
expect(value.x + value.y).toBe(3);
|
|
});
|
|
|
|
it("compile into checks with the declared semantics", () => {
|
|
const validator = compile(obj({ name: str({ minLength: 1 }), tags: opt(arr(str(), { uniqueItems: true })) }));
|
|
expect(validator.check({ name: "a" })).toBe(true);
|
|
expect(validator.check({ name: "a", tags: ["x", "y"] })).toBe(true);
|
|
expect(validator.errors({ name: "", tags: ["x", "x"], other: 1 })).toEqual([
|
|
{ path: "/name", keyword: "minLength", message: "expected at least 1 characters" },
|
|
{ path: "/tags", keyword: "uniqueItems", message: "expected unique items" },
|
|
{ path: "/other", keyword: "additionalProperties", message: "unknown field" },
|
|
]);
|
|
});
|
|
});
|