UNPKG

n4s

Version:

Assertion library for form validations

428 lines (393 loc) 13.6 kB
'use strict'; var vestUtils = require('vest-utils'); var context = require('context'); const ctx = context.createCascade((ctxRef, parentContext) => { const base = { value: ctxRef.value, meta: ctxRef.meta || {}, }; if (!parentContext) { return vestUtils.assign(base, { parent: emptyParent, }); } else if (ctxRef.set) { return vestUtils.assign(base, { parent: () => stripContext(parentContext), }); } return parentContext; }); function stripContext(ctx) { return { value: ctx.value, meta: ctx.meta, parent: ctx.parent, }; } function emptyParent() { return null; } function endsWith(value, arg1) { return vestUtils.isStringValue(value) && vestUtils.isStringValue(arg1) && value.endsWith(arg1); } const doesNotEndWith = vestUtils.bindNot(endsWith); function equals(value, arg1) { return value === arg1; } const notEquals = vestUtils.bindNot(equals); function greaterThanOrEquals(value, gte) { return vestUtils.numberEquals(value, gte) || vestUtils.greaterThan(value, gte); } function inside(value, arg1) { if (vestUtils.isArray(arg1)) { return arg1.indexOf(value) !== -1; } // both value and arg1 are strings if (vestUtils.isStringValue(arg1) && vestUtils.isStringValue(value)) { return arg1.indexOf(value) !== -1; } return false; } const notInside = vestUtils.bindNot(inside); function lessThan(value, lt) { return vestUtils.isNumeric(value) && vestUtils.isNumeric(lt) && Number(value) < Number(lt); } function lessThanOrEquals(value, lte) { return vestUtils.numberEquals(value, lte) || lessThan(value, lte); } function isBetween(value, min, max) { return greaterThanOrEquals(value, min) && lessThanOrEquals(value, max); } const isNotBetween = vestUtils.bindNot(isBetween); function isBlank(value) { return vestUtils.isNullish(value) || (vestUtils.isStringValue(value) && !value.trim()); } const isNotBlank = vestUtils.bindNot(isBlank); const isNotBoolean = vestUtils.bindNot(vestUtils.isBoolean); /** * Validates that a given value is an even number */ const isEven = (value) => { if (vestUtils.isNumeric(value)) { return value % 2 === 0; } return false; }; function isKeyOf(key, obj) { return key in obj; } const isNotKeyOf = vestUtils.bindNot(isKeyOf); function isNaN(value) { return Number.isNaN(value); } const isNotNaN = vestUtils.bindNot(isNaN); function isNegative(value) { return lessThan(value, 0); } function isNumber(value) { return Boolean(typeof value === 'number'); } const isNotNumber = vestUtils.bindNot(isNumber); /** * Validates that a given value is an odd number */ const isOdd = (value) => { if (vestUtils.isNumeric(value)) { return value % 2 !== 0; } return false; }; const isNotString = vestUtils.bindNot(vestUtils.isStringValue); function isTruthy(value) { return !!value; } const isFalsy = vestUtils.bindNot(isTruthy); function isValueOf(value, objectToCheck) { if (vestUtils.isNullish(objectToCheck)) { return false; } for (const key in objectToCheck) { if (objectToCheck[key] === value) { return true; } } return false; } const isNotValueOf = vestUtils.bindNot(isValueOf); function longerThanOrEquals(value, arg1) { return greaterThanOrEquals(value.length, arg1); } function matches(value, regex) { if (regex instanceof RegExp) { return regex.test(value); } else if (vestUtils.isStringValue(regex)) { return new RegExp(regex).test(value); } return false; } const notMatches = vestUtils.bindNot(matches); function condition(value, callback) { try { return callback(value); } catch (_a) { return false; } } function shorterThan(value, arg1) { return lessThan(value.length, arg1); } function shorterThanOrEquals(value, arg1) { return lessThanOrEquals(value.length, arg1); } function startsWith(value, arg1) { return vestUtils.isStringValue(value) && vestUtils.isStringValue(arg1) && value.startsWith(arg1); } const doesNotStartWith = vestUtils.bindNot(startsWith); // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, max-lines-per-function function rules() { return { condition, doesNotEndWith, doesNotStartWith, endsWith, equals, greaterThan: vestUtils.greaterThan, greaterThanOrEquals, gt: vestUtils.greaterThan, gte: greaterThanOrEquals, inside, isArray: vestUtils.isArray, isBetween, isBlank, isBoolean: vestUtils.isBoolean, isEmpty: vestUtils.isEmpty, isEven, isFalsy, isKeyOf, isNaN, isNegative, isNotArray: vestUtils.isNotArray, isNotBetween, isNotBlank, isNotBoolean, isNotEmpty: vestUtils.isNotEmpty, isNotKeyOf, isNotNaN, isNotNull: vestUtils.isNotNull, isNotNullish: vestUtils.isNotNullish, isNotNumber, isNotNumeric: vestUtils.isNotNumeric, isNotString, isNotUndefined: vestUtils.isNotUndefined, isNotValueOf, isNull: vestUtils.isNull, isNullish: vestUtils.isNullish, isNumber, isNumeric: vestUtils.isNumeric, isOdd, isPositive: vestUtils.isPositive, isString: vestUtils.isStringValue, isTruthy, isUndefined: vestUtils.isUndefined, isValueOf, lengthEquals: vestUtils.lengthEquals, lengthNotEquals: vestUtils.lengthNotEquals, lessThan, lessThanOrEquals, longerThan: vestUtils.longerThan, longerThanOrEquals, lt: lessThan, lte: lessThanOrEquals, matches, notEquals, notInside, notMatches, numberEquals: vestUtils.numberEquals, numberNotEquals: vestUtils.numberNotEquals, shorterThan, shorterThanOrEquals, startsWith, }; } const baseRules = rules(); function getRule(ruleName) { return baseRules[ruleName]; } function ruleReturn(pass, message) { const output = { pass }; if (message) { output.message = message; } return output; } function passing() { return ruleReturn(true); } function defaultToPassing(callback) { return vestUtils.defaultTo(callback, passing()); } /** * Transform the result of a rule into a standard format */ function transformResult(result, ruleName, value, ...args) { validateResult(result); // if result is boolean if (vestUtils.isBoolean(result)) { return ruleReturn(result); } return ruleReturn(result.pass, vestUtils.optionalFunctionValue(result.message, ruleName, value, ...args)); } function validateResult(result) { // if result is boolean, or if result.pass is boolean vestUtils.invariant(vestUtils.isBoolean(result) || (result && vestUtils.isBoolean(result.pass)), 'Incorrect return value for rule: ' + JSON.stringify(result)); } // eslint-disable-next-line max-lines-per-function function enforceEager(value) { const target = { message, pass: false, }; let customMessage = undefined; // We create a proxy intercepting access to the target object (which is empty). const proxy = new Proxy(target, { get: (_, key) => { // On property access, we identify if it is a rule or not. const rule = getRule(key); // If it is a rule, we wrap it with `genRuleCall` that adds the base enforce behavior if (rule) { return genRuleCall(proxy, rule, key); } return target[key]; }, }); return proxy; // This function is used to wrap a rule with the base enforce behavior // It takes the target object, the rule function, and the rule name // It then returns the rule, in a manner that can be used by enforce function genRuleCall(target, rule, ruleName) { return function ruleCall(...args) { // Order of operation: // 1. Create a context with the value being enforced // 2. Call the rule within the context, and pass over the arguments passed to it // 3. Transform the result to the correct output format const transformedResult = ctx.run({ value }, () => { return transformResult(rule(value, ...args), ruleName, value, ...args); }); function enforceMessage() { if (!vestUtils.isNullish(customMessage)) return vestUtils.StringObject(customMessage); if (vestUtils.isNullish(transformedResult.message)) { return `enforce/${ruleName} failed with ${JSON.stringify(value)}`; } return vestUtils.StringObject(transformedResult.message); } // On rule failure (the result is false), we either throw an error // or throw a string value if the rule has a message defined in it. vestUtils.invariant(transformedResult.pass, enforceMessage()); // This is not really needed because it will always be true // As we're throwing an error on failure // but it is here so that users have a sense of what is happening // when they try to log the result of enforce and not just see a proxy object target.pass = transformedResult.pass; return target; }; } function message(input) { customMessage = input; return proxy; } } // eslint-disable-next-line max-lines-per-function function genEnforceLazy(key) { const registeredRules = []; let lazyMessage; return addLazyRule(key); // eslint-disable-next-line max-lines-per-function function addLazyRule(ruleName) { // eslint-disable-next-line max-lines-per-function return (...args) => { const rule = getRule(ruleName); registeredRules.push((value) => transformResult(rule(value, ...args), ruleName, value, ...args)); let proxy = { run: (value) => { return defaultToPassing(vestUtils.mapFirst(registeredRules, (rule, breakout) => { var _a; const res = ctx.run({ value }, () => rule(value)); breakout(!res.pass, ruleReturn(!!res.pass, (_a = vestUtils.optionalFunctionValue(lazyMessage, value, res.message)) !== null && _a !== void 0 ? _a : res.message)); })); }, test: (value) => proxy.run(value).pass, message: (message) => { if (message) { lazyMessage = message; } return proxy; }, }; // reassigning the proxy here is not pretty // but it's a cleaner way of getting `run` and `test` for free proxy = new Proxy(proxy, { get: (target, key) => { if (getRule(key)) { return addLazyRule(key); } return target[key]; // already has `run` and `test` on it }, }); return proxy; }; } } /** * Enforce is quite complicated, I want to explain it in detail. * It is dynamic in nature, so a lot of proxy objects are involved. * * Enforce has two main interfaces * 1. eager * 2. lazy * * The eager interface is the most commonly used, and the easier to understand. * It throws an error when a rule is not satisfied. * The eager interface is declared in enforceEager.ts and it is quite simple to understand. * enforce is called with a value, and the return value is a proxy object that points back to all the rules. * When a rule is called, the value is mapped as its first argument, and if the rule passes, the same * proxy object is returned. Otherwise, an error is thrown. * * The lazy interface works quite differently. It is declared in genEnforceLazy.ts. * Rather than calling enforce directly, the lazy interface has all the rules as "methods" (only by proxy). * Calling the first function in the chain will initialize an array of calls. It stores the different rule calls * and the parameters passed to them. None of the rules are called yet. * The rules are only invoked in sequence once either of these chained functions are called: * 1. test(value) * 2. run(value) * * Calling run or test will call all the rules in sequence, with the difference that test will only return a boolean value, * while run will return an object with the validation result and an optional message created by the rule. */ function genEnforce() { const target = { context: () => ctx.useX(), extend: (customRules) => { vestUtils.assign(baseRules, customRules); }, }; return new Proxy(vestUtils.assign(enforceEager, target), { get: (target, key) => { if (key in target) { return target[key]; } if (!getRule(key)) { return; } // Only on the first rule access - start the chain of calls return genEnforceLazy(key); }, }); } const enforce = genEnforce(); exports.ctx = ctx; exports.enforce = enforce; //# sourceMappingURL=n4s.development.js.map