v8n
Version:
Dead simple fluent JavaScript validation library
539 lines (433 loc) • 15.5 kB
JavaScript
var v8n = (function () {
'use strict';
var Rule = function Rule(name, fn, args, modifiers) {
this.name = name;
this.fn = fn;
this.args = args;
this.modifiers = modifiers;
};
Rule.prototype._test = function _test (value) {
var fn = this.fn;
try {
testAux(this.modifiers.slice(), fn, this)(value);
} catch (ex) {
fn = function () { return false; };
}
try {
return testAux(this.modifiers.slice(), fn, this)(value);
} catch (ex$1) {
return false;
}
};
Rule.prototype._check = function _check (value) {
try {
testAux(this.modifiers.slice(), this.fn, this)(value);
} catch (ex) {
if (testAux(this.modifiers.slice(), function (it) { return it; }, this)(false)) {
return;
}
}
if (!testAux(this.modifiers.slice(), this.fn, this)(value)) {
throw null;
}
};
Rule.prototype._testAsync = function _testAsync (value) {
var this$1 = this;
return new Promise(function (resolve, reject) {
testAsyncAux(
this$1.modifiers.slice(),
this$1.fn,
this$1
)(value)
.then(function (valid) {
if (valid) {
resolve(value);
} else {
reject(null);
}
})
.catch(function (ex) { return reject(ex); });
});
};
function pickFn(fn, variant) {
if ( variant === void 0 ) variant = 'simple';
return typeof fn === 'object' ? fn[variant] : fn;
}
function testAux(modifiers, fn, rule) {
if (modifiers.length) {
var modifier = modifiers.shift();
var nextFn = testAux(modifiers, fn, rule);
return modifier.perform(nextFn, rule);
} else {
return pickFn(fn);
}
}
function testAsyncAux(modifiers, fn, rule) {
if (modifiers.length) {
var modifier = modifiers.shift();
var nextFn = testAsyncAux(modifiers, fn, rule);
return modifier.performAsync(nextFn, rule);
} else {
return function (value) { return Promise.resolve(pickFn(fn, 'async')(value)); };
}
}
var Modifier = function Modifier(name, perform, performAsync) {
this.name = name;
this.perform = perform;
this.performAsync = performAsync;
};
var ValidationError = /*@__PURE__*/(function (Error) {
function ValidationError(rule, value, cause, target) {
var remaining = [], len = arguments.length - 4;
while ( len-- > 0 ) remaining[ len ] = arguments[ len + 4 ];
Error.call(this, remaining);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ValidationError);
}
this.rule = rule;
this.value = value;
this.cause = cause;
this.target = target;
}
if ( Error ) ValidationError.__proto__ = Error;
ValidationError.prototype = Object.create( Error && Error.prototype );
ValidationError.prototype.constructor = ValidationError;
return ValidationError;
}(Error));
var Context = function Context(chain, nextRuleModifiers) {
if ( chain === void 0 ) chain = [];
if ( nextRuleModifiers === void 0 ) nextRuleModifiers = [];
this.chain = chain;
this.nextRuleModifiers = nextRuleModifiers;
};
Context.prototype._applyRule = function _applyRule (ruleFn, name) {
var this$1 = this;
return function () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
this$1.chain.push(
new Rule(name, ruleFn.apply(this$1, args), args, this$1.nextRuleModifiers)
);
this$1.nextRuleModifiers = [];
return this$1;
};
};
Context.prototype._applyModifier = function _applyModifier (modifier, name) {
this.nextRuleModifiers.push(
new Modifier(name, modifier.simple, modifier.async)
);
return this;
};
Context.prototype._clone = function _clone () {
return new Context(this.chain.slice(), this.nextRuleModifiers.slice());
};
Context.prototype.test = function test (value) {
return this.chain.every(function (rule) { return rule._test(value); });
};
Context.prototype.testAll = function testAll (value) {
var err = [];
this.chain.forEach(function (rule) {
try {
rule._check(value);
} catch (ex) {
err.push(new ValidationError(rule, value, ex));
}
});
return err;
};
Context.prototype.check = function check (value) {
this.chain.forEach(function (rule) {
try {
rule._check(value);
} catch (ex) {
throw new ValidationError(rule, value, ex);
}
});
};
Context.prototype.testAsync = function testAsync (value) {
var this$1 = this;
return new Promise(function (resolve, reject) {
executeAsyncRules(value, this$1.chain.slice(), resolve, reject);
});
};
function executeAsyncRules(value, rules, resolve, reject) {
if (rules.length) {
var rule = rules.shift();
rule._testAsync(value).then(
function () {
executeAsyncRules(value, rules, resolve, reject);
},
function (cause) {
reject(new ValidationError(rule, value, cause));
}
);
} else {
resolve(value);
}
}
var consideredEmpty = function (value, considerTrimmedEmptyString) {
if (
considerTrimmedEmptyString &&
typeof value === 'string' &&
value.trim().length === 0
) {
return true;
}
return value === undefined || value === null;
};
function optional (validation, considerTrimmedEmptyString) {
if ( considerTrimmedEmptyString === void 0 ) considerTrimmedEmptyString = false;
return ({
simple: function (value) { return consideredEmpty(value, considerTrimmedEmptyString) ||
validation.check(value) === undefined; },
async: function (value) { return consideredEmpty(value, considerTrimmedEmptyString) ||
validation.testAsync(value); },
});
}
function v8n() {
return typeof Proxy !== 'undefined'
? proxyContext(new Context())
: proxylessContext(new Context());
}
// Custom rules
var customRules = {};
v8n.extend = function(newRules) {
Object.assign(customRules, newRules);
};
v8n.clearCustomRules = function() {
customRules = {};
};
function proxyContext(context) {
return new Proxy(context, {
get: function get(obj, prop) {
if (prop in obj) {
return obj[prop];
}
var newContext = proxyContext(context._clone());
if (prop in availableModifiers) {
return newContext._applyModifier(availableModifiers[prop], prop);
}
if (prop in customRules) {
return newContext._applyRule(customRules[prop], prop);
}
if (prop in availableRules) {
return newContext._applyRule(availableRules[prop], prop);
}
},
});
}
function proxylessContext(context) {
var addRuleSet = function (ruleSet, targetContext) {
Object.keys(ruleSet).forEach(function (prop) {
targetContext[prop] = function () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var newContext = proxylessContext(targetContext._clone());
var contextWithRuleApplied = newContext._applyRule(
ruleSet[prop],
prop
).apply(void 0, args);
return contextWithRuleApplied;
};
});
return targetContext;
};
var contextWithAvailableRules = addRuleSet(availableRules, context);
var contextWithAllRules = addRuleSet(
customRules,
contextWithAvailableRules
);
Object.keys(availableModifiers).forEach(function (prop) {
Object.defineProperty(contextWithAllRules, prop, {
get: function () {
var newContext = proxylessContext(contextWithAllRules._clone());
return newContext._applyModifier(availableModifiers[prop], prop);
},
});
});
return contextWithAllRules;
}
var availableModifiers = {
not: {
simple: function (fn) { return function (value) { return !fn(value); }; },
async: function (fn) { return function (value) { return Promise.resolve(fn(value))
.then(function (result) { return !result; })
.catch(function () { return true; }); }; },
},
some: {
simple: function (fn) { return function (value) {
return split(value).some(function (item) {
try {
return fn(item);
} catch (ex) {
return false;
}
});
}; },
async: function (fn) { return function (value) {
return Promise.all(
split(value).map(function (item) {
try {
return fn(item).catch(function () { return false; });
} catch (ex) {
return false;
}
})
).then(function (result) { return result.some(Boolean); });
}; },
},
every: {
simple: function (fn) { return function (value) { return value !== false && split(value).every(fn); }; },
async: function (fn) { return function (value) { return Promise.all(split(value).map(fn)).then(function (result) { return result.every(Boolean); }); }; },
},
strict: {
simple: function (fn, rule) { return function (value) {
if (isSchemaRule(rule) && value && typeof value === 'object') {
return (
Object.keys(rule.args[0]).length === Object.keys(value).length &&
fn(value)
);
}
return fn(value);
}; },
async: function (fn, rule) { return function (value) { return Promise.resolve(fn(value))
.then(function (result) {
if (isSchemaRule(rule) && value && typeof value === 'object') {
return (
Object.keys(rule.args[0]).length === Object.keys(value).length &&
result
);
}
return result;
})
.catch(function () { return false; }); }; },
},
};
function isSchemaRule(rule) {
return (
rule &&
rule.name === 'schema' &&
rule.args.length > 0 &&
typeof rule.args[0] === 'object'
);
}
function split(value) {
if (typeof value === 'string') {
return value.split('');
}
return value;
}
var availableRules = {
// Value
equal: function (expected) { return function (value) { return value == expected; }; },
exact: function (expected) { return function (value) { return value === expected; }; },
// Types
number: function (allowInfinite) {
if ( allowInfinite === void 0 ) allowInfinite = true;
return function (value) { return typeof value === 'number' && (allowInfinite || isFinite(value)); };
},
integer: function () { return function (value) {
var isInteger = Number.isInteger || isIntegerPolyfill;
return isInteger(value);
}; },
numeric: function () { return function (value) { return !isNaN(parseFloat(value)) && isFinite(value); }; },
string: function () { return testType('string'); },
boolean: function () { return testType('boolean'); },
undefined: function () { return testType('undefined'); },
null: function () { return testType('null'); },
array: function () { return testType('array'); },
object: function () { return testType('object'); },
instanceOf: function (instance) { return function (value) { return value instanceof instance; }; },
// Pattern
pattern: function (expected) { return function (value) { return expected.test(value); }; },
lowercase: function () { return function (value) {
return (
typeof value === 'boolean' ||
(value === value.toLowerCase() && value.trim() !== '')
);
}; },
uppercase: function () { return function (value) { return value === value.toUpperCase() && value.trim() !== ''; }; },
vowel: function () { return function (value) { return /^[aeiou]+$/i.test(value); }; },
consonant: function () { return function (value) { return /^(?=[^aeiou])([a-z]+)$/i.test(value); }; },
// Value at
first: function (expected) { return function (value) { return value[0] == expected; }; },
last: function (expected) { return function (value) { return value[value.length - 1] == expected; }; },
// Length
empty: function () { return function (value) { return value.length === 0; }; },
length: function (min, max) { return function (value) { return value.length >= min && value.length <= (max || min); }; },
minLength: function (min) { return function (value) { return value.length >= min; }; },
maxLength: function (max) { return function (value) { return value.length <= max; }; },
// Range
negative: function () { return function (value) { return value < 0; }; },
positive: function () { return function (value) { return value >= 0; }; },
between: function (a, b) { return function (value) { return value >= a && value <= b; }; },
range: function (a, b) { return function (value) { return value >= a && value <= b; }; },
lessThan: function (n) { return function (value) { return value < n; }; },
lessThanOrEqual: function (n) { return function (value) { return value <= n; }; },
greaterThan: function (n) { return function (value) { return value > n; }; },
greaterThanOrEqual: function (n) { return function (value) { return value >= n; }; },
// Divisible
even: function () { return function (value) { return value % 2 === 0; }; },
odd: function () { return function (value) { return value % 2 !== 0; }; },
includes: function (expected) { return function (value) { return ~value.indexOf(expected); }; },
schema: function (schema) { return testSchema(schema); },
// branching
passesAnyOf: function () {
var validations = [], len = arguments.length;
while ( len-- ) validations[ len ] = arguments[ len ];
return function (value) { return validations.some(function (validation) { return validation.test(value); }); };
},
optional: optional,
};
function testType(expected) {
return function (value) {
return (
(Array.isArray(value) && expected === 'array') ||
(value === null && expected === 'null') ||
typeof value === expected
);
};
}
function isIntegerPolyfill(value) {
return (
typeof value === 'number' && isFinite(value) && Math.floor(value) === value
);
}
function testSchema(schema) {
return {
simple: function (value) {
var causes = [];
Object.keys(schema).forEach(function (key) {
var nestedValidation = schema[key];
try {
nestedValidation.check((value || {})[key]);
} catch (ex) {
ex.target = key;
causes.push(ex);
}
});
if (causes.length > 0) {
throw causes;
}
return true;
},
async: function (value) {
var causes = [];
var nested = Object.keys(schema).map(function (key) {
var nestedValidation = schema[key];
return nestedValidation.testAsync((value || {})[key]).catch(function (ex) {
ex.target = key;
causes.push(ex);
});
});
return Promise.all(nested).then(function () {
if (causes.length > 0) {
throw causes;
}
return true;
});
},
};
}
return v8n;
}());