Skip to content

Instantly share code, notes, and snippets.

@Nashorn
Forked from dschnare/aop.js
Created September 16, 2021 07:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nashorn/48f8b40594deaf49feec4c0b2f37961c to your computer and use it in GitHub Desktop.
Save Nashorn/48f8b40594deaf49feec4c0b2f37961c to your computer and use it in GitHub Desktop.
AopJS - A lightweight aspect oriented programming (AOP) API for JavaScript.
// 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