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.
100 lines
2.8 KiB
JavaScript
100 lines
2.8 KiB
JavaScript
"use strict";
|
|
|
|
const asExpression = require("as-expression");
|
|
const defaultValue = require("default-value");
|
|
const assureArray = require("assure-array");
|
|
const flatten = require("flatten");
|
|
const arrayUnion = require("array-union");
|
|
|
|
const ValidationError = require("@validatem/error");
|
|
const validationResult = require("@validatem/validation-result");
|
|
const annotateErrors = require("@validatem/annotate-errors");
|
|
const combinator = require("@validatem/combinator");
|
|
|
|
function containsRules(rules) {
|
|
// TODO: We cannot use normalize-rules here (for now) since that would create a cyclical dependency; figure out a way to work around this, maybe by removing the hasShape feature in `normalize-rules`, or moving the flattening itself out into a separate `flatten-rules` package?
|
|
if (rules == null) {
|
|
return false;
|
|
} else {
|
|
// TODO: Switch to `Array#flat` once Node 10.x goes EOL (April 2021)
|
|
let flattenedRules = flatten(assureArray(rules));
|
|
|
|
if (!flattenedRules.some((rule) => rule != null)) {
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = function hasShape(rules) {
|
|
let validator = combinator((object, applyValidators, context) => {
|
|
let allowExtraProperties = defaultValue(context.allowExtraProperties, false);
|
|
|
|
let unexpectedPropertyErrors = asExpression(() => {
|
|
if (!allowExtraProperties) {
|
|
return Object.keys(object).map((propertyName) => {
|
|
if (!containsRules(rules[propertyName])) {
|
|
return new ValidationError(`Encountered an unexpected property '${propertyName}'`);
|
|
} else {
|
|
return null;
|
|
}
|
|
}).filter((error) => {
|
|
return (error != null);
|
|
});
|
|
} else {
|
|
return [];
|
|
}
|
|
});
|
|
|
|
if (unexpectedPropertyErrors.length > 0) {
|
|
return validationResult({
|
|
errors: unexpectedPropertyErrors,
|
|
newValue: object
|
|
});
|
|
} else {
|
|
let errors = [];
|
|
let newObject = {};
|
|
|
|
// We need to consider the keys from the ruleset (for detecting missing required properties) *and* the keys from the actual object (for handling extraneous values).
|
|
let allKeys = arrayUnion(
|
|
Object.keys(rules),
|
|
Object.keys(object)
|
|
);
|
|
|
|
for (let key of allKeys) {
|
|
let value = object[key];
|
|
let rule = rules[key];
|
|
|
|
if (rule != null) {
|
|
let { errors: keyErrors, newValue } = applyValidators(value, rule);
|
|
|
|
let annotatedErrors = annotateErrors({
|
|
pathSegments: [ key ],
|
|
errors: keyErrors
|
|
});
|
|
|
|
errors.push(... annotatedErrors);
|
|
|
|
// Don't scatter explicit `undefined`s across the result object, just because an optional rule existed for some properties but the corresponding value didn't
|
|
if (newValue !== undefined) {
|
|
newObject[key] = newValue;
|
|
}
|
|
} else {
|
|
// Extraneous property
|
|
newObject[key] = value;
|
|
}
|
|
}
|
|
|
|
return validationResult({
|
|
errors: errors,
|
|
newValue: newObject
|
|
});
|
|
}
|
|
});
|
|
|
|
validator.callIfNull = false;
|
|
|
|
return validator;
|
|
};
|