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
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;
|
|
};
|