Skip to content

Commit 91ff68b

Browse files
authored
Merge pull request #332 from FerX/feature/custom-array
improving the custom function with arrays of elements
2 parents 9c15049 + cd9aa78 commit 91ff68b

File tree

4 files changed

+284
-13
lines changed

4 files changed

+284
-13
lines changed

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,86 @@ console.log(check({ name: "John", phone: "36-70-123-4567" }));
13391339
13401340
>Please note: the custom function must return the `value`. It means you can also sanitize it.
13411341
1342+
### Chaining custom functions and global definitions
1343+
You can define the `custom` property as an array of functions, allowing you to chain various validation logics.
1344+
1345+
Additionally, you can define custom functions globally, making them reusable.
1346+
```js
1347+
1348+
let v = new Validator({
1349+
debug: true,
1350+
useNewCustomCheckerFunction: true,
1351+
messages: {
1352+
// Register our new error message text
1353+
evenNumber: "The '{field}' field must be an even number! Actual: {actual}",
1354+
realNumber: "The '{field}' field must be a real number! Actual: {actual}",
1355+
notPermitNumber: "The '{field}' cannot have the value {actual}",
1356+
compareGt: "The '{field}' field must be greater than {gt}! Actual: {actual}",
1357+
compareGte: "The '{field}' field must be greater than or equal to {gte}! Actual: {actual}",
1358+
compareLt: "The '{field}' field must be less than {lt}! Actual: {actual}",
1359+
compareLte: "The '{field}' field must be less than or equal to {lte}! Actual: {actual}"
1360+
},
1361+
customFunctions:{
1362+
even: (value, errors)=>{
1363+
if(value % 2 != 0 ){
1364+
errors.push({ type: "evenNumber", actual: value });
1365+
}
1366+
return value;
1367+
},
1368+
real: (value, errors)=>{
1369+
if(value <0 ){
1370+
errors.push({ type: "realNumber", actual: value });
1371+
}
1372+
return value;
1373+
},
1374+
compare: (value, errors, schema)=>{
1375+
if( typeof schema.custom.gt==="number" && value <= schema.custom.gt ){
1376+
errors.push({ type: "compareGt", actual: value, gt: schema.custom.gt });
1377+
}
1378+
if( typeof schema.custom.gte==="number" && value < schema.custom.gte ){
1379+
errors.push({ type: "compareGte", actual: value, gte: schema.custom.gte });
1380+
}
1381+
if( typeof schema.custom.lt==="number" && value >= schema.custom.lt ){
1382+
errors.push({ type: "compareLt", actual: value, lt: schema.custom.lt });
1383+
}
1384+
if( typeof schema.custom.lte==="number" && value > schema.custom.lte ){
1385+
errors.push({ type: "compareLte", actual: value, lte: schema.custom.lte });
1386+
}
1387+
return value;
1388+
}
1389+
}
1390+
});
1391+
1392+
1393+
1394+
const schema = {
1395+
people:{
1396+
type: "number",
1397+
custom: [
1398+
"compare|gte:-100|lt:200", // extended definition with additional parameters - equal to: {type:"compare",gte:-100, lt:200},
1399+
"even",
1400+
"real",
1401+
function (value, errors){
1402+
if(value === "3" ){
1403+
errors.push({ type: "notPermitNumber", actual: value });
1404+
}
1405+
return value;
1406+
}
1407+
]
1408+
}
1409+
};
1410+
1411+
console.log(v.validate({people:-200}, schema));
1412+
console.log(v.validate({people:200}, schema));
1413+
console.log(v.validate({people:5}, schema));
1414+
console.log(v.validate({people:-5}, schema));
1415+
console.log(v.validate({people:3}, schema));
1416+
1417+
```
1418+
1419+
1420+
1421+
13421422
## Asynchronous custom validations
13431423
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.
13441424
In this case you should use `async/await` keywords, or return a `Promise` in the custom validator functions.

examples/custom-functions.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
let Validator = require("../index");
2+
3+
4+
let v = new Validator({
5+
debug: true,
6+
useNewCustomCheckerFunction: true,
7+
messages: {
8+
// Register our new error message text
9+
evenNumber: "The '{field}' field must be an even number! Actual: {actual}",
10+
realNumber: "The '{field}' field must be a real number! Actual: {actual}",
11+
notPermitNumber: "The '{field}' cannot have the value {actual}",
12+
},
13+
customFunctions:{
14+
even: (value, errors)=>{
15+
if(value % 2 != 0 ){
16+
errors.push({ type: "evenNumber", actual: value });
17+
}
18+
return value;
19+
},
20+
real: (value, errors)=>{
21+
if(value <0 ){
22+
errors.push({ type: "realNumber", actual: value });
23+
}
24+
return value;
25+
}
26+
}
27+
});
28+
29+
30+
31+
const schema = {
32+
people:{
33+
type: "number",
34+
custom: [
35+
"even",
36+
"real",
37+
function (value, errors){
38+
if(value === "3" ){
39+
errors.push({ type: "notPermitNumber", actual: value });
40+
}
41+
return value;
42+
}
43+
]
44+
}
45+
};
46+
47+
console.log(v.validate({people:5}, schema));
48+
console.log(v.validate({people:-5}, schema));
49+
console.log(v.validate({people:3}, schema));

