// JSON Validation Syntax
import { get as _get } from '@kitmi/utils';
import JsvError from './JsvError';
import { initContext, getChildContext } from './config';
import { isOperator } from './utils';
import ops from './validateOperators';
function getUnmatchedExplanation(op, leftValue, rightValue, context) {
if (context.ERROR) {
return context.ERROR;
}
const formatter = context.config.messages.validationErrors?.[op];
if (formatter == null) {
throw new Error('Missing validation error formatter for operator: ' + op);
}
return formatter(context.name, leftValue, rightValue, context);
}
/**
* Tests whether a left-hand value satisfies a given operator and right-hand value.
* @param {*} left - The left-hand value to test.
* @param {string} op - The operator to use for the test.
* @param {*} right - The right-hand value to test against.
* @param {Object} options - Options to use for the test.
* @param {Object} context - The current context of the data structure being validated.
* @returns {*} The result of the test.
* @throws {Error} If the specified operator does not have a registered validator.
*/
export function test(left, op, right, options, context) {
const handler = context.config.getValidator(op);
if (!handler) {
throw new Error(context.config.messages.INVALID_TEST_HANLDER(op));
}
return handler(left, right, options, context);
}
export function handleErrors(errors, options, context) {
if (errors.length > 0) {
if (options.throwError) {
throw new JsvError(errors, context);
}
if (!options.asPredicate) {
context.ERROR = errors.length === 1 && options.plainError ? errors[0] : errors;
}
return false;
}
return true;
}
export function handleResult(result, options, context) {
if (result !== true) {
if (!options.asPredicate) {
context.ERROR = result;
}
return false;
}
return true;
}
/**
* Validate the given object with JSON Expression Syntax (JES)
* @param {*} actual - The object to match
* @param {*} jsv - Expected state in JSON Expression Syntax
* @param {*} options - Validation options
* @param {*} [context] - Validation context
* @property {Object} context.config - The configuration object for the current validation context
* @property {string} [context.path] - The current path of the data structure being validated
* @property {*} [context.THIS] - The current value being validated
* @property {*} [context.ROOT] - The root of the data structure being validated
* @property {*} [context.PARENT] - The parent of the current field being validated
* @property {string} [context.KEY] - The key of the current field being validated
* @property {string|Array} [context.ERROR] - The error message to display if the validation fails
* @property {Object} [context.mapOfNames] - A map of field names to use for error messages
* @property {string} [context.name] - The name of the current field being validated, provided by the user
* @returns {array} - [ {boolean} matched, {string} unmatchedReason ]
*/
function validate(actual, jsv, options, context) {
if (jsv == null) {
return true;
}
context = initContext(context, actual);
const type = typeof jsv;
if (type === 'string') {
// validator does not support unary operator
throw new Error(context.config.messages.SYNTAX_INVALID_EXPR(jsv));
}
const { throwError, abortEarly, asPredicate, plainError } = options;
if (Array.isArray(jsv)) {
return validate(actual, { $all: jsv }, options, context);
}
if (type !== 'object') {
return validate(actual, { $equal: jsv }, options, context);
}
const errors = [];
const _options = !abortEarly && throwError ? { ...options, throwError: false } : options;
for (let fieldName in jsv) {
let op, left, _context;
const opValue = jsv[fieldName];
if (isOperator(fieldName)) {
//validator
op = context.config.getValidatorTag(fieldName);
if (!op) {
throw new Error(context.config.messages.UNSUPPORTED_VALIDATION_OP(fieldName, context.path));
}
left = actual;
_context = context;
} else {
let isComplexKey = fieldName.indexOf('.') !== -1;
//pick a field and then apply manipulation
left = actual != null ? (isComplexKey ? _get(actual, fieldName) : actual[fieldName]) : undefined;
_context = getChildContext(context, actual, fieldName, left);
if (opValue != null && typeof opValue === 'object') {
op = ops.MATCH;
} else {
op = ops.EQUAL;
}
}
if (test(left, op, opValue, _options, _context) !== true) {
if (asPredicate) {
return false;
}
const reason = getUnmatchedExplanation(op, left, opValue, _context);
if (abortEarly && throwError) {
throw new JsvError(reason, _context);
}
errors.push(plainError ? reason : new JsvError(reason, _context));
if (abortEarly) {
break;
}
}
}
if (errors.length > 0) {
if (asPredicate) {
return false;
}
if (throwError) {
throw new JsvError(errors, context);
}
return errors.length === 1 && plainError ? errors[0] : errors;
}
return true;
}
export default validate;