Skip to content

Commit

Permalink
BREAKING; custom output validation per resp status
Browse files Browse the repository at this point in the history
see #8
  • Loading branch information
aheckmann committed Nov 21, 2015
1 parent 833a005 commit 150b47d
Show file tree
Hide file tree
Showing 5 changed files with 683 additions and 49 deletions.
37 changes: 13 additions & 24 deletions joi-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var parse = require('co-body');
var Joi = require('joi');
var slice = require('sliced');
var delegate = require('delegates');
var OutputValidator = require('./output-validator');

module.exports = Router;

Expand Down Expand Up @@ -216,6 +217,10 @@ function checkValidators(spec) {
assert(/json|form|multipart|stream/i.test(spec.validate.type), text);
}

if (spec.validate.output) {
spec.validate._outputValidator = new OutputValidator(spec.validate.output);
}

// default HTTP status code for failures
if (!spec.validate.failure) {
spec.validate.failure = 400;
Expand Down Expand Up @@ -325,9 +330,14 @@ function makeValidator(spec) {

yield* next;

if (spec.validate.output) {
err = validateOutput(this, spec);
if (err) return this.throw(err);
if (spec.validate._outputValidator) {
debug('validating output');

err = spec.validate._outputValidator.validate(this);
if (err) {
err.status = 500;
return this.throw(err);
}
}
};
}
Expand Down Expand Up @@ -374,27 +384,6 @@ function validateInput(prop, request, validate) {
}
}

/**
* Validates output data with the defined validation schema.
*
* @param {koa context} ctx
* @param {Object} spec
* @api private
*/

function validateOutput(ctx, spec) {
debug('validating output');

var res = Joi.validate(ctx.body, spec.validate.output);
if (res.error) {
res.error.status = 500;
return res.error;
}

// update request w/ the casted values
ctx.body = res.value;
}

/**
* Routing shortcuts for all HTTP methods
*
Expand Down
128 changes: 128 additions & 0 deletions output-validation-rule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
'use strict';

var assert = require('assert');
var Joi = require('joi');

module.exports = OutputValidationRule;

function OutputValidationRule(status, spec) {
assert(status, 'OutputValidationRule: missing status param');
assert(spec, 'OutputValidationRule: missing spec param');

this.ranges = status.split(',').map(trim).filter(Boolean).map(rangify);
this.status = status;
this.spec = spec;

this.validateInput();
}

/**
* Validates it's input values
*
* @throws Error
*/

OutputValidationRule.prototype.validateInput = function validateInput() {
assert(/^[0-9,\-]+|\*$/.test(this.status),
'output validation key: "' + this.status + '" must match "0-9,-*"'
);

var ok = this.spec.body || this.spec.headers;
if (ok) return;

throw new Error('output validation key: ' + this.status +
' must have either a body or headers validator specified');
};

OutputValidationRule.prototype.toString = function toString() {
return this.status;
};

/**
* Determines if this rule has overlapping logic
* with `ruleB`.
*
* @returns Boolean
*/

OutputValidationRule.prototype.overlaps = function overlaps(ruleB) {
return OutputValidationRule.overlaps(this, ruleB);
};

/**
* Checks if this rule should be run against the
* given `ctx` response data.
*
* @returns Boolean
*/

OutputValidationRule.prototype.matches = function matches(ctx) {
for (var i = 0; i < this.ranges.length; ++i) {
var range = this.ranges[i];
if (ctx.status >= range.lower && ctx.status <= range.upper) {
return true;
}
}

return false;
};

/**
* Validates this rule against the given `ctx`.
*/

OutputValidationRule.prototype.validateOutput = function validateOutput(ctx) {
var result;

if (this.spec.headers) {
result = Joi.validate(ctx.response.headers, this.spec.headers);
if (result.error) return result.error;
// use casted values
ctx.set(result.value);
}

if (this.spec.body) {
result = Joi.validate(ctx.body, this.spec.body);
if (result.error) return result.error;
// use casted values
ctx.body = result.value;
}
};

// static

/**
* Determines if ruleA has overlapping logic
* with `ruleB`.
*
* @returns Boolean
*/

OutputValidationRule.overlaps = function overlaps(a, b) {
return a.ranges.some(function checkRangeA(rangeA) {
return b.ranges.some(function checkRangeB(rangeB) {
if (rangeA.upper >= rangeB.lower && rangeA.lower <= rangeB.upper) {
return true;
}
return false;
});
});
};

// helpers

function trim(s) {
return s.trim();
}

function rangify(rule) {
if (rule === '*') {
return { lower: 0, upper: Infinity };
}

var parts = rule.split('-');
var lower = parts[0];
var upper = parts.length > 1 ? parts[1] : lower;

return { lower: parseInt(lower, 10), upper: parseInt(upper, 10) };
}
50 changes: 50 additions & 0 deletions output-validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

var OutputValidationRule = require('./output-validation-rule');
var assert = require('assert');

module.exports = OutputValidator;

function OutputValidator(output) {
assert.equal('object', typeof output, 'spec.validate.output must be an object');

this.rules = OutputValidator.tokenizeRules(output);
OutputValidator.assertNoOverlappingStatusRules(this.rules);

this.output = output;
}

OutputValidator.tokenizeRules = function tokenizeRules(output) {
return Object.keys(output).map(function createRule(status) {
return new OutputValidationRule(status, output[status]);
});
};

OutputValidator.assertNoOverlappingStatusRules =
function assertNoOverlappingStatusRules(rules) {
for (var i = 0; i < rules.length; ++i) {
var ruleA = rules[i];

for (var j = 0; j < rules.length; ++j) {
if (i === j) continue;

var ruleB = rules[j];
if (ruleA.overlaps(ruleB)) {
throw new Error(
'Output validation rules may not overlap: ' + ruleA + ' <=> ' + ruleB
);
}
}
}
};

OutputValidator.prototype.validate = function(ctx) {
assert(ctx, 'missing request context!');

for (var i = 0; i < this.rules.length; ++i) {
var rule = this.rules[i];
if (rule.matches(ctx)) {
return rule.validateOutput(ctx);
}
}
};

0 comments on commit 150b47d

Please sign in to comment.