forked from validatem/core
Add support for rest operator in arguments
parent
859d86834b
commit
be5fd46005
@ -0,0 +1,27 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { validateArguments, RemainingArguments } = require(".");
|
||||||
|
const isString = require("@validatem/is-string");
|
||||||
|
const isBoolean = require("@validatem/is-boolean");
|
||||||
|
const isNumber = require("@validatem/is-number");
|
||||||
|
const required = require("@validatem/required");
|
||||||
|
const arrayOf = require("@validatem/array-of");
|
||||||
|
|
||||||
|
function testFunction(one, two, three, ... rest) {
|
||||||
|
validateArguments(arguments, {
|
||||||
|
one: [ required, isString ],
|
||||||
|
two: [ required, isNumber ],
|
||||||
|
three: [ required, isBoolean ],
|
||||||
|
[RemainingArguments]: [ required, arrayOf(isString) ],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
testFunction("foo", "bar", null, "baz", 42);
|
||||||
|
|
||||||
|
/*
|
||||||
|
AggregrateValidationError: One or more validation errors occurred:
|
||||||
|
- At two: Must be a number
|
||||||
|
- At three: Required value is missing
|
||||||
|
- At array -> 1: Must be a string
|
||||||
|
- At mapping -> d -> (value): Must be a boolean
|
||||||
|
*/
|
@ -1,85 +0,0 @@
|
|||||||
"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 applyValidators = require("../apply-validators");
|
|
||||||
const createValidationMethod = require("./validation-method");
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
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 usesArrayAPI = (Array.isArray(argumentDefinitions));
|
|
||||||
|
|
||||||
let definitionCount = (usesArrayAPI)
|
|
||||||
? argumentDefinitions.length
|
|
||||||
: Object.keys(argumentDefinitions).length;
|
|
||||||
|
|
||||||
if (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 normalizedDefinitions = asExpression(() => {
|
|
||||||
if (usesArrayAPI) {
|
|
||||||
return argumentDefinitions.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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return Object.entries(argumentDefinitions).map(([ key, value ]) => {
|
|
||||||
return {
|
|
||||||
name: key,
|
|
||||||
rules: value
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let results = normalizedDefinitions.map(({ name, rules }, i) => {
|
|
||||||
let argument = args[i];
|
|
||||||
let validatorResult = applyValidators(argument, rules);
|
|
||||||
|
|
||||||
return {
|
|
||||||
errors: annotateErrors({
|
|
||||||
pathSegments: [ name ],
|
|
||||||
errors: validatorResult.errors
|
|
||||||
}),
|
|
||||||
newValue: (validatorResult.newValue !== undefined)
|
|
||||||
? validatorResult.newValue
|
|
||||||
: argument
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -0,0 +1,122 @@
|
|||||||
|
"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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,3 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = Symbol("RemainingArguments");
|
@ -0,0 +1,22 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
// FIXME: Move this into its own package
|
||||||
|
|
||||||
|
module.exports = function splitArray(array, indexes) {
|
||||||
|
let segments = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
|
for (let index of indexes) {
|
||||||
|
if (index < array.length) {
|
||||||
|
segments.push(array.slice(lastIndex, index));
|
||||||
|
lastIndex = index;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Attempted to split at index ${index}, but array only has ${array.length} items`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final segment, from last specified index to end of array; this is because the indexes only define the *split points*, and so the total amount of segments is always one more than that.
|
||||||
|
segments.push(array.slice(lastIndex));
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
};
|
Loading…
Reference in New Issue