Repository: icebob/fastest-validator
Pull request #135 (2020-05-07 20:53:47)

custom validation for arrays & update README.md, examples and benchmark


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 958a19e..94f5f3d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,67 @@
 --------------------------------------------------
+<a name="1.4.0"></a>
+# 1.4.0 (2020-05-08)
+
+## New `custom` function signature
+Thanks for [@erfanium](https://github.com/erfanium), in this version there is a new signature of custom check functions.
+In this new function you should always return the value. It means you can change the value, thus you can also sanitize the input value.
+
+**Old custom function:**
+```js
+const v = new Validator({});
+
+const schema = {
+    weight: {
+        type: "custom",
+        minWeight: 10,
+        check(value, schema) {
+            return (value < schema.minWeight)
+                ? [{ type: "weightMin", expected: schema.minWeight, actual: value }]
+                : true;
+        }
+    }
+};
+```
+
+**New custom function:**
+```js
+const v = new Validator({
+    useNewCustomCheckerFunction: true, // using new version
+});
+
+const schema = {
+    name: { type: "string", min: 3, max: 255 },
+    weight: {
+        type: "custom",
+        minWeight: 10,
+        check(value, errors, schema) {
+            if (value < minWeight) errors.push({ type: "weightMin", expected: schema.minWeight, actual: value });
+            if (value > 100) value = 100
+            return value
+        }
+    }
+};
+```
+
+>Please note: the old version will be removed in the version 2.0.0!
+
+The signature is used in `custom` function of built-in rules.
+
+```js
+const v = new Validator({
+    useNewCustomCheckerFunction: true // using new version
+});
+
+const schema = {
+    phone: { type: "string", length: 15, custom(v, errors) => {
+        if (!v.startWith("+")) errors.push({ type: "phoneNumber" })
+        return v.replace(/[^\d+]/g, ""); // Sanitize: remove all special chars except numbers
+    } }	
+};
+```
+
+## Changes
+--------------------------------------------------
 <a name="1.3.0"></a>
 # 1.3.0 (2020-04-29)
 
diff --git a/README.md b/README.md
index 2ceeead..1fdd487 100644
--- a/README.md
+++ b/README.md
@@ -885,7 +885,7 @@ Property | Default  | Description
 You can also create your custom validator.
 
 ```js
-let v = new Validator({
+const v = new Validator({
     messages: {
         // Register our new error message text
         evenNumber: "The '{field}' field must be an even number! Actual: {actual}"
@@ -926,7 +926,8 @@ console.log(v.validate({ name: "John", age: 19 }, schema));
 
 Or you can use the `custom` type with an inline checker function:
 ```js
-let v = new Validator({
+const v = new Validator({
+    useNewCustomCheckerFunction: true, // using new version
     messages: {
         // Register our new error message text
         weightMin: "The weight must be greater than {expected}! Actual: {actual}"
@@ -938,10 +939,10 @@ const schema = {
     weight: {
         type: "custom",
         minWeight: 10,
-        check(value, schema) {
-            return (value < schema.minWeight)
-                ? [{ type: "weightMin", expected: schema.minWeight, actual: value }]
-                : true;
+        check(value, errors, schema) {
+            if (value < minWeight) errors.push({ type: "weightMin", expected: schema.minWeight, actual: value });
+            if (value > 100) value = 100
+            return value
         }
     }
 };
@@ -959,11 +960,20 @@ console.log(v.validate({ name: "John", weight: 8 }, schema));
         message: 'The weight must be greater than 10! Actual: 8'
     }]
 */
+const o = { name: "John", weight: 110 }
+console.log(v.validate(o, schema));
+/* Returns: true
+   o.weight is 100
+*/
 ```
+>Please note: the custom function must return the `value`. It means you can also sanitize it.
+
+## Custom validation for built-in rules
+You can define a `custom` function in the schema for built-in rules. With it you can extend any built-in rules.
 
-Or you can use built-in validator with a `custom` checker function:
 ```js
-let v = new Validator({
+const v = new Validator({
+    useNewCustomCheckerFunction: true, // using new version
     messages: {
         // Register our new error message text
         phoneNumber: "The phone number must be started with '+'!"
@@ -972,8 +982,10 @@ let v = new Validator({
 
 const schema = {
     name: { type: "string", min: 3, max: 255 },
-    phone: { type: "string", length: 15, custom: v => 
-        v.startsWith("+") ? true : [{ type: "phoneNumber" }] 
+    phone: { type: "string", length: 15, custom(v, errors) => {
+            if (!v.startWith("+")) errors.push({ type: "phoneNumber" })
+            return v.replace(/[^\d+]/g, ""); // Sanitize: remove all special chars except numbers
+        }
     }	
 };
 
@@ -990,22 +1002,7 @@ console.log(v.validate({ name: "John", phone: "36-70-123-4567" }, schema));
 */
 ```
 
-## Custom validation for built-in rules
-You can define a `custom` function in the schema for built-in rules. With it you can extend any built-in rules.
-
-```js
-const schema = {
-    num: {
-        type: "number",
-        min: 10,
-        max: 15,
-        integer: true,
-        custom (value, schema) {
-            if (value % 2 !== 0) return [{ type: "evenNumber", actual: value }];
-        }
-    }
-}
-```
+>Please note: the custom function must return the `value`. It means you can also sanitize it.
 
 # Custom error messages (l10n)
 You can set your custom messages in the validator constructor.
diff --git a/benchmark/suites/email.js b/benchmark/suites/email.js
index 272dfce..0166ff2 100644
--- a/benchmark/suites/email.js
+++ b/benchmark/suites/email.js
@@ -43,4 +43,26 @@ const obj = {
 })();
 
 
-bench.run();
\ No newline at end of file
+bench.run();
+
+/*
+===============================
+  Fastest validator benchmark
+===============================
+
+Platform info:
+==============
+   Windows_NT 10.0.18363 x64
+   Node.JS: 12.14.1
+   V8: 7.7.299.13-node.16
+   Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz × 8
+
+Suite: Email validating methods
+√ mode: 'precise'         9,479,300 rps
+√ mode: 'basic'          10,351,349 rps
+
+   mode: 'precise'        -8.42%      (9,479,300 rps)   (avg: 105ns)
+   mode: 'basic'              0%     (10,351,349 rps)   (avg: 96ns)
+-----------------------------------------------------------------------
+
+*/
diff --git a/benchmark/suites/simple.js b/benchmark/suites/simple.js
index 29ce986..3038269 100644
--- a/benchmark/suites/simple.js
+++ b/benchmark/suites/simple.js
@@ -120,19 +120,19 @@ bench.run();
 
 Platform info:
 ==============
-   Windows_NT 6.1.7601 x64
-   Node.JS: 8.11.0
-   V8: 6.2.414.50
+   Windows_NT 10.0.18363 x64
+   Node.JS: 12.14.1
+   V8: 7.7.299.13-node.16
    Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz × 8
 
 Suite: Simple object
-√ compile & validate                        1,115,239 rps
-√ validate with pre-compiled schema         3,986,017 rps
-√ validate with wrong obj                     704,992 rps
+√ validate                            8,259,364 rps
+√ validate with sanitizations         6,096,325 rps
+√ validate with wrong obj             1,466,290 rps
 
-   compile & validate (#)                       0%      (1,115,239 rps)   (avg: 896ns)
-   validate with pre-compiled schema      +257.41%      (3,986,017 rps)   (avg: 250ns)
-   validate with wrong obj                 -36.79%        (704,992 rps)   (avg: 1μs)
+   validate                               0%      (8,259,364 rps)   (avg: 121ns)
+   validate with sanitizations       -26.19%      (6,096,325 rps)   (avg: 164ns)
+   validate with wrong obj           -82.25%      (1,466,290 rps)   (avg: 681ns)
 -----------------------------------------------------------------------
 
 */
diff --git a/examples/custom.js b/examples/custom.js
index b5dcf46..39932b7 100644
--- a/examples/custom.js
+++ b/examples/custom.js
@@ -2,6 +2,7 @@ let Validator = require("../index");
 
 let v = new Validator({
 	messages: {
+		useNewCustomCheckerFunction: true,
 		// Register our new error message text
 		evenNumber: "The '{field}' field must be an even number! Actual: {actual}",
 	}
@@ -29,10 +30,9 @@ const schema = {
 			// Register our new error message text
 			weightMin: "The weight must be greater than {expected}! Actual: {actual}"
 		},
-		check(value, schema) {
-			return (value < schema.minWeight)
-				? [{ type: "weightMin", expected: schema.minWeight, actual: value }]
-				: true;
+		check(value, errors, schema) {
+			if (value < schema.minWeight) errors.push({ type: "weightMin", expected: schema.minWeight, actual: value });
+			return value;
 		}
 	}
 };
diff --git a/examples/with-react-nextjs/pages/index.js b/examples/with-react-nextjs/pages/index.js
index cdcaaad..99aa53a 100644
--- a/examples/with-react-nextjs/pages/index.js
+++ b/examples/with-react-nextjs/pages/index.js
@@ -1,23 +1,24 @@
 import React from "react";
 import Validator from "../../../dist/index";
 
-class Index extends React.Component {
-	render() {
+const v = new Validator();
 
-		const v = new Validator();
+const schema = {
+	multi: [
+		{ type: "string", min: 3, max: 255 },
+		{ type: "boolean" }
+	]
+};
 
-		const schema = {
-			multi: [
-				{ type: "string", min: 3, max: 255 },
-				{ type: "boolean" }
-			]
-		};
+const check = v.compile(schema)
 
+class Index extends React.Component {
+	render() {
 		return (
 			<div>
-				Multi schema validator: {v.validate({ multi: 'john' }, schema) === true ? 'yey!' : 'nope..'}
+				Multi schema validator: {check({ multi: 'john' }) === true ? 'yey!' : 'nope..'}
 				<br />
-				Failed Multi schema validator: {JSON.stringify(v.validate({ multi: 123 }, schema))}
+				Failed Multi schema validator: {JSON.stringify(check)}
 			</div>
 		);
 	}
diff --git a/lib/rules/array.js b/lib/rules/array.js
index 04da100..f4a457b 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 }, path, context) {
 	const src = [];
 
 	src.push(`
@@ -82,7 +82,11 @@ module.exports = function({ schema, messages }, path, context) {
 		`);
 
 		const rule = this.getRuleFromSchema(schema.items);
-		src.push(this.compileRule(rule, context, path, "arr[i] = context.fn[%%INDEX%%](arr[i], (parentField ? parentField : \"\") + \"[\" + i + \"]\", parent, errors, context);", "arr[i]"));
+		const innerSource = `
+			arr[i] = context.fn[%%INDEX%%](arr[i], (parentField ? parentField : "") + "[" + i + "]", parent, errors, context);
+			${this.makeCustomValidator({ vName: "arr[i]", path, schema: rule.schema, context, messages })}
+		`;
+		src.push(this.compileRule(rule, context, path, innerSource, "arr[i]"));
 		/*
 		const res = rule.ruleFunction.call(this, rule, path, context);
 		context.rules[context.index] = rule;


Test patch:
diff --git a/test/rules/array.spec.js b/test/rules/array.spec.js
index 5403530..98acc52 100644
--- a/test/rules/array.spec.js
+++ b/test/rules/array.spec.js
@@ -2,8 +2,12 @@
 
 const Validator = require("../../lib/validator");
 
-const v = new Validator();
-
+const v = new Validator({
+	useNewCustomCheckerFunction: true,
+	messages: {
+		evenNumber: "The '' field must be an even number!"
+	}
+});
 describe("Test rule: array", () => {
 
 	it("should check type of value", () => {
@@ -74,10 +78,10 @@ describe("Test rule: array", () => {
 	it("check unique", () => {
 		const check = v.compile({ $$root: true, type: "array", unique: true });
 
-		expect(check(["bob","john","bob"])).toEqual([{ type: "arrayUnique", expected: ["bob"], actual: ["bob", "john", "bob"], message: "The 'bob,john,bob' value in '' field does not unique the 'bob' values." }]);
-		expect(check(["bob","john","bob","bob","john"])).toEqual([{ type: "arrayUnique", expected: ["bob","john"], actual: ["bob","john","bob","bob","john"], message: "The 'bob,john,bob,bob,john' value in '' field does not unique the 'bob,john' values." }]);
-		expect(check([1,2,1,false,true,false])).toEqual([{ type: "arrayUnique", expected: [1,false], actual: [1,2,1,false,true,false], message: "The '1,2,1,false,true,false' value in '' field does not unique the '1,false' values." }]);
-		expect(check([{name:"bob"},{name:"john"},{name:"bob"}])).toEqual(true);
+		expect(check(["bob", "john", "bob"])).toEqual([{ type: "arrayUnique", expected: ["bob"], actual: ["bob", "john", "bob"], message: "The 'bob,john,bob' value in '' field does not unique the 'bob' values." }]);
+		expect(check(["bob", "john", "bob", "bob", "john"])).toEqual([{ type: "arrayUnique", expected: ["bob", "john"], actual: ["bob", "john", "bob", "bob", "john"], message: "The 'bob,john,bob,bob,john' value in '' field does not unique the 'bob,john' values." }]);
+		expect(check([1, 2, 1, false, true, false])).toEqual([{ type: "arrayUnique", expected: [1, false], actual: [1, 2, 1, false, true, false], message: "The '1,2,1,false,true,false' value in '' field does not unique the '1,false' values." }]);
+		expect(check([{ name: "bob" }, { name: "john" }, { name: "bob" }])).toEqual(true);
 		expect(check(["john", "bob"])).toEqual(true);
 	});
 
@@ -96,8 +100,8 @@ describe("Test rule: array", () => {
 		expect(check([])).toEqual(true);
 		expect(check(["human"])).toEqual(true);
 		expect(check(["male", 3, "female", true])).toEqual([
-			{ type: "string", field: "[1]", actual: 3, message: "The '[1]' field must be a string."},
-			{ type: "string", field: "[3]", actual: true, message: "The '[3]' field must be a string."}
+			{ type: "string", field: "[1]", actual: 3, message: "The '[1]' field must be a string." },
+			{ type: "string", field: "[3]", actual: true, message: "The '[3]' field must be a string." }
 		]);
 	});
 
@@ -135,5 +139,30 @@ describe("Test rule: array", () => {
 			});
 		});
 
+		it("should call items custom checker function", () => {
+			const check = v.compile({
+				a: {
+					type: "array",
+					items: {
+						type: "number",
+						custom(value, errors) {
+							if (value % 2 !== 0) errors.push({ type: "evenNumber" });
+							return value * 2;
+						}
+					}
+				}
+			});
+
+			const o = {
+				a: [1, 2, 4]
+			};
+
+			const errors = check(o);
+
+			expect(Array.isArray(errors)).toBe(true);
+			expect(errors.length).toBe(1);
+			expect(errors[0].type).toBe("evenNumber");
+			expect(o.a).toEqual([2, 4, 8]);
+		});
 	});
 });