Repository: icebob/fastest-validator
Pull request #119 (2020-03-16 19:07:36)

Custom validation for built-in rules
Related to #100 
Customizing built-in rules, without creating new rule. 
For example:
```js
check = v.compile({
   num: {
      type: "number",
      min: 10,
      max: 15,
      integer: true,
      custom (value, schema) {
         if (value % 2 !== 0) return [{ type: "evenNumber", actual: value }];
      }
   }
});
```


Patch:
diff --git a/examples/full.js b/examples/full.js
index 72296f8..187c817 100644
--- a/examples/full.js
+++ b/examples/full.js
@@ -37,6 +37,7 @@ const schema = {
 	apikey: "forbidden",
 	uuidv4: { type: "uuid", version: 4 },
 	uuid: "uuid",
+	phone: { type: "string", length: 15, custom: v => v.startsWith("+") ? true : [{ type: "wrongPhoneFormat" }] },
 	action: "function",
 	created: "date",
 	now: { type: "date", convert: true }
@@ -86,6 +87,7 @@ const obj = {
 	apikey: null,
 	uuidv4: "10ba038e-48da-487b-96e8-8d3b99b6d18a",
 	uuid:   "10ba038e-48da-487b-96e8-8d3b99b6d18a",
+	phone: "+36-70-123-4567",
 	action: () => {},
 	created: new Date(),
 	now: Date.now()
diff --git a/index.d.ts b/index.d.ts
index fb1d3f4..786d622 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -442,6 +442,14 @@ declare module 'fastest-validator' {
 		 */
 		default?: any;
 
+		/**
+		 *
+		 * @param {{any}} value Value that should be validated
+		 * @param {ValidationRuleObject} schema Validation schema that describes current custom validator
+		 * @return {{true} | ValidationError[]} true if result is valid or array of validation error messages
+		 */
+		custom?: (value: unknown, schema: ValidationRuleObject, path: string, parent?: object, context?: any) => true | ValidationError[];
+
 		/**
 		 * You can define any additional options for custom validators
 		 */
@@ -772,7 +780,7 @@ declare module 'fastest-validator' {
 	 	* @param {string} name
 	 	* @param validationRule
 	 	*/
-		 alias(name: string, validationRule: ValidationRule): void;
+		alias(name: string, validationRule: ValidationRule): void;
 
 		/**
 		 * Build error message
diff --git a/lib/rules/array.js b/lib/rules/array.js
index 04da100..fdab681 100644
--- a/lib/rules/array.js
+++ b/lib/rules/array.js
@@ -2,7 +2,7 @@
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path, context) {
+module.exports = function({ schema, messages, customValidation }, path, context) {
 	const src = [];
 
 	src.push(`
@@ -99,6 +99,7 @@ module.exports = function({ schema, messages }, path, context) {
 	}
 
 	src.push(`
+		${customValidation("value")}
 		return value;
 	`);
 
diff --git a/lib/rules/boolean.js b/lib/rules/boolean.js
index 03b9504..5e59bc5 100644
--- a/lib/rules/boolean.js
+++ b/lib/rules/boolean.js
@@ -2,7 +2,7 @@
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path) {
+module.exports = function({ schema, messages, customValidation }, path) {
 	const src = [];
 	let sanitized = false;
 
@@ -37,6 +37,8 @@ module.exports = function({ schema, messages }, path) {
 		if (typeof value !== "boolean")
 			${this.makeError({ type: "boolean",  actual: "origValue", messages })}
 
+		${customValidation("value")}
+
 		return value;
 	`);
 
diff --git a/lib/rules/date.js b/lib/rules/date.js
index 3ff3571..f90c19a 100644
--- a/lib/rules/date.js
+++ b/lib/rules/date.js
@@ -2,7 +2,7 @@
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path) {
+module.exports = function({ schema, messages, customValidation }, path) {
 	const src = [];
 	let sanitized = false;
 
@@ -23,6 +23,8 @@ module.exports = function({ schema, messages }, path) {
 		if (!(value instanceof Date) || isNaN(value.getTime()))
 			${this.makeError({ type: "date",  actual: "origValue", messages })}
 
+		${customValidation("value")}
+
 		return value;
 	`);
 
diff --git a/lib/rules/email.js b/lib/rules/email.js
index 6395f8d..cc8b563 100644
--- a/lib/rules/email.js
+++ b/lib/rules/email.js
@@ -5,7 +5,7 @@ const BASIC_PATTERN = /^\S+@\S+\.\S+$/;
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path) {
+module.exports = function({ schema, messages, customValidation }, path) {
 	const src = [];
 
 	const pattern = schema.mode == "precise" ? PRECISE_PATTERN : BASIC_PATTERN;
@@ -29,6 +29,8 @@ module.exports = function({ schema, messages }, path) {
 		if (!${pattern.toString()}.test(value))
 			${this.makeError({ type: "email",  actual: "value", messages })}
 
+		${customValidation("value")}
+
 		return value;
 	`);
 
diff --git a/lib/rules/enum.js b/lib/rules/enum.js
index f0a3e39..5042a33 100644
--- a/lib/rules/enum.js
+++ b/lib/rules/enum.js
@@ -2,12 +2,14 @@
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path) {
+module.exports = function({ schema, messages, customValidation }, path) {
 	const enumStr = JSON.stringify(schema.values || []);
 	return {
 		source: `
 			if (${enumStr}.indexOf(value) === -1)
 				${this.makeError({ type: "enumValue", expected: "\"" + schema.values.join(", ") + "\"", actual: "value", messages })}
+			
+			${customValidation("value")}
 
 			return value;
 		`
diff --git a/lib/rules/equal.js b/lib/rules/equal.js
index 6377d8d..bb4413f 100644
--- a/lib/rules/equal.js
+++ b/lib/rules/equal.js
@@ -2,7 +2,7 @@
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path) {
+module.exports = function({ schema, messages, customValidation }, path) {
 	const src = [];
 
 	if (schema.field) {
@@ -34,6 +34,7 @@ module.exports = function({ schema, messages }, path) {
 	}
 
 	src.push(`
+		${customValidation("value")}
 		return value;
 	`);
 
diff --git a/lib/rules/forbidden.js b/lib/rules/forbidden.js
index c9c376c..d7421c0 100644
--- a/lib/rules/forbidden.js
+++ b/lib/rules/forbidden.js
@@ -2,7 +2,7 @@
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function checkForbidden({ schema, messages }, path) {
+module.exports = function checkForbidden({ schema, messages, customValidation }, path) {
 	const src = [];
 
 	src.push(`
@@ -22,6 +22,9 @@ module.exports = function checkForbidden({ schema, messages }, path) {
 
 	src.push(`
 		}
+
+		${customValidation("value")}
+
 		return value;
 	`);
 
diff --git a/lib/rules/function.js b/lib/rules/function.js
index 997fc73..dee8c9a 100644
--- a/lib/rules/function.js
+++ b/lib/rules/function.js
@@ -2,12 +2,14 @@
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path) {
+module.exports = function({ schema, messages, customValidation }, path) {
 	return {
 		source: `
 			if (typeof value !== "function")
 				${this.makeError({ type: "function",  actual: "value", messages })}
 
+			${customValidation("value")}
+
 			return value;
 		`
 	};
diff --git a/lib/rules/luhn.js b/lib/rules/luhn.js
index 6c443a4..049446a 100644
--- a/lib/rules/luhn.js
+++ b/lib/rules/luhn.js
@@ -9,7 +9,7 @@
  *
  *	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path) {
+module.exports = function({ schema, messages, customValidation }, path) {
 	return {
 		source: `
 			if (typeof value !== "string") {
@@ -34,6 +34,8 @@ module.exports = function({ schema, messages }, path) {
 				${this.makeError({ type: "luhn",  actual: "value", messages })}
 			}
 
+			${customValidation("value")}
+
 			return value;
 		`
 	};
diff --git a/lib/rules/mac.js b/lib/rules/mac.js
index abd5c64..e156b0d 100644
--- a/lib/rules/mac.js
+++ b/lib/rules/mac.js
@@ -4,7 +4,7 @@ const PATTERN = /^((([a-f0-9][a-f0-9]+[-]){5}|([a-f0-9][a-f0-9]+[:]){5})([a-f0-9
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path) {
+module.exports = function({ schema, messages, customValidation }, path) {
 	return {
 		source: `
 			if (typeof value !== "string") {
@@ -16,7 +16,7 @@ module.exports = function({ schema, messages }, path) {
 			if (!${PATTERN.toString()}.test(v)) {
 				${this.makeError({ type: "mac",  actual: "value", messages })}
 			}
-
+			${customValidation("value")}
 			return value;
 		`
 	};
diff --git a/lib/rules/multi.js b/lib/rules/multi.js
index e5a901a..dc56164 100644
--- a/lib/rules/multi.js
+++ b/lib/rules/multi.js
@@ -2,7 +2,7 @@
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path, context) {
+module.exports = function({ schema, messages, customValidation }, path, context) {
 	const src = [];
 
 	src.push(`
@@ -33,7 +33,7 @@ module.exports = function({ schema, messages }, path, context) {
 		if (hasValid) {
 			errors.length = prevErrLen;
 		}
-
+		${customValidation("newVal")}
 		return newVal;
 	`);
 
diff --git a/lib/rules/number.js b/lib/rules/number.js
index a676888..c5da372 100644
--- a/lib/rules/number.js
+++ b/lib/rules/number.js
@@ -2,7 +2,7 @@
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path) {
+module.exports = function({ schema, messages, customValidation }, path) {
 	const src = [];
 
 	src.push(`
@@ -88,6 +88,7 @@ module.exports = function({ schema, messages }, path) {
 	}
 
 	src.push(`
+		${customValidation("value")}
 		return value;
 	`);
 
diff --git a/lib/rules/object.js b/lib/rules/object.js
index 764f697..f635636 100644
--- a/lib/rules/object.js
+++ b/lib/rules/object.js
@@ -31,7 +31,7 @@ function escapeEvalString(str) {
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path, context) {
+module.exports = function({ schema, messages, customValidation }, path, context) {
 	const sourceCode = [];
 
 	sourceCode.push(`
@@ -101,6 +101,7 @@ module.exports = function({ schema, messages }, path, context) {
 		`);
 	} else {
 		sourceCode.push(`
+			${customValidation("value")}
 			return value;
 		`);
 	}
diff --git a/lib/rules/string.js b/lib/rules/string.js
index c15a1d9..40478d7 100644
--- a/lib/rules/string.js
+++ b/lib/rules/string.js
@@ -7,7 +7,7 @@ const ALPHADASH_PATTERN = /^[a-zA-Z0-9_-]+$/;
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function checkString({ schema, messages }, path, context) {
+module.exports = function checkString({ schema, messages, customValidation }, path, context) {
 	const src = [];
 	let sanitized = false;
 
@@ -191,6 +191,7 @@ module.exports = function checkString({ schema, messages }, path, context) {
 	}
 
 	src.push(`
+		${customValidation("value")}
 		return value;
 	`);
 
diff --git a/lib/rules/url.js b/lib/rules/url.js
index cda7ab3..84800d6 100644
--- a/lib/rules/url.js
+++ b/lib/rules/url.js
@@ -6,7 +6,7 @@ const PATTERN = /^https?:\/\/\S+/;
 
 /**	Signature: function(value, field, parent, errors, context)
  */
-module.exports = function({ schema, messages }, path) {
+module.exports = function({ schema, messages, customValidation }, path) {
 	const src = [];
 	src.push(`
 		if (typeof value !== "string") {
@@ -18,6 +18,8 @@ module.exports = function({ schema, messages }, path) {
 			${this.makeError({ type: "url",  actual: "value", messages })}
 		}
 
+		${customValidation("value")}
+
 		return value;
 	`);
 
diff --git a/lib/validator.js b/lib/validator.js
index 3bfeded..0f0c8b3 100644
--- a/lib/validator.js
+++ b/lib/validator.js
@@ -219,6 +219,17 @@ class Validator {
 			this.cache.set(rule.schema, rule);
 			rule.index = context.index;
 			context.rules[context.index] = rule;
+			if (typeof rule.schema.custom === "function") {
+				context.customs[path] = { schema: rule.schema, messages: rule.messages };
+				rule.customValidation = value => `
+					const rule = context.customs["${path}"];
+					const res = rule.schema.custom.call(this, ${value}, rule.schema, "${path}", parent, context);
+					if (Array.isArray(res)) {
+						res.forEach(err => errors.push(Object.assign({ message: rule.messages[err.type], field }, err)));
+					}
+				`;
+			}
+
 			context.index++;
 			const res = rule.ruleFunction.call(this, rule, path, context);
 			if (res.source) {
@@ -296,7 +307,8 @@ class Validator {
 		const rule = {
 			messages: Object.assign({}, this.messages, schema.messages),
 			schema: schema,
-			ruleFunction: ruleFunction
+			ruleFunction: ruleFunction,
+			customValidation: () => ""
 		};
 
 		return rule;


Test patch:
diff --git a/test/validator.spec.js b/test/validator.spec.js
index 0be3919..d067692 100644
--- a/test/validator.spec.js
+++ b/test/validator.spec.js
@@ -365,4 +365,44 @@ describe("Test aliases", () => {
 	});
 });
 
+describe("Test custom validation for built-in rules", () => {
+	const v = new Validator({
+		messages: {
+			evenNumber: "The '{field}' field must be an even number! Actual: {actual}"
+		}
+	});
+
+	let check;
+	const fn = jest.fn();
+
+
+	it("should compile without error", () => {
+		
+		check = v.compile({
+			num: {
+				type: "number",
+				min: 10,
+				max: 15,
+				integer: true,
+				custom(value){
+					fn(value);
+					if (value % 2 !== 0) return [{ type: "evenNumber", actual: value }];
+				}
+			}
+		});
+
+		expect(typeof check).toBe("function");
+	});
+
+	it("should work correctly with custom validator", () => {
+		const res = check({num: 12});
+		expect(res).toBe(true);
+		expect(fn).toBeCalledWith(12);
+
+		expect(check({num: 8})[0].type).toEqual("numberMin");
+		expect(check({num: 18})[0].type).toEqual("numberMax");
+		expect(check({num: 13})[0].type).toEqual("evenNumber");
+	});
+});
+