Skip to content

Commit f379fcb

Browse files
committed
add custom async validator support
1 parent a671905 commit f379fcb

File tree

5 files changed

+192
-32
lines changed

5 files changed

+192
-32
lines changed

Diff for: README.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ Use "%s" in the message to have the field name or label printed in the message:
320320
321321
field("sport", "favorite sport").custom(function(value, source) {
322322
if (!source.country) {
323-
throw new error('unable to validate %s');
323+
throw new Error('unable to validate %s');
324324
}
325325
326326
switch (source.country) {
@@ -338,6 +338,15 @@ Use "%s" in the message to have the field name or label printed in the message:
338338
}
339339
340340
});
341+
342+
Asynchronous custom validator (3 argument function signature)
343+
344+
form.field('username').custom(function(value, source, callback) {
345+
username.check(value, function(err) {
346+
if (err) return callback(new Error('Invalid %s'));
347+
callback(null);
348+
});
349+
});
341350

342351

343352
### http.ServerRequest.prototype.form

Diff for: lib/field.js

+62-15
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var validator = require("validator")
44
, externalFilter = new validator.Filter()
55
, externalValidator = new validator.Validator()
66
, object = require("object-additions").object
7+
, async = require("async")
78
, utils = require("./utils");
89

910
function Field(property, label) {
@@ -24,7 +25,7 @@ function Field(property, label) {
2425
return this;
2526
};
2627

27-
this.run = function (source, form, options) {
28+
this.run = function (source, form, options, cb) {
2829
var self = this
2930
, errors = []
3031
, value = utils.getProp(property, form) || utils.getProp(property, source);
@@ -38,36 +39,62 @@ function Field(property, label) {
3839
});
3940
}
4041

41-
function runStack(foo) {
42+
function runStack(foo, cb) {
4243

43-
stack.forEach(function (proc) {
44-
var result = proc(foo, source); // Pass source for "equals" proc.
45-
if (result.valid) return;
44+
async.eachSeries(stack, function(proc, cb) {
45+
46+
if (proc.length == 3) {
47+
// run the async validator/filter
48+
return proc(foo, source, function(err, result) {
49+
if (err) {
50+
errors.push(err.message.replace("%s", fieldLabel));
51+
return cb(null);
52+
}
53+
54+
// filters return values
55+
if (result != null) {
56+
foo = result
57+
}
58+
59+
cb(null);
60+
61+
});
62+
}
63+
64+
// run the sync validator/filter
65+
var result = proc(foo, source);
66+
if (result.valid) return cb(null);
4667
if (result.error) {
4768
// If this field is not required and it doesn't have a value, ignore error.
48-
if (!utils.hasValue(value) && !self.__required) return;
69+
if (!utils.hasValue(value) && !self.__required) return cb(null);
4970

50-
return errors.push(result.error.replace("%s", fieldLabel));
71+
errors.push(result.error.replace("%s", fieldLabel));
72+
return cb(null);
5173
}
5274
foo = result;
75+
cb(null);
76+
77+
}, function(err) {
78+
cb(null, foo);
5379
});
54-
55-
return foo;
5680
}
5781

5882
if (isArray) {
5983
if (!utils.hasValue(value)) value = [];
6084
if (!Array.isArray(value)) value = [value];
61-
value = value.map(runStack);
85+
async.mapSeries(value, runStack, function(err, value) {
86+
utils.setProp(property, form, value);
87+
cb(null, errors);
88+
});
89+
6290

6391
} else {
6492
if (Array.isArray(value)) value = value[0];
65-
value = runStack(value);
93+
runStack(value, function(err, value) {
94+
utils.setProp(property, form, value);
95+
cb(null, errors);
96+
});
6697
}
67-
68-
utils.setProp(property, form, value);
69-
70-
if (errors.length) return errors;
7198
};
7299
}
73100

@@ -92,7 +119,25 @@ Field.prototype.arrLength = function (from, to) {
92119
// HYBRID METHODS
93120

94121
Field.prototype.custom = function(func, message) {
122+
123+
// custom function is async
124+
if (func.length == 3) {
125+
return this.add(function(value, source, cb) {
126+
func(value, source, function(err, result) {
127+
if (err) return cb(new Error(message || err.message || "%s is invalid"));
128+
129+
// functions that return values are filters
130+
if (result != null) return cb(null, result);
131+
132+
// value passed validator
133+
cb(null, null);
134+
});
135+
});
136+
}
137+
138+
// custom function is sync
95139
return this.add(function (value, source) {
140+
96141
try {
97142
var result = func(value, source);
98143
} catch (e) {
@@ -101,7 +146,9 @@ Field.prototype.custom = function(func, message) {
101146
// Functions that return values are filters.
102147
if (result != null) return result;
103148

149+
// value passed validator
104150
return { valid: true };
151+
105152
});
106153
};
107154

Diff for: lib/form.js

+23-15
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
* MIT Licensed
55
*/
66

7-
var utils = require("./utils")
7+
var async = require("async")
8+
, utils = require("./utils")
89
, Field = require("./field");
910

1011
function form() {
@@ -82,25 +83,32 @@ function form() {
8283
}
8384
});
8485

85-
routines.forEach(function (routine) {
86-
var result = routine.run(mergedSource, req.form, options);
87-
88-
if (!Array.isArray(result) || !result.length) return;
86+
//routines.forEach(function (routine) {
87+
async.each(routines, function(routine, cb) {
88+
routine.run(mergedSource, req.form, options, function(err, result) {
89+
90+
// return early if no errors
91+
if (!Array.isArray(result) || !result.length) return cb(null);
8992

90-
var errors = req.form.errors = req.form.errors || []
91-
, name = routine.name;
93+
var errors = req.form.errors = req.form.errors || []
94+
, name = routine.name;
9295

93-
map[name] = map[name] || [];
96+
map[name] = map[name] || [];
9497

95-
result.forEach(function (error) {
96-
errors.push(error);
97-
map[name].push(error);
98+
result.forEach(function (error) {
99+
errors.push(error);
100+
map[name].push(error);
101+
});
102+
103+
cb(null);
104+
98105
});
106+
}, function(err) {
107+
108+
if (options.flashErrors) req.form.flashErrors();
109+
if (next) next();
110+
99111
});
100-
101-
if (options.flashErrors) req.form.flashErrors();
102-
103-
if (next) next();
104112
}
105113
}
106114

Diff for: package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
],
2222
"dependencies": {
2323
"validator": "0.4.x",
24-
"object-additions": ">= 0.5.0"
24+
"object-additions": ">= 0.5.0",
25+
"async": "~0.2.9"
2526
},
2627
"peerDependencies": {
2728
"express": "3.x"

Diff for: test/validate.test.js

+95
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,101 @@ module.exports = {
568568
assert.equal(request.form.errors.length, 1);
569569
},
570570

571+
"validation: custom : async": function(done) {
572+
var request = { body: { field1: "value1", field2: "value2" }};
573+
var next = function next() {
574+
assert.strictEqual(request.form.isValid, false);
575+
assert.strictEqual(request.form.errors.length, 1);
576+
assert.strictEqual(request.form.errors[0], 'Invalid field1');
577+
done();
578+
};
579+
580+
form(validate("field1").custom(function(value, source, callback) {
581+
process.nextTick(function() {
582+
assert.strictEqual(value, 'value1');
583+
callback(new Error("Invalid %s"));
584+
});
585+
}))(request, {}, next);
586+
},
587+
588+
"validation : custom : async : success": function(done) {
589+
var request = { body: { field1: "value1", field2: "value2" }};
590+
var callbackCalled = false;
591+
var next = function next() {
592+
assert.strictEqual(callbackCalled, true);
593+
assert.strictEqual(request.form.isValid, true);
594+
assert.strictEqual(request.form.errors.length, 0);
595+
done();
596+
};
597+
form(validate("field1").custom(function(value, source, callback) {
598+
process.nextTick(function() {
599+
assert.strictEqual(value, 'value1');
600+
callbackCalled = true;
601+
callback(null);
602+
});
603+
}))(request, {}, next);
604+
},
605+
606+
"validation : custom : async : chaining": function(done) {
607+
var request = { body: { field1: "value1", field2: "value2" }};
608+
var callbackCalled = 0;
609+
var next = function next() {
610+
assert.strictEqual(callbackCalled, 2);
611+
assert.strictEqual(request.form.isValid, false);
612+
assert.strictEqual(request.form.errors.length, 2);
613+
assert.strictEqual(request.form.errors[0], 'Fail! field1');
614+
assert.strictEqual(request.form.errors[1], 'yes sync custom funcs still work !! field1');
615+
done();
616+
};
617+
618+
form(validate("field1")
619+
.custom(function(value, source, callback) {
620+
process.nextTick(function() {
621+
++callbackCalled;
622+
callback(null);
623+
});
624+
})
625+
.custom(function(value, source, callback) {
626+
process.nextTick(function() {
627+
++callbackCalled;
628+
callback(new Error('Fail! %s'));
629+
});
630+
})
631+
.custom(function(value, source) {
632+
throw new Error('yes sync custom funcs still work !! %s');
633+
})
634+
)(request, {}, next);
635+
},
636+
637+
"validation : custom : async : multiple fields": function(done) {
638+
var request = { body: { field1: "value1", field2: "value2" }};
639+
var callbackCalled = 0;
640+
var next = function next() {
641+
assert.strictEqual(callbackCalled, 2);
642+
assert.strictEqual(request.form.isValid, false);
643+
assert.strictEqual(request.form.errors.length, 2);
644+
assert.strictEqual(request.form.errors[0], 'field1 error');
645+
assert.strictEqual(request.form.errors[1], 'field2 error');
646+
done();
647+
};
648+
form(
649+
validate("field1").custom(function(value, source, callback) {
650+
process.nextTick(function() {
651+
++callbackCalled;
652+
assert.strictEqual(value, 'value1')
653+
callback(new Error('%s error'));
654+
});
655+
}),
656+
validate("field2").custom(function(value, source, callback) {
657+
process.nextTick(function() {
658+
++callbackCalled;
659+
assert.strictEqual(value, 'value2');
660+
callback(new Error('%s error'));
661+
});
662+
})
663+
)(request, {}, next);
664+
},
665+
571666
"validation : request.form property-pollution": function() {
572667
var request = { body: { }};
573668
form()(request, {});

0 commit comments

Comments
 (0)