hooker.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. /*
  2. * JavaScript Hooker
  3. * http://github.com/cowboy/javascript-hooker
  4. *
  5. * Copyright (c) 2012 "Cowboy" Ben Alman
  6. * Licensed under the MIT license.
  7. * http://benalman.com/about/license/
  8. */
  9. (function(exports) {
  10. // Get an array from an array-like object with slice.call(arrayLikeObject).
  11. var slice = [].slice;
  12. // Get an "[object [[Class]]]" string with toString.call(value).
  13. var toString = {}.toString;
  14. // I can't think of a better way to ensure a value is a specific type other
  15. // than to create instances and use the `instanceof` operator.
  16. function HookerOverride(v) { this.value = v; }
  17. function HookerPreempt(v) { this.value = v; }
  18. function HookerFilter(c, a) { this.context = c; this.args = a; }
  19. // When a pre- or post-hook returns the result of this function, the value
  20. // passed will be used in place of the original function's return value. Any
  21. // post-hook override value will take precedence over a pre-hook override
  22. // value.
  23. exports.override = function(value) {
  24. return new HookerOverride(value);
  25. };
  26. // When a pre-hook returns the result of this function, the value passed will
  27. // be used in place of the original function's return value, and the original
  28. // function will NOT be executed.
  29. exports.preempt = function(value) {
  30. return new HookerPreempt(value);
  31. };
  32. // When a pre-hook returns the result of this function, the context and
  33. // arguments passed will be applied into the original function.
  34. exports.filter = function(context, args) {
  35. return new HookerFilter(context, args);
  36. };
  37. // Execute callback(s) for properties of the specified object.
  38. function forMethods(obj, props, callback) {
  39. var prop;
  40. if (typeof props === "string") {
  41. // A single prop string was passed. Create an array.
  42. props = [props];
  43. } else if (props == null) {
  44. // No props were passed, so iterate over all properties, building an
  45. // array. Unfortunately, Object.keys(obj) doesn't work everywhere yet, so
  46. // this has to be done manually.
  47. props = [];
  48. for (prop in obj) {
  49. if (obj.hasOwnProperty(prop)) {
  50. props.push(prop);
  51. }
  52. }
  53. }
  54. // Execute callback for every method in the props array.
  55. var i = props.length;
  56. while (i--) {
  57. // If the property isn't a function...
  58. if (toString.call(obj[props[i]]) !== "[object Function]" ||
  59. // ...or the callback returns false...
  60. callback(obj, props[i]) === false) {
  61. // ...remove it from the props array to be returned.
  62. props.splice(i, 1);
  63. }
  64. }
  65. // Return an array of method names for which the callback didn't fail.
  66. return props;
  67. }
  68. // Monkey-patch (hook) a method of an object.
  69. exports.hook = function(obj, props, options) {
  70. // If the props argument was omitted, shuffle the arguments.
  71. if (options == null) {
  72. options = props;
  73. props = null;
  74. }
  75. // If just a function is passed instead of an options hash, use that as a
  76. // pre-hook function.
  77. if (typeof options === "function") {
  78. options = {pre: options};
  79. }
  80. // Hook the specified method of the object.
  81. return forMethods(obj, props, function(obj, prop) {
  82. // The original (current) method.
  83. var orig = obj[prop];
  84. // The new hooked function.
  85. function hooked() {
  86. var result, origResult, tmp;
  87. // Get an array of arguments.
  88. var args = slice.call(arguments);
  89. // If passName option is specified, prepend prop to the args array,
  90. // passing it as the first argument to any specified hook functions.
  91. if (options.passName) {
  92. args.unshift(prop);
  93. }
  94. // If a pre-hook function was specified, invoke it in the current
  95. // context with the passed-in arguments, and store its result.
  96. if (options.pre) {
  97. result = options.pre.apply(this, args);
  98. }
  99. if (result instanceof HookerFilter) {
  100. // If the pre-hook returned hooker.filter(context, args), invoke the
  101. // original function with that context and arguments, and store its
  102. // result.
  103. origResult = result = orig.apply(result.context, result.args);
  104. } else if (result instanceof HookerPreempt) {
  105. // If the pre-hook returned hooker.preempt(value) just use the passed
  106. // value and don't execute the original function.
  107. origResult = result = result.value;
  108. } else {
  109. // Invoke the original function in the current context with the
  110. // passed-in arguments, and store its result.
  111. origResult = orig.apply(this, arguments);
  112. // If the pre-hook returned hooker.override(value), use the passed
  113. // value, otherwise use the original function's result.
  114. result = result instanceof HookerOverride ? result.value : origResult;
  115. }
  116. if (options.post) {
  117. // If a post-hook function was specified, invoke it in the current
  118. // context, passing in the result of the original function as the
  119. // first argument, followed by any passed-in arguments.
  120. tmp = options.post.apply(this, [origResult].concat(args));
  121. if (tmp instanceof HookerOverride) {
  122. // If the post-hook returned hooker.override(value), use the passed
  123. // value, otherwise use the previously computed result.
  124. result = tmp.value;
  125. }
  126. }
  127. // Unhook if the "once" option was specified.
  128. if (options.once) {
  129. exports.unhook(obj, prop);
  130. }
  131. // Return the result!
  132. return result;
  133. }
  134. // Re-define the method.
  135. obj[prop] = hooked;
  136. // Fail if the function couldn't be hooked.
  137. if (obj[prop] !== hooked) { return false; }
  138. // Store a reference to the original method as a property on the new one.
  139. obj[prop]._orig = orig;
  140. });
  141. };
  142. // Get a reference to the original method from a hooked function.
  143. exports.orig = function(obj, prop) {
  144. return obj[prop]._orig;
  145. };
  146. // Un-monkey-patch (unhook) a method of an object.
  147. exports.unhook = function(obj, props) {
  148. return forMethods(obj, props, function(obj, prop) {
  149. // Get a reference to the original method, if it exists.
  150. var orig = exports.orig(obj, prop);
  151. // If there's no original method, it can't be unhooked, so fail.
  152. if (!orig) { return false; }
  153. // Unhook the method.
  154. obj[prop] = orig;
  155. });
  156. };
  157. }(typeof exports === "object" && exports || this));