Repository: icebob/fastest-validator
Pull request #332 (2023-07-14 06:17:16)

improving the custom function with arrays of elements
I have enhanced the custom handling functionality to accommodate the reception of an array containing anonymous functions or function names defined in the global custom functions. 

please refer to the detailed description provided in issue #328.

I require assistance in updating the documentation!


Patch:
diff --git a/README.md b/README.md
index 4f1efdd..7c2a38a 100644
--- a/README.md
+++ b/README.md
@@ -1339,6 +1339,86 @@ console.log(check({ name: "John", phone: "36-70-123-4567" }));
 
 >Please note: the custom function must return the `value`. It means you can also sanitize it.
 
+### Chaining custom functions and global definitions
+You can define the `custom` property as an array of functions, allowing you to chain various validation logics. 
+
+Additionally, you can define custom functions globally, making them reusable.
+```js
+
+let v = new Validator({
+	debug: true,
+	useNewCustomCheckerFunction: true,
+	messages: {
+		// Register our new error message text
+		evenNumber: "The '{field}' field must be an even number! Actual: {actual}",
+		realNumber: "The '{field}' field must be a real number! Actual: {actual}",
+		notPermitNumber: "The '{field}'  cannot have the value  {actual}",
+        compareGt: "The '{field}' field must be greater than {gt}! Actual: {actual}",
+        compareGte: "The '{field}' field must be greater than or equal to {gte}! Actual: {actual}",
+        compareLt: "The '{field}' field must be less than {lt}! Actual: {actual}",
+        compareLte: "The '{field}' field must be less than or equal to {lte}! Actual: {actual}"
+	},
+	customFunctions:{
+		even: (value, errors)=>{
+			if(value % 2 != 0 ){
+				errors.push({ type: "evenNumber",  actual: value });
+			}
+			return value;
+		},
+		real: (value, errors)=>{
+			if(value <0 ){
+				errors.push({ type: "realNumber",  actual: value });
+			}
+			return value;
+		},
+        compare: (value, errors, schema)=>{
+				if( typeof schema.custom.gt==="number" && value <= schema.custom.gt ){
+					errors.push({ type: "compareGt",  actual: value, gt: schema.custom.gt });
+				}
+				if( typeof schema.custom.gte==="number" && value < schema.custom.gte ){
+					errors.push({ type: "compareGte",  actual: value, gte: schema.custom.gte });
+				}
+				if( typeof schema.custom.lt==="number" && value >= schema.custom.lt ){
+					errors.push({ type: "compareLt",  actual: value, lt: schema.custom.lt });
+				}
+				if( typeof schema.custom.lte==="number" && value > schema.custom.lte ){
+					errors.push({ type: "compareLte",  actual: value, lte: schema.custom.lte });
+				}
+				return value;
+			}
+	}
+});
+
+
+
+const schema = {
+	people:{
+		type: "number",
+		custom: [
+            "compare|gte:-100|lt:200",  // extended definition with additional parameters - equal to: {type:"compare",gte:-100, lt:200}, 
+			"even",
+			"real",
+			function (value, errors){
+				if(value === "3" ){
+					errors.push({ type: "notPermitNumber",  actual: value });
+				}
+				return value;
+			}
+		]
+	}
+};
+
+console.log(v.validate({people:-200}, schema));
+console.log(v.validate({people:200}, schema));
+console.log(v.validate({people:5}, schema));
+console.log(v.validate({people:-5}, schema));
+console.log(v.validate({people:3}, schema));
+
+```
+
+
+
+
 ## Asynchronous custom validations
 You can also use async custom validators. This can be useful if you need to check something in a database or in a remote location.
 In this case you should use `async/await` keywords, or return a `Promise` in the custom validator functions.
