forked from validatem/either
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.
81 lines
3.6 KiB
JavaScript
81 lines
3.6 KiB
JavaScript
"use strict";
|
|
|
|
// TODO: Rename this to something more appropriate, like any/anyOf? How to make sure that doesn't cause confusion with `oneOf`?
|
|
|
|
const flatten = require("flatten");
|
|
|
|
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");
|
|
|
|
function unpackNestedEithers(errors) {
|
|
// NOTE: We only unpack `either` errors that occurred *at the same level* as this error, ie. where there's directly a case of `either(either(...), ...)`, without any kind of data nesting (like `arrayOf` or `hasShape`) inbetween. Nested-data failures should still be shown separately, as their resolution strategy is actually different; unlike same-level nested `either` errors, where the nesting is purely an implementation detail that allows composing sets of alternatives together.
|
|
|
|
return flatten(errors.map((error) => {
|
|
if (error.path.length === 0 && error.__isValidatemEitherError) {
|
|
return error.subErrors;
|
|
} else {
|
|
return error;
|
|
}
|
|
}));
|
|
}
|
|
|
|
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 if (errors.length === 1) {
|
|
allErrors.push(errors[0]);
|
|
} else {
|
|
allErrors.push(new ValidationError("Must satisfy all of the following:", {
|
|
subErrors: 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 nestedErrors = allErrors.filter((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: unpackNestedEithers(allErrors),
|
|
__isValidatemEitherError: true
|
|
});
|
|
}
|
|
});
|
|
}
|
|
};
|