praetorian
Advanced tools
Comparing version 0.0.2 to 0.0.3
/** | ||
* @name /.js | ||
* @name /lib/praetorian.js | ||
* @description Validates all inputs against a required structure and returns any errors that it finds | ||
@@ -10,36 +10,125 @@ * @since Fri Oct 04 12:20:21 BST 2013 | ||
// get some stuff we need to support Praetorian | ||
var _ = require( 'underscore' ), moment = require( 'moment' ); | ||
var praetorian = function() {}; | ||
var _ = require( 'lodash' ), moment = require( 'moment' ); | ||
var praetorian = function( options ) { | ||
// create a shorthand then export the Praetorian object for NodeJS | ||
// default options | ||
this.options = { | ||
automaticTypeConversion: true | ||
} | ||
// if there's any overriding options, blat them over the defaults | ||
if( _.isObject( options ) ) { | ||
this.options = _.extend( this.options, options ); | ||
} | ||
}; | ||
// create a shorthand then export the Praetorian object for Nodejs | ||
exports = module.exports = praetorian; | ||
praetorian.prototype.errors = []; | ||
praetorian.prototype.validate = function( json, schema, callback ) { | ||
praetorian.prototype.validate = function( data, structure, callback ) { | ||
// grab the options from the protoype and make them available inside this function | ||
var options = this.options; | ||
function check( field, data, message ) { | ||
// store some state for this check() call | ||
// types we accept | ||
var primitiveTypes = { | ||
ARRAY: 'array', | ||
BOOLEAN: 'boolean', | ||
DECIMAL: 'decimal', | ||
INTEGER: 'integer', | ||
OBJECT: 'object', | ||
STRING: 'string', | ||
DATE: 'date' // ISO 8601 | ||
} | ||
/* | ||
* An array of objects that define all the errors generated by the json against the schema | ||
*/ | ||
var errors = []; | ||
var messagInvalidTypeMustBeArray = 'Invalid type, expected Array'; | ||
var messagInvalidTypeMustBeObject = 'Invalid type, expected Object'; | ||
var addError = function( element, message ) { | ||
errors.push( { 'element': element, 'message': message } ); | ||
} | ||
// little closure to manage rule adherence | ||
var rules = function( data ) { | ||
// state for lifetime of the rules call (rules are chainable) | ||
var check = { | ||
hasError: false, | ||
field: field, | ||
data: data == null || ( isNaN( data ) && data.length == undefined ) ? '' : data + '', | ||
message: message | ||
data: data == null || ( isNaN( data ) && data.length == undefined ) ? '' : data, | ||
error: null | ||
} | ||
return { | ||
isInteger: function() { | ||
// console.log( 'isInteger() on ', check.data ); | ||
if( !_.isNumber( check.data ) ) { | ||
check.error = 'Not a number'; | ||
} | ||
return this; | ||
}, | ||
isString: function() { | ||
console.log( 'isString() check.data', check.data ); | ||
// if not a string... | ||
// maybe this should return stuff? | ||
// console.log( 'add an error to Praetorian', self.praetorian.errors.push( 'error' ) ); | ||
// console.log( 'isString() on ', check.data ); | ||
if( !_.isString( check.data ) ) { | ||
check.error = 'Not a string'; | ||
} | ||
return this; | ||
}, | ||
isInteger: function() { | ||
console.log( 'isInteger() check.data', check.data ); | ||
this.error = 'Not an integer'; | ||
isDate: function() { | ||
// console.log( 'isDate() on ', check.data ); | ||
// few checks on the date here: | ||
// 1. has the user passed in too many or too few days for the selected month/year combination (factors in leap years) | ||
// 2. is the month too high or low (>12 or <1) | ||
// 3. date must be YYYY-MM-DD format | ||
if( _.isString( check.data ) ) { | ||
var date = check.data.split( '-' ); | ||
// must have 3 parts | ||
if( date.length === 3 ) { | ||
var selectedYear = date[0]; | ||
// parseInt incase we have preceeding zeros (we don't want em) | ||
var selectedMonth = parseInt( date[1] ); | ||
var selectedDay = parseInt( date[2] ); | ||
// how long is the selected month? | ||
var daysInMonth = new Date( selectedYear, selectedMonth, 0 ).getDate(); | ||
// have we got too many days? | ||
if( selectedDay > daysInMonth ) { | ||
check.error = 'Too many days for month'; | ||
} else if( selectedDay < 1 ) { | ||
// have we got too few days? | ||
check.error ='Too few days for month'; | ||
} | ||
// check months is not greater than 12... | ||
if( selectedMonth > 12 ) { | ||
check.error = 'Month value too high'; | ||
} else if( selectedMonth < 1 ) { | ||
// and not less than 1... | ||
check.error = 'Month value too low'; | ||
} | ||
} else { | ||
// split on hyphen failed to produce 3 parts | ||
check.error = 'Invalid date format'; | ||
} | ||
} else { | ||
// not a string :/ | ||
check.error = 'Invalid date format'; | ||
} | ||
return this; | ||
}, | ||
go: function() { | ||
return this.error; | ||
lengthBetween: function( min, max ) { | ||
// console.log( 'lengthBetween() on ', check.data ); | ||
if ( check.data.length < min ) { | ||
check.error = 'Value is too short'; | ||
} else if ( ( max !== undefined ) && ( check.data.length > max ) ) { | ||
check.error = 'Value is too long'; | ||
} | ||
return this; | ||
}, | ||
execute: function( callback ) { | ||
callback( check.error ); | ||
} | ||
@@ -49,14 +138,155 @@ }; | ||
// if the schema says the value should be boolean, integer or decimal, make sure it is | ||
var forceType = function( schema, value ) { | ||
// by default, always pass back what came in so if we can't match it, stuff doesn't go missing | ||
var result = value; | ||
switch( schema.type ) { | ||
case 'boolean': | ||
// true booleans can stay as they are, if we match string bools, boolean them up bitches | ||
result = ( value === 'true' || value === 'false' ) ? !!value: value; | ||
break; | ||
case 'integer': | ||
result = !_.isNaN( parseInt( value ) ) ? parseInt( value ): value; | ||
break; | ||
case 'decimal': | ||
result = !_.isNaN( parseFloat( value ) ) ? parseFloat( value ): value; | ||
break; | ||
default: | ||
break; | ||
} | ||
return result; | ||
} | ||
// check all the fields that are "required" by the schema are present | ||
// if we hit a complex type (object, array), only recurse if data is passed for a sub-schema | ||
var requiredJson = function( json, schema, elementPrefix ){ | ||
// check json also matches the schema requirements | ||
_.each( schema, function( levelValue, levelKey ) { | ||
// is this field "required" (explicit true, don't want people passing no 1 or "true" shit) | ||
if( ( levelValue.required === true ) && ( _.isUndefined( json[levelKey] ) ) ) { | ||
// BOOO!, #fail | ||
addError( elementPrefix + levelKey, 'Required but not found' ); | ||
} else { | ||
// "required" is false or undefined, ignore and continue | ||
} | ||
// if the json actually has a value... | ||
if( json[levelKey] ) { | ||
// schema says type ARRAY and we have "items" or OBJECT and we have "properties" we continue | ||
if( ( !_.isUndefined( levelValue.items ) ) && ( levelValue.type === primitiveTypes.ARRAY ) && _.isArray( json[levelKey] ) ) { | ||
_.each( json[levelKey], function( jsonValue, jsonKey ) { | ||
requiredJson( jsonValue, levelValue.items, elementPrefix + levelKey + '[' + jsonKey + '].' ); | ||
} ); | ||
} else if( ( !_.isUndefined( levelValue.properties ) ) && ( levelValue.type === primitiveTypes.OBJECT ) && _.isObject( json[levelKey] ) ) { | ||
requiredJson( json[levelKey], levelValue.properties, elementPrefix + levelKey + '.' ); | ||
} | ||
} | ||
} ); | ||
} | ||
var parseJson = function( json, schema, schemaType, elementPrefix ) { | ||
// for this level, loop over all the json and see if its required by the schema | ||
_.each( json, function( levelValue, levelKey ) { | ||
// is it a node in the schema? | ||
if( !schema[levelKey] ) { | ||
// it's not, can it | ||
delete json[levelKey]; | ||
} else { | ||
// only convert types if the consumer is cool with that | ||
if( options.automaticTypeConversion ) { | ||
// type conversion for primitive types that might get passed as a string | ||
json[levelKey] = forceType( schema[levelKey], levelValue ); | ||
} | ||
// start doing stuff | ||
if( schema[levelKey].type === primitiveTypes.ARRAY ) { | ||
/* Array */ | ||
// check its ACTUALLY an array | ||
if( _.isArray( json[levelKey] ) ) { | ||
// loop each element in the array and parse those as individual objects against their schema | ||
_.each( json[levelKey], function( arrayElement, loopCount ) { | ||
parseJson( arrayElement, schema[levelKey].items, schema[levelKey].type, levelKey + '[' + loopCount + '].' ); | ||
} ); | ||
} else { | ||
// ERROR: this value should be an ARRAY but it's not (object of some other description?) | ||
addError( elementPrefix + levelKey, messagInvalidTypeMustBeArray ); | ||
} | ||
} else if( schema[levelKey].type === primitiveTypes.OBJECT ) { | ||
/* Object */ | ||
// must be an object (not null or undefined or anything silly) | ||
if( _.isObject( json[levelKey] ) && !_.isArray( json[levelKey] ) && !_.isNull( json[levelKey] ) && !_.isUndefined( json[levelKey] ) ) { | ||
parseJson( json[levelKey], schema[levelKey].properties, schema[levelKey].type, levelKey + '.' ); | ||
} else { | ||
// ERROR: this value should be an OBJECT but its an array | ||
addError( elementPrefix + levelKey, messagInvalidTypeMustBeObject ); | ||
} | ||
} else if( ( !_.isUndefined( schema[levelKey].validation ) ) && ( !_.isUndefined( schema[levelKey].validation.rules ) ) ) { | ||
// Handle anything else which has rules here (primitives? string, number...) | ||
_.each( schema[levelKey].validation.rules, function( rule, ruleKey ) { | ||
var validator = new rules( json[levelKey] ); | ||
switch( ruleKey ) { | ||
case 'type': | ||
// basic type checking | ||
switch( rule ) { | ||
case 'string': | ||
validator.isString(); | ||
break; | ||
case 'integer': | ||
validator.isInteger(); | ||
break; | ||
case 'date': | ||
validator.isDate(); | ||
break; | ||
} | ||
break; | ||
case 'lengthBetween': // max and min (inclusive) | ||
validator.lengthBetween( rule.min, rule.max ); | ||
break; | ||
default: | ||
// case not handled | ||
break; | ||
} | ||
// execute the validation routines and push any errors onto the Praetorian stack | ||
validator.execute( function( err ) { | ||
if( err ){ | ||
addError( elementPrefix + levelKey, err ); | ||
} | ||
} ); | ||
} ); | ||
} else { | ||
// no validation found, whatevs... | ||
} | ||
} | ||
} ); | ||
} | ||
try { | ||
// console.log( 'Praetorian.check()' ); | ||
// console.log( self.data ); | ||
// console.log( self.structure ); | ||
// ensure data and structure are both objects | ||
if( !_.isObject( data ) ) { | ||
throw new TypeError( 'parameter "data" is not [object]' ); | ||
// ensure json and schema are both objects | ||
if( !_.isObject( json ) ) { | ||
throw new TypeError( 'parameter "json" is not [object]' ); | ||
} | ||
if( !_.isObject( structure ) ) { | ||
throw new TypeError( 'parameter "structure" is not [object]' ); | ||
if( !_.isObject( schema ) ) { | ||
throw new TypeError( 'parameter "schema" is not [object]' ); | ||
} | ||
@@ -67,55 +297,41 @@ if( !_.isFunction( callback ) ) { | ||
// -1. Do we treat the whole thing as a stack? Or reusable "function" | ||
// #-1. Do we treat the whole thing as a stack? Or reusable "function" | ||
// 0. benchmark against Doorman for validating a data set against a structure | ||
// 1. remove all junk data (recursive) keys that dont exist in the structure | ||
// 2. ensure items that are present (if REQUIRED) and are of the correct type | ||
// #1. remove all junk data (recursive) keys that dont exist in the structure | ||
// #2. ensure items that are present (if REQUIRED) and are of the correct type | ||
// 3. check for ITEMS | ||
// 4. check for STRUCTURES | ||
// 5. what happens when we find an OBJECT? i.e. roomInfo on POST book | ||
// 6. deal with error handling (pass back up stack, modified data) | ||
// #5. what happens when we find an OBJECT? i.e. roomInfo on POST book | ||
// #6. deal with error handling (pass back up stack, modified data) | ||
// 7. individual validation routines | ||
// 8. config for Praetorian, build Doorman config in to somewhere but can be overridable :/ | ||
// 9. isValidDate, isNumber etc, can we use Underscore (isFinite)? | ||
// 10. Identical objects are equal. `0 === -0`, but they aren't identical. | ||
// #10. Identical objects are equal. `0 === -0`, but they aren't identical. | ||
// 11. better error messageing structure (consolidated) | ||
// 12. requirements() for outputting errors from a stack | ||
// 13. figure out what sort of return structures we will provide | ||
// #13. figure out what sort of return structures we will provide | ||
// #14. what is the difference between ARRAY and ITEMS | ||
// #15. list types, OBJECT ARRAY, STRUCTURE, ITEMS, CHECKSUM | ||
// #16. in Revolver, book roomInfo is ARRAY but is passed as [object]... | ||
// 17. requirements needs to recurse (see test/index.js test) | ||
// #18. transpose underscore to lodash | ||
// 19. turn off default type conversion | ||
// 20. rules passed in on construction but don't sit inside Praetorian module, basic types should be basked in + date | ||
// 21. type should always be checked as a validation routine itself, validation then works afterwards | ||
// 22. internationalisation, abstract errors to a lib GB and default settings go to GB | ||
// #23. why does index.js throw 2 errors [location && dave] only when new field "dave" was added? | ||
// console.log( 'this', this ); | ||
// loop over all data and remove anything that isnt required | ||
_.each( data, function( data, dataKey ) { | ||
if( _.isUndefined( structure[dataKey] ) ) { | ||
// console.log( dataKey + ' is not a needed, data binned' ); | ||
// the key is not required at this level, remove it | ||
delete data[dataKey]; | ||
} else if( ( !_.isUndefined( structure[dataKey].validation ) ) && ( !_.isUndefined( structure[dataKey].validation.rules ) ) ) { | ||
// console.log( dataKey + ' is present but no suggested validation' ); | ||
// console.log( 'rulez', structure[dataKey].validation.rules , data ); | ||
// push these onto an error stack?? | ||
console.log( 'go()', check( dataKey, data ).isString().isInteger().go() ); | ||
parseJson( json, schema, undefined, '' ); | ||
} else { | ||
// "required" but no validation prescribed | ||
} | ||
requiredJson( json, schema, '' ); | ||
} ); | ||
// console.log( 'final self.hasProblem', this.praetorian.errors ); | ||
// check we have all required fields | ||
_.each( structure, function( structure, structureKey ) { | ||
// is this field "required" | ||
if( ( structure['required'] ) && ( _.isUndefined( data[structureKey] ) ) ) { | ||
// BOOO!, #fail | ||
throw "get yo shit together"; | ||
} else { | ||
// "required" is false or undefined, ignore and continue | ||
} | ||
} ); | ||
// send the decent data back | ||
callback( null, data ); | ||
if( errors.length > 0 ) { | ||
// send the errors back | ||
callback( errors ); | ||
} else { | ||
// send the decent json back | ||
callback( null, json ); | ||
} | ||
} catch ( err ) { | ||
@@ -134,3 +350,3 @@ | ||
praetorian.prototype.requirements = function( structure, callback ) { | ||
praetorian.prototype.requirements = function( schema, callback ) { | ||
@@ -140,6 +356,6 @@ try { | ||
var results = []; | ||
if( structure == null ) return results; | ||
if( schema == null ) return results; | ||
function addRequirement( message, field ) { | ||
// returns the field "requirement" details in a consistent structure | ||
// returns the field "requirement" details in a consistent schema | ||
return { | ||
@@ -152,4 +368,4 @@ 'example': message, | ||
if( !_.isObject( structure ) ) { | ||
throw new TypeError( 'parameter "structure" is not [object]' ); | ||
if( !_.isObject( schema ) ) { | ||
throw new TypeError( 'parameter "schema" is not [object]' ); | ||
} | ||
@@ -160,3 +376,3 @@ if( !_.isFunction( callback ) ) { | ||
_.each( structure, function( field, fieldKey ) { | ||
_.each( schema, function( field, fieldKey ) { | ||
// add some details to the requirements | ||
@@ -163,0 +379,0 @@ results[fieldKey] = addRequirement( ( field.validation && field.validation.example ) ? field.validation.example : 'No example given', field ); |
{ | ||
"name": "praetorian", | ||
"version": "0.0.2", | ||
"description": "A structured JSON parser and validator", | ||
"main": "index.js", | ||
"directories": { | ||
"test": "./test", | ||
"lib": "./lib" | ||
}, | ||
"dependencies": { | ||
"underscore": "1.5.x", | ||
"moment": "2.0.0" | ||
}, | ||
"devDependencies": { | ||
"vows": "0.7.0" | ||
}, | ||
"author": { | ||
"name": "Kevin Hodges", | ||
"email": "kevin.hodges@holidayextras.com" | ||
}, | ||
"license": "MIT" | ||
"name": "praetorian", | ||
"version": "0.0.3", | ||
"description": "A structured JSON parser and validator", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"directories": { | ||
"test": "./test", | ||
"lib": "./lib" | ||
}, | ||
"dependencies": { | ||
"moment": "2.0.0", | ||
"lodash": "~2.3.0" | ||
}, | ||
"devDependencies": { | ||
"vows": "0.7.0" | ||
}, | ||
"author": { | ||
"name": "Kevin Hodges", | ||
"email": "kevin.hodges@holidayextras.com" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "http://github.com/kevinhodges/praetorian.git" | ||
} | ||
} |
Praetorian | ||
========== | ||
== | ||
Praetorian is a structured JSON validator. It take both a JSON data set and a structure and tells you if anything, whats wrong with it. | ||
Version | ||
-- | ||
0.0.2 (in development) | ||
Installation | ||
-- | ||
```sh | ||
$ npm install praetorian | ||
``` | ||
Schema | ||
-- | ||
Example: | ||
```sh | ||
{ | ||
"shield": { | ||
"required": true | ||
"validation": { | ||
} | ||
}, | ||
"weapon": { | ||
"required": true | ||
"type": "array", | ||
"items": { | ||
"sword": { | ||
"validation": { | ||
} | ||
}, | ||
"dagger": { | ||
"required": true | ||
} | ||
} | ||
}, | ||
"helmet": { | ||
"type": "object", | ||
"properties": { | ||
"noseGuard": { | ||
"required": true | ||
}, | ||
"chinStrap": { | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
Notes: | ||
* By specifying "type", Praetorian will automatically cast values | ||
* Objects have "properties" | ||
* Arrays have "items" | ||
* Validation configuration should be used to ensure any type dependencies are met | ||
Usage | ||
-- | ||
```sh | ||
var Praetorian = require( '../index' ); | ||
praetorian = new Praetorian(); | ||
// pass your data and structure in like this | ||
praetorian.validate( data, structure, function( err, data ) { | ||
if( err ) { | ||
console.log( 'check err', err ); | ||
// requirements will tell you for the passed structure | ||
// how to fulfill the validation | ||
praetorian.requirements( structure, function( err, data ) { | ||
if( err ) { | ||
// console.log( 'requirements err', err ); | ||
} else { | ||
// console.log( 'requirements success', data ); | ||
} | ||
} ); | ||
} else { | ||
console.log( 'check success', data ); | ||
} | ||
} ); | ||
``` | ||
Testing | ||
-- | ||
To run the test harness do the following: | ||
```sh | ||
cd praetorian | ||
node test/sanity.js | ||
``` | ||
License | ||
-- | ||
[MIT](http://en.wikipedia.org/wiki/MIT_License "MIT") |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
17596
95
0
368
+ Addedlodash@~2.3.0
+ Addedlodash@2.3.0(transitive)
- Removedunderscore@1.5.x
- Removedunderscore@1.5.2(transitive)