diff --git a/README.md b/README.md index 2ceab41..c921238 100644 --- a/README.md +++ b/README.md @@ -764,6 +764,13 @@ These are available from the `@hyperjump/json-schema/experimental` export. A curried function for validating an instance against a compiled schema. This can be useful for creating custom output formats. +* **serialize**: (compiledSchema: CompiledSchema) => string + + Serialize a compiled schema as JSON so it can be restored at a later time + without needing to recompile. +* **deserialize**: (serialized: string) => CompiledSchema + + Restore a serialized compiled schema. * **OutputFormat**: **FLAG** | **BASIC** diff --git a/lib/compiled-schema-serialization.js b/lib/compiled-schema-serialization.js new file mode 100644 index 0000000..e272af4 --- /dev/null +++ b/lib/compiled-schema-serialization.js @@ -0,0 +1,58 @@ +import * as Pact from "@hyperjump/pact"; +import { getKeyword } from "./keywords.js"; + + +const REGEXP_MARKER = "a6d8f3e1-9b2c-4f7a-8d5b-1c2e3f4a5b6c"; + +export const serialize = (compiledSchema) => { + const plugins = []; + for (const plugin of compiledSchema.ast.plugins) { + if (!plugin.id) { + throw Error("Cannot serialize plugin without id"); + } + plugins.push(plugin.id); + } + + const toSerialize = { + ...compiledSchema, + ast: { + ...compiledSchema.ast, + plugins + } + }; + + return JSON.stringify(toSerialize, (_key, value) => { + if (value instanceof RegExp) { + return { [REGEXP_MARKER]: { source: value.source, flags: value.flags } }; + } + + return value; + }); +}; + +export const deserialize = (serialized) => { + const parsed = JSON.parse(serialized, (_key, value) => { + if (value?.[REGEXP_MARKER]) { + return new RegExp(value[REGEXP_MARKER].source, value[REGEXP_MARKER].flags); + } + + return value; + }); + + parsed.ast.plugins = Pact.pipe( + parsed.ast.plugins, + Pact.map((pluginUri) => resolvePlugin(pluginUri)), + Pact.collectSet + ); + + return parsed; +}; + +const resolvePlugin = (pluginUri) => { + const keyword = getKeyword(pluginUri); + if (keyword?.plugin?.id === pluginUri) { + return keyword.plugin; + } + + throw Error(`Plugin with id '${pluginUri}' is not found`); +}; diff --git a/lib/compiled-schema-serialization.spec.ts b/lib/compiled-schema-serialization.spec.ts new file mode 100644 index 0000000..5d5368f --- /dev/null +++ b/lib/compiled-schema-serialization.spec.ts @@ -0,0 +1,51 @@ +import { afterEach, describe, expect, test } from "vitest"; +import { registerSchema, unregisterSchema } from "../v1/index.js"; +import { compile, deserialize, getSchema, interpret, serialize } from "./experimental.js"; +import * as Instance from "./instance.js"; + + +describe("Compiled Schema Serialization", () => { + const schemaUri = "schema:compiled-serialization"; + const dialectUri = "https://json-schema.org/v1"; + + afterEach(() => { + unregisterSchema(schemaUri); + }); + + test("round-trips RegExp keyword values", async () => { + registerSchema({ pattern: "^a+$" }, schemaUri, dialectUri); + const schema = await getSchema(schemaUri); + const compiled = await compile(schema); + + const restored = deserialize(serialize(compiled)); + + expect(interpret(restored, Instance.fromJs("aaa"))).to.eql({ valid: true }); + expect(interpret(restored, Instance.fromJs("bbb"))).to.eql({ valid: false }); + }); + + test("restores built-in evaluation plugins", async () => { + registerSchema({ unevaluatedProperties: false }, schemaUri, dialectUri); + const schema = await getSchema(schemaUri); + const compiled = await compile(schema); + + const restored = deserialize(serialize(compiled)); + + expect(interpret(restored, Instance.fromJs({ extra: 1 }))).to.eql({ valid: false }); + }); + + test("throws if plugin id cannot be resolved", () => { + const pluginUri = "https://example.com/plugins/missing"; + const serialized = JSON.stringify({ + schemaUri: "schema:missing#", + ast: { + "metaData": {}, + "plugins": [pluginUri], + "schema:missing#": true + } + }); + + expect(() => { + deserialize(serialized); + }).to.throw(`Plugin with id '${pluginUri}' is not found`); + }); +}); diff --git a/lib/experimental.d.ts b/lib/experimental.d.ts index 1718cf9..8ecd99f 100644 --- a/lib/experimental.d.ts +++ b/lib/experimental.d.ts @@ -17,9 +17,12 @@ export type CompiledSchema = { ast: AST; }; +export const serialize: (compiledSchema: CompiledSchema) => string; +export const deserialize: (serialized: string) => CompiledSchema; + type AST = { metaData: Record; - plugins: EvaluationPlugin[]; + plugins: Set; } & Record[] | boolean>; type Node = [keywordId: string, schemaUri: string, keywordValue: A]; diff --git a/lib/experimental.js b/lib/experimental.js index f4d3d02..9236a18 100644 --- a/lib/experimental.js +++ b/lib/experimental.js @@ -10,3 +10,4 @@ export { default as Validation } from "./keywords/validation.js"; export * from "./evaluation-plugins/basic-output.js"; export * from "./evaluation-plugins/detailed-output.js"; export * from "./evaluation-plugins/annotations.js"; +export * from "./compiled-schema-serialization.js";