565 lines
17 KiB
JavaScript
565 lines
17 KiB
JavaScript
"use strict";
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.validateSchema = void 0;
|
|
const getValueType = value => {
|
|
const typeOf = typeof value;
|
|
switch (typeOf) {
|
|
case 'number':
|
|
return Number.isInteger(value) ? 'integer' : 'number';
|
|
case 'boolean':
|
|
case 'string':
|
|
return typeOf;
|
|
case 'object':
|
|
if (value === null) {
|
|
return 'null';
|
|
} else if (Array.isArray(value)) {
|
|
return 'array';
|
|
} else {
|
|
return 'object';
|
|
}
|
|
default:
|
|
return typeOf;
|
|
}
|
|
};
|
|
const isDeepEqual = (a, b) => {
|
|
if (a === b) {
|
|
return true;
|
|
} else if (a === null || b === null) {
|
|
return false;
|
|
} else if (typeof a !== typeof b) {
|
|
return false;
|
|
} else if (Array.isArray(a)) {
|
|
if (!Array.isArray(b) || a.length !== b.length) {
|
|
return false;
|
|
}
|
|
for (let idx = 0; idx < a.length; idx++) {
|
|
if (!isDeepEqual(a[idx], b[idx])) return false;
|
|
}
|
|
return true;
|
|
} else if (typeof a === 'object') {
|
|
const keysA = Object.keys(a);
|
|
const keysB = Object.keys(b);
|
|
if (!isDeepEqual(keysA, keysB)) {
|
|
return false;
|
|
}
|
|
for (let idx = 0; idx < keysA.length; idx++) {
|
|
if (!isDeepEqual(a[keysA[idx]], b[keysA[idx]])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
};
|
|
const areArrayValuesUnique = array => {
|
|
for (let i = 0; i < array.length; i++) {
|
|
for (let j = 0; j < array.length; j++) {
|
|
if (i !== j && isDeepEqual(array[i], array[j])) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
const dateRe = /^\d{4}-\d{2}-\d{2}$/;
|
|
const dateTimeRe = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/;
|
|
const timeRe = /^\d{2}:\d{2}:\d{2}$/;
|
|
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
const hostnameRe = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
const uriRe = /^https?:\/\//;
|
|
const validateFormat = (format, value) => {
|
|
// NOTE: Left out ipv4 and ipv6
|
|
switch (format) {
|
|
case 'date-time':
|
|
return dateTimeRe.test(value);
|
|
case 'date':
|
|
return dateRe.test(value);
|
|
case 'time':
|
|
return timeRe.test(value);
|
|
case 'email':
|
|
return emailRe.test(value);
|
|
case 'hostname':
|
|
return hostnameRe.test(value);
|
|
case 'uri':
|
|
return uriRe.test(value);
|
|
default:
|
|
throw new TypeError(`Unsupported format "${format}"`);
|
|
}
|
|
};
|
|
const isEnumValue = (enumValues, value) => {
|
|
if (!enumValues.length) {
|
|
throw new TypeError('Empty enum array');
|
|
}
|
|
for (let idx = 0; idx < enumValues.length; idx++) {
|
|
if (isDeepEqual(value, enumValues[idx])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
const validateString = (schema, value, path) => {
|
|
if (schema.minLength != null && value.length < schema.minLength) {
|
|
return {
|
|
message: `String must be at least ${schema.minLength} characters`,
|
|
keyword: 'minLength',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.maxLength != null && value.length > schema.maxLength) {
|
|
return {
|
|
message: `String must be at most ${schema.maxLength} characters`,
|
|
keyword: 'maxLength',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.pattern != null && !new RegExp(schema.pattern).test(value)) {
|
|
return {
|
|
message: `String does not match pattern: ${schema.pattern}`,
|
|
keyword: 'pattern',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.format != null && !validateFormat(schema.format, value)) {
|
|
return {
|
|
message: `String does not match format: ${schema.format}`,
|
|
keyword: 'format',
|
|
path,
|
|
value
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
const validateNumber = (schema, value, path) => {
|
|
if (schema.multipleOf != null && value % schema.multipleOf !== 0) {
|
|
return {
|
|
message: `Number must be multiple of ${schema.multipleOf}`,
|
|
keyword: 'multipleOf',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.minimum != null && value < schema.minimum) {
|
|
return {
|
|
message: `Number must be equal or greater than ${schema.minimum}`,
|
|
keyword: 'minimum',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.maximum != null && value > schema.maximum) {
|
|
return {
|
|
message: `Number must be equal or less than ${schema.maximum}`,
|
|
keyword: 'maximum',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.exclusiveMaximum === true && schema.maximum != null && value >= schema.maximum) {
|
|
return {
|
|
message: `Number must be less than ${schema.maximum}`,
|
|
keyword: 'exclusiveMaximum',
|
|
path,
|
|
value
|
|
};
|
|
} else if (typeof schema.exclusiveMaximum === 'number' && value >= schema.exclusiveMaximum) {
|
|
return {
|
|
message: `Number must be less than ${schema.exclusiveMaximum}`,
|
|
keyword: 'exclusiveMaximum',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.exclusiveMinimum === true && schema.minimum != null && value <= schema.minimum) {
|
|
return {
|
|
message: `Number must be greater than ${schema.minimum}`,
|
|
keyword: 'exclusiveMinimum',
|
|
path,
|
|
value
|
|
};
|
|
} else if (typeof schema.exclusiveMinimum === 'number' && value <= schema.exclusiveMinimum) {
|
|
return {
|
|
message: `Number must be greater than ${schema.exclusiveMinimum}`,
|
|
keyword: 'exclusiveMinimum',
|
|
path,
|
|
value
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
const validateContains = (containsSchema, value, path) => {
|
|
for (let idx = 0; idx < value.length; idx++) {
|
|
if (validateSchema(containsSchema, value[idx], path) === null) {
|
|
return null;
|
|
}
|
|
}
|
|
return {
|
|
message: 'Array must contain at least one item matching the contains schema',
|
|
keyword: 'contains',
|
|
path,
|
|
value
|
|
};
|
|
};
|
|
const validateItems = (itemsSchema, additionalItems, value, path) => {
|
|
let child;
|
|
if (Array.isArray(itemsSchema)) {
|
|
let idx = 0;
|
|
for (idx = 0; idx < itemsSchema.length; idx++) {
|
|
if ((child = validateSchema(itemsSchema[idx], value[idx], `${path}[${idx}]`)) != null) {
|
|
return child;
|
|
}
|
|
}
|
|
if (idx < value.length) {
|
|
if (additionalItems === true) {
|
|
return null;
|
|
} else if (additionalItems) {
|
|
for (; idx < value.length; idx++) {
|
|
if ((child = validateSchema(additionalItems, value[idx], `${path}[${idx}]`)) != null) {
|
|
return child;
|
|
}
|
|
}
|
|
return null;
|
|
} else {
|
|
return {
|
|
message: `Array contained ${value.length - idx} more items than items schema`,
|
|
keyword: 'additionalItems',
|
|
path,
|
|
value
|
|
};
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
for (let idx = 0; idx < value.length; idx++) {
|
|
if ((child = validateSchema(itemsSchema, value[idx], `${path}[${idx}]`)) != null) {
|
|
return child;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
const validateArray = (schema, value, path) => {
|
|
let child;
|
|
if (schema.minItems != null && value.length < schema.minItems) {
|
|
return {
|
|
message: `Array must have at least ${schema.minItems} items`,
|
|
keyword: 'minItems',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.maxItems != null && value.length > schema.maxItems) {
|
|
return {
|
|
message: `Array must have at most ${schema.maxItems} items`,
|
|
keyword: 'maxItems',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.uniqueItems && !areArrayValuesUnique(value)) {
|
|
return {
|
|
message: 'Array items must be unique',
|
|
keyword: 'uniqueItems',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.contains != null && (child = validateContains(schema.contains, value, path)) != null) {
|
|
return child;
|
|
} else if (schema.items != null && (child = validateItems(schema.items, schema.additionalItems, value, path)) != null) {
|
|
return child;
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
const validateRequired = (keys, value, path) => {
|
|
for (let idx = 0; idx < keys.length; idx++) {
|
|
if (value[keys[idx]] === undefined) {
|
|
return {
|
|
message: `Required property "${keys[idx]}" is missing`,
|
|
keyword: 'required',
|
|
path: `${path}.${keys[idx]}`,
|
|
value
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
const validateProperties = (properties, value, path) => {
|
|
let child;
|
|
for (const key in properties) {
|
|
if (value[key] !== undefined && (child = validateSchema(properties[key], value[key], `${path}.${key}`)) != null) {
|
|
return child;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
const validatePatternProperties = (validatedProperties, patternProperties, keys, value, path) => {
|
|
let child;
|
|
for (const pattern in patternProperties) {
|
|
const propertyRe = new RegExp(pattern);
|
|
for (let idx = 0; idx < keys.length; idx++) {
|
|
const key = keys[idx];
|
|
const childSchema = patternProperties[pattern];
|
|
if (propertyRe.test(key)) {
|
|
validatedProperties.add(key);
|
|
if ((child = validateSchema(childSchema, value[key], `${path}.${key}`)) != null) {
|
|
return child;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
const validateAdditionalProperties = (additionalProperties, properties, visitedPatternProperties, keys, value, path) => {
|
|
if (additionalProperties === true) {
|
|
return null;
|
|
}
|
|
let child;
|
|
for (let idx = 0; idx < keys.length; idx++) {
|
|
const key = keys[idx];
|
|
if (!visitedPatternProperties.has(key) && !properties?.[key]) {
|
|
if (additionalProperties === false) {
|
|
return {
|
|
message: `Additional property "${key}" is not allowed`,
|
|
keyword: 'additionalProperties',
|
|
path: `${path}.${key}`,
|
|
value: value[key]
|
|
};
|
|
} else if ((child = validateSchema(additionalProperties, value[key], `${path}.${key}`)) != null) {
|
|
return child;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
const validatePropertyNames = (propertyNames, keys, path) => {
|
|
let child;
|
|
for (let idx = 0; idx < keys.length; idx++) {
|
|
const key = keys[idx];
|
|
if ((child = validateString(propertyNames, key, `${path}.${key}`)) != null) {
|
|
child.message = `Property name "${key}" does not match schema. ${child.message}`;
|
|
return child;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
const validateDependencies = (dependencies, value, path) => {
|
|
let child;
|
|
for (const key in dependencies) {
|
|
if (value[key] !== undefined) {
|
|
if (Array.isArray(dependencies[key])) {
|
|
for (let idx = 0; idx < dependencies[key].length; idx++) {
|
|
if (value[dependencies[key][idx]] === undefined) {
|
|
return {
|
|
message: `Property "${dependencies[key][idx]}" is required when "${key}" is present`,
|
|
keyword: 'dependencies',
|
|
path: `${path}.${dependencies[key][idx]}`,
|
|
value: undefined
|
|
};
|
|
}
|
|
}
|
|
} else if (dependencies[key] != null && (child = validateSchema(dependencies[key], value, path)) != null) {
|
|
return child;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
const validateObject = (schema, value, path) => {
|
|
const keys = Object.keys(value);
|
|
const visitedPatternProperties = new Set();
|
|
let child;
|
|
if (schema.minProperties != null && keys.length < schema.minProperties) {
|
|
return {
|
|
message: `Object must have at least ${schema.minProperties} properties`,
|
|
keyword: 'minProperties',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.maxProperties != null && keys.length > schema.maxProperties) {
|
|
return {
|
|
message: `Object must have at most ${schema.maxProperties} properties`,
|
|
keyword: 'maxProperties',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.required != null && (child = validateRequired(schema.required, value, path)) != null) {
|
|
return child;
|
|
} else if (schema.properties != null && (child = validateProperties(schema.properties, value, path)) != null) {
|
|
return child;
|
|
} else if (schema.patternProperties != null && (child = validatePatternProperties(visitedPatternProperties, schema.patternProperties, keys, value, path)) != null) {
|
|
return child;
|
|
} else if (schema.additionalProperties != null && (child = validateAdditionalProperties(schema.additionalProperties, schema.properties, visitedPatternProperties, keys, value, path)) != null) {
|
|
return child;
|
|
} else if (schema.propertyNames != null && (child = validatePropertyNames(schema.propertyNames, keys, path)) != null) {
|
|
return child;
|
|
} else if (schema.dependencies != null && (child = validateDependencies(schema.dependencies, value, path)) != null) {
|
|
return child;
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
const validateType = (schemaType, valueType, path) => {
|
|
if (Array.isArray(schemaType)) {
|
|
if (valueType === 'integer' && schemaType.includes('number')) {
|
|
return null;
|
|
}
|
|
return !schemaType.includes(valueType) ? {
|
|
message: `Expected type ${schemaType.join(' or ')}, got ${valueType}`,
|
|
keyword: 'type',
|
|
path,
|
|
value: undefined
|
|
} : null;
|
|
} else {
|
|
if (valueType === 'integer' && schemaType === 'number') {
|
|
return null;
|
|
}
|
|
return schemaType !== valueType ? {
|
|
message: `Expected type ${schemaType}, got ${valueType}`,
|
|
keyword: 'type',
|
|
path,
|
|
value: undefined
|
|
} : null;
|
|
}
|
|
};
|
|
const validateAllOf = (schemas, value, path) => {
|
|
let child;
|
|
for (let idx = 0; idx < schemas.length; idx++) {
|
|
if ((child = validateSchema(schemas[idx], value, path)) != null) {
|
|
return child;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
const validateAnyOf = (schemas, value, path) => {
|
|
let child;
|
|
const cause = [];
|
|
for (let idx = 0; idx < schemas.length; idx++) {
|
|
if ((child = validateSchema(schemas[idx], value, path)) != null) {
|
|
cause.push(child);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
return {
|
|
message: 'No schema matched anyOf type',
|
|
keyword: 'anyOf',
|
|
path,
|
|
value,
|
|
cause
|
|
};
|
|
};
|
|
const validateOneOf = (schemas, value, path) => {
|
|
let child;
|
|
const cause = [];
|
|
for (let idx = 0; idx < schemas.length; idx++) {
|
|
if ((child = validateSchema(schemas[idx], value, path)) != null) {
|
|
cause.push(child);
|
|
}
|
|
}
|
|
if (cause.length >= schemas.length) {
|
|
return {
|
|
message: 'Value does not match any of the oneOf schemas',
|
|
keyword: 'oneOf',
|
|
path,
|
|
value,
|
|
cause
|
|
};
|
|
} else if (cause.length < schemas.length - 1) {
|
|
return {
|
|
message: `Value matches ${schemas.length - cause.length} schemas, but exactly one is required`,
|
|
keyword: 'oneOf',
|
|
path,
|
|
value,
|
|
cause
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
const validateConditional = (ifSchema, thenSchema, elseSchema, value, path) => {
|
|
let child;
|
|
if (validateSchema(ifSchema, value, path) != null) {
|
|
if (elseSchema != null && (child = validateSchema(elseSchema, value, path)) != null) {
|
|
return {
|
|
message: 'Value does not match "else" schema but did not match "if" condition schema',
|
|
keyword: 'else',
|
|
path,
|
|
value,
|
|
cause: [child]
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
} else if (thenSchema != null && (child = validateSchema(thenSchema, value, path)) != null) {
|
|
return {
|
|
message: 'Value does not match "then" schema but did match "if" condition schema',
|
|
keyword: 'then',
|
|
path,
|
|
value,
|
|
cause: [child]
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
const validateSchema = (schema, value, path) => {
|
|
if (typeof schema === 'boolean') {
|
|
// Draft 07: Schemas can be booleans
|
|
return !schema ? {
|
|
message: 'Schema is false',
|
|
keyword: 'schema',
|
|
path,
|
|
value: undefined
|
|
} : null;
|
|
}
|
|
const valueType = getValueType(value);
|
|
let child;
|
|
if (schema.type !== undefined && (child = validateType(schema.type, valueType, path)) != null) {
|
|
return child;
|
|
} else if (schema.const !== undefined && !isDeepEqual(value, schema.const)) {
|
|
return {
|
|
message: `Value must be equal to ${JSON.stringify(schema.const)}`,
|
|
keyword: 'const',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.enum != null && !isEnumValue(schema.enum, value)) {
|
|
return {
|
|
message: `Value must be one of ${JSON.stringify(schema.enum)}`,
|
|
keyword: 'enum',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.allOf != null && (child = validateAllOf(schema.allOf, value, path)) != null) {
|
|
return child;
|
|
} else if (schema.anyOf != null && (child = validateAnyOf(schema.anyOf, value, path)) != null) {
|
|
return child;
|
|
} else if (schema.oneOf != null && (child = validateOneOf(schema.oneOf, value, path)) != null) {
|
|
return child;
|
|
} else if (schema.not != null && validateSchema(schema.not, value, path) == null) {
|
|
return {
|
|
message: 'Value matches the not schema, but should not',
|
|
keyword: 'not',
|
|
path,
|
|
value
|
|
};
|
|
} else if (schema.if != null && (schema.then != null || schema.else != null) && (child = validateConditional(schema.if, schema.then, schema.else, value, path)) != null) {
|
|
return child;
|
|
}
|
|
switch (valueType) {
|
|
case 'string':
|
|
return validateString(schema, value, path);
|
|
case 'number':
|
|
case 'integer':
|
|
return validateNumber(schema, value, path);
|
|
case 'array':
|
|
return validateArray(schema, value, path);
|
|
case 'object':
|
|
return validateObject(schema, value, path);
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
exports.validateSchema = validateSchema;
|
|
//# sourceMappingURL=validate.js.map
|