Skip to content

Commit

Permalink
Closes #1382
Browse files Browse the repository at this point in the history
  • Loading branch information
Eran Hammer committed Feb 8, 2014
1 parent 0c3cf12 commit 1c5d485
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 52 deletions.
25 changes: 21 additions & 4 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
102 changes: 60 additions & 42 deletions lib/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};


Expand All @@ -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);
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 37 additions & 1 deletion test/integration/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down
30 changes: 30 additions & 0 deletions test/integration/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 1c5d485

Please sign in to comment.