diff --git a/examples/custom-functions.js b/examples/custom-functions.js
new file mode 100644
index 0000000..2419e24
--- /dev/null
+++ b/examples/custom-functions.js
@@ -0,0 +1,49 @@
+let Validator = require("../index");
+
+
+let v = new Validator({
+	debug: true,
+	useNewCustomCheckerFunction: true,
+	messages: {
+		// Register our new error message text
+		evenNumber: "The '{field}' field must be an even number! Actual: {actual}",
+		realNumber: "The '{field}' field must be a real number! Actual: {actual}",
+		notPermitNumber: "The '{field}' cannot have the value {actual}",
+	},
+	customFunctions:{
+		even: (value, errors)=>{
+			if(value % 2 != 0 ){
+				errors.push({ type: "evenNumber",  actual: value });
+			}
+			return value;
+		},
+		real: (value, errors)=>{
+			if(value <0 ){
+				errors.push({ type: "realNumber",  actual: value });
+			}
+			return value;
+		}
+	}
+});
+
+
+
+const schema = {
+	people:{
+		type: "number",
+		custom: [
+			"even",
+			"real",
+			function (value, errors){
+				if(value === "3" ){
+					errors.push({ type: "notPermitNumber",  actual: value });
+				}
+				return value;
+			}
+		]
+	}
+};
+
+console.log(v.validate({people:5}, schema));
+console.log(v.validate({people:-5}, schema));
+console.log(v.validate({people:3}, schema));
diff --git a/lib/validator.js b/lib/validator.js
index 1295326..9eaa8c0 100644
--- a/lib/validator.js
+++ b/lib/validator.js
@@ -57,6 +57,7 @@ class Validator {
 		this.rules = loadRules();
 		this.aliases = {};
 		this.cache = new Map();
+		this.customFunctions = {};
 
 		if (opts) {
 			deepExtend(this.opts, opts);
@@ -74,6 +75,10 @@ class Validator {
 				for (const ruleName in opts.customRules) this.add(ruleName, opts.customRules[ruleName]);
 			}
 
+			if (opts.customFunctions) {
+				for (const customName in opts.customFunctions) this.addCustomFunction(customName, opts.customFunctions[customName]);
+			}
+
 			if (opts.plugins) {
 				const plugins = opts.plugins;
 				if (!Array.isArray(plugins)) throw new Error("Plugins type must be array");
@@ -204,6 +209,7 @@ class Validator {
 			rules: [],
 			fn: [],
 			customs: {},
+			customFunctions : this.customFunctions,
 			utils: {
 				replace,
 			},
@@ -439,32 +445,73 @@ class Validator {
 	makeCustomValidator({ vName = "value", fnName = "custom", ruleIndex, path, schema, context, messages }) {
 		const ruleVName = "rule" + ruleIndex;
 		const fnCustomErrorsVName = "fnCustomErrors" + ruleIndex;
-		if (typeof schema[fnName] == "function") {
+
+		if (typeof schema[fnName] == "function" || (Array.isArray(schema[fnName]))) {
 			if (context.customs[ruleIndex]) {
 				context.customs[ruleIndex].messages = messages;
 				context.customs[ruleIndex].schema = schema;
+			} else {
+				context.customs[ruleIndex] = { messages, schema };
 			}
-			else context.customs[ruleIndex] = { messages, schema };
-
+			const ret = [];
 			if (this.opts.useNewCustomCheckerFunction) {
-				return `
+				ret.push( `
                		const ${ruleVName} = context.customs[${ruleIndex}];
 					const ${fnCustomErrorsVName} = [];
+				`);
+
+				if(Array.isArray(schema[fnName])){
+					for (let i = 0; i < schema[fnName].length; i++) {
+
+						let custom = schema[fnName][i];
+
+						if (typeof custom === "string") {
+							custom = this.parseShortHand(custom);
+							schema[fnName][i] = custom;
+						}
+
+						const customIndex = ruleIndex*1000+i;
+						context.customs[customIndex] = { messages, schema: Object.assign({}, schema, { custom, index: i }) };
+
+						ret.push( `
+							const ${ruleVName}_${i} = context.customs[${customIndex}];
+
+					 	`);
+
+						if(custom.type){
+							ret.push( `
+							 ${vName} = ${context.async ? "await " : ""}context.customFunctions[${ruleVName}.schema.${fnName}[${i}].type].call(this, ${vName}, ${fnCustomErrorsVName} , ${ruleVName}_${i}.schema, "${path}", parent, context);
+							`);
+						}
+						if(typeof custom==="function"){
+							ret.push( `
+							${vName} = ${context.async ? "await " : ""}${ruleVName}.schema.${fnName}[${i}].call(this, ${vName}, ${fnCustomErrorsVName} , ${ruleVName}.schema, "${path}", parent, context);
+							`);
+						}
+					}
+				}else{
+					ret.push( `
 					${vName} = ${context.async ? "await " : ""}${ruleVName}.schema.${fnName}.call(this, ${vName}, ${fnCustomErrorsVName} , ${ruleVName}.schema, "${path}", parent, context);
+					`);
+				}
+
+				ret.push( `
 					if (Array.isArray(${fnCustomErrorsVName} )) {
                   		${fnCustomErrorsVName} .forEach(err => errors.push(Object.assign({ message: ${ruleVName}.messages[err.type], field }, err)));
 					}
-				`;
+				`);
+			}else{
+				const result = "res_" + ruleVName;
+				ret.push( `
+					const ${ruleVName} = context.customs[${ruleIndex}];
+					const ${result} = ${context.async ? "await " : ""}${ruleVName}.schema.${fnName}.call(this, ${vName}, ${ruleVName}.schema, "${path}", parent, context);
+					if (Array.isArray(${result})) {
+						${result}.forEach(err => errors.push(Object.assign({ message: ${ruleVName}.messages[err.type], field }, err)));
+					}
+			`);
 			}
+			return ret.join("\n");
 
-			const result = "res_" + ruleVName;
-			return `
-				const ${ruleVName} = context.customs[${ruleIndex}];
-				const ${result} = ${context.async ? "await " : ""}${ruleVName}.schema.${fnName}.call(this, ${vName}, ${ruleVName}.schema, "${path}", parent, context);
-				if (Array.isArray(${result})) {
-					${result}.forEach(err => errors.push(Object.assign({ message: ${ruleVName}.messages[err.type], field }, err)));
-				}
-		`;
 		}
 		return "";
 	}
@@ -479,6 +526,16 @@ class Validator {
 		this.rules[type] = fn;
 	}
 
+	/**
+	 * Add a custom function
+	 *
+	 * @param {String} type
+	 * @param {Function} fn
+	 */
+	addCustomFunction(name, fn) {
+		this.customFunctions[name] = fn;
+	}
+
 	/**
 	 * Add a message
 	 *


Test patch:
diff --git a/test/validator.spec.js b/test/validator.spec.js
index c6ed2b1..e61948e 100644
--- a/test/validator.spec.js
+++ b/test/validator.spec.js
@@ -642,6 +642,91 @@ describe("Test custom validation", () => {
 	});
 });
 
+
+describe("Test custom validation with array", () => {
+
+	const v = new Validator({
+		useNewCustomCheckerFunction: true,
+		customFunctions:{
+			even: (value, errors)=>{
+				if(value % 2 != 0 ){
+					errors.push({ type: "evenNumber",  actual: value });
+				}
+				return value;
+			},
+			real: (value, errors)=>{
+				if(value <0 ){
+					errors.push({ type: "realNumber",  actual: value });
+				}
+				return value;
+			},
+			compare: (value, errors, schema)=>{
+				if( typeof schema.custom.gt==="number" && value <= schema.custom.gt ){
+					errors.push({ type: "compareGt",  actual: value, gt: schema.custom.gt });
+				}
+				if( typeof schema.custom.gte==="number" && value < schema.custom.gte ){
+					errors.push({ type: "compareGte",  actual: value, gte: schema.custom.gte });
+				}
+				if( typeof schema.custom.lt==="number" && value >= schema.custom.lt ){
+					errors.push({ type: "compareLt",  actual: value, lt: schema.custom.lt });
+				}
+				if( typeof schema.custom.lte==="number" && value > schema.custom.lte ){
+					errors.push({ type: "compareLte",  actual: value, lte: schema.custom.lte });
+				}
+				return value;
+			}
+		},
+		messages: {
+			evenNumber: "The '{field}' field must be an even number! Actual: {actual}",
+			realNumber: "The '{field}' field must be a real number! Actual: {actual}",
+			permitNumber: "The '{field}' cannot have the value {actual}",
+			compareGt: "The '{field}' field must be greater than {gt}! Actual: {actual}",
+			compareGte: "The '{field}' field must be greater than or equal to {gte}! Actual: {actual}",
+			compareLt: "The '{field}' field must be less than {lt}! Actual: {actual}",
+			compareLte: "The '{field}' field must be less than or equal to {lte}! Actual: {actual}"
+		}
+	});
+
+	let check;
+
+	it("should compile without error", () => {
+
+		check = v.compile({
+			num: {
+				type: "number",
+				custom: [
+					"compare|gte:-100|lt:200",  // equal to: {type:"compare",gte:-100, lt:200}, 
+					"even",
+					"real",
+					(value, errors) => {
+						if ([-3,2,4,198].includes(value) ) errors.push({ type: "permitNumber", actual: value });
+						return value;
+					}
+
+				]
+			}
+		});
+
+		expect(typeof check).toBe("function");
+	});
+
+	it("should work correctly with array custom validator", () => {
+		expect(check({ num: 12 })).toBe(true);
+		expect(check({ num: 0 })).toBe(true);
+		expect(check({ num: 196 })).toBe(true);
+		expect(check({ num: 3 })[0].type).toEqual("evenNumber");
+		expect(check({ num: -12 })[0].type).toEqual("realNumber");
+		expect(check({ num: -8 })[0].type).toEqual("realNumber");
+		expect(check({ num: 198 })[0].type).toEqual("permitNumber");
+		expect(check({ num: 4 })[0].type).toEqual("permitNumber");
+		expect(check({ num: 202 })[0].type).toEqual("compareLt");
+		expect(check({ num: -3 }).map(e=>e.type)).toEqual(["evenNumber","realNumber","permitNumber"]);
+	});
+
+	
+});
+
+
 describe("Test default values", () => {
 	const v = new Validator({
 		useNewCustomCheckerFunction: true