UNPKG

v8n

Version:

Dead simple fluent JavaScript validation library

314 lines (246 loc) 7.24 kB
import Context from './Context'; import optional from './rules/optional'; function v8n() { return typeof Proxy !== 'undefined' ? proxyContext(new Context()) : proxylessContext(new Context()); } // Custom rules let customRules = {}; v8n.extend = function(newRules) { Object.assign(customRules, newRules); }; v8n.clearCustomRules = function() { customRules = {}; }; function proxyContext(context) { return new Proxy(context, { get(obj, prop) { if (prop in obj) { return obj[prop]; } const 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) { const addRuleSet = (ruleSet, targetContext) => { Object.keys(ruleSet).forEach(prop => { targetContext[prop] = (...args) => { const newContext = proxylessContext(targetContext._clone()); const contextWithRuleApplied = newContext._applyRule( ruleSet[prop], prop, )(...args); return contextWithRuleApplied; }; }); return targetContext; }; const contextWithAvailableRules = addRuleSet(availableRules, context); const contextWithAllRules = addRuleSet( customRules, contextWithAvailableRules, ); Object.keys(availableModifiers).forEach(prop => { Object.defineProperty(contextWithAllRules, prop, { get: () => { const newContext = proxylessContext(contextWithAllRules._clone()); return newContext._applyModifier(availableModifiers[prop], prop); }, }); }); return contextWithAllRules; } const availableModifiers = { not: { simple: fn => value => !fn(value), async: fn => value => Promise.resolve(fn(value)) .then(result => !result) .catch(() => true), }, some: { simple: fn => value => { return split(value).some(item => { try { return fn(item); } catch (ex) { return false; } }); }, async: fn => value => { return Promise.all( split(value).map(item => { try { return fn(item).catch(() => false); } catch (ex) { return false; } }), ).then(result => result.some(Boolean)); }, }, every: { simple: fn => value => value !== false && split(value).every(fn), async: fn => value => Promise.all(split(value).map(fn)).then(result => result.every(Boolean)), }, strict: { simple: (fn, rule) => 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: (fn, rule) => value => Promise.resolve(fn(value)) .then(result => { if (isSchemaRule(rule) && value && typeof value === 'object') { return ( Object.keys(rule.args[0]).length === Object.keys(value).length && result ); } return result; }) .catch(() => 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; } const availableRules = { // Value equal: expected => value => value == expected, exact: expected => value => value === expected, // Types number: (allowInfinite = true) => value => typeof value === 'number' && (allowInfinite || isFinite(value)), integer: () => value => { const isInteger = Number.isInteger || isIntegerPolyfill; return isInteger(value); }, numeric: () => value => !isNaN(parseFloat(value)) && isFinite(value), string: () => testType('string'), boolean: () => testType('boolean'), undefined: () => testType('undefined'), null: () => testType('null'), array: () => testType('array'), object: () => testType('object'), instanceOf: instance => value => value instanceof instance, // Pattern pattern: expected => value => expected.test(value), lowercase: () => value => { return ( typeof value === 'boolean' || (value === value.toLowerCase() && value.trim() !== '') ); }, uppercase: () => value => value === value.toUpperCase() && value.trim() !== '', vowel: () => value => /^[aeiou]+$/i.test(value), consonant: () => value => /^(?=[^aeiou])([a-z]+)$/i.test(value), // Value at first: expected => value => value[0] == expected, last: expected => value => value[value.length - 1] == expected, // Length empty: () => value => value.length === 0, length: (min, max) => value => value.length >= min && value.length <= (max || min), minLength: min => value => value.length >= min, maxLength: max => value => value.length <= max, // Range negative: () => value => value < 0, positive: () => value => value >= 0, between: (a, b) => value => value >= a && value <= b, range: (a, b) => value => value >= a && value <= b, lessThan: n => value => value < n, lessThanOrEqual: n => value => value <= n, greaterThan: n => value => value > n, greaterThanOrEqual: n => value => value >= n, // Divisible even: () => value => value % 2 === 0, odd: () => value => value % 2 !== 0, includes: expected => value => ~value.indexOf(expected), schema: schema => testSchema(schema), // branching passesAnyOf: (...validations) => value => validations.some(validation => validation.test(value)), optional, }; function testType(expected) { return 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: value => { const causes = []; Object.keys(schema).forEach(key => { const 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: value => { const causes = []; const nested = Object.keys(schema).map(key => { const nestedValidation = schema[key]; return nestedValidation.testAsync((value || {})[key]).catch(ex => { ex.target = key; causes.push(ex); }); }); return Promise.all(nested).then(() => { if (causes.length > 0) { throw causes; } return true; }); }, }; } export default v8n;