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.

225 lines
7.5 KiB
JavaScript

"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 = "<unknown>") {
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;
};