Skip to content

Commit

Permalink
Merge pull request #332 from FerX/feature/custom-array
Browse files Browse the repository at this point in the history
improving the custom function with arrays of elements
  • Loading branch information
icebob authored Jul 28, 2024
2 parents 9c15049 + cd9aa78 commit 91ff68b
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 13 deletions.
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
49 changes: 49 additions & 0 deletions examples/custom-functions.js
Original file line number Diff line number Diff line change
@@ -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));
83 changes: 70 additions & 13 deletions lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Validator {
this.rules = loadRules();
this.aliases = {};
this.cache = new Map();
this.customFunctions = {};

if (opts) {
deepExtend(this.opts, opts);
Expand All @@ -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");
Expand Down Expand Up @@ -204,6 +209,7 @@ class Validator {
rules: [],
fn: [],
customs: {},
customFunctions : this.customFunctions,
utils: {
replace,
},
Expand Down Expand Up @@ -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 "";
}
Expand All @@ -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
*
Expand Down
85 changes: 85 additions & 0 deletions test/validator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,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
Expand Down

0 comments on commit 91ff68b

Please sign in to comment.