lib/validator.js

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class Validator {
5757
this.rules = loadRules();
5858
this.aliases = {};
5959
this.cache = new Map();
60+
this.customFunctions = {};
6061

6162
if (opts) {
6263
deepExtend(this.opts, opts);
@@ -74,6 +75,10 @@ class Validator {
7475
for (const ruleName in opts.customRules) this.add(ruleName, opts.customRules[ruleName]);
7576
}
7677

78+
if (opts.customFunctions) {
79+
for (const customName in opts.customFunctions) this.addCustomFunction(customName, opts.customFunctions[customName]);
80+
}
81+
7782
if (opts.plugins) {
7883
const plugins = opts.plugins;
7984
if (!Array.isArray(plugins)) throw new Error("Plugins type must be array");
@@ -204,6 +209,7 @@ class Validator {
204209
rules: [],
205210
fn: [],
206211
customs: {},
212+
customFunctions : this.customFunctions,
207213
utils: {
208214
replace,
209215
},
@@ -439,32 +445,73 @@ class Validator {
439445
makeCustomValidator({ vName = "value", fnName = "custom", ruleIndex, path, schema, context, messages }) {
440446
const ruleVName = "rule" + ruleIndex;
441447
const fnCustomErrorsVName = "fnCustomErrors" + ruleIndex;
442-
if (typeof schema[fnName] == "function") {
448+
449+
if (typeof schema[fnName] == "function" || (Array.isArray(schema[fnName]))) {
443450
if (context.customs[ruleIndex]) {
444451
context.customs[ruleIndex].messages = messages;
445452
context.customs[ruleIndex].schema = schema;
453+
} else {
454+
context.customs[ruleIndex] = { messages, schema };
446455
}
447-
else context.customs[ruleIndex] = { messages, schema };
448-
456+
const ret = [];
449457
if (this.opts.useNewCustomCheckerFunction) {
450-
return `
458+
ret.push( `
451459
const ${ruleVName} = context.customs[${ruleIndex}];
452460
const ${fnCustomErrorsVName} = [];
461+
`);
462+
463+
if(Array.isArray(schema[fnName])){
464+
for (let i = 0; i < schema[fnName].length; i++) {
465+
466+
let custom = schema[fnName][i];
467+
468+
if (typeof custom === "string") {
469+
custom = this.parseShortHand(custom);
470+
schema[fnName][i] = custom;
471+
}
472+
473+
const customIndex = ruleIndex*1000+i;
474+
context.customs[customIndex] = { messages, schema: Object.assign({}, schema, { custom, index: i }) };
475+
476+
ret.push( `
477+
const ${ruleVName}_${i} = context.customs[${customIndex}];
478+
479+
`);
480+
481+
if(custom.type){
482+
ret.push( `
483+
${vName} = ${context.async ? "await " : ""}context.customFunctions[${ruleVName}.schema.${fnName}[${i}].type].call(this, ${vName}, ${fnCustomErrorsVName} , ${ruleVName}_${i}.schema, "${path}", parent, context);
484+
`);
485+
}
486+
if(typeof custom==="function"){
487+
ret.push( `
488+
${vName} = ${context.async ? "await " : ""}${ruleVName}.schema.${fnName}[${i}].call(this, ${vName}, ${fnCustomErrorsVName} , ${ruleVName}.schema, "${path}", parent, context);
489+
`);
490+
}
491+
}
492+
}else{
493+
ret.push( `
453494
${vName} = ${context.async ? "await " : ""}${ruleVName}.schema.${fnName}.call(this, ${vName}, ${fnCustomErrorsVName} , ${ruleVName}.schema, "${path}", parent, context);
495+
`);
496+
}
497+
498+
ret.push( `
454499
if (Array.isArray(${fnCustomErrorsVName} )) {
455500
${fnCustomErrorsVName} .forEach(err => errors.push(Object.assign({ message: ${ruleVName}.messages[err.type], field }, err)));
456501
}
457-
`;
502+
`);
503+
}else{
504+
const result = "res_" + ruleVName;
505+
ret.push( `
506+
const ${ruleVName} = context.customs[${ruleIndex}];
507+
const ${result} = ${context.async ? "await " : ""}${ruleVName}.schema.${fnName}.call(this, ${vName}, ${ruleVName}.schema, "${path}", parent, context);
508+
if (Array.isArray(${result})) {
509+
${result}.forEach(err => errors.push(Object.assign({ message: ${ruleVName}.messages[err.type], field }, err)));
510+
}
511+
`);
458512
}
513+
return ret.join("\n");
459514

460-
const result = "res_" + ruleVName;
461-
return `
462-
const ${ruleVName} = context.customs[${ruleIndex}];
463-
const ${result} = ${context.async ? "await " : ""}${ruleVName}.schema.${fnName}.call(this, ${vName}, ${ruleVName}.schema, "${path}", parent, context);
464-
if (Array.isArray(${result})) {
465-
${result}.forEach(err => errors.push(Object.assign({ message: ${ruleVName}.messages[err.type], field }, err)));
466-
}
467-
`;
468515
}
469516
return "";
470517
}
@@ -479,6 +526,16 @@ class Validator {
479526
this.rules[type] = fn;
480527
}
481528

529+
/**
530+
* Add a custom function
531+
*
532+
* @param {String} type
533+
* @param {Function} fn
534+
*/
535+
addCustomFunction(name, fn) {
536+
this.customFunctions[name] = fn;
537+
}
538+
482539
/**
483540
* Add a message
484541
*

test/validator.spec.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,91 @@ describe("Test custom validation", () => {
647647
});
648648
});
649649

650+
651+
describe("Test custom validation with array", () => {
652+
653+
const v = new Validator({
654+
useNewCustomCheckerFunction: true,
655+
customFunctions:{
656+
even: (value, errors)=>{
657+
if(value % 2 != 0 ){
658+
errors.push({ type: "evenNumber", actual: value });
659+
}
660+
return value;
661+
},
662+
real: (value, errors)=>{
663+
if(value <0 ){
664+
errors.push({ type: "realNumber", actual: value });
665+
}
666+
return value;
667+
},
668+
compare: (value, errors, schema)=>{
669+
if( typeof schema.custom.gt==="number" && value <= schema.custom.gt ){
670+
errors.push({ type: "compareGt", actual: value, gt: schema.custom.gt });
671+
}
672+
if( typeof schema.custom.gte==="number" && value < schema.custom.gte ){
673+
errors.push({ type: "compareGte", actual: value, gte: schema.custom.gte });
674+
}
675+
if( typeof schema.custom.lt==="number" && value >= schema.custom.lt ){
676+
errors.push({ type: "compareLt", actual: value, lt: schema.custom.lt });
677+
}
678+
if( typeof schema.custom.lte==="number" && value > schema.custom.lte ){
679+
errors.push({ type: "compareLte", actual: value, lte: schema.custom.lte });
680+
}
681+
return value;
682+
}
683+
},
684+
messages: {
685+
evenNumber: "The '{field}' field must be an even number! Actual: {actual}",
686+
realNumber: "The '{field}' field must be a real number! Actual: {actual}",
687+
permitNumber: "The '{field}' cannot have the value {actual}",
688+
compareGt: "The '{field}' field must be greater than {gt}! Actual: {actual}",
689+
compareGte: "The '{field}' field must be greater than or equal to {gte}! Actual: {actual}",
690+
compareLt: "The '{field}' field must be less than {lt}! Actual: {actual}",
691+
compareLte: "The '{field}' field must be less than or equal to {lte}! Actual: {actual}"
692+
}
693+
});
694+
695+
let check;
696+
697+
it("should compile without error", () => {
698+
699+
check = v.compile({
700+
num: {
701+
type: "number",
702+
custom: [
703+
"compare|gte:-100|lt:200", // equal to: {type:"compare",gte:-100, lt:200},
704+
"even",
705+
"real",
706+
(value, errors) => {
707+
if ([-3,2,4,198].includes(value) ) errors.push({ type: "permitNumber", actual: value });
708+
return value;
709+
}
710+
711+
]
712+
}
713+
});
714+
715+
expect(typeof check).toBe("function");
716+
});
717+
718+
it("should work correctly with array custom validator", () => {
719+
expect(check({ num: 12 })).toBe(true);
720+
expect(check({ num: 0 })).toBe(true);
721+
expect(check({ num: 196 })).toBe(true);
722+
expect(check({ num: 3 })[0].type).toEqual("evenNumber");
723+
expect(check({ num: -12 })[0].type).toEqual("realNumber");
724+
expect(check({ num: -8 })[0].type).toEqual("realNumber");
725+
expect(check({ num: 198 })[0].type).toEqual("permitNumber");
726+
expect(check({ num: 4 })[0].type).toEqual("permitNumber");
727+
expect(check({ num: 202 })[0].type).toEqual("compareLt");
728+
expect(check({ num: -3 }).map(e=>e.type)).toEqual(["evenNumber","realNumber","permitNumber"]);
729+
});
730+
731+
732+
});
733+
734+
650735
describe("Test default values", () => {
651736
const v = new Validator({
652737
useNewCustomCheckerFunction: true

0 commit comments

Comments
 (0)