should-equal.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. 'use strict';
  2. function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
  3. var t = _interopDefault(require('should-type'));
  4. function format(msg) {
  5. var args = arguments;
  6. for (var i = 1, l = args.length; i < l; i++) {
  7. msg = msg.replace(/%s/, args[i]);
  8. }
  9. return msg;
  10. }
  11. var hasOwnProperty = Object.prototype.hasOwnProperty;
  12. function EqualityFail(a, b, reason, path) {
  13. this.a = a;
  14. this.b = b;
  15. this.reason = reason;
  16. this.path = path;
  17. }
  18. function typeToString(tp) {
  19. return tp.type + (tp.cls ? '(' + tp.cls + (tp.sub ? ' ' + tp.sub : '') + ')' : '');
  20. }
  21. var PLUS_0_AND_MINUS_0 = '+0 is not equal to -0';
  22. var DIFFERENT_TYPES = 'A has type %s and B has type %s';
  23. var EQUALITY = 'A is not equal to B';
  24. var EQUALITY_PROTOTYPE = 'A and B have different prototypes';
  25. var WRAPPED_VALUE = 'A wrapped value is not equal to B wrapped value';
  26. var FUNCTION_SOURCES = 'function A is not equal to B by source code value (via .toString call)';
  27. var MISSING_KEY = '%s has no key %s';
  28. var SET_MAP_MISSING_KEY = 'Set/Map missing key %s';
  29. var DEFAULT_OPTIONS = {
  30. checkProtoEql: true,
  31. checkSubType: true,
  32. plusZeroAndMinusZeroEqual: true,
  33. collectAllFails: false
  34. };
  35. function setBooleanDefault(property, obj, opts, defaults) {
  36. obj[property] = typeof opts[property] !== 'boolean' ? defaults[property] : opts[property];
  37. }
  38. var METHOD_PREFIX = '_check_';
  39. function EQ(opts, a, b, path) {
  40. opts = opts || {};
  41. setBooleanDefault('checkProtoEql', this, opts, DEFAULT_OPTIONS);
  42. setBooleanDefault('plusZeroAndMinusZeroEqual', this, opts, DEFAULT_OPTIONS);
  43. setBooleanDefault('checkSubType', this, opts, DEFAULT_OPTIONS);
  44. setBooleanDefault('collectAllFails', this, opts, DEFAULT_OPTIONS);
  45. this.a = a;
  46. this.b = b;
  47. this._meet = opts._meet || [];
  48. this.fails = opts.fails || [];
  49. this.path = path || [];
  50. }
  51. function ShortcutError(fail) {
  52. this.name = 'ShortcutError';
  53. this.message = 'fail fast';
  54. this.fail = fail;
  55. }
  56. ShortcutError.prototype = Object.create(Error.prototype);
  57. EQ.checkStrictEquality = function(a, b) {
  58. this.collectFail(a !== b, EQUALITY);
  59. };
  60. EQ.add = function add(type, cls, sub, f) {
  61. var args = Array.prototype.slice.call(arguments);
  62. f = args.pop();
  63. EQ.prototype[METHOD_PREFIX + args.join('_')] = f;
  64. };
  65. EQ.prototype = {
  66. check: function() {
  67. try {
  68. this.check0();
  69. } catch (e) {
  70. if (e instanceof ShortcutError) {
  71. return [e.fail];
  72. }
  73. throw e;
  74. }
  75. return this.fails;
  76. },
  77. check0: function() {
  78. var a = this.a;
  79. var b = this.b;
  80. // equal a and b exit early
  81. if (a === b) {
  82. // check for +0 !== -0;
  83. return this.collectFail(a === 0 && (1 / a !== 1 / b) && !this.plusZeroAndMinusZeroEqual, PLUS_0_AND_MINUS_0);
  84. }
  85. var typeA = t(a);
  86. var typeB = t(b);
  87. // if objects has different types they are not equal
  88. if (typeA.type !== typeB.type || typeA.cls !== typeB.cls || typeA.sub !== typeB.sub) {
  89. return this.collectFail(true, format(DIFFERENT_TYPES, typeToString(typeA), typeToString(typeB)));
  90. }
  91. // as types the same checks type specific things
  92. var name1 = typeA.type, name2 = typeA.type;
  93. if (typeA.cls) {
  94. name1 += '_' + typeA.cls;
  95. name2 += '_' + typeA.cls;
  96. }
  97. if (typeA.sub) {
  98. name2 += '_' + typeA.sub;
  99. }
  100. var f = this[METHOD_PREFIX + name2] || this[METHOD_PREFIX + name1] || this[METHOD_PREFIX + typeA.type] || this.defaultCheck;
  101. f.call(this, this.a, this.b);
  102. },
  103. collectFail: function(comparison, reason, showReason) {
  104. if (comparison) {
  105. var res = new EqualityFail(this.a, this.b, reason, this.path);
  106. res.showReason = !!showReason;
  107. this.fails.push(res);
  108. if (!this.collectAllFails) {
  109. throw new ShortcutError(res);
  110. }
  111. }
  112. },
  113. checkPlainObjectsEquality: function(a, b) {
  114. // compare deep objects and arrays
  115. // stacks contain references only
  116. //
  117. var meet = this._meet;
  118. var m = this._meet.length;
  119. while (m--) {
  120. var st = meet[m];
  121. if (st[0] === a && st[1] === b) {
  122. return;
  123. }
  124. }
  125. // add `a` and `b` to the stack of traversed objects
  126. meet.push([a, b]);
  127. // TODO maybe something else like getOwnPropertyNames
  128. var key;
  129. for (key in b) {
  130. if (hasOwnProperty.call(b, key)) {
  131. if (hasOwnProperty.call(a, key)) {
  132. this.checkPropertyEquality(key);
  133. } else {
  134. this.collectFail(true, format(MISSING_KEY, 'A', key));
  135. }
  136. }
  137. }
  138. // ensure both objects have the same number of properties
  139. for (key in a) {
  140. if (hasOwnProperty.call(a, key)) {
  141. this.collectFail(!hasOwnProperty.call(b, key), format(MISSING_KEY, 'B', key));
  142. }
  143. }
  144. meet.pop();
  145. if (this.checkProtoEql) {
  146. //TODO should i check prototypes for === or use eq?
  147. this.collectFail(Object.getPrototypeOf(a) !== Object.getPrototypeOf(b), EQUALITY_PROTOTYPE, true);
  148. }
  149. },
  150. checkPropertyEquality: function(propertyName) {
  151. var _eq = new EQ(this, this.a[propertyName], this.b[propertyName], this.path.concat([propertyName]));
  152. _eq.check0();
  153. },
  154. defaultCheck: EQ.checkStrictEquality
  155. };
  156. EQ.add(t.NUMBER, function(a, b) {
  157. this.collectFail((a !== a && b === b) || (b !== b && a === a) || (a !== b && a === a && b === b), EQUALITY);
  158. });
  159. [t.SYMBOL, t.BOOLEAN, t.STRING].forEach(function(tp) {
  160. EQ.add(tp, EQ.checkStrictEquality);
  161. });
  162. EQ.add(t.FUNCTION, function(a, b) {
  163. // functions are compared by their source code
  164. this.collectFail(a.toString() !== b.toString(), FUNCTION_SOURCES);
  165. // check user properties
  166. this.checkPlainObjectsEquality(a, b);
  167. });
  168. EQ.add(t.OBJECT, t.REGEXP, function(a, b) {
  169. // check regexp flags
  170. var flags = ['source', 'global', 'multiline', 'lastIndex', 'ignoreCase', 'sticky', 'unicode'];
  171. while (flags.length) {
  172. this.checkPropertyEquality(flags.shift());
  173. }
  174. // check user properties
  175. this.checkPlainObjectsEquality(a, b);
  176. });
  177. EQ.add(t.OBJECT, t.DATE, function(a, b) {
  178. //check by timestamp only (using .valueOf)
  179. this.collectFail(+a !== +b, EQUALITY);
  180. // check user properties
  181. this.checkPlainObjectsEquality(a, b);
  182. });
  183. [t.NUMBER, t.BOOLEAN, t.STRING].forEach(function(tp) {
  184. EQ.add(t.OBJECT, tp, function(a, b) {
  185. //primitive type wrappers
  186. this.collectFail(a.valueOf() !== b.valueOf(), WRAPPED_VALUE);
  187. // check user properties
  188. this.checkPlainObjectsEquality(a, b);
  189. });
  190. });
  191. EQ.add(t.OBJECT, function(a, b) {
  192. this.checkPlainObjectsEquality(a, b);
  193. });
  194. [t.ARRAY, t.ARGUMENTS, t.TYPED_ARRAY].forEach(function(tp) {
  195. EQ.add(t.OBJECT, tp, function(a, b) {
  196. this.checkPropertyEquality('length');
  197. this.checkPlainObjectsEquality(a, b);
  198. });
  199. });
  200. EQ.add(t.OBJECT, t.ARRAY_BUFFER, function(a, b) {
  201. this.checkPropertyEquality('byteLength');
  202. this.checkPlainObjectsEquality(a, b);
  203. });
  204. EQ.add(t.OBJECT, t.ERROR, function(a, b) {
  205. this.checkPropertyEquality('name');
  206. this.checkPropertyEquality('message');
  207. this.checkPlainObjectsEquality(a, b);
  208. });
  209. EQ.add(t.OBJECT, t.BUFFER, function(a) {
  210. this.checkPropertyEquality('length');
  211. var l = a.length;
  212. while (l--) {
  213. this.checkPropertyEquality(l);
  214. }
  215. //we do not check for user properties because
  216. //node Buffer have some strange hidden properties
  217. });
  218. [t.MAP, t.SET, t.WEAK_MAP, t.WEAK_SET].forEach(function(tp) {
  219. EQ.add(t.OBJECT, tp, function(a, b) {
  220. this._meet.push([a, b]);
  221. var iteratorA = a.entries();
  222. for (var nextA = iteratorA.next(); !nextA.done; nextA = iteratorA.next()) {
  223. var iteratorB = b.entries();
  224. var keyFound = false;
  225. for (var nextB = iteratorB.next(); !nextB.done; nextB = iteratorB.next()) {
  226. // try to check keys first
  227. var r = eq(nextA.value[0], nextB.value[0], { collectAllFails: false, _meet: this._meet });
  228. if (r.length === 0) {
  229. keyFound = true;
  230. // check values also
  231. eq(nextA.value[1], nextB.value[1], this);
  232. }
  233. }
  234. if (!keyFound) {
  235. // no such key at all
  236. this.collectFail(true, format(SET_MAP_MISSING_KEY, nextA.value[0]));
  237. }
  238. }
  239. this._meet.pop();
  240. this.checkPlainObjectsEquality(a, b);
  241. });
  242. });
  243. function eq(a, b, opts) {
  244. return new EQ(opts, a, b).check();
  245. }
  246. eq.EQ = EQ;
  247. module.exports = eq;