import t from 'should-type'; function format(msg) { var args = arguments; for (var i = 1, l = args.length; i < l; i++) { msg = msg.replace(/%s/, args[i]); } return msg; } var hasOwnProperty = Object.prototype.hasOwnProperty; function EqualityFail(a, b, reason, path) { this.a = a; this.b = b; this.reason = reason; this.path = path; } function typeToString(tp) { return tp.type + (tp.cls ? "(" + tp.cls + (tp.sub ? " " + tp.sub : "") + ")" : ""); } var PLUS_0_AND_MINUS_0 = "+0 is not equal to -0"; var DIFFERENT_TYPES = "A has type %s and B has type %s"; var EQUALITY = "A is not equal to B"; var EQUALITY_PROTOTYPE = "A and B have different prototypes"; var WRAPPED_VALUE = "A wrapped value is not equal to B wrapped value"; var FUNCTION_SOURCES = "function A is not equal to B by source code value (via .toString call)"; var MISSING_KEY = "%s has no key %s"; var SET_MAP_MISSING_KEY = "Set/Map missing key %s"; var DEFAULT_OPTIONS = { checkProtoEql: true, checkSubType: true, plusZeroAndMinusZeroEqual: true, collectAllFails: false }; function setBooleanDefault(property, obj, opts, defaults) { obj[property] = typeof opts[property] !== "boolean" ? defaults[property] : opts[property]; } var METHOD_PREFIX = "_check_"; function EQ(opts, a, b, path) { opts = opts || {}; setBooleanDefault("checkProtoEql", this, opts, DEFAULT_OPTIONS); setBooleanDefault("plusZeroAndMinusZeroEqual", this, opts, DEFAULT_OPTIONS); setBooleanDefault("checkSubType", this, opts, DEFAULT_OPTIONS); setBooleanDefault("collectAllFails", this, opts, DEFAULT_OPTIONS); this.a = a; this.b = b; this._meet = opts._meet || []; this.fails = opts.fails || []; this.path = path || []; } function ShortcutError(fail) { this.name = "ShortcutError"; this.message = "fail fast"; this.fail = fail; } ShortcutError.prototype = Object.create(Error.prototype); EQ.checkStrictEquality = function(a, b) { this.collectFail(a !== b, EQUALITY); }; EQ.add = function add(type, cls, sub, f) { var args = Array.prototype.slice.call(arguments); f = args.pop(); EQ.prototype[METHOD_PREFIX + args.join("_")] = f; }; EQ.prototype = { check: function() { try { this.check0(); } catch (e) { if (e instanceof ShortcutError) { return [e.fail]; } throw e; } return this.fails; }, check0: function() { var a = this.a; var b = this.b; // equal a and b exit early if (a === b) { // check for +0 !== -0; return this.collectFail(a === 0 && 1 / a !== 1 / b && !this.plusZeroAndMinusZeroEqual, PLUS_0_AND_MINUS_0); } var typeA = t(a); var typeB = t(b); // if objects has different types they are not equal if (typeA.type !== typeB.type || typeA.cls !== typeB.cls || typeA.sub !== typeB.sub) { return this.collectFail(true, format(DIFFERENT_TYPES, typeToString(typeA), typeToString(typeB))); } // as types the same checks type specific things var name1 = typeA.type, name2 = typeA.type; if (typeA.cls) { name1 += "_" + typeA.cls; name2 += "_" + typeA.cls; } if (typeA.sub) { name2 += "_" + typeA.sub; } var f = this[METHOD_PREFIX + name2] || this[METHOD_PREFIX + name1] || this[METHOD_PREFIX + typeA.type] || this.defaultCheck; f.call(this, this.a, this.b); }, collectFail: function(comparison, reason, showReason) { if (comparison) { var res = new EqualityFail(this.a, this.b, reason, this.path); res.showReason = !!showReason; this.fails.push(res); if (!this.collectAllFails) { throw new ShortcutError(res); } } }, checkPlainObjectsEquality: function(a, b) { // compare deep objects and arrays // stacks contain references only // var meet = this._meet; var m = this._meet.length; while (m--) { var st = meet[m]; if (st[0] === a && st[1] === b) { return; } } // add `a` and `b` to the stack of traversed objects meet.push([a, b]); // TODO maybe something else like getOwnPropertyNames var key; for (key in b) { if (hasOwnProperty.call(b, key)) { if (hasOwnProperty.call(a, key)) { this.checkPropertyEquality(key); } else { this.collectFail(true, format(MISSING_KEY, "A", key)); } } } // ensure both objects have the same number of properties for (key in a) { if (hasOwnProperty.call(a, key)) { this.collectFail(!hasOwnProperty.call(b, key), format(MISSING_KEY, "B", key)); } } meet.pop(); if (this.checkProtoEql) { //TODO should i check prototypes for === or use eq? this.collectFail(Object.getPrototypeOf(a) !== Object.getPrototypeOf(b), EQUALITY_PROTOTYPE, true); } }, checkPropertyEquality: function(propertyName) { var _eq = new EQ(this, this.a[propertyName], this.b[propertyName], this.path.concat([propertyName])); _eq.check0(); }, defaultCheck: EQ.checkStrictEquality }; EQ.add(t.NUMBER, function(a, b) { this.collectFail((a !== a && b === b) || (b !== b && a === a) || (a !== b && a === a && b === b), EQUALITY); }); [t.SYMBOL, t.BOOLEAN, t.STRING].forEach(function(tp) { EQ.add(tp, EQ.checkStrictEquality); }); EQ.add(t.FUNCTION, function(a, b) { // functions are compared by their source code this.collectFail(a.toString() !== b.toString(), FUNCTION_SOURCES); // check user properties this.checkPlainObjectsEquality(a, b); }); EQ.add(t.OBJECT, t.REGEXP, function(a, b) { // check regexp flags var flags = ["source", "global", "multiline", "lastIndex", "ignoreCase", "sticky", "unicode"]; while (flags.length) { this.checkPropertyEquality(flags.shift()); } // check user properties this.checkPlainObjectsEquality(a, b); }); EQ.add(t.OBJECT, t.DATE, function(a, b) { //check by timestamp only (using .valueOf) this.collectFail(+a !== +b, EQUALITY); // check user properties this.checkPlainObjectsEquality(a, b); }); [t.NUMBER, t.BOOLEAN, t.STRING].forEach(function(tp) { EQ.add(t.OBJECT, tp, function(a, b) { //primitive type wrappers this.collectFail(a.valueOf() !== b.valueOf(), WRAPPED_VALUE); // check user properties this.checkPlainObjectsEquality(a, b); }); }); EQ.add(t.OBJECT, function(a, b) { this.checkPlainObjectsEquality(a, b); }); [t.ARRAY, t.ARGUMENTS, t.TYPED_ARRAY].forEach(function(tp) { EQ.add(t.OBJECT, tp, function(a, b) { this.checkPropertyEquality("length"); this.checkPlainObjectsEquality(a, b); }); }); EQ.add(t.OBJECT, t.ARRAY_BUFFER, function(a, b) { this.checkPropertyEquality("byteLength"); this.checkPlainObjectsEquality(a, b); }); EQ.add(t.OBJECT, t.ERROR, function(a, b) { this.checkPropertyEquality("name"); this.checkPropertyEquality("message"); this.checkPlainObjectsEquality(a, b); }); EQ.add(t.OBJECT, t.BUFFER, function(a) { this.checkPropertyEquality("length"); var l = a.length; while (l--) { this.checkPropertyEquality(l); } //we do not check for user properties because //node Buffer have some strange hidden properties }); function checkMapByKeys(a, b) { var iteratorA = a.keys(); for (var nextA = iteratorA.next(); !nextA.done; nextA = iteratorA.next()) { var key = nextA.value; var hasKey = b.has(key); this.collectFail(!hasKey, format(SET_MAP_MISSING_KEY, key)); if (hasKey) { var valueB = b.get(key); var valueA = a.get(key); eq(valueA, valueB, this); } } } function checkSetByKeys(a, b) { var iteratorA = a.keys(); for (var nextA = iteratorA.next(); !nextA.done; nextA = iteratorA.next()) { var key = nextA.value; var hasKey = b.has(key); this.collectFail(!hasKey, format(SET_MAP_MISSING_KEY, key)); } } EQ.add(t.OBJECT, t.MAP, function(a, b) { this._meet.push([a, b]); checkMapByKeys.call(this, a, b); checkMapByKeys.call(this, b, a); this._meet.pop(); this.checkPlainObjectsEquality(a, b); }); EQ.add(t.OBJECT, t.SET, function(a, b) { this._meet.push([a, b]); checkSetByKeys.call(this, a, b); checkSetByKeys.call(this, b, a); this._meet.pop(); this.checkPlainObjectsEquality(a, b); }); function eq(a, b, opts) { return new EQ(opts, a, b).check(); } eq.EQ = EQ; export default eq;