Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions lib/compiled-schema-serialization.js
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) {
Comment on lines +7 to +8
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ast is always present and is always a Set. These checks are unnecessary.

ast.plugins = [...ast.plugins].map((plugin) => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 @hyperjump/pact's map function that works on any iterable instead of just on functions (Pact.map((plugin) => ..., ast.plugins). Or, just use a normal for loop to build the array.

if (!plugin?.id) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plugin should never be undefined. The ? is unnecessary.

throw Error("Cannot serialize plugin without id");
}
return { id: plugin.id };
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need for this to be an object. return plugin.id should be sufficient.

});
}

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 };
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 { [REGEXP_MARKER]: true, source: value.source, flags: value.flags };
return { [REGEXP_MARKER]: { source: value.source, flags: value.flags } };

}

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the previous suggested change, this can simply be ...

Suggested change
if (!value || typeof value !== "object") {
return value;
}
if (value[REGEXP_MARKER]) {
return new RegExp(value.source, value.flags);
}
if (key === REGEXP_MARKER) {
return new RegExp(value.source, value.flags);
}


return value;
});

if (Array.isArray(parsed?.ast?.plugins)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary. plugins will always be present and always be an array.

parsed.ast.plugins = new Set(parsed.ast.plugins.map((plugin) => resolvePlugin(plugin.id, options)));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can avoid the intermediate array with @hyperjump/pact.

Suggested change
parsed.ast.plugins = new Set(parsed.ast.plugins.map((plugin) => resolvePlugin(plugin.id, options)));
parsed.ast.plugins = Pact.pipe(
parsed.ast.plugins,
Pact.map((pluginUri) => resolvePlugin(pluginUri, options))
Pact.collectSet
);

}

return parsed;
};

const resolveBuiltInPlugin = (pluginId) => {
if (typeof pluginId !== "string" || !pluginId.endsWith("#plugin")) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pluginId would never not be a string.

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`);
};
74 changes: 74 additions & 0 deletions lib/compiled-schema-serialization.spec.ts
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");
});
});
9 changes: 8 additions & 1 deletion lib/experimental.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ export type CompiledSchema = {
ast: AST;
};

export type DeserializeOptions = {
pluginsById?: Record<string, EvaluationPlugin>;
};

export const serialize: (compiledSchema: CompiledSchema) => string;
export const deserialize: (serialized: string, options?: DeserializeOptions) => CompiledSchema;

type AST = {
metaData: Record<string, MetaData>;
plugins: EvaluationPlugin[];
plugins: Set<EvaluationPlugin>;
} & Record<string, Node<unknown>[] | boolean>;

type Node<A> = [keywordId: string, schemaUri: string, keywordValue: A];
Expand Down
1 change: 1 addition & 0 deletions lib/experimental.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Loading