n4s
Version:
Assertion library for form validations
428 lines (393 loc) • 13.6 kB
JavaScript
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
;