forked from validatem/core
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
112 lines
4.2 KiB
JavaScript
112 lines
4.2 KiB
JavaScript
"use strict";
|
|
|
|
const ValidationError = require("@validatem/error");
|
|
const matchValidationError = require("@validatem/match-validation-error");
|
|
const validationResult = require("@validatem/validation-result");
|
|
|
|
const isRequiredMarker = require("./is-special/required");
|
|
const isValidationResult = require("./is-special/validation-result");
|
|
const isCombinator = require("./is-special/combinator");
|
|
const applyValidators = require("./apply-validators");
|
|
|
|
// NOTE: If a validator returns a transformed value, the *next* validator in line will receive this *transformed* value instead of the original value. This allows composing/chaining different transformations, and keeping that model consistent with how providing an array of validators would work. If this behaviour is not desirable, the user can wrap `ignoreResult` around the offending validator to retain the previous (potentially original input) value.
|
|
|
|
// NOTE: Assigning to a property *on* module.exports as a cyclic dependency handling workaround for compose-rules -> apply-validators -> compose-rules -> ...
|
|
// This works because apply-validators gets a reference the default `module.exports` object when it requires compose-rules (this module) and it can complete initialization, *after* which we make our compose-rules implementation available as a property on said (already-referenced) object.
|
|
module.exports.compose = function composeValidators(validators) {
|
|
let isRequired = validators.some((rule) => isRequiredMarker(rule));
|
|
let nonMarkerRules = validators.filter((rule) => !isRequiredMarker(rule));
|
|
|
|
function callRule({ rule, value, context }) {
|
|
try {
|
|
let result = isCombinator(rule)
|
|
? rule.callback(value, applyValidators, context)
|
|
: rule(value, context);
|
|
|
|
if (result !== undefined) {
|
|
if (isValidationResult(result)) {
|
|
if (Array.isArray(result.errors)) {
|
|
let nonValidationErrors = result.errors.filter((error) => !matchValidationError(error));
|
|
|
|
if (nonValidationErrors.length === 0) {
|
|
return {
|
|
errors: result.errors,
|
|
newValue: result.newValue
|
|
};
|
|
} else {
|
|
// We should never reach this point, but it could possibly occur if someone erroneously includes non-ValidationError errors in a validationResult. Note that this is a last-ditch handler, and so we only throw the first non-ValidationError error and let the user sort out the rest, if any.
|
|
throw nonValidationErrors[0];
|
|
}
|
|
} else {
|
|
throw new Error(`The 'errors' in a validationResult must be an array`);
|
|
}
|
|
} else if (result instanceof Error) {
|
|
// We could interpret returned Errors as either values or a throw substitute. Let's wait for users to file issues, so that we know what people *actually* need here.
|
|
throw new Error(`It is currently not allowed to return an Error object from a validator. If you have a reason to need this, please file a bug!`);
|
|
} else {
|
|
return {
|
|
errors: [],
|
|
newValue: result
|
|
};
|
|
}
|
|
} else {
|
|
return {
|
|
errors: [],
|
|
newValue: undefined
|
|
};
|
|
}
|
|
} catch (error) {
|
|
if (matchValidationError(error)) {
|
|
return {
|
|
errors: [ error ],
|
|
newValue: undefined
|
|
};
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
return function composedValidator(value, context) {
|
|
if (isRequired && value == null) {
|
|
return validationResult({
|
|
errors: [ new ValidationError(`Required value is missing`) ],
|
|
newValue: value
|
|
});
|
|
} else {
|
|
let lastValue = value;
|
|
|
|
let errors = [];
|
|
|
|
for (let rule of nonMarkerRules) {
|
|
if (value != null || rule.callIfNull === true) {
|
|
let result = callRule({
|
|
rule: rule,
|
|
value: lastValue,
|
|
context: context
|
|
});
|
|
|
|
if (result.newValue != null) {
|
|
lastValue = result.newValue;
|
|
}
|
|
|
|
if (result.errors.length > 0) {
|
|
errors = result.errors;
|
|
break;
|
|
}
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return validationResult({
|
|
errors: errors,
|
|
// NOTE: The below conditional is to make a composed series of validator mirror a normal validator, in the sense that it only returns a `newValue` if something has actually changed. For transparent composability, we want to be as close to the behaviour of a non-composed validator as possible.
|
|
newValue: (lastValue !== value)
|
|
? lastValue
|
|
: undefined
|
|
});
|
|
}
|
|
};
|
|
};
|