json-schema-library
Version:
Customizable and hackable json-validator and json-schema utilities for traversal, data generation and validation
123 lines (109 loc) • 4.34 kB
text/typescript
import { mergeSchema } from "../utils/mergeSchema";
import { JsonSchema, SchemaNode } from "../types";
import { isObject } from "../utils/isObject";
import {
Keyword,
JsonSchemaReducerParams,
JsonSchemaResolverParams,
JsonSchemaValidatorParams,
ValidationResult
} from "../Keyword";
import { getValue } from "../utils/getValue";
import { validateNode } from "../validateNode";
export const patternPropertiesKeyword: Keyword = {
id: "patternProperties",
keyword: "patternProperties",
parse: parsePatternProperties,
addReduce: (node) => node.patternProperties != null,
reduce: reducePatternProperties,
addResolve: (node) => node.patternProperties != null,
resolve: patternPropertyResolver,
addValidate: (node) => node.patternProperties != null,
validate: validatePatternProperties
};
export function parsePatternProperties(node: SchemaNode) {
const { schema } = node;
if (!isObject(schema.patternProperties)) {
return;
}
const patterns = Object.keys(schema.patternProperties);
if (patterns.length === 0) {
return;
}
node.patternProperties = patterns.map((pattern) => ({
name: pattern,
pattern: new RegExp(pattern, "u"),
node: node.compileSchema(
schema.patternProperties[pattern],
`${node.evaluationPath}/patternProperties/${pattern}`,
`${node.schemaLocation}/patternProperties/${pattern}`
)
}));
}
function patternPropertyResolver({ node, key }: JsonSchemaResolverParams) {
return node.patternProperties?.find(({ pattern }) => pattern.test(`${key}`))?.node;
}
function reducePatternProperties({ node, data, key }: JsonSchemaReducerParams) {
const { patternProperties } = node;
if (patternProperties == null) {
return;
}
let mergedSchema: JsonSchema;
const dataProperties = Object.keys(data ?? {});
if (key) {
dataProperties.push(`${key}`);
}
let dynamicId = `${node.schemaLocation}(`;
dataProperties.push(...Object.keys(node.schema.properties ?? {}));
dataProperties.forEach((propertyName, index, list) => {
if (list.indexOf(propertyName) !== index) {
// duplicate
return;
}
// build schema of property
let propertySchema = node.schema.properties?.[propertyName] ?? {};
const matchingPatterns = patternProperties.filter((property) => property.pattern.test(propertyName));
matchingPatterns.forEach((pp) => (propertySchema = mergeSchema(propertySchema, pp.node.schema)));
if (matchingPatterns.length > 0) {
mergedSchema = mergedSchema ?? { properties: {} };
mergedSchema.properties[propertyName] = propertySchema;
dynamicId += `${matchingPatterns.map(({ name }) => `patternProperties/${name}`).join(",")}`;
}
});
if (mergedSchema == null) {
return node;
}
mergedSchema = mergeSchema(node.schema, mergedSchema, "patternProperties");
return node.compileSchema(mergedSchema, node.evaluationPath, node.schemaLocation, `${dynamicId})`);
}
function validatePatternProperties({ node, data, pointer, path }: JsonSchemaValidatorParams) {
if (!isObject(data)) {
return;
}
const { schema, patternProperties } = node;
const properties = schema.properties || {};
const patterns = Object.keys(schema.patternProperties).join(",");
const errors: ValidationResult[] = [];
const keys = Object.keys(data);
keys.forEach((key) => {
const value = getValue(data, key);
const matchingPatterns = patternProperties.filter((property) => property.pattern.test(key));
matchingPatterns.forEach(({ node }) => errors.push(...validateNode(node, value, `${pointer}/${key}`, path)));
if (properties[key]) {
return;
}
if (matchingPatterns.length === 0 && schema.additionalProperties === false) {
// this is an arrangement with additionalProperties
errors.push(
node.createError("no-additional-properties-error", {
key,
pointer: `${pointer}/${key}`,
schema,
value,
patterns
})
);
}
});
return errors;
}