json-schema-library
Version:
Customizable and hackable json-validator and json-schema utilities for traversal, data generation and validation
282 lines (281 loc) • 11 kB
JavaScript
import copy from "fast-copy";
import sanitizeErrors from "./utils/sanitizeErrors";
import settings from "./settings";
import { createSchema } from "./methods/createSchema";
import { toSchemaNodes } from "./methods/toSchemaNodes";
import { isJsonError } from "./types";
import { isObject } from "./utils/isObject";
import { join } from "@sagold/json-pointer";
import { joinId } from "./utils/joinId";
import { mergeNode } from "./mergeNode";
import { omit } from "./utils/omit";
import { pick } from "./utils/pick";
import { render } from "./errors/render";
import { validateNode } from "./validateNode";
import { hasProperty } from "./utils/hasProperty";
import { getNode } from "./getNode";
import { getNodeChild } from "./getNodeChild";
const { DYNAMIC_PROPERTIES } = settings;
export function isSchemaNode(value) {
return isObject(value) && Array.isArray(value === null || value === void 0 ? void 0 : value.reducers) && Array.isArray(value === null || value === void 0 ? void 0 : value.resolvers);
}
export function isReduceable(node) {
for (let i = 0, l = DYNAMIC_PROPERTIES.length; i < l; i += 1) {
// @ts-expect-error interface to object conversion
if (hasProperty(node, DYNAMIC_PROPERTIES[i])) {
return true;
}
}
return false;
}
function getDraft(drafts, $schema) {
var _a;
if (!Array.isArray(drafts) || drafts.length === 0) {
throw new Error(`Missing drafts in 'compileSchema({ $schema: "${$schema}" })'`);
}
if (drafts.length === 1) {
return drafts[0];
}
return (_a = drafts.find((d) => new RegExp(d.$schemaRegEx).test($schema))) !== null && _a !== void 0 ? _a : drafts[drafts.length - 1];
}
export function joinDynamicId(a, b) {
if (a == b) {
return a !== null && a !== void 0 ? a : "";
}
if (a == null || b == null) {
return a || b;
}
if (a.startsWith(b)) {
return a;
}
if (b.startsWith(a)) {
return b;
}
return `${a}+${b}`;
}
export const SchemaNodeMethods = {
/**
* Compiles a child-schema of this node to its context
* @returns SchemaNode representing the passed JSON Schema
*/
compileSchema(schema, evaluationPath = this.evaluationPath, schemaLocation, dynamicId) {
const nextFragment = evaluationPath.split("/$ref")[0];
const parentNode = this;
const node = {
lastIdPointer: parentNode.lastIdPointer, // ref helper
context: parentNode.context,
parent: parentNode,
evaluationPath,
dynamicId: joinDynamicId(parentNode.dynamicId, dynamicId),
schemaLocation: schemaLocation !== null && schemaLocation !== void 0 ? schemaLocation : join(parentNode.schemaLocation, nextFragment),
reducers: [],
resolvers: [],
validators: [],
schema,
...SchemaNodeMethods
};
addKeywords(node);
return node;
},
createError(code, data, message) {
var _a, _b, _c;
let errorMessage = message;
if (errorMessage === undefined) {
const error = (_c = (_b = (_a = this.schema) === null || _a === void 0 ? void 0 : _a.errorMessages) === null || _b === void 0 ? void 0 : _b[code]) !== null && _c !== void 0 ? _c : this.context.errors[code];
if (typeof error === "function") {
return error(data);
}
errorMessage = render(error !== null && error !== void 0 ? error : name, data);
}
return { type: "error", code, message: errorMessage, data };
},
createSchema,
getChildSelection(property) {
const node = this;
return node.context.methods.getChildSelection(node, property);
},
getNode,
getNodeChild,
/**
* @returns for $ref, the corresponding SchemaNode or undefined
*/
getNodeRef($ref) {
const node = this;
return node.compileSchema({ $ref }, "$dynamic").resolveRef();
},
getNodeRoot() {
const node = this;
return node.context.rootNode;
},
/**
* @returns draft version this JSON Schema is evaluated by
*/
getDraftVersion() {
return this.context.version;
},
/**
* @returns data that is valid to the schema of this node
*/
getData(data, options) {
const node = this;
const opts = {
recursionLimit: 1,
...node.context.getDataDefaultOptions,
cache: {},
...(options !== null && options !== void 0 ? options : {})
};
return node.context.methods.getData(node, data, opts);
},
/**
* @returns SchemaNode with a reduced JSON Schema matching the given data
*/
reduceNode(data, options = {}) {
const node = this;
const { key, pointer, path } = options;
// @ts-expect-error bool schema
if (node.schema === false) {
return { node, error: undefined };
// @ts-expect-error bool schema
}
else if (node.schema === true) {
const nextNode = node.compileSchema(createSchema(data), node.evaluationPath, node.schemaLocation);
path === null || path === void 0 ? void 0 : path.push({ pointer, node });
return { node: nextNode, error: undefined };
}
let schema;
// we need to copy node to prevent modification of source
// @todo does mergeNode break immutability?
let workingNode = node.compileSchema(node.schema, node.evaluationPath, node.schemaLocation);
const reducers = node.reducers;
for (let i = 0; i < reducers.length; i += 1) {
const result = reducers[i]({ data, key, node, pointer, path });
if (isJsonError(result)) {
return { node: undefined, error: result };
}
if (result) {
// @ts-expect-error bool schema - for undefined & false schema return false schema
if (result.schema === false) {
schema = false;
break;
}
// compilation result for data of current schemain order to merge results, we rebuild
// node from schema alternatively we would need to merge by node-property
workingNode = mergeNode(workingNode, result);
}
}
if (schema === false) {
// @ts-expect-error bool schema
return { node: { ...node, schema: false, reducers: [] }, error: undefined };
}
if (workingNode !== node) {
path === null || path === void 0 ? void 0 : path.push({ pointer, node });
}
// remove dynamic properties of node
workingNode.schema = omit(workingNode.schema, ...DYNAMIC_PROPERTIES);
// @ts-expect-error string accessing schema props
DYNAMIC_PROPERTIES.forEach((prop) => (workingNode[prop] = undefined));
return { node: workingNode, error: undefined };
},
/**
* @returns validation result of data validated by this node's JSON Schema
*/
validate(data, pointer = "#", path = []) {
var _a;
const errors = (_a = validateNode(this, data, pointer, path)) !== null && _a !== void 0 ? _a : [];
const syncErrors = [];
const flatErrorList = sanitizeErrors(Array.isArray(errors) ? errors : [errors]).filter(isJsonError);
const errorsAsync = [];
sanitizeErrors(Array.isArray(errors) ? errors : [errors]).forEach((error) => {
if (isJsonError(error)) {
syncErrors.push(error);
}
else if (error instanceof Promise) {
errorsAsync.push(error);
}
});
const result = {
valid: flatErrorList.length === 0,
errors: syncErrors,
errorsAsync
};
return result;
},
/**
* Register a JSON Schema as a remote-schema to be resolved by $ref, $anchor, etc
* @returns the current node (not the remote schema-node)
*/
addRemoteSchema(url, schema) {
var _a;
// @draft >= 6
schema.$id = joinId(schema.$id || url);
const { context } = this;
const draft = getDraft(context.drafts, (_a = schema === null || schema === void 0 ? void 0 : schema.$schema) !== null && _a !== void 0 ? _a : this.context.rootNode.$schema);
const node = {
evaluationPath: "#",
lastIdPointer: "#",
schemaLocation: "#",
dynamicId: "",
reducers: [],
resolvers: [],
validators: [],
schema,
context: {
...context,
refs: {},
anchors: {},
...copy(pick(draft, "methods", "keywords", "version", "formats", "errors"))
},
...SchemaNodeMethods
};
node.context.rootNode = node;
node.context.remotes[joinId(url)] = node;
addKeywords(node);
return this;
},
/**
* @returns a list of all sub-schema as SchemaNode
*/
toSchemaNodes() {
return toSchemaNodes(this);
},
/**
* @returns a list of values (including objects and arrays) and their corresponding JSON Schema as SchemaNode
*/
toDataNodes(data, pointer) {
const node = this;
return node.context.methods.toDataNodes(node, data, pointer);
},
toJSON() {
var _a;
return { ...this, context: undefined, errors: undefined, parent: (_a = this.parent) === null || _a === void 0 ? void 0 : _a.evaluationPath };
}
};
const whitelist = ["$ref", "if", "$defs"];
const noRefMergeDrafts = ["draft-04", "draft-06", "draft-07"];
export function addKeywords(node) {
if (node.schema.$ref && noRefMergeDrafts.includes(node.context.version)) {
// for these draft versions only ref is validated
node.context.keywords
.filter(({ keyword }) => whitelist.includes(keyword))
.forEach((keyword) => execKeyword(keyword, node));
return;
}
const keys = Object.keys(node.schema);
node.context.keywords
.filter(({ keyword }) => keys.includes(keyword) || whitelist.includes(keyword))
.forEach((keyword) => execKeyword(keyword, node));
}
export function execKeyword(keyword, node) {
var _a, _b, _c, _d;
// @todo consider first parsing all nodes
(_a = keyword.parse) === null || _a === void 0 ? void 0 : _a.call(keyword, node);
if ((_b = keyword.addReduce) === null || _b === void 0 ? void 0 : _b.call(keyword, node)) {
node.reducers.push(keyword.reduce);
}
if ((_c = keyword.addResolve) === null || _c === void 0 ? void 0 : _c.call(keyword, node)) {
node.resolvers.push(keyword.resolve);
}
if ((_d = keyword.addValidate) === null || _d === void 0 ? void 0 : _d.call(keyword, node)) {
node.validators.push(keyword.validate);
}
}