Repository: icebob/fastest-validator
Pull request #191 (2020-09-28 14:50:36)

[feat] Nullable fields
Related #113


Patch:
diff --git a/README.md b/README.md
index cee1958..b5b920c 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,9 @@ $ npm run bench
     - [Fast method](#fast-method)
     - [Browser usage](#browser-usage)
     - [Deno usage](#deno-usage)
-- [Optional & required fields](#optional--required-fields)
+- [Optional, Required & Nullable fields](#optional--required---nullable-fields)
+  * [Optional](#optional)
+  * [Nullable](#nullable)
 - [Strict validation](#strict-validation)
   - [Remove additional fields](#remove-additional-fields)
 - [Multiple validators](#multiple-validators)
@@ -211,7 +213,8 @@ const check = v.compile({
 console.log(check({ name: "Erf", age: 18 })); //true
 ```
 
-# Optional & required fields
+# Optional, Required & Nullable fields
+## Optional
 Every field in the schema will be required by default. If you'd like to define optional fields, set `optional: true`.
 
 ```js
@@ -225,6 +228,19 @@ v.validate({ name: "John" }, schema); // Valid
 v.validate({ age: 42 }, schema); // Fail because name is required
 ```
 
+## Nullable
+If you want disallow `undefined` value but allow `null` value, use `nullable` instead of `optional`.
+```js
+const schema = {
+    age: { type: "number", nullable: true }
+}
+
+v.validate({ age: 42 }, schema); // Valid
+v.validate({ age: null }, schema); // Valid
+v.validate({ age: undefined }, schema); // Fail because undefined is disallowed
+v.validate({}, schema); // Fail because undefined is disallowed
+```
+
 # Strict validation
 Object properties which are not specified on the schema are ignored by default. If you set the `$$strict` option to `true` any additional properties will result in an `strictObject` error.
 
diff --git a/index.d.ts b/index.d.ts
index a01766e..f59dd99 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -505,6 +505,13 @@ declare module "fastest-validator" {
 		 * @default false
 		 */
 		optional?: boolean;
+
+		/**
+		 * If you want disallow `undefined` value but allow `null` value, use `nullable` instead of `optional`.
+		 * @default false
+		 */
+		nullable?: boolean;
+
 		/**
 		 * You can set your custom messages in the validator constructor
 		 * Sometimes the standard messages are too generic. You can customise messages per validation type per field
diff --git a/lib/validator.js b/lib/validator.js
index 50171b9..93f116c 100644
--- a/lib/validator.js
+++ b/lib/validator.js
@@ -97,41 +97,40 @@ class Validator {
 	 */
 	wrapRequiredCheckSourceCode(rule, innerSrc, context, resVar) {
 		const src = [];
-		let defaultValue = rule.schema.default != null ? JSON.stringify(rule.schema.default) : null;
-		if (typeof rule.schema.default === "function") {
-			if (!context.customs[rule.index]) context.customs[rule.index] = {};
-			context.customs[rule.index].defaultFn = rule.schema.default;
-
-			defaultValue = `context.customs[${rule.index}].defaultFn()`;
-		}
-		
-
-		// Required, optional, forbidden
-		src.push(`
-			if (value === undefined || value === null) {
-		`);
-		if (rule.schema.optional === true || rule.schema.type == "forbidden") {
-			// Optional field
-			if (defaultValue != null && resVar) {
-				src.push(`${resVar} = ${defaultValue};`);
+		let handleNoValue;
+
+		let skipUndefinedValue = rule.schema.optional === true || rule.schema.type === "forbidden";
+		let skipNullValue = rule.schema.optional === true || rule.schema.nullable === true || rule.schema.type === "forbidden";
+
+		if (rule.schema.default != null) {
+			// We should set default-value when value is undefined or null, not skip! (Except when null is allowed)
+			skipUndefinedValue = false;
+			if (rule.schema.nullable !== true) skipNullValue = false;
+			
+			let defaultValue;
+			if (typeof rule.schema.default === "function") {
+				if (!context.customs[rule.index]) context.customs[rule.index] = {};
+				context.customs[rule.index].defaultFn = rule.schema.default;
+				defaultValue = `context.customs[${rule.index}].defaultFn()`;
 			} else {
-				src.push("// Do nothing, it's an optional field");
+				defaultValue = JSON.stringify(rule.schema.default);
 			}
+
+			handleNoValue = `
+				value = ${defaultValue};
+				${resVar} = value;
+			`;
+			
 		} else {
-			// Required field
-			if (defaultValue != null && resVar) {
-				src.push(`${resVar} = ${defaultValue};`);
-			} else {
-				src.push(this.makeError({ type: "required", actual: "value", messages: rule.messages }));
-			}
+			handleNoValue = this.makeError({ type: "required", actual: "value", messages: rule.messages });
 		}
-		src.push("} else {");
-
-		if (innerSrc)
-			src.push(innerSrc);
 
-		src.push("\t\t}"); // Required, optional
 
+		src.push(`
+			${`if (value === undefined) { ${skipUndefinedValue ? "\n// allow undefined\n" : handleNoValue} }`}
+			${`else if (value === null) { ${skipNullValue ? "\n// allow null\n" : handleNoValue} }`}
+			${innerSrc ? `else { ${innerSrc} }` : ""}
+		`);
 		return src.join("\n");
 	}
 


Test patch:
diff --git a/test/integration.spec.js b/test/integration.spec.js
index 1e5768d..1648fe5 100644
--- a/test/integration.spec.js
+++ b/test/integration.spec.js
@@ -1032,3 +1032,116 @@ describe("Test default value sanitizer", () => {
 	});
 });
 
+describe("Test optional option", () => {
+	const v = new Validator();
+
+	it("should not throw error if value is undefined", () => {
+		const schema = { foo: { type: "number", optional: true } };
+		const check = v.compile(schema);
+
+		expect(check({})).toBe(true);
+		expect(check({ foo: undefined })).toBe(true);
+	});
+
+	it("should not throw error if value is null", () => {
+		const schema = { foo: { type: "number", optional: true } };
+		const check = v.compile(schema);
+
+		const o = { foo: null };
+		expect(check(o)).toBe(true);
+		expect(o.foo).toBe(null);
+	});
+
+	it("should not throw error if value exist", () => {
+		const schema = { foo: { type: "number", optional: true } };
+		const check = v.compile(schema);
+
+		expect(check({ foo: 2 })).toBe(true);
+	});
+
+	it("should set default value if there is a default", () => {
+		const schema = { foo: { type: "number", optional: true, default: 5 } };
+		const check = v.compile(schema);
+
+		const o1 = { foo: 2 };
+		expect(check(o1)).toBe(true);
+		expect(o1.foo).toBe(2);
+
+		const o2 = {};
+		expect(check(o2)).toBe(true);
+		expect(o2.foo).toBe(5);
+	});
+});
+
+describe("Test nullable option", () => {
+	const v = new Validator();
+
+	it("should throw error if value is undefined", () => {
+		const schema = { foo: { type: "number", nullable: true } };
+		const check = v.compile(schema);
+
+		expect(check(check)).toBeInstanceOf(Array);
+		expect(check({ foo: undefined })).toBeInstanceOf(Array);
+	});
+
+	it("should not throw error if value is null", () => {
+		const schema = { foo: { type: "number", nullable: true } };
+		const check = v.compile(schema);
+
+		const o = { foo: null };
+		expect(check(o)).toBe(true);
+		expect(o.foo).toBe(null);
+	});
+
+	it("should not throw error if value exist", () => {
+		const schema = { foo: { type: "number", nullable: true } };
+		const check = v.compile(schema);
+		expect(check({ foo: 2 })).toBe(true);
+	});
+
+	it("should set default value if there is a default", () => {
+		const schema = { foo: { type: "number", nullable: true, default: 5 } };
+		const check = v.compile(schema);
+
+		const o1 = { foo: 2 };
+		expect(check(o1)).toBe(true);
+		expect(o1.foo).toBe(2);
+
+		const o2 = {};
+		expect(check(o2)).toBe(true);
+		expect(o2.foo).toBe(5);
+	});
+
+	it("should not set default value if current value is null", () => {
+		const schema = { foo: { type: "number", nullable: true, default: 5 } };
+		const check = v.compile(schema);
+
+		const o = { foo: null };
+		expect(check(o)).toBe(true);
+		expect(o.foo).toBe(null);
+	});
+
+	it("should work with optional", () => {
+		const schema = { foo: { type: "number", nullable: true, optional: true } };
+		const check = v.compile(schema);
+
+		expect(check({ foo: 3 })).toBe(true);
+		expect(check({ foo: null })).toBe(true);
+		expect(check({})).toBe(true);
+	});
+
+	it("should work with optional and default", () => {
+		const schema = { foo: { type: "number", nullable: true, optional: true, default: 5 } };
+		const check = v.compile(schema);
+
+		expect(check({ foo: 3 })).toBe(true);
+
+		const o1 = { foo: null };
+		expect(check(o1)).toBe(true);
+		expect(o1.foo).toBe(null);
+
+		const o2 = {};
+		expect(check(o2)).toBe(true);
+		expect(o2.foo).toBe(5);
+	});
+});
\ No newline at end of file
diff --git a/test/typescript/integration.spec.ts b/test/typescript/integration.spec.ts
index 247737f..df6585b 100644
--- a/test/typescript/integration.spec.ts
+++ b/test/typescript/integration.spec.ts
@@ -362,25 +362,25 @@ describe('TypeScript Definitions', () => {
         });
 
 
-		it("should work with optional (legacy)", () => {
-
-			let schemaOptional = {
-				a: [
-					{ type: "number", optional: true },
-					{ type: "string", optional: true },
-				]
-			};
-
-			let checkOptional = v.compile(schemaOptional);
-
-			expect(checkOptional({})).toBe(true);
-			expect(checkOptional({ a: 5 })).toBe(true);
-			expect(checkOptional({ a: "five" })).toBe(true);
-			expect(checkOptional({ a: false })).toEqual([
-				{ type: "number", field: "a", actual: false, message: "The 'a' field must be a number." },
-				{ type: "string", field: "a", actual: false, message: "The 'a' field must be a string." },
-			]);
-		});
+        it("should work with optional (legacy)", () => {
+
+            let schemaOptional = {
+                a: [
+                    { type: "number", optional: true },
+                    { type: "string", optional: true },
+                ]
+            };
+
+            let checkOptional = v.compile(schemaOptional);
+
+            expect(checkOptional({})).toBe(true);
+            expect(checkOptional({ a: 5 })).toBe(true);
+            expect(checkOptional({ a: "five" })).toBe(true);
+            expect(checkOptional({ a: false })).toEqual([
+                { type: "number", field: "a", actual: false, message: "The 'a' field must be a number." },
+                { type: "string", field: "a", actual: false, message: "The 'a' field must be a string." },
+            ]);
+        });
 
     });
 
@@ -1053,5 +1053,119 @@ describe('TypeScript Definitions', () => {
             });
         });
     });
-});
+
+    describe("Test optional option", () => {
+        const v = new Validator();
+
+        it("should not throw error if value is undefined", () => {
+            const schema = { foo: { type: "number", optional: true } };
+            const check = v.compile(schema);
+
+            expect(check({})).toBe(true);
+            expect(check({ foo: undefined })).toBe(true);
+        });
+
+        it("should not throw error if value is null", () => {
+            const schema = { foo: { type: "number", optional: true } };
+            const check = v.compile(schema);
+
+            const o = { foo: null };
+            expect(check(o)).toBe(true);
+            expect(o.foo).toBe(null);
+        });
+
+        it("should not throw error if value exist", () => {
+            const schema = { foo: { type: "number", optional: true } };
+            const check = v.compile(schema);
+
+            expect(check({ foo: 2 })).toBe(true);
+        });
+
+        it("should set default value if there is a default", () => {
+            const schema = { foo: { type: "number", optional: true, default: 5 } };
+            const check = v.compile(schema);
+
+            const o1 = { foo: 2 };
+            expect(check(o1)).toBe(true);
+            expect(o1.foo).toBe(2);
+
+            const o2: { foo?: number }  = {};
+            expect(check(o2)).toBe(true);
+            expect(o2.foo).toBe(5);
+        });
+    });
+
+    describe("Test nullable option", () => {
+        const v = new Validator();
+
+        it("should throw error if value is undefined", () => {
+            const schema = { foo: { type: "number", nullable: true } };
+            const check = v.compile(schema);
+
+            expect(check(check)).toBeInstanceOf(Array);
+            expect(check({ foo: undefined })).toBeInstanceOf(Array);
+        });
+
+        it("should not throw error if value is null", () => {
+            const schema = { foo: { type: "number", nullable: true } };
+            const check = v.compile(schema);
+
+            const o = { foo: null };
+            expect(check(o)).toBe(true);
+            expect(o.foo).toBe(null);
+        });
+
+        it("should not throw error if value exist", () => {
+            const schema = { foo: { type: "number", nullable: true } };
+            const check = v.compile(schema);
+            expect(check({ foo: 2 })).toBe(true);
+        });
+
+        it("should set default value if there is a default", () => {
+            const schema = { foo: { type: "number", nullable: true, default: 5 } };
+            const check = v.compile(schema);
+
+            const o1 = { foo: 2 };
+            expect(check(o1)).toBe(true);
+            expect(o1.foo).toBe(2);
+
+            const o2: { foo?: number } = {};
+            expect(check(o2)).toBe(true);
+            expect(o2.foo).toBe(5);
+        });
+
+        it("should not set default value if current value is null", () => {
+            const schema = { foo: { type: "number", nullable: true, default: 5 } };
+            const check = v.compile(schema);
+
+            const o = { foo: null };
+            expect(check(o)).toBe(true);
+            expect(o.foo).toBe(null);
+        });
+
+        it("should work with optional", () => {
+            const schema = { foo: { type: "number", nullable: true, optional: true } };
+            const check = v.compile(schema);
+
+            expect(check({ foo: 3 })).toBe(true);
+            expect(check({ foo: null })).toBe(true);
+            expect(check({})).toBe(true);
+        });
+
+        it("should work with optional and default", () => {
+            const schema = { foo: { type: "number", nullable: true, optional: true, default: 5 } };
+            const check = v.compile(schema);
+
+            expect(check({ foo: 3 })).toBe(true);
+
+            const o1 = { foo: null };
+            expect(check(o1)).toBe(true);
+            expect(o1.foo).toBe(null);
+
+            const o2: { foo?: number }  = {};
+            expect(check(o2)).toBe(true);
+            expect(o2.foo).toBe(5);
+        });
+    });
+})