From bfe0b87242028a9ec1c020c7d49f10d69c4f24ed Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sat, 25 Apr 2026 15:59:36 +0300 Subject: [PATCH 1/9] adding serialization module for serializing and restoring compiled schema --- lib/compiled-schema-serialization.js | 91 ++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 lib/compiled-schema-serialization.js diff --git a/lib/compiled-schema-serialization.js b/lib/compiled-schema-serialization.js new file mode 100644 index 0000000..4803730 --- /dev/null +++ b/lib/compiled-schema-serialization.js @@ -0,0 +1,91 @@ +import { getKeyword } from "./keywords.js"; + + +const isPlugin = (value) => { + return !!value + && typeof value === "object" + && typeof value.id === "string" + && ( + typeof value.beforeSchema === "function" + || typeof value.beforeKeyword === "function" + || typeof value.afterKeyword === "function" + || typeof value.afterSchema === "function" + ); +}; + +const serializePluginReference = (plugin) => { + if (!plugin?.id) { + throw Error("Cannot serialize plugin without id"); + } + + return { __type: "Plugin", id: plugin.id }; +}; + +const resolveBuiltInPlugin = (pluginId) => { + if (typeof pluginId !== "string" || !pluginId.endsWith("#plugin")) { + return; + } + + const keywordId = pluginId.slice(0, -"#plugin".length); + const plugin = getKeyword(keywordId)?.plugin; + if (plugin?.id === pluginId) { + return plugin; + } +}; + +const resolvePlugin = (pluginId, options) => { + const builtInPlugin = resolveBuiltInPlugin(pluginId); + if (builtInPlugin) { + return builtInPlugin; + } + + const customPlugin = options?.pluginsById?.[pluginId]; + if (customPlugin) { + return customPlugin; + } + + throw Error(`Plugin with id '${pluginId}' is not found`); +}; + +export const serialize = (compiledSchema) => { + return JSON.stringify(compiledSchema, (_key, value) => { + if (value instanceof RegExp) { + return { __type: "RegExp", source: value.source, flags: value.flags }; + } + + if (value instanceof Set) { + return { + __type: "PluginSet", + values: [...value].map((plugin) => serializePluginReference(plugin)) + }; + } + + if (isPlugin(value)) { + return serializePluginReference(value); + } + + return value; + }); +}; + +export const deserialize = (serialized, options = {}) => { + return JSON.parse(serialized, (_key, value) => { + if (!value || typeof value !== "object") { + return value; + } + + if (value.__type === "RegExp") { + return new RegExp(value.source, value.flags); + } + + if (value.__type === "Plugin") { + return resolvePlugin(value.id, options); + } + + if (value.__type === "PluginSet") { + return new Set(value.values); + } + + return value; + }); +}; From 7c3b3136acec7cf34368c8b78181b966dbea27d3 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sat, 25 Apr 2026 16:59:23 +0300 Subject: [PATCH 2/9] wiring compiled schema serialization helpers with experimental API --- lib/experimental.d.ts | 7 +++++++ lib/experimental.js | 2 ++ 2 files changed, 9 insertions(+) diff --git a/lib/experimental.d.ts b/lib/experimental.d.ts index 1718cf9..994100d 100644 --- a/lib/experimental.d.ts +++ b/lib/experimental.d.ts @@ -17,6 +17,13 @@ export type CompiledSchema = { ast: AST; }; +export type DeserializeOptions = { + pluginsById?: Record; +}; + +export const serialize: (compiledSchema: CompiledSchema) => string; +export const deserialize: (serialized: string, options?: DeserializeOptions) => CompiledSchema; + type AST = { metaData: Record; plugins: EvaluationPlugin[]; diff --git a/lib/experimental.js b/lib/experimental.js index f4d3d02..7112c95 100644 --- a/lib/experimental.js +++ b/lib/experimental.js @@ -10,3 +10,5 @@ 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"; + From 18c4e27c96dd200fb2a54a492433e7247dc5eca4 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sat, 25 Apr 2026 20:58:32 +0300 Subject: [PATCH 3/9] adding testing module for serialization --- lib/compiled-schema-serialization.spec.ts | 80 +++++++++++++++++++++++ lib/experimental.js | 1 - 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 lib/compiled-schema-serialization.spec.ts diff --git a/lib/compiled-schema-serialization.spec.ts b/lib/compiled-schema-serialization.spec.ts new file mode 100644 index 0000000..a55933b --- /dev/null +++ b/lib/compiled-schema-serialization.spec.ts @@ -0,0 +1,80 @@ +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("restores custom plugins from pluginsById", () => { + const plugin = { + id: "https://example.com/plugins/custom", + beforeSchema() {} + }; + + const serialized = JSON.stringify({ + schemaUri: "schema:custom#", + ast: { + "metaData": {}, + "plugins": { + __type: "PluginSet", + values: [{ __type: "Plugin", id: plugin.id }] + }, + "schema:custom#": true + } + }); + + const restored = deserialize(serialized, { + pluginsById: { + [plugin.id]: plugin + } + }); + + expect(restored.ast.plugins).to.be.instanceOf(Set); + }); + + test("throws if plugin id cannot be resolved", () => { + const serialized = JSON.stringify({ + schemaUri: "schema:missing#", + ast: { + "metaData": {}, + "plugins": { + __type: "PluginSet", + values: [{ __type: "Plugin", id: "https://example.com/plugins/missing" }] + }, + "schema:missing#": true + } + }); + + expect(() => { + deserialize(serialized); + }).to.throw("Plugin with id 'https://example.com/plugins/missing' is not found"); + }); +}); diff --git a/lib/experimental.js b/lib/experimental.js index 7112c95..9236a18 100644 --- a/lib/experimental.js +++ b/lib/experimental.js @@ -11,4 +11,3 @@ 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"; - From 1ba4c26c154194786d23168978122559c8de6697 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 1 May 2026 01:31:11 +0300 Subject: [PATCH 4/9] using UUID markers and pre-convert plugins --- lib/compiled-schema-serialization.js | 112 +++++++++++++-------------- 1 file changed, 54 insertions(+), 58 deletions(-) diff --git a/lib/compiled-schema-serialization.js b/lib/compiled-schema-serialization.js index 4803730..f8e7ed9 100644 --- a/lib/compiled-schema-serialization.js +++ b/lib/compiled-schema-serialization.js @@ -1,67 +1,26 @@ import { getKeyword } from "./keywords.js"; +const PLUGIN_MARKER = "fa3b6a9e-7c2d-4e9f-b1a0-8c2e1d3f4b5a"; +const REGEXP_MARKER = "a6d8f3e1-9b2c-4f7a-8d5b-1c2e3f4a5b6c"; -const isPlugin = (value) => { - return !!value - && typeof value === "object" - && typeof value.id === "string" - && ( - typeof value.beforeSchema === "function" - || typeof value.beforeKeyword === "function" - || typeof value.afterKeyword === "function" - || typeof value.afterSchema === "function" - ); -}; - -const serializePluginReference = (plugin) => { - if (!plugin?.id) { - throw Error("Cannot serialize plugin without id"); - } - - return { __type: "Plugin", id: plugin.id }; -}; - -const resolveBuiltInPlugin = (pluginId) => { - if (typeof pluginId !== "string" || !pluginId.endsWith("#plugin")) { - return; - } - - const keywordId = pluginId.slice(0, -"#plugin".length); - const plugin = getKeyword(keywordId)?.plugin; - if (plugin?.id === pluginId) { - return plugin; - } -}; - -const resolvePlugin = (pluginId, options) => { - const builtInPlugin = resolveBuiltInPlugin(pluginId); - if (builtInPlugin) { - return builtInPlugin; - } - - const customPlugin = options?.pluginsById?.[pluginId]; - if (customPlugin) { - return customPlugin; +export const serialize = (compiledSchema) => { + // Convert known plugin locations to marker objects so JSON.stringify + // doesn't have to pattern-match plugin shapes everywhere. + const ast = compiledSchema?.ast ? { ...compiledSchema.ast } : undefined; + if (ast && (Array.isArray(ast.plugins) || ast.plugins instanceof Set)) { + ast.plugins = [...ast.plugins].map((plugin) => { + if (!plugin?.id) { + throw Error("Cannot serialize plugin without id"); + } + return { [PLUGIN_MARKER]: true, id: plugin.id }; + }); } - throw Error(`Plugin with id '${pluginId}' is not found`); -}; + const toSerialize = ast ? { ...compiledSchema, ast } : compiledSchema; -export const serialize = (compiledSchema) => { - return JSON.stringify(compiledSchema, (_key, value) => { + return JSON.stringify(toSerialize, (_key, value) => { if (value instanceof RegExp) { - return { __type: "RegExp", source: value.source, flags: value.flags }; - } - - if (value instanceof Set) { - return { - __type: "PluginSet", - values: [...value].map((plugin) => serializePluginReference(plugin)) - }; - } - - if (isPlugin(value)) { - return serializePluginReference(value); + return { [REGEXP_MARKER]: true, source: value.source, flags: value.flags }; } return value; @@ -74,18 +33,55 @@ export const deserialize = (serialized, options = {}) => { return value; } + + if (value[REGEXP_MARKER]) { + return new RegExp(value.source, value.flags); + } + + // Legacy support: old tests/serializations used a __type marker. if (value.__type === "RegExp") { return new RegExp(value.source, value.flags); } + if (value[PLUGIN_MARKER]) { + return resolvePlugin(value.id, options); + } + if (value.__type === "Plugin") { return resolvePlugin(value.id, options); } if (value.__type === "PluginSet") { - return new Set(value.values); + // Legacy PluginSet: restore to a Set of plugin objects + return new Set((value.values || []).map((p) => resolvePlugin(p.id, options))); } return value; }); }; + +const resolveBuiltInPlugin = (pluginId) => { + if (typeof pluginId !== "string" || !pluginId.endsWith("#plugin")) { + return; + } + + const keywordId = pluginId.slice(0, -"#plugin".length); + const plugin = getKeyword(keywordId)?.plugin; + if (plugin?.id === pluginId) { + return plugin; + } +}; + +const resolvePlugin = (pluginId, options) => { + const builtInPlugin = resolveBuiltInPlugin(pluginId); + if (builtInPlugin) { + return builtInPlugin; + } + + const customPlugin = options?.pluginsById?.[pluginId]; + if (customPlugin) { + return customPlugin; + } + + throw Error(`Plugin with id '${pluginId}' is not found`); +}; From 3a6ef6245c6c73135ed1af22d13266168316f78e Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 1 May 2026 01:41:11 +0300 Subject: [PATCH 5/9] treating plugins as an array --- lib/compiled-schema-serialization.js | 12 ++++++------ lib/compiled-schema-serialization.spec.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/compiled-schema-serialization.js b/lib/compiled-schema-serialization.js index f8e7ed9..af664c8 100644 --- a/lib/compiled-schema-serialization.js +++ b/lib/compiled-schema-serialization.js @@ -1,11 +1,10 @@ import { getKeyword } from "./keywords.js"; + const PLUGIN_MARKER = "fa3b6a9e-7c2d-4e9f-b1a0-8c2e1d3f4b5a"; const REGEXP_MARKER = "a6d8f3e1-9b2c-4f7a-8d5b-1c2e3f4a5b6c"; export const serialize = (compiledSchema) => { - // Convert known plugin locations to marker objects so JSON.stringify - // doesn't have to pattern-match plugin shapes everywhere. const ast = compiledSchema?.ast ? { ...compiledSchema.ast } : undefined; if (ast && (Array.isArray(ast.plugins) || ast.plugins instanceof Set)) { ast.plugins = [...ast.plugins].map((plugin) => { @@ -28,12 +27,11 @@ export const serialize = (compiledSchema) => { }; export const deserialize = (serialized, options = {}) => { - return JSON.parse(serialized, (_key, value) => { + const parsed = JSON.parse(serialized, (_key, value) => { if (!value || typeof value !== "object") { return value; } - if (value[REGEXP_MARKER]) { return new RegExp(value.source, value.flags); } @@ -52,12 +50,14 @@ export const deserialize = (serialized, options = {}) => { } if (value.__type === "PluginSet") { - // Legacy PluginSet: restore to a Set of plugin objects - return new Set((value.values || []).map((p) => resolvePlugin(p.id, options))); + // Legacy PluginSet: restore to an array of plugin objects + return (value.values || []).map((p) => resolvePlugin(p.id, options)); } return value; }); + + return parsed; }; const resolveBuiltInPlugin = (pluginId) => { diff --git a/lib/compiled-schema-serialization.spec.ts b/lib/compiled-schema-serialization.spec.ts index a55933b..3c23ee5 100644 --- a/lib/compiled-schema-serialization.spec.ts +++ b/lib/compiled-schema-serialization.spec.ts @@ -57,7 +57,7 @@ describe("Compiled Schema Serialization", () => { } }); - expect(restored.ast.plugins).to.be.instanceOf(Set); + expect(Array.isArray(restored.ast.plugins)).to.equal(true); }); test("throws if plugin id cannot be resolved", () => { From e60d99a839f51fb52c76454efd22944b5caafa0f Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 7 May 2026 22:24:00 +0300 Subject: [PATCH 6/9] restore ast.plugins as Set after deserialization --- lib/compiled-schema-serialization.js | 29 ++++++----------------- lib/compiled-schema-serialization.spec.ts | 12 +++------- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/lib/compiled-schema-serialization.js b/lib/compiled-schema-serialization.js index af664c8..08a5ba9 100644 --- a/lib/compiled-schema-serialization.js +++ b/lib/compiled-schema-serialization.js @@ -1,17 +1,16 @@ import { getKeyword } from "./keywords.js"; -const PLUGIN_MARKER = "fa3b6a9e-7c2d-4e9f-b1a0-8c2e1d3f4b5a"; const REGEXP_MARKER = "a6d8f3e1-9b2c-4f7a-8d5b-1c2e3f4a5b6c"; export const serialize = (compiledSchema) => { - const ast = compiledSchema?.ast ? { ...compiledSchema.ast } : undefined; - if (ast && (Array.isArray(ast.plugins) || ast.plugins instanceof Set)) { + const ast = compiledSchema.ast ? { ...compiledSchema.ast } : undefined; + if (ast && ast.plugins instanceof Set) { ast.plugins = [...ast.plugins].map((plugin) => { if (!plugin?.id) { throw Error("Cannot serialize plugin without id"); } - return { [PLUGIN_MARKER]: true, id: plugin.id }; + return { id: plugin.id }; }); } @@ -36,27 +35,13 @@ export const deserialize = (serialized, options = {}) => { return new RegExp(value.source, value.flags); } - // Legacy support: old tests/serializations used a __type marker. - if (value.__type === "RegExp") { - return new RegExp(value.source, value.flags); - } - - if (value[PLUGIN_MARKER]) { - return resolvePlugin(value.id, options); - } - - if (value.__type === "Plugin") { - return resolvePlugin(value.id, options); - } - - if (value.__type === "PluginSet") { - // Legacy PluginSet: restore to an array of plugin objects - return (value.values || []).map((p) => resolvePlugin(p.id, options)); - } - return value; }); + if (Array.isArray(parsed?.ast?.plugins)) { + parsed.ast.plugins = new Set(parsed.ast.plugins.map((plugin) => resolvePlugin(plugin.id, options))); + } + return parsed; }; diff --git a/lib/compiled-schema-serialization.spec.ts b/lib/compiled-schema-serialization.spec.ts index 3c23ee5..9044ab4 100644 --- a/lib/compiled-schema-serialization.spec.ts +++ b/lib/compiled-schema-serialization.spec.ts @@ -43,10 +43,7 @@ describe("Compiled Schema Serialization", () => { schemaUri: "schema:custom#", ast: { "metaData": {}, - "plugins": { - __type: "PluginSet", - values: [{ __type: "Plugin", id: plugin.id }] - }, + "plugins": [{ id: plugin.id }], "schema:custom#": true } }); @@ -57,7 +54,7 @@ describe("Compiled Schema Serialization", () => { } }); - expect(Array.isArray(restored.ast.plugins)).to.equal(true); + expect(restored.ast.plugins instanceof Set).to.equal(true); }); test("throws if plugin id cannot be resolved", () => { @@ -65,10 +62,7 @@ describe("Compiled Schema Serialization", () => { schemaUri: "schema:missing#", ast: { "metaData": {}, - "plugins": { - __type: "PluginSet", - values: [{ __type: "Plugin", id: "https://example.com/plugins/missing" }] - }, + "plugins": [{ id: "https://example.com/plugins/missing" }], "schema:missing#": true } }); From 54ac81184c6ec37e9dde81c329ff21e865fdf4d3 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 7 May 2026 22:26:31 +0300 Subject: [PATCH 7/9] making type declaration of ast.plugins as Set --- lib/experimental.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/experimental.d.ts b/lib/experimental.d.ts index 994100d..2cf555a 100644 --- a/lib/experimental.d.ts +++ b/lib/experimental.d.ts @@ -26,7 +26,7 @@ export const deserialize: (serialized: string, options?: DeserializeOptions) => type AST = { metaData: Record; - plugins: EvaluationPlugin[]; + plugins: Set; } & Record[] | boolean>; type Node = [keywordId: string, schemaUri: string, keywordValue: A]; From 28e35e88ac97379902ffb608a08d11f0d69691a2 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Mon, 11 May 2026 02:38:28 +0300 Subject: [PATCH 8/9] removing unnecessary checks and using pact to avoid intermediate array --- lib/compiled-schema-serialization.js | 37 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/compiled-schema-serialization.js b/lib/compiled-schema-serialization.js index 08a5ba9..c3df0a5 100644 --- a/lib/compiled-schema-serialization.js +++ b/lib/compiled-schema-serialization.js @@ -1,24 +1,25 @@ +import { pipe, map, collectSet } from "@hyperjump/pact"; import { getKeyword } from "./keywords.js"; const REGEXP_MARKER = "a6d8f3e1-9b2c-4f7a-8d5b-1c2e3f4a5b6c"; export const serialize = (compiledSchema) => { - const ast = compiledSchema.ast ? { ...compiledSchema.ast } : undefined; - if (ast && ast.plugins instanceof Set) { - ast.plugins = [...ast.plugins].map((plugin) => { - if (!plugin?.id) { - throw Error("Cannot serialize plugin without id"); - } - return { id: plugin.id }; - }); + const ast = compiledSchema.ast; + const plugins = []; + for (const plugin of ast.plugins) { + if (!plugin.id) { + throw Error("Cannot serialize plugin without id"); + } + plugins.push(plugin.id); } + ast.plugins = plugins; const toSerialize = ast ? { ...compiledSchema, ast } : compiledSchema; return JSON.stringify(toSerialize, (_key, value) => { if (value instanceof RegExp) { - return { [REGEXP_MARKER]: true, source: value.source, flags: value.flags }; + return { [REGEXP_MARKER]: { source: value.source, flags: value.flags } }; } return value; @@ -27,32 +28,30 @@ export const serialize = (compiledSchema) => { export const deserialize = (serialized, options = {}) => { const parsed = JSON.parse(serialized, (_key, value) => { - if (!value || typeof value !== "object") { - return value; - } - if (value[REGEXP_MARKER]) { - return new RegExp(value.source, value.flags); + return new RegExp(value[REGEXP_MARKER].source, value[REGEXP_MARKER].flags); } return value; }); - if (Array.isArray(parsed?.ast?.plugins)) { - parsed.ast.plugins = new Set(parsed.ast.plugins.map((plugin) => resolvePlugin(plugin.id, options))); - } + parsed.ast.plugins = pipe( + parsed.ast.plugins, + map((plugin) => resolvePlugin(plugin.id ?? plugin, options)), + collectSet + ); return parsed; }; const resolveBuiltInPlugin = (pluginId) => { - if (typeof pluginId !== "string" || !pluginId.endsWith("#plugin")) { + if (!pluginId.endsWith("#plugin")) { return; } const keywordId = pluginId.slice(0, -"#plugin".length); const plugin = getKeyword(keywordId)?.plugin; - if (plugin?.id === pluginId) { + if (plugin.id === pluginId) { return plugin; } }; From d63c6758b61755b084d9ddbc8e60f0ab5c6a81d0 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Wed, 13 May 2026 15:51:32 -0700 Subject: [PATCH 9/9] Remove the custom plugins option, cleanup, and Readme updates --- README.md | 7 ++++ lib/compiled-schema-serialization.js | 51 +++++++++-------------- lib/compiled-schema-serialization.spec.ts | 29 ++----------- lib/experimental.d.ts | 6 +-- 4 files changed, 30 insertions(+), 63 deletions(-) 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 index c3df0a5..e272af4 100644 --- a/lib/compiled-schema-serialization.js +++ b/lib/compiled-schema-serialization.js @@ -1,21 +1,25 @@ -import { pipe, map, collectSet } from "@hyperjump/pact"; +import * as Pact from "@hyperjump/pact"; import { getKeyword } from "./keywords.js"; const REGEXP_MARKER = "a6d8f3e1-9b2c-4f7a-8d5b-1c2e3f4a5b6c"; export const serialize = (compiledSchema) => { - const ast = compiledSchema.ast; const plugins = []; - for (const plugin of ast.plugins) { + for (const plugin of compiledSchema.ast.plugins) { if (!plugin.id) { throw Error("Cannot serialize plugin without id"); } plugins.push(plugin.id); } - ast.plugins = plugins; - const toSerialize = ast ? { ...compiledSchema, ast } : compiledSchema; + const toSerialize = { + ...compiledSchema, + ast: { + ...compiledSchema.ast, + plugins + } + }; return JSON.stringify(toSerialize, (_key, value) => { if (value instanceof RegExp) { @@ -26,46 +30,29 @@ export const serialize = (compiledSchema) => { }); }; -export const deserialize = (serialized, options = {}) => { +export const deserialize = (serialized) => { const parsed = JSON.parse(serialized, (_key, value) => { - if (value[REGEXP_MARKER]) { + if (value?.[REGEXP_MARKER]) { return new RegExp(value[REGEXP_MARKER].source, value[REGEXP_MARKER].flags); } return value; }); - parsed.ast.plugins = pipe( + parsed.ast.plugins = Pact.pipe( parsed.ast.plugins, - map((plugin) => resolvePlugin(plugin.id ?? plugin, options)), - collectSet + Pact.map((pluginUri) => resolvePlugin(pluginUri)), + Pact.collectSet ); return parsed; }; -const resolveBuiltInPlugin = (pluginId) => { - if (!pluginId.endsWith("#plugin")) { - return; - } - - const keywordId = pluginId.slice(0, -"#plugin".length); - const plugin = getKeyword(keywordId)?.plugin; - if (plugin.id === pluginId) { - return plugin; - } -}; - -const resolvePlugin = (pluginId, options) => { - const builtInPlugin = resolveBuiltInPlugin(pluginId); - if (builtInPlugin) { - return builtInPlugin; - } - - const customPlugin = options?.pluginsById?.[pluginId]; - if (customPlugin) { - return customPlugin; +const resolvePlugin = (pluginUri) => { + const keyword = getKeyword(pluginUri); + if (keyword?.plugin?.id === pluginUri) { + return keyword.plugin; } - throw Error(`Plugin with id '${pluginId}' is not found`); + 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 index 9044ab4..5d5368f 100644 --- a/lib/compiled-schema-serialization.spec.ts +++ b/lib/compiled-schema-serialization.spec.ts @@ -33,42 +33,19 @@ describe("Compiled Schema Serialization", () => { expect(interpret(restored, Instance.fromJs({ extra: 1 }))).to.eql({ valid: false }); }); - test("restores custom plugins from pluginsById", () => { - const plugin = { - id: "https://example.com/plugins/custom", - beforeSchema() {} - }; - - const serialized = JSON.stringify({ - schemaUri: "schema:custom#", - ast: { - "metaData": {}, - "plugins": [{ id: plugin.id }], - "schema:custom#": true - } - }); - - const restored = deserialize(serialized, { - pluginsById: { - [plugin.id]: plugin - } - }); - - expect(restored.ast.plugins instanceof Set).to.equal(true); - }); - 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": [{ id: "https://example.com/plugins/missing" }], + "plugins": [pluginUri], "schema:missing#": true } }); expect(() => { deserialize(serialized); - }).to.throw("Plugin with id 'https://example.com/plugins/missing' is not found"); + }).to.throw(`Plugin with id '${pluginUri}' is not found`); }); }); diff --git a/lib/experimental.d.ts b/lib/experimental.d.ts index 2cf555a..8ecd99f 100644 --- a/lib/experimental.d.ts +++ b/lib/experimental.d.ts @@ -17,12 +17,8 @@ export type CompiledSchema = { ast: AST; }; -export type DeserializeOptions = { - pluginsById?: Record; -}; - export const serialize: (compiledSchema: CompiledSchema) => string; -export const deserialize: (serialized: string, options?: DeserializeOptions) => CompiledSchema; +export const deserialize: (serialized: string) => CompiledSchema; type AST = { metaData: Record;