"use strict"; const util = require("util"); const createStringValidator = require("./validator-functions/string"); const createNumberValidator = require("./validator-functions/number"); const createBooleanValidator = require("./validator-functions/boolean"); const createFunctionValidator = require("./validator-functions/function"); const createNothingValidator = require("./validator-functions/nothing"); const createNullValidator = require("./validator-functions/null"); const createUndefinedValidator = require("./validator-functions/undefined"); const getValueType = require("./util/get-value-type"); const errors = require("./errors"); module.exports = function generateValidator(rule, name = "") { let baseRule; if (rule._baseType != null) { let validatorFactories = { string: createStringValidator, boolean: createBooleanValidator, number: createNumberValidator, guardedFunction: createFunctionValidator, nothing: createNothingValidator, null: createNullValidator, undefined: createUndefinedValidator }; let factory = validatorFactories[rule._baseType]; if (factory != null) { baseRule = factory(rule._options, name); } else { throw new Error(`Unrecognized base type: ${rule._baseType}`); } } else if (rule._collectionType != null) { baseRule = () => true; /* FIXME */ } else if (rule._modifierType != null) { if (rule._modifierType === "either") { let validators = rule._types.map((type) => generateValidator(type)); baseRule = function (value) { let matched = false; for (let validator of validators) { if (validator.call(this, value) === true) { matched = true; break; } } if (matched === true) { return true; } else { let acceptableTypes = rule._types.map((type) => getValueType(type, false)).join(", "); return new errors.ValidationError(`Expected one of (${acceptableTypes}), got ${getValueType(value)} instead`); } }; } else { throw new Error(`Unrecognized modifier: ${rule._modifierType}`); } } else if (rule._isTypeAlias === true) { baseRule = generateValidator(rule._alias, name); } else if (rule._isCustomType === true) { let validator = rule._validator; baseRule = function (value) { if (value._isDeserializationPlaceholder === true) { return true; } else if (validator.call(this, value) === true) { return true; } else { return new errors.ValidationError(`Expected ${getValueType(rule)}, got ${getValueType(value)} instead`); } }; } else if (rule._isCustomRegistryType) { /* HACK: We performantly memoize the validator for a registry-stored alias, by storing it in the validator generation scope. */ let aliasValidator; baseRule = function (value) { /* FIXME: Better error when the type is unknown */ if (value._isDeserializationPlaceholder === true) { return true; } else { let actualType = rule._registry._getType(rule._name); if (actualType._isCustomType) { if (actualType._validator.call(this, value) === true) { return true; } else { return new errors.ValidationError(`Expected ${getValueType(rule)}, got ${getValueType(value)} instead`); } } else { if (aliasValidator == null) { aliasValidator = generateValidator(actualType._alias, name); } let validatorResult = aliasValidator.call(this, value); if (validatorResult === true) { return true; } else { // return new errors.ValidationError(`Expected ${getValueType(actualType._alias)}, got ${getValueType(value)} instead`); /* FIXME: Possible this is done wrong elsewhere too, swallowing the actual validation error? */ return validatorResult; } } } }; } else if (rule._isTrait === true) { baseRule = function (value) { if (value._isDeserializationPlaceholder === true) { return true; } else if (value._type != null && value._type._implementedTraits != null && value._type._implementedTraits.has(rule)) { return true; } else { return new errors.ValidationError(`Expected object of a type with the ${rule._name} trait, got ${getValueType(value)} instead`); } }; } else if (rule._isRegistryTrait === true) { baseRule = function (value) { if (value._isDeserializationPlaceholder === true) { return true; } else { /* TODO: The below approach requires that traits be defined before the types that use them, and disallows trait registry references in `.implements` calls, due to _implementedTraits always needing to contain actual trait definitions; in the future, a better approach needs to be found for this such that trait registry references can be used everywhere. */ let actualRule = rule._registry._getTrait(rule._name); /* FIXME: Better error when the trait is unknown */ if (value._type != null && value._type._implementedTraits != null && value._type._implementedTraits.has(actualRule)) { return true; } else { return new errors.ValidationError(`Expected object of a type with the ${rule._name} trait, got ${getValueType(value)} instead`); } } }; } else if (rule._isSelfRule === true) { baseRule = function (value) { if (value instanceof this._type) { return true; } else { return new errors.ValidationError(`Expected ${getValueType(this._type)}, got ${getValueType(value)} instead`); } }; } else if (rule._constructorType != null) { /* This is for handling third-party constructors and their instances. */ baseRule = function (value) { if (value instanceof rule._constructorType) { return true; } else { return new errors.ValidationError(`Expected ${getValueType(rule)}, got ${getValueType(value)} instead`); } }; } else { throw new Error(`Unrecognized rule type for rule ${util.inspect(rule)}`); /* FIXME? */ } let compositeFunction; if (rule._constraints.length === 0) { compositeFunction = function baseRuleWrapper(value) { if (value === undefined && rule._allowUndefined === true) { return true; } else if (value == null) { return new errors.ValidationError(`Value is required for property '${name}'`, { property: name }); } else { return baseRule.call(this, value); } }; } else { let hasDefaultValue = false, defaultValue; let filteredConstraints = rule._constraints.filter((constraint) => { if (constraint.type === "default") { hasDefaultValue = true; defaultValue = constraint.value; return false; } else { return true; } }); let rules = [baseRule].concat(filteredConstraints.map((constraint) => { if (constraint.type === "validate") { return constraint.validator; } else { throw new Error(`Encountered unrecognized constraint type: ${constraint.type}`); } })); compositeFunction = function complexRuleWrapper(value) { if (value == null) { if (hasDefaultValue) { return { _default: defaultValue }; } else if (value === undefined && rule._allowUndefined === true) { return true; } else { return new errors.ValidationError(`Value is required for property '${name}'`, { property: name }); } } else { /* TODO: Possibly special-case (for better performance) if the only extra rule is a 'default value' rule? This would avoid a `for` loop in the case where a value is explicitly specified. */ for (let rule of rules) { let result = rule.call(this, value); if (result === true) { continue; } else { return result; } } return true; } }; } compositeFunction._rule = rule; return compositeFunction; };