-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
BREAKING; custom output validation per resp status
see #8
- Loading branch information
Showing
5 changed files
with
683 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
}; |
Oops, something went wrong.