-
-
Notifications
You must be signed in to change notification settings - Fork 41
Serialize and restore compiled schema #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bfe0b87
7c3b313
18c4e27
1ba4c26
3a6ef62
e60d99a
54ac811
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,72 @@ | ||||||||||||||||||||||
| 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) => { | ||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This pattern isn't great because it creates an intermediate array that isn't necessary. You can use |
||||||||||||||||||||||
| if (!plugin?.id) { | ||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||
| throw Error("Cannot serialize plugin without id"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return { id: plugin.id }; | ||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no need for this to be an object. |
||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 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 }; | ||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we slightly change the way this is represented, we can simplify the deserialize reviver function.
Suggested change
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return value; | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 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); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+30
to
+36
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the previous suggested change, this can simply be ...
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return value; | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (Array.isArray(parsed?.ast?.plugins)) { | ||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is unnecessary. |
||||||||||||||||||||||
| parsed.ast.plugins = new Set(parsed.ast.plugins.map((plugin) => resolvePlugin(plugin.id, options))); | ||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can avoid the intermediate array with
Suggested change
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return parsed; | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const resolveBuiltInPlugin = (pluginId) => { | ||||||||||||||||||||||
| if (typeof pluginId !== "string" || !pluginId.endsWith("#plugin")) { | ||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||
| 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`); | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| 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": [{ 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 serialized = JSON.stringify({ | ||
| schemaUri: "schema:missing#", | ||
| ast: { | ||
| "metaData": {}, | ||
| "plugins": [{ 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"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
astis always present and is always aSet. These checks are unnecessary.