"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 Reflect.ownKeys(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( Reflect.ownKeys(rules), Reflect.ownKeys(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 // FIXME: Assign non-enumerable if the source property was non-enumerable! newObject[key] = value; } } return validationResult({ errors: errors, newValue: newObject }); } }); validator.callIfNull = false; return validator; };