Last active
February 28, 2023 01:58
-
-
Save dschnare/1235559 to your computer and use it in GitHub Desktop.
AopJS - A lightweight aspect oriented programming (AOP) API for JavaScript.
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
// Author: Darren Schnare | |
// Keywords: aop,aspect,oriented,programming,javascript,pointcut | |
// License: MIT ( http://www.opensource.org/licenses/mit-license.php ) | |
// Repo: https://gist.github.com/1235559 | |
// Inspiration: http://karlagius.com/2008/04/25/aspect-oriented-programming-in-javascript/ | |
// Reference: http://docs.jboss.org/jbossaop/docs/2.0.0.GA/docs/aspect-framework/reference/en/html/advices.html | |
// Reference: http://static.springsource.org/spring/docs/2.0.x/reference/aop.html | |
// Appends the aop namespace to the specified scope (defaults to global). | |
(function(scope) { | |
/////////////////////// | |
// Utility Functions // | |
/////////////////////// | |
var slice = ([]).slice; | |
function contains(o, v) { | |
if (o) { | |
for (var k in o) { | |
if (o[k] === v && o.hasOwnProperty(k)) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
function getOwnPropertyNames(o) { | |
var names = []; | |
if (o) { | |
for (var key in o) { | |
if (o.hasOwnProperty(key)) names.push(key); | |
} | |
} | |
return names; | |
} | |
function create(o) { | |
function F(){ | |
this.constructor = F; | |
} | |
F.prototype = o; | |
return new F(); | |
} | |
/////////////////// | |
// AOP namespace // | |
/////////////////// | |
scope.aop = (function() { | |
/////////////////////// | |
// Private Functions // | |
/////////////////////// | |
// Apply a list of pointcuts to an object. | |
function applyPointcuts(pointcuts, o, advices) { | |
var propertyNames = getOwnPropertyNames(o); | |
var pointcut, matchedPropertyNames, propertyName; | |
// Extend the object. | |
o = create(o); | |
// Iterate over the pointcuts and find the matching property names on the object. | |
for (var i = 0, l = pointcuts.length; i < l; i += 1) { | |
pointcut = pointcuts[i]; | |
matchedPropertyNames = findPropertyNames(pointcut, propertyNames); | |
// Iterate over all the advices but only process the ones that actually exist | |
// as part of the 'aop' API. | |
for (var advice in advices) { | |
if (typeof aop[advice] === 'function' && | |
// Ensure that the advice was defined as a function. | |
typeof advices[advice] === 'function') { | |
// Iterate over the matched property names and apply the advice | |
// if the property refers to a function. | |
for (var ii = 0, ll = matchedPropertyNames.length; ii < ll; ii += 1) { | |
propertyName = matchedPropertyNames[ii]; | |
if (typeof o[propertyName] === 'function') { | |
o[propertyName] = aop[advice](o[propertyName], advices[advice]); | |
} | |
} | |
} | |
} | |
} | |
return o; | |
} | |
// Find property names that match a pointcut. | |
function findPropertyNames(pointcut, propertyNames) { | |
var reg, matches, s = propertyNames.join(','); | |
if (pointcut.indexOf("*") >= 0) { | |
if (pointcut === "*") { | |
return propertyNames.slice(); | |
} | |
pointcut = pointcut.replace(/\*/g, "[^,]*"); | |
reg = new RegExp(pointcut, "gi"), | |
matches = s.match(reg); | |
return matches || []; | |
} else { | |
return contains(propertyNames, pointcut) ? [pointcut] : []; | |
} | |
} | |
////////////////////// | |
// Public Interface // | |
////////////////////// | |
/* | |
* Applies advices to the specified joinpoints of the specified object. | |
* Joinpoints are specified by the 'pointcut' property on the 'aspect' argument. | |
* Returns an extended version of the object with the advices applied to the joinpoints. | |
* | |
* Example: | |
* o = { | |
* doStuff: function () { | |
* // something complex | |
* }, | |
* doMoreStuff: function () { | |
* // do more complex stuff | |
* } | |
* }; | |
* | |
* o = aop(o, { | |
* pointcut: 'doStuff', | |
* before: function () { | |
* // the 'before' advice | |
* }, | |
* // other advices | |
* }); | |
* | |
* // Can use '*' as the wildcard for a joinpoint. | |
* o = aop(o, { | |
* // Matches all functions starting with 'do'. | |
* pointcut: 'do*', | |
* before: function () { | |
* // the 'before' advice | |
* }, | |
* // other advices | |
* }); | |
* | |
* o = aop(o, { | |
* // Matches all functions starting with 'do' and ending with 'f'. | |
* pointcut: 'do*f', | |
* before: function () { | |
* // the 'before' advice | |
* }, | |
* // other advices | |
* }); | |
* | |
* o = aop(o, { | |
* // Matches all functions ending with 'stuff' AND contains 'more' (point cuts are case insensitive). | |
* pointcut: ['*stuff', '*more*'], | |
* before: function () { | |
* // the 'before' advice | |
* }, | |
* // other advices | |
* }); | |
*/ | |
var aop = function (o, aspect) { | |
var pointcuts = aspect.pointcut; | |
delete aspect.pointcut; | |
pointcuts = typeof pointcuts === 'string' ? [pointcuts] : pointcuts; | |
if (Object.prototype.toString.call(pointcuts) !== '[object Array]') { | |
throw new Error('Expected property "pointcuts" to be a an Array.'); | |
} | |
return applyPointcuts(pointcuts, o, aspect); | |
}; | |
// Advices // | |
/* | |
* Applies an 'around' advice to the specified function. | |
* The function 'fn' will be bound to the this variable | |
* and the arguments of the function called automatically. | |
* | |
* The this object for 'aroundFn' will be boud to the original | |
* this object of the source invocation. | |
* | |
* Example: | |
* myFn = aop.around(myFn, function (fn) { | |
* // TODO: log stuff | |
* // Decide to call the wrapped funciton. | |
* // NOTE: We don't have to worry about binding to this | |
* // or passing any arguments. | |
* var result = fn(); | |
* // TODO: log the result | |
* return result; | |
* }); | |
* | |
* // The signature for myFn() hasn't changed, only its behaviour. | |
* myFn(1, 2, 3); | |
*/ | |
aop.around = function (fn, aroundFn) { | |
return function () { | |
var args = slice.call(arguments); | |
var self = this; | |
return aroundFn.call(this, function () { | |
return fn.apply(self, args); | |
}); | |
}; | |
}; | |
/* | |
* Applies a 'before' advice to the specified function. | |
* The function 'fn' will be called after 'beforeFn' is called | |
* and its result will be returned. | |
* | |
* The this object for 'beforeFn' will be boud to the original | |
* this object of the source invocation. | |
* | |
* Example: | |
* myFn = aop.before(myFn, function () { | |
* // All arguments are passed. | |
* console.log('arguments:', ([]).slice.call(arguments)); | |
* }); | |
*/ | |
aop.before = function (fn, beforeFn) { | |
return function () { | |
var args = slice.call(arguments); | |
beforeFn.apply(this, args); | |
return fn.apply(this, args); | |
}; | |
}; | |
/* | |
* Applies the 'after' advice to the specified function. | |
* The function 'fn' will be caled beofe 'afterFn' is called, | |
* but if 'afterFn' returns a non-undefined result then this | |
* value will be returned instead of the result from calling 'fn'. | |
* | |
* The this object for 'afterFn' will be boud to the original | |
* this object of the source invocation. | |
* | |
* Example: | |
* sum = aop.after(sum, function (a, b) { | |
* // Always return 1 more than the actual sum. | |
* return a + b + 1; | |
* }); | |
*/ | |
aop.after = function (fn, afterFn) { | |
return function () { | |
var args = slice.call(arguments); | |
var resultOverride = afterFn.apply(this, args); | |
var result = fn.apply(this, args); | |
return resultOverride === undefined ? result : resultOverride; | |
}; | |
}; | |
/* | |
* Applies the 'error' advice to the specified function. | |
* The function 'fn' will be called and if an error occurs then | |
* 'errorFn' will be called with the error object and the original | |
* arguments passed into the source invocation. | |
* | |
* NOTE: The error is still thrown. | |
* | |
* The this object for 'errorFn' will be boud to the original | |
* this object of the source invocation. | |
* | |
* Example: | |
* myFn = aop.error(myFn, function (error) { | |
* console.log('Error(myFn):', error + '', 'args:', ([]).slice.call(arguments, 1)); | |
* }); | |
*/ | |
aop.error = function (fn, errorFn) { | |
return function () { | |
var args = slice.call(arguments); | |
var result; | |
try { | |
result = fn.apply(this, args); | |
} catch (error) { | |
errorFn.call(this, error, args); | |
throw error; | |
} | |
return result; | |
}; | |
}; | |
// Return the aop function. | |
return aop; | |
}()); | |
}(this)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment