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.
core/src/api/validate-arguments/index.js

123 lines
4.8 KiB
JavaScript

"use strict";
const isArguments = require("is-arguments");
const flatten = require("flatten");
const asExpression = require("as-expression");
const ValidationError = require("@validatem/error");
const annotateErrors = require("@validatem/annotate-errors");
const virtualProperty = require("@validatem/virtual-property");
const splitArray = require("../../split-array");
const applyValidators = require("../../apply-validators");
const createValidationMethod = require("../validation-method");
const RemainingArguments = require("./remaining-arguments-symbol");
// TODO: Simplify below by treating it like an array or object? Or would that introduce too much complexity through specialcasing?
// TODO: Find a way to produce validatem-style errors for the below invocation errors, without introducing recursion
// TODO: Maybe create a validatedFunction(rules, (foo, bar) => ...) type abstraction for "pre-processed", more performant validation usecases? At the cost of having to specify the validation rules outside of the function, which you'd need to do anyway in that case. Upside would be that you don't need to specify `arguments` manually anymore.
function arrayToNormalized(array) {
return array.map((definition) => {
let [ name, ...rules ] = definition;
if (typeof name !== "string") {
throw new Error("First item in the argument rules list must be the argument name");
} else {
return {
name: name,
rules: rules
};
}
});
}
function objectToNormalized(object) {
// NOTE: Not using Object.entries because we also want to catch Symbol keys
// NOTE: Reflect.ownKeys is not ordered correctly, Symbols always come last! When object syntax is used, we just assume that the user correctly specified any RemainingArguments symbol at the end.
return Reflect.ownKeys(object).map((key) => {
let value = object[key];
return {
name: key,
rules: value
};
});
}
function applyDefinitions(args, definitions, remainingArgumentsIndex) {
let results = definitions.map(({ name, rules }, i) => {
let argument = args[i];
let validatorResult = applyValidators(argument, rules);
return {
errors: annotateErrors({
pathSegments: (name === RemainingArguments)
? [ virtualProperty(`arguments from ${remainingArgumentsIndex} onwards`) ] // TODO: Better description?
: [ name ],
errors: validatorResult.errors
}),
newValue: (validatorResult.newValue !== undefined)
? validatorResult.newValue
: argument
};
});
// FIXME: Investigate syncpipe usage here
let combinedErrors = results.map((result) => result.errors);
let flattenedErrors = flatten(combinedErrors); // TODO: Switch to `Array#flat` once Node 10.x goes EOL (April 2021)
let newValues = results.map((result) => result.newValue);
return {
errors: flattenedErrors,
newValue: newValues
};
}
module.exports = createValidationMethod((args, argumentDefinitions) => {
if (!isArguments(args)) {
throw new Error(`First argument is not an 'arguments' object; maybe you forgot to put it before the validation rules?`);
} else if (argumentDefinitions == null) {
throw new Error(`Validation rules (second argument) are missing; maybe you forgot to specify them?`);
} else {
let normalizedDefinitions = (Array.isArray(argumentDefinitions))
? arrayToNormalized(argumentDefinitions)
: objectToNormalized(argumentDefinitions);
let definitionCount = normalizedDefinitions.length;
let remainingArgumentsIndex = normalizedDefinitions.findIndex(({ name }) => name === RemainingArguments);
let hasRemainingArguments = (remainingArgumentsIndex !== -1);
let remainingArgumentsComeLast = (remainingArgumentsIndex === definitionCount - 1);
if (hasRemainingArguments && !remainingArgumentsComeLast) {
throw new Error(`A RemainingArguments entry can only be the last item`);
} else if (!hasRemainingArguments && args.length > definitionCount) {
return {
errors: [ new ValidationError(`Got ${args.length} arguments, but only expected ${definitionCount}`) ],
// Cast the below to an array, for consistency with *success* output, in case we ever want to expose the new values to the user in an error case too.
newValue: Array.from(args)
};
} else {
let normalizedArguments = asExpression(() => {
let argumentsAsArray = Array.from(args);
if (hasRemainingArguments) {
let [ positionalArguments, remainingArguments ] = splitArray(argumentsAsArray, [ remainingArgumentsIndex ]);
if (remainingArguments.length > 0) {
// Add all of the remaining arguments as a *single array value*, to make applying validation rules easier
return positionalArguments.concat([ remainingArguments ]);
} else {
return positionalArguments;
}
} else {
return argumentsAsArray;
}
});
return applyDefinitions(normalizedArguments, normalizedDefinitions, remainingArgumentsIndex);
}
}
});