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

121 lines
4.8 KiB
JavaScript

"use strict";
const isArguments = require("is-arguments");
const asExpression = require("as-expression");
const syncpipe = require("syncpipe");
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.
// TODO: Special-case callsite extraction from stacktrace when using validateArguments, as the user will probably be interested in where the validator-fenced code was *called from*, not where the rules are defined (unlike with validateValue)
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
};
});
return {
errors: syncpipe(results, [
(_) => _.map((result) => result.errors),
(_) => _.flat()
]),
newValue: results.map((result) => result.newValue)
};
}
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);
}
}
});