Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improving the custom function with arrays of elements #332

Merged
merged 6 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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
Expand Down
Loading