src/Rule/index.js
const { getErrorFromObject, getErrorFromFunctionOrString } = require('./util');
const { TEST_FUNCTIONS, OPTIONAL } = require('../testFunctions');
const { AND, OR, isObject } = require('./../util');
const OPERATORS = {
'&': AND,
'|': OR,
};
/**
* The Rule class validates only one value
* once a rule is created it can be used multiple times
*/
class Rule {
/**
*
* @param {String|Object} obj the rule object describes the tests that are ran by the Rule
* @param {String} error the error returned when the tested input is not correct
*/
constructor(obj, error) {
if (typeof obj === 'string') {
this.rule = { type: obj };
} else {
this.rule = obj;
}
this.error = error;
this.testEntryObject();
}
/**
*
* @param {any} val the value to be tested
* @param {Object|String} obj the error object or string thats showed on error
* @param {String} path the path to the tested value this is used when
* using validator to keep track of the prop value ex: obj.min
*
* @return {boolean}
*/
test(val, path, obj) {
const types = this.getTypes();
const operators = this.getRuleOperators();
let ret = this.testOneRule(val, types[0], path, obj);
for (let i = 1; i < types.length; i += 1) {
const operator = operators[i] || operators[i - 1];
ret = operator(ret, this.testOneRule(val, types[i], path, obj));
}
return ret;
}
/**
* converts array from string if multiple types given in type
* its the case for exemple int|float
* @private
* @return {[String]}
*/
getTypes() {
return this.rule.type.split(/[&|]/);
}
/**
* Returns a list of the operators when multiple types given
* its the case for example int|float
* @private
* @returns {[String]}
*/
getRuleOperators() {
const ret = [];
const operators = this.rule.type.match(/[&|]/g) || '&';
for (let i = 0; i < operators.length; i += 1) {
ret.push(OPERATORS[operators[i]]);
}
return ret;
}
/**
* @private
* @param val value to be tested
* @param {String} type the type from getTypes()
* @param {String} path the path to the value if Validator is used
* @param {any} obj full object beeing tested
*
* @returns {boolean}
*/
testOneRule(val, type, path, obj) {
if (Rule.TEST_FUNCTIONS[type].optional(val, this.rule.optional) === true) {
return true;
}
const keys = Object.keys(this.rule).sort((key) => {
if (key === 'type') return -1;
return 0;
});
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
const testFunction = Rule.TEST_FUNCTIONS[type][key];
if (testFunction(val, this.rule[key], path, obj) === false && testFunction !== OPTIONAL) {
return false;
}
}
return true;
}
getFailingRules(val) {
const keys = Object.keys(this.rule).sort((key) => {
if (key === 'type') return -1;
return 0;
});
return this.getTypes().reduce((acc, type) => {
if (Rule.TEST_FUNCTIONS[type].optional(val, this.rule.optional) === true) {
return acc;
}
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
const testFunction = Rule.TEST_FUNCTIONS[type][key];
if (testFunction(val, this.rule[key]) === false && testFunction !== OPTIONAL) {
acc.push(key);
break;
}
}
return acc;
}, []);
}
/**
* Tests the validity of the constructor object
* thows an error if the object is invalid
*/
testEntryObject() {
if (!this.rule.type) {
throw Error('`type` is required');
}
const types = this.getTypes();
types.forEach((type) => {
this.testEntryObjectOneType(type);
});
}
/**
* Tests the validity of the constructor object
* thows an error if the object is invalid
* tests if all the keys are valid
*/
testEntryObjectOneType(type) {
const keys = Object.keys(this.rule);
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
if (!Rule.TEST_FUNCTIONS[type]) {
throw Error(`The \`${type}\` type doesn't exist`);
}
if (!Rule.TEST_FUNCTIONS[type][key]) {
throw new Error(`\`${type}\` doesn't have "${key}" test!`);
}
}
}
/**
* returns a list of errors if they are present
* @return {[String]}
*/
getError(path, value) {
if (isObject(this.error)) {
return this.getFailingRules(value)
.map(key => getErrorFromObject(this.error, path, value, key));
}
return [getErrorFromFunctionOrString(this.error, path, value)];
}
/**
* Add custom rule to the Rule class
* @param {String} name the name of the rule
* @param {Function} rule the validation function
*/
static addCustom(name, rule) {
Rule.TEST_FUNCTIONS[name] = rule;
Rule.TEST_FUNCTIONS[name].optional = OPTIONAL;
}
}
Rule.TEST_FUNCTIONS = TEST_FUNCTIONS;
module.exports = Rule;