-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
tjwebb
committed
Apr 26, 2014
1 parent
99823e7
commit 1a996f4
Showing
2 changed files
with
106 additions
and
448 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,166 +1,85 @@ | ||
(function () { | ||
'use strict'; | ||
var _ = require('lodash'); | ||
var fnv = require('fnv-plus'); | ||
|
||
var _ = require('lodash'); | ||
var fnv = require('fnv-plus'); | ||
/** | ||
* The congruence API. | ||
* @module congruence | ||
*/ | ||
var congruence = exports; | ||
|
||
_.mixin({ | ||
|
||
/** | ||
* Compute a hash of an object's keys. | ||
*/ | ||
essentialize: function (val) { | ||
return _.isPlainObject(val) ? fnv.hash(JSON.stringify(_.keys(val))).str() : val; | ||
}, | ||
|
||
not: function (val) { | ||
return !val; | ||
} | ||
}); | ||
_.extend(congruence, /** @exports congruence */ { | ||
|
||
/** | ||
* The congruence API. | ||
* @module congruence | ||
* Returns true if an object is congruent to the specified template. | ||
* | ||
* @static | ||
* @param template {Object} - the congruence template to test the object against | ||
* @param object {Object} - the object to test | ||
* @returns true if congruent, false otherwise | ||
*/ | ||
var congruence = exports; | ||
|
||
_.extend(congruence, /** @exports congruence */ { | ||
|
||
/** | ||
* Returns true if an object matches a template. | ||
* | ||
* @static | ||
* @param template {Object} - the congruence template to test the object against | ||
* @param object {Object} - the object to test | ||
* @param errors {Array=} - an optional array that will be populated if any errors occur | ||
* @returns true if congruent, false otherwise | ||
*/ | ||
congruent: function(template, object, _errors) { | ||
var errors = _errors || [ ]; | ||
|
||
if (!_.isPlainObject(object)) { | ||
logError(errors, '\'object\' must be a valid js object'); | ||
} | ||
if (!_.isPlainObject(template)) { | ||
logError(errors, '\'template\' must be a valid js object'); | ||
} | ||
return !errors.length && _testSubtree(_.clone(template), object, errors); | ||
}, | ||
|
||
/** | ||
* Returns true if the value is validated by any one of the provided | ||
* predicate functions. | ||
* | ||
* @static | ||
* @param {...Function} - the OR condition operands | ||
*/ | ||
or: function () { | ||
var predicates = _.toArray(arguments); | ||
return function or (value, errors) { | ||
return _.any(predicates, function (predicate) { | ||
return _testSubtree(predicate, value, errors); | ||
}); | ||
}; | ||
} | ||
}); | ||
congruent: function(template, object) { | ||
var valid = validateArguments(template, object); | ||
if (valid) return valid; | ||
|
||
/** | ||
* Recurse into a subtree and test each node against the template. | ||
* @private | ||
*/ | ||
function _testSubtree (templateNode, objectNode, errors) { | ||
var templateKeys = _.keys(template), | ||
objectKeys = _.keys(object); | ||
|
||
// a leaf is reached | ||
if (!_.isPlainObject(templateNode) || !_.isPlainObject(objectNode)) { | ||
return _testNode(templateNode, objectNode, errors); | ||
if (_.intersection(objectKeys, templateKeys).length !== objectKeys.length) { | ||
return false; | ||
} | ||
|
||
var templateKeys = _.keys(templateNode); | ||
|
||
return _.all(_.map( | ||
_.reject(_.union(templateKeys, _.keys(objectNode)), function (key) { | ||
return (!optional(key) && _.contains(templateKeys, '(?)' + key)) || key === '(+)'; | ||
}), | ||
function (_key) { | ||
var subtree, key, oldlen = errors.length; | ||
|
||
if (optional(_key)) { | ||
key = normalizeOptional(_key); | ||
if (_.isUndefined(objectNode[key])) { | ||
// ignore an optional key with no value | ||
return true; | ||
} | ||
} | ||
else { | ||
key = _key; | ||
} | ||
subtree = _testSubtree(templateNode[_key], objectNode[key], errors); | ||
if (!subtree) { | ||
if (_.has(templateNode, '(+)')) { | ||
subtree = _testSubtree(templateNode['(+)'], objectNode[key], errors); | ||
errors.splice(-1, (errors.length - oldlen)); | ||
} | ||
else { | ||
return false; | ||
} | ||
} | ||
return subtree; | ||
} | ||
)); | ||
} | ||
return _.all(templateKeys, function (key) { | ||
return visitNode(template[key], object[key]); | ||
}); | ||
}, | ||
|
||
/** | ||
* Test a node against a particular predicate function or value. | ||
* @private | ||
* Returns true if an object is similar to the specified template. | ||
* | ||
* @static | ||
* @param template {Object} - the congruence template to test the object against | ||
* @param object {Object} - the object to test | ||
* @returns true if similar, false otherwise | ||
*/ | ||
function _testNode (predicate, value, errors) { | ||
return _testPredicate(predicate, value, errors); | ||
similar: function (template, object) { | ||
var valid = validateArguments(template, object); | ||
if (valid) return valid; | ||
|
||
return _.all(_.keys(template), function (key) { | ||
return visitNode(template[key], object[key]); | ||
}); | ||
}, | ||
|
||
/** Compute a hash of an object's keys. */ | ||
essentialize: function (val) { | ||
return _.isObject(val) ? fnv.hash(JSON.stringify(_.keys(val))).str() : val; | ||
}, | ||
|
||
not: function (val) { | ||
return !val; | ||
} | ||
|
||
/** | ||
* Test a value/predicate combo, and report any errors. | ||
*/ | ||
function _testPredicate(predicate, value, errors) { | ||
var result = _.not(_.isUndefined(predicate)) && _.any([ | ||
_.isRegExp(predicate) && predicate.test(value), | ||
_.isFunction(predicate) && predicate(value, [ ]), | ||
predicate === value | ||
]); | ||
|
||
if (result) return true; | ||
|
||
if (_.isUndefined(predicate)) { | ||
logError(errors, 'no match for ' + JSON.stringify(value)); | ||
} | ||
else if (_.isPlainObject(predicate) && !_.isPlainObject(value)) { | ||
logError(errors, 'expected (' + value + ') to be an object'); | ||
} | ||
else if (_.isRegExp(predicate) && !predicate.test(value)) { | ||
logError(errors, 'expected ' + predicate + ' to match ' + value); | ||
} | ||
else if (_.isFunction(predicate) && !predicate(value, errors)) { | ||
var f = predicate.name || _.find(_.functions(_), function (name) { | ||
return _[name] == predicate; | ||
}); | ||
if (value && !_.contains([ 'or', 'not' ], f)) { | ||
logError(errors, (f || 'anonymous') + '(' + value + ') returned false'); | ||
} | ||
} | ||
else if (predicate !== value) { | ||
logError(errors, 'expected (' + predicate + ') to equal ' + value); | ||
} | ||
|
||
return false; | ||
} | ||
|
||
function optional (key) { | ||
return (/^\(\?\)/).test(key); | ||
} | ||
function normalizeOptional (key) { | ||
return key.slice().replace('(?)', ''); | ||
}); | ||
|
||
/** | ||
* Validate the required arguments for the congruence API | ||
*/ | ||
function validateArguments (template, object, method) { | ||
if (!_.isPlainObject(template)) { | ||
throw new TypeError('template must be a valid js object'); | ||
} | ||
function logError (list, err) { | ||
if (!_.contains(list, err)) list.push(err); | ||
else if (!_.isPlainObject(object)) { | ||
return _.curry(congruence.similar)(template); | ||
} | ||
|
||
})(); | ||
} | ||
|
||
/** | ||
* Visit a node in the object graph, and return true if the predicate | ||
* is valid. | ||
*/ | ||
function visitNode (templateNode, objectNode) { | ||
return _.any([ | ||
_.isFunction(templateNode) && templateNode(objectNode), | ||
_.isRegExp(templateNode) && templateNode.test(objectNode), | ||
templateNode === objectNode | ||
]); | ||
} |
Oops, something went wrong.