"use strict"; // TODO: Rename this to something more appropriate, like any/anyOf? How to make sure that doesn't cause confusion with `oneOf`? const splitFilter = require("split-filter"); const ValidationError = require("@validatem/error"); const combinator = require("@validatem/combinator"); const validationResult = require("@validatem/validation-result"); const areErrorsFromSameOrigin = require("./src/are-errors-from-same-origin"); module.exports = function (alternatives) { if (!Array.isArray(alternatives)) { throw new Error(`Must specify an array of alternatives`); } else if (alternatives.length < 2) { // This doesn't interfere with conditionally-specified alternatives using ternary expressions, because in those cases there is still *some* item specified, it's just going to have a value of `undefined` (and will subsequently be filtered out) throw new Error("Must specify at least two alternatives"); } else if (arguments.length > 1) { throw new Error(`Only one argument is accepted; maybe you forgot to wrap the different alternatives into an array?`); } else { return combinator((value, applyValidators) => { let allErrors = []; for (let alternative of alternatives) { let { errors, newValue } = applyValidators(value, alternative); if (errors.length === 0) { return newValue; } else { allErrors.push(...errors); } } /* We want to separate out the errors that occurred at this "level"; that is, errors that *didn't* originate from a nested validation combinator like `has-shape` or `array-of`. Otherwise, the user could get very confusing errors that combine the `either` alternatives and some deeper validation error into a single list, without denoting the path. An example of such a confusing error: - At options -> credentials: Must satisfy at least one of: "Must be a plain object (eg. object literal)", "Encountered an unexpected property 'foo'" With this implementation, it becomes a perfectly reasonable error instead: - At options -> credentials -> 1: Encountered an unexpected property 'foo'" */ let [ sameLevelErrors, nestedErrors ] = splitFilter(allErrors, (error) => { return (error.path.length === 0); }); let sameOrigin = areErrorsFromSameOrigin(nestedErrors); if (nestedErrors.length > 0 && sameOrigin) { // One of the alternatives *did* match, but it failed somewhere further down the tree, and we don't have any errors originating from *other* nested rules. Chances are that the user intended to match the failing branch, so we pretend that the other alternatives don't exist, and pass through the original error(s). return validationResult({ errors: nestedErrors }); } else { throw new ValidationError(`Must satisfy at least one of:`, { subErrors: allErrors }); } }); } };