Skip to content

Commit

Permalink
huge refactor, added #similar
Browse files Browse the repository at this point in the history
  • Loading branch information
tjwebb committed Apr 26, 2014
1 parent 99823e7 commit 1a996f4
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 448 deletions.
221 changes: 70 additions & 151 deletions index.js
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
]);
}
Loading

0 comments on commit 1a996f4

Please sign in to comment.