From 1c5d485ff3ba317f311aa97b02d187169c6a4b20 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Fri, 7 Feb 2014 16:07:18 -0800 Subject: [PATCH] Closes #1382 --- docs/Reference.md | 25 ++++++-- lib/schema.js | 8 +-- lib/validation.js | 102 +++++++++++++++++++-------------- package.json | 2 +- test/integration/response.js | 38 +++++++++++- test/integration/validation.js | 30 ++++++++++ 6 files changed, 153 insertions(+), 52 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index a803afeab..ef1ffea53 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -447,18 +447,30 @@ The following options are available when adding a route: `request.query` prior to validation. Values allowed: - `true` - any query parameters allowed (no validation performed). This is the default. - `false` - no query parameters allowed. - - a validation rules object as described in the [Joi](http://github.com/spumko/joi) module. + - a [Joi](http://github.com/spumko/joi) validation object. + - a validation function using the signature `function(value, options, next)` where: + - `value` - the object containing the query parameters. + - `options` - the server validation options. + - `next(err)` - the callback function called when validation is completed. - `payload` - validation rules for an incoming request payload (request body). Values allowed: - `true` - any payload allowed (no validation performed). This is the default. - `false` - no payload allowed. - - a validation rules object as described in the [Joi](http://github.com/spumko/joi) module. + - a [Joi](http://github.com/spumko/joi) validation object. + - a validation function using the signature `function(value, options, next)` where: + - `value` - the object containing the payload object. + - `options` - the server validation options. + - `next(err)` - the callback function called when validation is completed. - `path` - validation rules for incoming request path parameters, after matching the path against the route and extracting any parameters then stored in `request.params`. Values allowed: - `true` - any path parameters allowed (no validation performed). This is the default. - `false` - no path variables allowed. - - a validation rules object as described in the [Joi](http://github.com/spumko/joi) module. + - a [Joi](http://github.com/spumko/joi) validation object. + - a validation function using the signature `function(value, options, next)` where: + - `value` - the object containing the path parameters. + - `options` - the server validation options. + - `next(err)` - the callback function called when validation is completed. - `errorFields` - an optional object with error fields copied into every validation error response. - `failAction` - determines how to handle invalid requests. Allowed values are: @@ -506,7 +518,12 @@ The following options are available when adding a route: - `true` - any payload allowed (no validation performed). This is the default. - `false` - no payload allowed. - an object with the following options: - - `schema` - the validation schema as described in the [Joi](http://github.com/spumko/joi) module. + - `schema` - the response object validation rules expressed as one of: + - a [Joi](http://github.com/spumko/joi) validation object. + - a validation function using the signature `function(value, options, next)` where: + - `value` - the object containing the response object. + - `options` - the server validation options. + - `next(err)` - the callback function called when validation is completed. - `sample` - the percent of responses validated (0 - 100). Set to `0` to disable all validation. Defaults to `100` (all responses). - `failAction` - defines what to do when a response fails validation. Options are: - `error` - return an Internal Server Error (500) error response. This is the default value. diff --git a/lib/schema.js b/lib/schema.js index ae4a15034..cce57a370 100755 --- a/lib/schema.js +++ b/lib/schema.js @@ -234,14 +234,14 @@ internals.routeConfigSchema = { Joi.string() ], validate: Joi.object({ - payload: Joi.object().allow(null, false, true), - query: Joi.object().allow(null, false, true), - path: Joi.object().allow(null, false, true), + payload: [Joi.object().allow(null, false, true), Joi.func()], + query: [Joi.object().allow(null, false, true), Joi.func()], + path: [Joi.object().allow(null, false, true), Joi.func()], failAction: [Joi.string().valid('error', 'log', 'ignore'), Joi.func()], errorFields: Joi.object() }), response: Joi.object({ - schema: Joi.object().allow(null), + schema: [Joi.object().allow(null), Joi.func()], sample: Joi.number().min(0).max(100), failAction: Joi.string().valid('error', 'log') }).allow(true, false), diff --git a/lib/validation.js b/lib/validation.js index a63a5274f..e81904312 100755 --- a/lib/validation.js +++ b/lib/validation.js @@ -40,52 +40,61 @@ internals.input = function (source, key, request, next) { return next(Boom.unsupportedMediaType(source + ' must represent an object')); } - var err = Joi.validate(request[key], request.route.validate[source] || {}, request.server.settings.validation); - if (!err) { - return next(); - } + var postValidate = function (err) { + + if (!err) { + return next(); + } - // failAction: 'error', 'log', 'ignore', function (source, err, next) + // failAction: 'error', 'log', 'ignore', function (source, err, next) - if (request.route.validate.failAction === 'ignore') { - return next(); - } + if (request.route.validate.failAction === 'ignore') { + return next(); + } - // Prepare error + // Prepare error - var error = Boom.badRequest(err.message); - error.output.payload.validation = { source: source, keys: [] }; - if (err._errors) { - for (var i = 0, il = err._errors.length; i < il; ++i) { - error.output.payload.validation.keys.push(err._errors[i].path); + var error = Boom.badRequest(err.message); + error.output.payload.validation = { source: source, keys: [] }; + if (err.details) { + for (var i = 0, il = err.details.length; i < il; ++i) { + error.output.payload.validation.keys.push(err.details[i].path); + } } - } - - if (request.route.validate.errorFields) { - var fields = Object.keys(request.route.validate.errorFields); - for (var f = 0, fl = fields.length; f < fl; ++f) { - var field = fields[f]; - error.output.payload[field] = request.route.validate.errorFields[field]; + + if (request.route.validate.errorFields) { + var fields = Object.keys(request.route.validate.errorFields); + for (var f = 0, fl = fields.length; f < fl; ++f) { + var field = fields[f]; + error.output.payload[field] = request.route.validate.errorFields[field]; + } } - } - request.log(['hapi', 'validation', 'error', source], error); + request.log(['hapi', 'validation', 'error', source], error); - // Log only + // Log only - if (request.route.validate.failAction === 'log') { - return next(); - } + if (request.route.validate.failAction === 'log') { + return next(); + } - // Custom handler + // Custom handler - if (typeof request.route.validate.failAction === 'function') { - return request.route.validate.failAction(source, error, next); - } + if (typeof request.route.validate.failAction === 'function') { + return request.route.validate.failAction(source, error, next); + } + + // Return error + + return next(error); + }; - // Return error + var schema = request.route.validate[source] || {}; + if (typeof schema === 'function') { + return schema(request[key], request.server.settings.validation, postValidate); + } - return next(error); + return Joi.validateCallback(request[key], schema, request.server.settings.validation, postValidate); }; @@ -110,17 +119,26 @@ exports.response = function (request, next) { return next(Boom.badImplementation('Cannot validate non-object response')); } - var error = Joi.validate(request.response.source, request.route.response.schema || {}, request.server.settings.validation); - if (!error) { - return next(); - } + var postValidate = function (err) { - // failAction: 'error', 'log' + if (!err) { + return next(); + } - if (request.route.response.failAction === 'log') { - request.log(['hapi', 'validation', 'error'], error.message); - return next(); + // failAction: 'error', 'log' + + if (request.route.response.failAction === 'log') { + request.log(['hapi', 'validation', 'error'], err.message); + return next(); + } + + return next(Boom.badImplementation(err.message)); + }; + + var schema = request.route.response.schema || {}; + if (typeof schema === 'function') { + return schema(request.response.source, request.server.settings.validation, postValidate); } - next(Boom.badImplementation(error.message)); + return Joi.validateCallback(request.response.source, schema, request.server.settings.validation, postValidate); }; \ No newline at end of file diff --git a/package.json b/package.json index 686e8745c..befcb0598 100755 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "dependencies": { "hoek": "^1.4.x", "boom": "2.x.x", - "joi": "^2.4.x", + "joi": "^2.7.x", "catbox": "1.x.x", "shot": "1.x.x", "nipple": "2.x.x", diff --git a/test/integration/response.js b/test/integration/response.js index 2640ab6ab..0b7a84c6e 100755 --- a/test/integration/response.js +++ b/test/integration/response.js @@ -533,7 +533,43 @@ describe('Response', function () { }; var server = new Hapi.Server({ debug: false }); - server.route({ method: 'GET', path: '/', config: { response: { schema: { some: Hapi.types.String() } } }, handler: handler }); + server.route({ method: 'GET', path: '/', config: { response: { schema: { some: Hapi.types.string() } } }, handler: handler }); + + server.inject('/', function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('{"some":"value"}'); + + server.inject('/', function (res) { + + expect(res.statusCode).to.equal(500); + done(); + }); + }); + }); + + it('validates response using custom validation function', function (done) { + + var i = 0; + var handler = function (request, reply) { + + reply({ some: i++ ? null : 'value' }); + }; + + var server = new Hapi.Server({ debug: false }); + server.route({ + method: 'GET', + path: '/', + config: { + response: { + schema: function (value, options, next) { + + return next(value.some === 'value' ? null : new Error('Bad response')); + } + } + }, + handler: handler + }); server.inject('/', function (res) { diff --git a/test/integration/validation.js b/test/integration/validation.js index 841a90a47..7f4f13e48 100755 --- a/test/integration/validation.js +++ b/test/integration/validation.js @@ -43,6 +43,36 @@ describe('Validation', function () { }); }); + it('validates using custom validator', function (done) { + + var server = new Hapi.Server(); + server.route({ + method: 'GET', + path: '/', + handler: function (request, reply) { reply('ok'); }, + config: { + validate: { + query: function (value, options, next) { + + return next(value.a === '123' ? null : new Error('Bad query')); + } + } + } + }); + + server.inject('/?a=123', function (res) { + + expect(res.statusCode).to.equal(200); + + server.inject('/?a=456', function (res) { + + expect(res.statusCode).to.equal(400); + expect(res.result.message).to.equal('Bad query'); + done(); + }); + }); + }); + it('casts input to desired type', function (done) { var server = new Hapi.Server();