"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)); return function composedValidator(value, context) { if (isRequired && value == null) { return validationResult({ errors: [ new ValidationError(`Required value is missing`) ], newValue: value }); } else if (value != null) { let lastValue = value; let errors = []; for (let rule of nonMarkerRules) { try { let result = isCombinator(rule) ? rule.callback(lastValue, applyValidators, context) : rule(lastValue, context); if (result !== undefined) { let transformedValue; if (isValidationResult(result)) { if (Array.isArray(result.errors)) { let nonValidationErrors = result.errors.filter((error) => !matchValidationError(error)); if (nonValidationErrors.length === 0) { errors = result.errors; transformedValue = 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 result.errors[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 { transformedValue = result; } if (transformedValue != null) { lastValue = transformedValue; } } } catch (error) { if (matchValidationError(error)) { errors = [ error ]; } else { throw error; } } if (errors.length > 0) { break; } } 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 }); } else { return validationResult({ newValue: undefined }); } }; };