botbook/node_modules/json-schema-library/lib/validation/keyword.ts
Rodrigo Rodriguez 6ae15fe3e5 Updated.
2024-09-04 13:13:15 -03:00

516 lines
18 KiB
TypeScript

import getTypeOf from "../getTypeOf";
import settings from "../config/settings";
import ucs2decode from "../utils/punycode.ucs2decode";
import { isObject } from "../utils/isObject";
import { JsonValidator, isJsonError, JsonError } from "../types";
import { validateAllOf } from "../features/allOf";
import { validateAnyOf } from "../features/anyOf";
import { validateDependencies } from "../features/dependencies";
import { validateOneOf } from "../features/oneOf";
import { getPrecision } from "../utils/getPrecision";
import deepEqual from "fast-deep-equal";
const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasProperty = (value: Record<string, unknown>, property: string) =>
!(value[property] === undefined || !hasOwnProperty.call(value, property));
// list of validation keywords: http://json-schema.org/latest/json-schema-validation.html#rfc.section.5
const KeywordValidation: Record<string, JsonValidator> = {
additionalProperties: (draft, schema, value: Record<string, unknown>, pointer) => {
if (schema.additionalProperties === true || schema.additionalProperties == null) {
return undefined;
}
if (
getTypeOf(schema.patternProperties) === "object" &&
schema.additionalProperties === false
) {
// this is an arrangement with patternProperties. patternProperties validate before additionalProperties:
// https://spacetelescope.github.io/understanding-json-schema/reference/object.html#index-5
return undefined;
}
const errors: JsonError[] = [];
let receivedProperties = Object.keys(value).filter(
(prop) => settings.propertyBlacklist.includes(prop) === false
);
const expectedProperties = Object.keys(schema.properties || {});
if (getTypeOf(schema.patternProperties) === "object") {
// filter received properties by matching patternProperties
const patterns = Object.keys(schema.patternProperties).map(
(pattern) => new RegExp(pattern)
);
receivedProperties = receivedProperties.filter((prop) => {
for (let i = 0; i < patterns.length; i += 1) {
if (patterns[i].test(prop)) {
return false; // remove
}
}
return true;
});
}
// adds an error for each an unexpected property
for (let i = 0, l = receivedProperties.length; i < l; i += 1) {
const property = receivedProperties[i];
if (expectedProperties.indexOf(property) === -1) {
const additionalIsObject = isObject(schema.additionalProperties);
// additionalProperties { oneOf: [] }
if (additionalIsObject && Array.isArray(schema.additionalProperties.oneOf)) {
const result = draft.resolveOneOf(
value[property],
schema.additionalProperties,
`${pointer}/${property}`
);
if (isJsonError(result)) {
errors.push(
draft.errors.additionalPropertiesError({
pointer,
schema: schema.additionalProperties,
value,
property: receivedProperties[i],
properties: expectedProperties,
// pass all validation errors
errors: result.data.errors
})
);
} else {
errors.push(...draft.validate(value[property], result, pointer));
}
// additionalProperties {}
} else if (additionalIsObject) {
errors.push(
...draft.validate(
value[property],
schema.additionalProperties,
`${pointer}/${property}`
)
);
} else {
errors.push(
draft.errors.noAdditionalPropertiesError({
pointer,
schema,
value,
property: receivedProperties[i],
properties: expectedProperties
})
);
}
}
}
return errors;
},
allOf: validateAllOf,
anyOf: validateAnyOf,
dependencies: validateDependencies,
enum: (draft, schema, value, pointer) => {
const type = getTypeOf(value);
if (type === "object" || type === "array") {
const valueStr = JSON.stringify(value);
for (let i = 0; i < schema.enum.length; i += 1) {
if (JSON.stringify(schema.enum[i]) === valueStr) {
return undefined;
}
}
} else if (schema.enum.includes(value)) {
return undefined;
}
return draft.errors.enumError({ pointer, schema, value, values: schema.enum });
},
format: (draft, schema, value, pointer) => {
if (draft.validateFormat[schema.format]) {
const errors = draft.validateFormat[schema.format](draft, schema, value, pointer);
return errors;
}
// fail silently if given format is not defined
return undefined;
},
items: (draft, schema, value: unknown[], pointer) => {
// @draft >= 7 bool schema
if (schema.items === false) {
if (Array.isArray(value) && value.length === 0) {
return undefined;
}
return draft.errors.invalidDataError({ pointer, value, schema });
}
const errors: JsonError[] = [];
for (let i = 0; i < value.length; i += 1) {
const itemData = value[i];
// @todo reevaluate: incomplete schema is created here
const itemSchema = draft.step(i, schema, value, pointer);
if (isJsonError(itemSchema)) {
return [itemSchema];
}
const itemErrors = draft.validate(itemData, itemSchema, `${pointer}/${i}`);
errors.push(...itemErrors);
}
return errors;
},
maximum: (draft, schema, value, pointer) => {
if (isNaN(schema.maximum)) {
return undefined;
}
if (schema.maximum && schema.maximum < value) {
return draft.errors.maximumError({
maximum: schema.maximum,
length: value,
value,
pointer,
schema
});
}
if (schema.maximum && schema.exclusiveMaximum === true && schema.maximum === value) {
return draft.errors.maximumError({
maximum: schema.maximum,
length: value,
pointer,
schema,
value
});
}
return undefined;
},
maxItems: (draft, schema, value: unknown[], pointer) => {
if (isNaN(schema.maxItems)) {
return undefined;
}
if (schema.maxItems < value.length) {
return draft.errors.maxItemsError({
maximum: schema.maxItems,
length: value.length,
schema,
value,
pointer
});
}
return undefined;
},
maxLength: (draft, schema, value: string, pointer) => {
if (isNaN(schema.maxLength)) {
return undefined;
}
const lengthOfString = ucs2decode(value).length;
if (schema.maxLength < lengthOfString) {
return draft.errors.maxLengthError({
maxLength: schema.maxLength,
length: lengthOfString,
pointer,
schema,
value
});
}
return undefined;
},
maxProperties: (draft, schema, value, pointer) => {
const propertyCount = Object.keys(value).length;
if (isNaN(schema.maxProperties) === false && schema.maxProperties < propertyCount) {
return draft.errors.maxPropertiesError({
maxProperties: schema.maxProperties,
length: propertyCount,
pointer,
schema,
value
});
}
return undefined;
},
minLength: (draft, schema, value: string, pointer) => {
if (isNaN(schema.minLength)) {
return undefined;
}
const lengthOfString = ucs2decode(value).length;
if (schema.minLength > lengthOfString) {
if (schema.minLength === 1) {
return draft.errors.minLengthOneError({
minLength: schema.minLength,
length: lengthOfString,
pointer,
schema,
value
});
}
return draft.errors.minLengthError({
minLength: schema.minLength,
length: lengthOfString,
pointer,
schema,
value
});
}
return undefined;
},
minimum: (draft, schema, value, pointer) => {
if (isNaN(schema.minimum)) {
return undefined;
}
if (schema.minimum > value) {
return draft.errors.minimumError({
minimum: schema.minimum,
length: value,
pointer,
schema,
value
});
}
if (schema.exclusiveMinimum === true && schema.minimum === value) {
return draft.errors.minimumError({
minimum: schema.minimum,
length: value,
pointer,
schema,
value
});
}
return undefined;
},
minItems: (draft, schema, value: unknown[], pointer) => {
if (isNaN(schema.minItems)) {
return undefined;
}
if (schema.minItems > value.length) {
if (schema.minItems === 1) {
return draft.errors.minItemsOneError({
minItems: schema.minItems,
length: value.length,
pointer,
schema,
value
});
}
return draft.errors.minItemsError({
minItems: schema.minItems,
length: value.length,
pointer,
schema,
value
});
}
return undefined;
},
minProperties: (draft, schema, value, pointer) => {
if (isNaN(schema.minProperties)) {
return undefined;
}
const propertyCount = Object.keys(value).length;
if (schema.minProperties > propertyCount) {
return draft.errors.minPropertiesError({
minProperties: schema.minProperties,
length: propertyCount,
pointer,
schema,
value
});
}
return undefined;
},
multipleOf: (draft, schema, value: number, pointer) => {
if (isNaN(schema.multipleOf) || typeof value !== "number") {
return undefined;
}
const valuePrecision = getPrecision(value);
const multiplePrecision = getPrecision(schema.multipleOf);
if (valuePrecision > multiplePrecision) {
// value with higher precision then multipleOf-precision can never be multiple
return draft.errors.multipleOfError({
multipleOf: schema.multipleOf,
value,
pointer,
schema
});
}
const precision = Math.pow(10, multiplePrecision);
const val = Math.round(value * precision);
const multiple = Math.round(schema.multipleOf * precision);
if ((val % multiple) / precision !== 0) {
return draft.errors.multipleOfError({
multipleOf: schema.multipleOf,
value,
pointer,
schema
});
}
// maybe also check overflow
// https://stackoverflow.com/questions/1815367/catch-and-compute-overflow-during-multiplication-of-two-large-integers
return undefined;
},
not: (draft, schema, value, pointer) => {
const errors: JsonError[] = [];
if (draft.validate(value, schema.not, pointer).length === 0) {
errors.push(draft.errors.notError({ value, not: schema.not, pointer, schema }));
}
return errors;
},
oneOf: validateOneOf,
pattern: (draft, schema, value: string, pointer) => {
const pattern = new RegExp(schema.pattern, "u");
if (pattern.test(value) === false) {
return draft.errors.patternError({
pattern: schema.pattern,
description: schema.patternExample || schema.pattern,
received: value,
schema,
value,
pointer
});
}
return undefined;
},
patternProperties: (draft, schema, value: Record<string, unknown>, pointer) => {
const properties = schema.properties || {};
const pp = schema.patternProperties;
if (getTypeOf(pp) !== "object") {
return undefined;
}
const errors: JsonError[] = [];
const keys = Object.keys(value);
const patterns = Object.keys(pp).map((expr) => ({
regex: new RegExp(expr),
patternSchema: pp[expr]
}));
keys.forEach((key) => {
let patternFound = false;
for (let i = 0, l = patterns.length; i < l; i += 1) {
if (patterns[i].regex.test(key)) {
patternFound = true;
const valErrors = draft.validate(
value[key],
patterns[i].patternSchema,
`${pointer}/${key}`
);
if (valErrors && valErrors.length > 0) {
errors.push(...valErrors);
}
}
}
if (properties[key]) {
return;
}
if (patternFound === false && schema.additionalProperties === false) {
// this is an arrangement with additionalProperties
errors.push(
draft.errors.patternPropertiesError({
key,
pointer,
schema,
value,
patterns: Object.keys(pp).join(",")
})
);
}
});
return errors;
},
properties: (draft, schema, value: Record<string, unknown>, pointer) => {
const errors: JsonError[] = [];
const keys = Object.keys(schema.properties || {});
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
if (hasProperty(value, key)) {
const itemSchema = draft.step(key, schema, value, pointer);
const keyErrors = draft.validate(value[key], itemSchema, `${pointer}/${key}`);
errors.push(...keyErrors);
}
}
return errors;
},
// @todo move to separate file: this is custom keyword validation for JsonEditor.properties keyword
propertiesRequired: (draft, schema, value: Record<string, unknown>, pointer) => {
const errors: JsonError[] = [];
const keys = Object.keys(schema.properties || {});
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
if (value[key] === undefined) {
errors.push(draft.errors.requiredPropertyError({ key, pointer, schema, value }));
} else {
const itemSchema = draft.step(key, schema, value, pointer);
const keyErrors = draft.validate(value[key], itemSchema, `${pointer}/${key}`);
errors.push(...keyErrors);
}
}
return errors;
},
required: (draft, schema, value: Record<string, unknown>, pointer) => {
if (Array.isArray(schema.required) === false) {
return undefined;
}
return schema.required.map((property: string) => {
if (!hasProperty(value, property)) {
return draft.errors.requiredPropertyError({
key: property,
pointer,
schema,
value
});
}
return undefined;
});
},
// @todo move to separate file: this is custom keyword validation for JsonEditor.required keyword
requiredNotEmpty: (draft, schema, value: Record<string, unknown>, pointer) => {
if (Array.isArray(schema.required) === false) {
return undefined;
}
return schema.required.map((property: string) => {
if (value[property] == null || value[property] === "") {
return draft.errors.valueNotEmptyError({
property,
pointer: `${pointer}/${property}`,
schema,
value
});
}
return undefined;
});
},
uniqueItems: (draft, schema, value: unknown[], pointer) => {
if ((Array.isArray(value) && schema.uniqueItems) === false) {
return undefined;
}
const duplicates: number[] = [];
const errors: JsonError[] = [];
value.forEach((item, index) => {
for (let i = index + 1; i < value.length; i += 1) {
if (deepEqual(item, value[i]) && !duplicates.includes(i)) {
errors.push(
draft.errors.uniqueItemsError({
pointer: `${pointer}/${i}`,
duplicatePointer: `${pointer}/${index}`,
arrayPointer: pointer,
value: JSON.stringify(item),
schema
})
);
duplicates.push(i);
}
}
});
return errors;
}
};
export default KeywordValidation;