diff --git a/.editorconfig b/.editorconfig
index 00d5871..7faad4b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -2,5 +2,4 @@ root = true
[*]
indent_style = tab
-indent_size = 4
end_of_line = LF
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index ec4a7f1..ce18a10 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
.idea/
-.npmignore
node_modules/
-projectFilesBackup/
+experiments/
\ No newline at end of file
diff --git a/.jshintignore b/.jshintignore
new file mode 100644
index 0000000..b7ee02c
--- /dev/null
+++ b/.jshintignore
@@ -0,0 +1,4 @@
+node_modules/
+.git/
+.idea/
+experiments/
diff --git a/.jshintrc b/.jshintrc
index dc1d7c9..6038c9c 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -1,73 +1,28 @@
{
- // Settings
- "passfail" : false, // Stop on first error.
- "maxerr" : 20, // Maximum error before stopping.
+ "browser": true,
+ "node": true,
-
- // Predefined globals whom JSHint will ignore.
- "browser" : true, // Standard browser globals e.g. `window`, `document`.
-
- "node" : true,
- "rhino" : false,
- "couch" : false,
- "wsh" : false, // Windows Scripting Host.
-
- "jquery" : false,
- "prototypejs" : false,
- "mootools" : false,
- "dojo" : false,
-
- "predef" : [ // Custom globals.
+ "predef": [
"define",
"module"
],
-
- // Development.
- "debug" : false, // Allow debugger statements e.g. browser breakpoints.
- "devel" : false, // Allow developments statements e.g. `console.log();`.
-
-
- // ECMAScript 5.
- "es5" : false, // Allow ECMAScript 5 syntax.
- "strict" : false, // Require `use strict` pragma in every file.
- "globalstrict" : false, // Allow global "use strict" (also enables 'strict').
-
-
- // The Good Parts.
- "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons).
- "laxbreak" : true, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons.
- "bitwise" : false, // Prohibit bitwise operators (&, |, ^, etc.).
- "boss" : true, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments.
- "curly" : true, // Require {} for every new block or scope.
- "eqeqeq" : false, // Require triple equals i.e. `===`.
- "eqnull" : true, // Tolerate use of `== null`.
- "evil" : false, // Tolerate use of `eval`.
- "expr" : true, // Tolerate `ExpressionStatement` as Programs.
- "forin" : false, // Tolerate `for in` loops without `hasOwnPrototype`.
- "immed" : false, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
- "latedef" : false, // Prohibit variable use before definition.
- "loopfunc" : false, // Allow functions to be defined within loops.
- "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`.
- "nonstandard" : false, // Defines non-standard but widely adopted globals such as escape and unescape.
- "regexp" : false, // Prohibit `.` and `[^...]` in regular expressions.
- "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`.
- "scripturl" : true, // Tolerate script-targeted URLs.
- "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`.
- "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`.
- "undef" : true, // Require all non-global variables be declared before they are used.
-
-
- // Personal styling preferences.
- "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`.
- "noempty" : false, // Prohibit use of empty blocks.
- "nonew" : true, // Prohibit use of constructors for side-effects.
- "nomen" : false, // Prohibit use of initial or trailing underbars in names.
- "onevar" : false, // Allow only one `var` statement per function.
- "plusplus" : false, // Prohibit use of `++` & `--`.
- "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`.
- "trailing" : true, // Prohibit trailing whitespaces.
- "white" : false, // Check against strict whitespace and indentation rules.
- "indent" : 4, // Specify indentation spacing
- "smarttabs" : true // Suppresses warnings about mixed tabs and spaces when the latter are used for alignmnent only
+ "boss": true,
+ "curly": true,
+ "eqnull": true,
+ "expr": true,
+ "globalstrict": false,
+ "laxbreak": true,
+ "newcap": true,
+ "noarg": true,
+ "noempty": true,
+ "nonew": true,
+ "quotmark": "single",
+ "strict": false,
+ "trailing": true,
+ "undef": true,
+ "unused": true,
+
+ "maxdepth": 4,
+ "maxcomplexity": 6
}
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..9d5dd12
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,9 @@
+/docs/
+/experiments/
+/test/
+projectFilesBackup/
+.idea/
+.*
+*~
+bower.json
+*.md
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index a3201c6..a94624c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,8 @@
language: node_js
node_js:
- - 0.6
- - 0.8
+ - "0.10"
+ - "0.12"
+ - "iojs"
branches:
only:
- master
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..91ba781
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,91 @@
+### 1.3.2
+
+* * Update `package.json` to use `"repository"` to avoid npm warnings.
+
+### 1.3.1
+
+* Added `bower.json`
+* Added a proper `npmignore` to install only what's needed.
+* Updated after advice docs with info about return values
+
+### 1.3.0
+
+* [`meld()`](docs/api.md#adding-aspects) is now a function that adds aspects.
+ * **DEPRECATED:** `meld.add()`. Use `meld()` instead.
+
+### 1.2.2
+
+* Remove stray `console.log`.
+
+### 1.2.1
+
+* Fix for IE8-specific issue with meld's internal use of `Object.defineProperty`.
+* Internally shim Object.create if not available to so that meld no longer requires an Object.create shim to advise constructors in pre-ES5 environments.
+
+### 1.2.0
+
+* `meld.joinpoint()` - [Access the current joinpoint](docs/api.md#meldjoinpoint) from any advice type.
+* [Bundled aspects](docs/aspects.md):
+ * trace: trace method call entry/return/throw
+ * memoize: simple memoization for methods and functions
+ * cache: configurable caching aspect to do more than simple memoization
+
+### 1.1.0
+
+* Advice can be applied directly to methods on a function.
+* Removed undocumented behavior that implicitly adds constructor prototype advice: to advise a prototype, pass the prototype as the advice target.
+
+### 1.0.0
+
+* **Removed browser global** - `window.meld` is no longer supported. See [this post on the cujo.js Google Group](https://groups.google.com/d/topic/cujojs/K0VGuvpYQ34/discussion) for an explanation.
+* No functional change beyond browser global removal.
+
+### 0.8.0
+
+* 1.0.0 Release Candidate 1
+* Documentation! Check out the new [reference](docs/reference.md) and [api](docs/api.md) docs.
+* **Deprecated browser global** - meld.js will drop support for browser global for 1.0.0 and will support modular environments only.
+
+### 0.7.2
+
+* Fix for context when advising constructors: `this` is now the constructed instance in all advice functions.
+
+### 0.7.1
+
+* Fix for global name when using meld as a browser global. Thanks [@scothis](https://github.com/scothis)
+* Update unit tests to run in browser using `buster server`, in addition to node. Thanks again, [@scothis](https://github.com/scothis) :)
+
+### 0.7.0
+
+* Advice can be applied directly to functions without a context.
+* Advice can be applied to constructors.
+* `joinpoint.proceed()` can be called multiple times. This makes it possible to implement "retry" types of advice.
+
+### 0.6.0
+
+* aop.js is now meld.js
+* Use [Travis CI](http://travis-ci.org/cujojs/meld)
+
+### 0.5.4
+
+* Optimizations to run time advice invocation, especially around advice
+* Fix for passing new args to `joinpoint.proceed()` in around advice
+* Added `joinpoint.proceedApply(array)` for proceeding and supplying new arguments as an array
+* Ported unit tests to [BusterJS](http://busterjs.org)
+
+### 0.5.3
+
+* First official release as part of [cujojs](http://github.com/cujojs)
+* Minor doc and package.json tweaks
+
+### 0.5.2
+
+* Revert to larger, more builder-friendly module boilerplate. No functional change.
+
+### 0.5.1
+
+* Minor corrections and updates to `package.json`
+
+### 0.5.0
+
+* Rewritten Advisor that allows entire aspects to be unwoven (removed) easily.
diff --git a/README.md b/README.md
index b26a656..cf53320 100644
--- a/README.md
+++ b/README.md
@@ -25,16 +25,23 @@ myObject.doSomething(1, 2); // Nothing logged
# Docs
-[API docs](blob/master/docs/api.md)
+* [API](docs/api.md)
+* [Reference](docs/reference.md)
+* [Aspects](docs/aspects.md)
# Quick Start
### AMD
-1. `git clone https://github.com/cujojs/meld` or `git submodule add https://github.com/cujojs/meld`
+1. Get it using one of the following
+ 1. `yeoman install meld`, or
+ 1. `bower install meld`, or
+ 1. `git clone https://github.com/cujojs/meld`, or
+ 1. `git submodule add https://github.com/cujojs/meld`
+
1. Configure your loader with a package:
- ```javascript
+ ```js
packages: [
{ name: 'meld', location: 'path/to/meld', main: 'meld' },
// ... other packages ...
@@ -43,12 +50,6 @@ myObject.doSomething(1, 2); // Nothing logged
1. `define(['meld', ...], function(meld, ...) { ... });` or `require(['meld', ...], function(meld, ...) { ... });`
-### Script Tag
-
-1. `git clone https://github.com/cujojs/meld` or `git submodule add https://github.com/cujojs/meld`
-1. ``
-1. `meld` will be available as `window.meld`
-
### Node
1. `npm install meld`
@@ -68,61 +69,45 @@ Install [buster.js](http://busterjs.org/)
Run unit tests in Node:
-1. `buster test -e node`
-
-Run unit tests in Browsers (and Node):
-
-1. `buster server` - this will print a url
-2. Point browsers at /capture, e.g. `localhost:1111/capture`
-3. `buster test` or `buster test -e browser`
-
-# Changelog
-
-### 0.7.2
-
-* Fix for context when advising constructors: `this` is now the constructed instance in all advice functions.
-
-### 0.7.1
-
-* Fix for global name when using meld as a browser global. Thanks [@scothis](https://github.com/scothis)
-* Update unit tests to run in browser using `buster server`, in addition to node. Thanks again, [@scothis](https://github.com/scothis) :)
+`buster test`
-### 0.7.0
+# What's New
-* Advice can be applied directly to functions without a context.
-* Advice can be applied to constructors.
-* `joinpoint.proceed()` can be called multiple times. This makes it possible to implement "retry" types of advice.
+### 1.3.0
-### 0.6.0
+* [`meld()`](docs/api.md#adding-aspects) is now a function that adds aspects.
+ * **DEPRECATED:** `meld.add()`. Use `meld()` instead.
-* aop.js is now meld.js
-* Use [Travis CI](http://travis-ci.org/cujojs/meld)
+### 1.2.2
-### 0.5.4
+* Remove stray `console.log`.
-* Optimizations to run time advice invocation, especially around advice
-* Fix for passing new args to `joinpoint.proceed()` in around advice
-* Added `joinpoint.proceedApply(array)` for proceeding and supplying new arguments as an array
-* Ported unit tests to [BusterJS](http://busterjs.org)
+### 1.2.1
-### 0.5.3
+* Fix for IE8-specific issue with meld's internal use of `Object.defineProperty`.
+* Internally shim Object.create if not available to so that meld no longer requires an Object.create shim to advise constructors in pre-ES5 environments.
-* First official release as part of [cujojs](http://github.com/cujojs)
-* Minor doc and package.json tweaks
+### 1.2.0
-### 0.5.2
+* `meld.joinpoint()` - [Access the current joinpoint](docs/api.md#meldjoinpoint) from any advice type.
+* [Bundled aspects](docs/aspects.md):
+ * trace: trace method call entry/return/throw
+ * memoize: simple memoization for methods and functions
+ * cache: configurable caching aspect to do more than simple memoization
-* Revert to larger, more builder-friendly module boilerplate. No functional change.
+### 1.1.0
-### 0.5.1
+* Advice can be applied directly to methods on a function.
+* Removed undocumented behavior that implicitly adds constructor prototype advice: to advise a prototype, pass the prototype as the advice target.
-* Minor corrections and updates to `package.json`
+### 1.0.0
-### 0.5.0
+* **Removed browser global** - `window.meld` is no longer supported. See [this post on the cujo.js Google Group](https://groups.google.com/d/topic/cujojs/K0VGuvpYQ34/discussion) for an explanation.
+* No functional change beyond browser global removal.
-* Rewritten Advisor that allows entire aspects to be unwoven (removed) easily.
+See the [full Changelog here](CHANGES.md)
-# Beers to:
+# References
* [AspectJ](http://www.eclipse.org/aspectj/) and [Spring Framework AOP](http://static.springsource.org/spring/docs/3.0.x/reference/meld.html) for inspiration and great docs
* Implementation ideas from @phiggins42's [uber.js AOP](https://github.com/phiggins42/uber.js/blob/master/lib/meld.js)
diff --git a/aspect/cache.js b/aspect/cache.js
new file mode 100644
index 0000000..10d4b8b
--- /dev/null
+++ b/aspect/cache.js
@@ -0,0 +1,50 @@
+/** @license MIT License (c) copyright 2011-2013 original author or authors */
+
+/**
+ * Caching aspect
+ * Requires JSON.stringify. See cujojs/poly if you need a JSON polyfill
+ *
+ * @author Brian Cavalier
+ * @author John Hann
+ */
+(function(define) {
+define(function() {
+ /**
+ * Creates a new caching aspect that uses the supplied cache
+ * to store values using keys generated by the supplied keyGenerator.
+ *
+ * Requires JSON.stringify. See cujojs/poly if you need a JSON polyfill
+ *
+ * @param {object} cache
+ * @param {function} cache.has returns true if a supplied key exists in the cache
+ * @param {function} cache.get returns the value associated with the supplied key
+ * @param {function} cache.set associates the supplied key with the supplied value
+ * @param {function} [keyGenerator] creates a hash key given an array. Used to generate
+ * cache keys from function invocation arguments
+ * @return {object} caching aspect that can be added with meld.add
+ */
+ return function(cache, keyGenerator) {
+ if(!keyGenerator) {
+ keyGenerator = JSON.stringify;
+ }
+
+ return {
+ around: function(joinpoint) {
+ var key, result;
+
+ key = keyGenerator(joinpoint.args);
+
+ if(cache.has(key)) {
+ result = cache.get(key);
+ } else {
+ result = joinpoint.proceed();
+ cache.set(key, result);
+ }
+
+ return result;
+ }
+ };
+ };
+
+});
+}(typeof define === 'function' && define.amd ? define : function(factory) { module.exports = factory(); }));
diff --git a/aspect/memoize.js b/aspect/memoize.js
new file mode 100644
index 0000000..0e7fb8f
--- /dev/null
+++ b/aspect/memoize.js
@@ -0,0 +1,36 @@
+/** @license MIT License (c) copyright 2011-2013 original author or authors */
+
+/**
+ * Simple memoization aspect
+ *
+ * @author Brian Cavalier
+ * @author John Hann
+ */
+(function(define) {
+define(function(require) {
+
+ var createCacheAspect = require('./cache');
+
+ function SimpleCache() {
+ this._cache = {};
+ }
+
+ SimpleCache.prototype = {
+ has: function(key) { return key in this._cache; },
+ get: function(key) { return this._cache[key]; },
+ set: function(key, value) { this._cache[key] = value; }
+ };
+
+ /**
+ * Creates a simple memoizing aspect that can be used to memoize
+ * a function or method.
+ * @param {function} [keyGenerator] creates a hash key given an array. Used to generate
+ * memo cache keys from function invocation arguments
+ * @return {object} memoizing aspect that can be added with meld.add
+ */
+ return function(keyGenerator) {
+ return createCacheAspect(new SimpleCache(), keyGenerator);
+ };
+
+});
+}(typeof define === 'function' && define.amd ? define : function(factory) { module.exports = factory(require); }));
diff --git a/aspect/trace.js b/aspect/trace.js
new file mode 100644
index 0000000..1b6313a
--- /dev/null
+++ b/aspect/trace.js
@@ -0,0 +1,84 @@
+/** @license MIT License (c) copyright 2011-2013 original author or authors */
+
+/**
+ * trace
+ * @author: brian@hovercraftstudios.com
+ *
+ * @author Brian Cavalier
+ * @author John Hann
+ */
+(function(define) {
+define(function(require) {
+
+ var meld, joinpoint, depth, padding, simpleReporter;
+
+ meld = require('../meld');
+ joinpoint = meld.joinpoint;
+
+ // Call stack depth tracking for the default reporter
+ depth = 0;
+
+ // Padding characters for indenting traces. This will get expanded as needed
+ padding = '................................';
+
+ simpleReporter = {
+ onCall: function(info) {
+ console.log(indent(++depth) + info.method + ' CALL ', info.args);
+ },
+ onReturn: function(info) {
+ console.log(indent(depth--) + info.method + ' RETURN ', info.result);
+ },
+ onThrow: function(info) {
+ console.log(indent(depth--) + info.method + ' THROW ' + info.exception);
+ }
+ };
+
+ /**
+ * Creates an aspect that traces method/function calls and reports them
+ * to the supplied reporter.
+ * @param {object} [reporter] optional reporting API to which method call/return/throw
+ * info will be reported
+ * @param {function} [reporter.onCall] invoked when a method has been called
+ * @param {function} [reporter.onReturn] invoked when a method returns successfully
+ * @param {function} [reporter.onThrow] invoked when a method throws an exception
+ * @return {object} a tracing aspect that can be added with meld.add
+ */
+ return function createTraceAspect(reporter) {
+
+ if(!reporter) {
+ reporter = simpleReporter;
+ }
+
+ return {
+ before: function() {
+ var jp = joinpoint();
+ reporter.onCall && reporter.onCall({ method: jp.method, target: jp.target, args: jp.args.slice() });
+ },
+
+ afterReturning: function(result) {
+ var jp = joinpoint();
+ reporter.onReturn && reporter.onReturn({ method: jp.method, target: jp.target, result: result });
+ },
+
+ afterThrowing: function(exception) {
+ var jp = joinpoint();
+ reporter.onThrow && reporter.onThrow({ method: jp.method, target: jp.target, exception: exception });
+ }
+ };
+ };
+
+ /**
+ * Create indentation padding for tracing info based on the supplied call stack depth
+ * @param {number} depth call stack depth
+ * @return {String} padding that can be used to indent tracing output
+ */
+ function indent(depth) {
+ if(depth > padding.length) {
+ padding += padding;
+ }
+
+ return padding.slice(0, depth-1);
+ }
+
+});
+}(typeof define === 'function' && define.amd ? define : function(factory) { module.exports = factory(require); }));
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..17f6ba7
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,23 @@
+{
+ "name": "meld",
+ "main": "meld.js",
+ "version": "1.3.2",
+ "authors": [
+ "Brian Cavalier ",
+ "John Hann "
+ ],
+ "description": "AOP for JS with before, around, on, afterReturning, afterThrowing, after advice, and pointcut support",
+ "moduleType": [ "amd", "node" ],
+ "keywords": [ "aop", "aspect", "cujo" ],
+ "license": "MIT",
+ "homepage": "http://cujojs.com/",
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "docs",
+ "experiments",
+ "package.json"
+ ]
+}
diff --git a/docs/TOC.md b/docs/TOC.md
new file mode 100644
index 0000000..bf4f1e0
--- /dev/null
+++ b/docs/TOC.md
@@ -0,0 +1,5 @@
+# meld.js Aspect Oriented Programming
+
+1. [Reference Docs](reference.md)
+2. [meld API](api.md)
+3. [Aspects](aspects.md)
diff --git a/docs/api.md b/docs/api.md
index f039977..0de5093 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -7,9 +7,11 @@
* [meld.afterReturning](#meldafterreturning)
* [meld.afterThrowing](#meldafterthrowing)
* [meld.after](#meldafter)
-1. [Adding Multiple Advices](#adding-multiple-advices)
- * [meld.add](#meldadd)
+1. [Adding Aspects](#adding-aspects)
+ * [meld](#meld)
1. [Removing Method Advice](#removing-method-advice)
+1. [Accessing the Joinpoint](#accessing-the-joinpoint)
+ * [meld.joinpoint](#meldjoinpoint)
1. [Constructor-specific Info](#constructor-specific-info)
# Adding Advice
@@ -42,7 +44,7 @@ function aroundFunction(joinpoint) {
var remover = meld.around(object, match, aroundFunction);
```
-Adds around advice to matching methods. The `beforeFunction` will be called around the matching method.
+Adds around advice to matching methods. The `aroundFunction` will be called around the matching method.
```js
var advisedFunction = meld.around(functionToAdvise, aroundFunction);
@@ -50,7 +52,7 @@ var advisedFunction = meld.around(functionToAdvise, aroundFunction);
var AdvisedConstructor = meld.around(ConstructorToAdvise, aroundFunction);
```
-Returns a new function or constructor that calls `beforeFunction` before executing the original behavior of `functionToAdvise`/`ConstructorToAdvice`, leaving the original untouched.
+Returns a new function or constructor that calls `aroundFunction` before executing the original behavior of `functionToAdvise`/`ConstructorToAdvice`, leaving the original untouched.
### joinpoint
@@ -100,7 +102,7 @@ var advisedFunction = meld.on(functionToAdvise, onFunction);
var AdvisedConstructor = meld.on(ConstructorToAdvise, onFunction);
```
-Returns a new function or constructor that calls `beforeFunction` immediately after executing the original behavior of `functionToAdvise`/`ConstructorToAdvice`, leaving the original untouched.
+Returns a new function or constructor that calls `onFunction` immediately after executing the original behavior of `functionToAdvise`/`ConstructorToAdvice`, leaving the original untouched.
## meld.afterReturning
@@ -114,6 +116,8 @@ var remover = meld.afterReturning(object, match, afterReturningFunction);
Adds afterReturning advice to matching methods. The `afterReturningFunction` will be called after the matching method returns successfully (i.e. does not throw), and will receive the return value as its only argument.
+AfterReturning advice *cannot* modify the return value of advised functions. If you need to modify the return value, use [around advice](#meldaround). If afterReturning advice throws an exception, it will be propagated.
+
**NOTE:** afterReturning advice *will not* be executed when the matching method throws.
```js
@@ -138,6 +142,8 @@ var remover = meld.afterThrowing(object, match, afterThrowingFunction);
Adds afterThrowing advice to matching methods. The `afterThrowingFunction` will be called after the matching method throws an exception, and will receive the thrown exception as its only argument.
+AfterThrowing advice *cannot* squelch exceptions. That is, it cannot turn the "throwing" case back into "returning". If you need to do that, use [around advice](#meldaround). If afterThrowing advice throws an exception, however, it will be propagated in place of the original exception.
+
**NOTE:** afterThrowing advice *will not* be executed when the matching method returns without throwing.
```js
@@ -161,6 +167,8 @@ var remover = meld.after(object, match, afterFunction);
Adds after advice to matching methods. The `afterFunction` will be called after the matching method returns successfully *or* throws an exception, and will receive either the return value or the thrown exception as its only argument.
+Like afterReturning and afterThrowing advices, after can neither squelch exceptions nor substitute a new return value. If you need to do either of those things, use [around advice](#meldaround). If after advice throws an exception, however, it will be propagated in place of the original exception.
+
**NOTE:** after advice will *always* be executed.
```js
@@ -173,11 +181,15 @@ Returns a new function or constructor that calls `afterFunction` after the origi
In the specific case of a constructor, the newly constructed instance acts as the return value, and will be the argument provided to the `afterFunction` when the constructor returns successfully.
-# Adding Multiple Advices
+# Adding Aspects
-Meld.js allows you to add any number of advices to a method, function, or constructor. In addition to adding them individually, as [shown here](api.md#advising-methods), using the individual advice methods (e.g. meld.before, meld.after, etc.), you can also add several advices at once using `meld.add()`.
+Meld.js allows you to add any number of advices to a method, function, or constructor. In addition to adding them individually, as [shown here](#advising-methods), using the individual advice methods (e.g. meld.before, meld.after, etc.), you can also add several advices at once using `meld()`.
-## meld.add
+For example, the [bundled aspects](aspects.md) are implemented this way, and can be added using `meld()`.
+
+## meld
+
+**DEPRECATED ALIAS:** meld.add()
```js
// Supply any or all of the advice types at once
@@ -193,15 +205,15 @@ var advices = {
}
}
-var remover = meld.add(object, match, advices);
+var remover = meld(object, match, advices);
```
Adds multiple advices to each matched method.
```js
-var advisedFunction = meld.add(functionToAdvise, advices);
+var advisedFunction = meld(functionToAdvise, advices);
-var AdvisedConstructor = meld.add(ConstructorToAdvise, advices);
+var AdvisedConstructor = meld(ConstructorToAdvise, advices);
```
Adds multiple advices to the supplied function or constructor.
@@ -210,9 +222,22 @@ Adds multiple advices to the supplied function or constructor.
See the [Removing Method Advice](reference.md#removing-method-advice) section of the Reference doc.
-# Constructor-specific Info
+# Accessing the Joinpoint
+
+[Around advice](#meldaround) receives the current [joinpoint](reference.md#joinpoint) as a parameter. Other advice types can use `meld.joinpoint()` to retrieve the current joinpoint.
-**IMPORTANT:** An ES5-compliant `Object.create()` must be available to advise constructors. If you're running in an environment that does not provide `Object.create()`, and need to advise constructors, you must use a shim, such as [cujo.js's poly](https://github.com/cujojs/poly), or [es5-shim](https://github.com/kriskowal/es5-shim)
+## meld.joinpoint
+
+```js
+function myBeforeAdvice() {
+ var joinpoint = meld.joinpoint();
+ // Use joinpoint fields as necessary
+}
+```
+
+**IMPORTANT:** The returned joinpoint is only valid within the advice function where it was retrieved by calling `meld.joinpoint`. You should not cache the joinpoint returned by `meld.joinpoint()` and attempt to use it outside of an advice function, or in different advice functions from that which it was originally retrieved.
+
+# Constructor-specific Info
Advising constructors has a few particulars that are worth noting, or different from advising non-constructor functions and methods. Specifically:
diff --git a/docs/aspects.md b/docs/aspects.md
new file mode 100644
index 0000000..fb63082
--- /dev/null
+++ b/docs/aspects.md
@@ -0,0 +1,125 @@
+# meld.js Bundled Aspects
+
+1. [Aspects](#aspects)
+1. [aspect/trace](#aspecttrace) - Method/function tracing
+1. [aspect/memoize](#aspectmemoize) - Simple memoization
+1. [aspect/cache](#aspectcache) - Caching
+
+# Aspects
+
+Besides adding individual advices, such as before, afterReturning, etc., meld supports adding aspects, which are essentially one or more pieces of advice that work together to implement some functionality. Aspects can be added using [meld](api.md#adding-aspects)
+
+Meld comes with several aspects, in the `aspect` dir, that you can use, and that serve as examples for implementing your own.
+
+# aspect/trace
+
+```js
+var trace = require('meld/aspect/trace');
+
+var traced = meld(object, pointcut, trace());
+// or
+var traced = meld(func, trace());
+```
+
+Creates an aspect that traces method and function calls, and to report when they are called, their parameters, and whether each returns successfully or throws an exception, with the associated return value or throw exception. By default, the trace aspect uses a builtin reporter that simply logs information using `console.log`.
+
+You can supply your own reporter, which will be called when a method/function is called, returns, or throws:
+
+```js
+var myReporter = {
+ // Called just before any traced method/function is called
+ onCall: function(info) {
+ // info == { target, method, args };
+ // target: context on which method was invoked
+ // method: name of the method
+ // args: Array of arguments passed to method
+ },
+
+ // Called after a traced method returns successfully
+ onReturn: function(info) {
+ // info == { target, method, result };
+ // target: context on which method was invoked
+ // method: name of the method
+ // result: return value
+ },
+
+ // Called after a traced method throws an exception
+ onThrow: function(info) {
+ // info == { target, method, exception };
+ // target: context on which method was invoked
+ // method: name of the method
+ // exception: exception that was thrown
+ }
+}
+
+var traced = meld(object, pointcut, trace(myReporter));
+```
+
+# aspect/memoize
+
+```js
+var memoize = require('meld/aspect/memoize');
+
+var memoized = meld(object, pointcut, memoize());
+// or
+var memoized = meld(func, memoize());
+```
+
+Creates an aspect that [memoizes](http://en.wikipedia.org/wiki/Memoization) a method or function. The first call to any memoized method or function with a specific set of params will always execute the original method/function. The result will be stored in a table for fast lookup the next time the method/function is invoked *with the same params*. Thus, subsequent calls to the memoized method/function with previously used params will always return a value from the map.
+
+The aspect relies on a key generation function that creates a unique hash key from an array of params. By default, it uses `JSON.stringify`, but you can supply your own key generator function:
+
+```js
+function myKeyGenerator(paramsArray) {
+ var key;
+
+ // create a string or numeric key to use in storing values for this set of params
+
+ return key;
+}
+
+var memoized = meld(object, pointcut, memoize(myKeyGenerator));
+```
+
+# aspect/cache
+
+```js
+var cache = require('meld/aspect/cache');
+
+var cached = meld(object, pointcut, cache(storage));
+// or
+var cached = meld(func, cache(storage));
+```
+
+Creates an aspect that can help in performing more sophisticated caching than the [memoize aspect](#aspectmemoize). You must supply a cache storage object that implements the following API:
+
+```js
+storage = {
+ // Return true if the supplied key exists in the cache, or false otherwise.
+ has: function(key) {},
+
+ // Return the value associated with the supplied key, or undefined if the
+ // key does not exist in the cache.
+ get: function(key) {},
+
+ // Add the supplied key and associated value to the cache, or overwrite
+ // the value if the key already exists in the cache.
+ set: function(key, value) {},
+};
+```
+
+This allows your cache storage to implement whatever caching strategy meets your needs. For example, it may limit the number of items that can be stored in the cache, or evict items after a certain amount of time, etc.
+
+You can also supply a key generation function that creates a unique hash key from an array of params. By default, it uses `JSON.stringify`, but you can supply your own key generator function as the second argument when creating the cache aspect:
+
+```js
+function myKeyGenerator(paramsArray) {
+ var key;
+
+ // create a string or numeric key to use in storing values for this set of params
+
+ return key;
+}
+
+var cached = meld(object, pointcut, cache(storage, myKeyGenerator));
+```
diff --git a/docs/examples.md b/docs/examples.md
deleted file mode 100644
index f9b0e4b..0000000
--- a/docs/examples.md
+++ /dev/null
@@ -1,59 +0,0 @@
-
-```js
-var myObject = {
- doSomething: function(a, b) {
- if(arguments.length < 2) {
- throw new Error('doSomething must be called with 2 arguments');
- }
- return a + b;
- }
-};
-
-meld.before(myObject, 'doSomething', function() {
- var args = Array.prototype.join.call(arguments);
- console.log('myObject.doSomething called with: ' + args);
-});
-
-meld.afterReturning(myObject, 'doSomething', function(result) {
- console.log('myObject.doSomething returned: ' + result);
-});
-
-meld.afterThrowing(myObject, 'doSomething', function(exception) {
- console.error('myObject.doSomething threw: ' + exception);
-});
-
-// myObject.doSomething called with: 1,2
-// myObject.doSomething returned: 3
-myObject.doSomething(1, 2);
-
-// myObject.doSomething called with: 1
-// myObject.doSomething threw: Error: doSomething must be called with 2 arguments
-myObject.doSomething(1);
-```
-
-```js
-var myObject = {
- doSomething: function(a, b) {
- if(arguments.length < 2) {
- throw new Error('doSomething must be called with 2 arguments');
- }
- return a + b;
- }
-};
-
-var cache = {};
-meld.around(myObject, 'doSomething', function(methodCall) {
- var cacheKey, result;
-
- cacheKey = methodCall.args.join();
-
- if(cacheKey in cache) {
- result = cache[cacheKey];
- } else {
- result = methodCall.proceed();
- cache[cacheKey] = result;
- }
-
- return result;
-});
-```
\ No newline at end of file
diff --git a/docs/reference.md b/docs/reference.md
index fdf737d..04c0aa6 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -8,7 +8,9 @@
* [After Throwing](#after-throwing)
* [After (Finally)](#after-finally)
* [Around](#around)
- * [Joinpoint](#joinpoint)
+1. [Joinpoint](#joinpoint)
+ 1. [Accessing the Joinpoint](#accessing-the-joinpoint)
+ 2. [Around Joinpoint](#around-joinpoint)
1. [Advice Order](#advice-order)
1. [Advising Methods](#advising-methods)
* [Matching method names](#matching-method-names)
@@ -102,13 +104,39 @@ result = myObject.doSomething(1, 2);
## Around
-*Around* is the most powerful type of advice and, as its name implies, executes *around* another function. It may execute code before and after the original function, modify the arguments passed to the original, or even elect to prevent the original function from executing at all (see the [Simple Caching Example](#simple-caching-example] above).
+*Around* is the most powerful type of advice and, as its name implies, executes *around* another function. It may execute code before and after the original function, modify the arguments passed to the original, or even elect to prevent the original function from executing at all (see the [Simple Caching Example](#simple-caching-example) above).
-Around advice receives a [Joinpoint](#joinpoint) as its only parameter, which it can use to access the original arguments, context, and method name of the original function, as well as allow the original function to continue, with either its original arguments, or a modified list of arguments.
+Around advice receives an [Around Joinpoint](#around-joinpoint) as its only parameter, which it can use to access the original arguments, context, and method name of the original function, as well as allow the original function to continue, with either its original arguments, or a modified list of arguments.
-### Joinpoint
+# Joinpoint
-A joinpoint is the point at which a method or function was intercepted. Think of it as a description of the original function invocation. It provides the arguments with which the original method was called, the method name (in the case of an object method), and its own methods for allowing the original method call to continue and getting its result.
+A joinpoint is the point at which a method or function was intercepted. Think of it as a description of the original function invocation. It provides the arguments with which the original method was called, the method name (in the case of an object method), and the context with which the method was called.
+
+A joinpoint has the following properties:
+
+* target - context with which the original method was called
+* method - String name of the method that was called
+* args - Array of arguments that were passed to the method
+
+## Accessing the Joinpoint
+
+Around advice always receives the current [around joinpoint](#around-joinpoint) as its only argument.
+
+From *inside* any other advice type (before, after, etc.) you may access the current joinpoint by calling `meld.joinpoint()`. It will return the joinpoint within which the current advice function is executing.
+
+When called from *outside* an advice function, `meld.joinpoint()` always returns undefined.
+
+**IMPORTANT:** The returned joinpoint is only valid within the advice function where it was retrieved by calling `meld.joinpoint`. You should not cache the joinpoint returned by `meld.joinpoint()` and attempt to use it outside of an advice function, or in different advice functions from that which it was originally retrieved.
+
+## Around Joinpoint
+
+Around advice receives as its only argument, an augmented joinpoint, sometimes called a "proceeding joinpoint". It has the same fields as a regular joinpoint, with the addition of "proceed" methods for allowing the original method call to continue and getting its result.
+
+An around joinpoint has the following additional properties:
+
+* proceed() - function that around advice should call to proceed to the original method invocation. You may pass arguments to `proceed()` and they will be used *instead of* the original arguments. It will return the result of the original method, or throw an exception if the original method throws.
+* proceedApply:`Function` - similar to `proceed()` but accepts an Array of new arguments, e.g. `proceedApply([1, 2, 3])` is equivalent to `proceed(1, 2, 3)`
+* proceedCount() - function that returns the number of times that `proceed()` and/or `proceedApply()` have been called *within the current around advice*.
# Advice Order
@@ -122,6 +150,97 @@ When multiple advices are added to the same method or function, they run in the
1. *AfterReturning* or *AfterThrowing* advice in FIFO order
1. *After* (finally) advice in FIFO order
+## Advice Order Example
+
+```js
+var myObject = {
+ doSomething: function(x) {
+ console.log(x);
+ }
+};
+
+// Before advice executes in LIFO order
+meld.before(myObject, 'doSomething', function(x) {
+ console.log(2);
+});
+
+meld.before(myObject, 'doSomething', function(x) {
+ console.log(1);
+});
+
+// Around advice wraps the intercepted method in layers
+// The first around is immediately around the intercepted method
+meld.around(myObject, 'doSomething', function(joinpoint) {
+ // code before joinpoint.proceed() executes LIFO
+ console.log(4);
+ var result = joinpoint.proceed();
+
+ // code after joinpoint.proceed() executes FIFO
+ console.log(8);
+
+ return result;
+});
+
+// The second around advice is layered around the first!
+meld.around(myObject, 'doSomething', function(joinpoint) {
+ // code before joinpoint.proceed() executes LIFO
+ console.log(3);
+ var result = joinpoint.proceed();
+
+ // code after joinpoint.proceed() executes FIFO
+ console.log(9);
+
+ return result;
+});
+
+// On advice executes in FIFO order inside around advice
+meld.on(myObject, 'doSomething', function(x) {
+ console.log(6);
+});
+
+meld.on(myObject, 'doSomething', function(x) {
+ console.log(7);
+});
+
+// After Returning advice executes in FIFO order
+meld.afterReturning(myObject, 'doSomething', function(x) {
+ console.log(10);
+});
+
+meld.afterReturning(myObject, 'doSomething', function(x) {
+ console.log(11);
+});
+
+// After advice executes in FIFO order
+meld.after(myObject, 'doSomething', function(x) {
+ console.log(12);
+});
+
+meld.after(myObject, 'doSomething', function(x) {
+ console.log(13);
+});
+
+myObject.doSomething(5);
+```
+
+Output:
+
+```js
+1 // before
+2 // before
+3 // around, before joinpoint.proceed
+4 // around, before joinpoint.proceed
+5 // **original myObject.doSomething**
+6 // on
+7 // on
+8 // around, after joinpoint.proceed
+9 // around, after joinpoint.proceed
+10 // afterReturning
+11 // afterReturning
+12 // after
+13 // after
+```
+
# Advising Methods
Adding advice to a method:
@@ -166,7 +285,7 @@ myObject.doSomething(1);
## Matching method names
-In addition to passing a String to specify what method to advise, you can also pass an Array, a RegExp, a Function as the second parameter, and meld will apply the supplied advice to each method that matches.
+In addition to passing a String to specify what method to advise, you can also pass an Array, a RegExp, or a Function as the second parameter, and meld will apply the supplied advice to each method that matches.
### String match
@@ -245,7 +364,7 @@ remover.remove();
myObject.doSomething(1, 2, 3); // Nothing logged
```
-When adding advice to functions and constructors, meld leaves the original function or constructor unmodified, and returns a *new function or constructor*. Thus, there is no need to "remove" advice. Simply discard the new function or constructor and use the original.
+**NOTE:** When adding advice to functions and constructors, meld leaves the original function or constructor unmodified, and returns a *new function or constructor*. Thus, there is no need to "remove" advice. Simply discard the new function or constructor and use the original.
# Advising Functions
diff --git a/meld.js b/meld.js
index 247ab1e..d31bda5 100644
--- a/meld.js
+++ b/meld.js
@@ -1,4 +1,4 @@
-/** @license MIT License (c) copyright B Cavalier & J Hann */
+/** @license MIT License (c) copyright 2011-2013 original author or authors */
/**
* meld
@@ -9,41 +9,77 @@
* Licensed under the MIT License at:
* http://www.opensource.org/licenses/mit-license.php
*
- * @version 0.7.2
+ * @author Brian Cavalier
+ * @author John Hann
+ * @version 1.3.1
*/
(function (define) {
define(function () {
- var ap, prepend, append, iterators, slice, isArray, defineProperty, freeze;
- freeze = Object.freeze || function (o) { return o; };
+ //
+ // Public API
+ //
- ap = Array.prototype;
- prepend = ap.unshift;
- append = ap.push;
- slice = ap.slice;
+ // Add a single, specific type of advice
+ // returns a function that will remove the newly-added advice
+ meld.before = adviceApi('before');
+ meld.around = adviceApi('around');
+ meld.on = adviceApi('on');
+ meld.afterReturning = adviceApi('afterReturning');
+ meld.afterThrowing = adviceApi('afterThrowing');
+ meld.after = adviceApi('after');
- isArray = Array.isArray || function(it) {
- return Object.prototype.toString.call(it) == '[object Array]';
- };
+ // Access to the current joinpoint in advices
+ meld.joinpoint = joinpoint;
- defineProperty = Object.defineProperty || function(obj, prop, descriptor) {
- obj[prop] = descriptor.value;
- };
+ // DEPRECATED: meld.add(). Use meld() instead
+ // Returns a function that will remove the newly-added aspect
+ meld.add = function() { return meld.apply(null, arguments); };
- iterators = {
- // Before uses reverse iteration
- before: forEachReverse,
- around: false
- };
+ /**
+ * Add an aspect to all matching methods of target, or to target itself if
+ * target is a function and no pointcut is provided.
+ * @param {object|function} target
+ * @param {string|array|RegExp|function} [pointcut]
+ * @param {object} aspect
+ * @param {function?} aspect.before
+ * @param {function?} aspect.on
+ * @param {function?} aspect.around
+ * @param {function?} aspect.afterReturning
+ * @param {function?} aspect.afterThrowing
+ * @param {function?} aspect.after
+ * @returns {{ remove: function }|function} if target is an object, returns a
+ * remover { remove: function } whose remove method will remove the added
+ * aspect. If target is a function, returns the newly advised function.
+ */
+ function meld(target, pointcut, aspect) {
+ var pointcutType, remove;
- // All other advice types use forward iteration
- // Around is a special case that uses recursion rather than
- // iteration. See Advisor._callAroundAdvice
- iterators.on
- = iterators.afterReturning
- = iterators.afterThrowing
- = iterators.after
- = forEach;
+ if(arguments.length < 3) {
+ return addAspectToFunction(target, pointcut);
+ } else {
+ if (isArray(pointcut)) {
+ remove = addAspectToAll(target, pointcut, aspect);
+ } else {
+ pointcutType = typeof pointcut;
+
+ if (pointcutType === 'string') {
+ if (typeof target[pointcut] === 'function') {
+ remove = addAspectToMethod(target, pointcut, aspect);
+ }
+
+ } else if (pointcutType === 'function') {
+ remove = addAspectToAll(target, pointcut(target), aspect);
+
+ } else {
+ remove = addAspectToMatches(target, pointcut, aspect);
+ }
+ }
+
+ return remove;
+ }
+
+ }
function Advisor(target, func) {
@@ -57,19 +93,14 @@ define(function () {
advisor = this;
advised = this.advised = function() {
- var context, args, callOrig, result, afterType, exception;
+ var context, joinpoint, args, callOrig, afterType;
// If called as a constructor (i.e. using "new"), create a context
// of the correct type, so that all advice types (including before!)
// are called with the correct context.
- // NOTE: Requires ES5 Object.create()
if(this instanceof advised) {
// shamelessly derived from https://github.com/cujojs/wire/blob/c7c55fe50238ecb4afbb35f902058ab6b32beb8f/lib/component.js#L25
- if (!Object.create) {
- throw new Error('An ES5 environment is required for advice on constructors');
- }
-
- context = Object.create(orig.prototype);
+ context = objectCreate(orig.prototype);
callOrig = function (args) {
return applyConstructor(orig, context, args);
};
@@ -85,26 +116,39 @@ define(function () {
args = slice.call(arguments);
afterType = 'afterReturning';
- advisor._callSimpleAdvice('before', context, args);
+ // Save the previous joinpoint and set the current joinpoint
+ joinpoint = pushJoinpoint({
+ target: context,
+ method: func,
+ args: args
+ });
try {
- result = advisor._callAroundAdvice(context, func, args, callOrigAndOn);
- } catch(e) {
- result = exception = e;
- // Switch to afterThrowing
- afterType = 'afterThrowing';
- }
+ advisor._callSimpleAdvice('before', context, args);
+
+ try {
+ joinpoint.result = advisor._callAroundAdvice(context, func, args, callOrigAndOn);
+ } catch(e) {
+ joinpoint.result = joinpoint.exception = e;
+ // Switch to afterThrowing
+ afterType = 'afterThrowing';
+ }
- args = [result];
+ args = [joinpoint.result];
- callAfter(afterType, args);
- callAfter('after', args);
+ callAfter(afterType, args);
+ callAfter('after', args);
- if(exception) {
- throw exception;
- }
+ if(joinpoint.exception) {
+ throw joinpoint.exception;
+ }
- return result;
+ return joinpoint.result;
+
+ } finally {
+ // Restore the previous joinpoint, if necessary.
+ popJoinpoint();
+ }
function callOrigAndOn(args) {
var result = callOrig(args);
@@ -118,18 +162,18 @@ define(function () {
}
};
- defineProperty(advised, '_advisor', { value: advisor });
+ defineProperty(advised, '_advisor', { value: advisor, configurable: true });
}
Advisor.prototype = {
- /**
- * Invoke all advice functions in the supplied context, with the supplied args
- *
- * @param adviceType
- * @param context
- * @param args
- */
+ /**
+ * Invoke all advice functions in the supplied context, with the supplied args
+ *
+ * @param adviceType
+ * @param context
+ * @param args
+ */
_callSimpleAdvice: function(adviceType, context, args) {
// before advice runs LIFO, from most-recently added to least-recently added.
@@ -183,7 +227,8 @@ define(function () {
proceedCalled = 0;
// Joinpoint is immutable
- joinpoint = freeze({
+ // TODO: Use Object.freeze once v8 perf problem is fixed
+ joinpoint = pushJoinpoint({
target: context,
method: method,
args: args,
@@ -192,8 +237,12 @@ define(function () {
proceedCount: proceedCount
});
- // Call supplied around advice function
- return around.call(context, joinpoint);
+ try {
+ // Call supplied around advice function
+ return around.call(context, joinpoint);
+ } finally {
+ popJoinpoint();
+ }
/**
* The number of times proceed() has been called
@@ -281,9 +330,9 @@ define(function () {
/**
* Returns the advisor for the target object-function pair. A new advisor
* will be created if one does not already exist.
- * @param target {*} target containing a method with tthe supplied methodName
+ * @param target {*} target containing a method with the supplied methodName
* @param methodName {String} name of method on target for which to get an advisor
- * @return {Object} existing or newly created advisor for the supplied method
+ * @return {Object|undefined} existing or newly created advisor for the supplied method
*/
Advisor.get = function(target, methodName) {
if(!(methodName in target)) {
@@ -307,60 +356,6 @@ define(function () {
return advisor;
};
- //
- // Public API
- //
-
- return {
- // General add aspect
- // Returns a function that will remove the newly-added aspect
- add: addAspect,
-
- // Add a single, specific type of advice
- // returns a function that will remove the newly-added advice
- before: adviceApi('before'),
- around: adviceApi('around'),
- on: adviceApi('on'),
- afterReturning: adviceApi('afterReturning'),
- afterThrowing: adviceApi('afterThrowing'),
- after: adviceApi('after')
- };
-
- function addAspect(target, pointcut, aspect) {
- // pointcut can be: string, Array of strings, RegExp, Function(targetObject): Array of strings
- // advice can be: object, Function(targetObject, targetMethodName)
-
- var pointcutType, remove;
-
- if(arguments.length < 3) {
- return addAspectToFunction(target, pointcut);
- } else {
- target = getPointcutTarget(target);
-
- if (isArray(pointcut)) {
- remove = addAspectToAll(target, pointcut, aspect);
-
- } else {
- pointcutType = typeof pointcut;
-
- if (pointcutType === 'string') {
- if (typeof target[pointcut] === 'function') {
- remove = addAspectToMethod(target, pointcut, aspect);
- }
-
- } else if (pointcutType === 'function') {
- remove = addAspectToAll(target, pointcut(target), aspect);
-
- } else {
- remove = addAspectToMatches(target, pointcut, aspect);
- }
- }
-
- return remove;
- }
-
- }
-
/**
* Add an aspect to a pure function, returning an advised version of it.
* NOTE: *only the returned function* is advised. The original (input) function
@@ -428,10 +423,6 @@ define(function () {
};
}
- function getPointcutTarget(target) {
- return typeof target == 'function' ? target.prototype||target : target;
- }
-
// Create an API function for the specified advice type
function adviceApi(type) {
return function(target, method, adviceFunc) {
@@ -439,10 +430,10 @@ define(function () {
if(arguments.length === 2) {
aspect[type] = method;
- return addAspect(target, aspect);
+ return meld(target, aspect);
} else {
aspect[type] = adviceFunc;
- return addAspect(target, method, aspect);
+ return meld(target, method, aspect);
}
};
}
@@ -504,7 +495,8 @@ define(function () {
function applyConstructor(C, instance, args) {
try {
- Object.defineProperty(instance, 'constructor', {
+ // Try to define a constructor, but don't care if it fails
+ defineProperty(instance, 'constructor', {
value: C,
enumerable: false
});
@@ -517,6 +509,57 @@ define(function () {
return instance;
}
+ var currentJoinpoint, joinpointStack,
+ ap, prepend, append, iterators, slice, isArray, defineProperty, objectCreate;
+
+ // TOOD: Freeze joinpoints when v8 perf problems are resolved
+// freeze = Object.freeze || function (o) { return o; };
+
+ joinpointStack = [];
+
+ ap = Array.prototype;
+ prepend = ap.unshift;
+ append = ap.push;
+ slice = ap.slice;
+
+ isArray = Array.isArray || function(it) {
+ return Object.prototype.toString.call(it) == '[object Array]';
+ };
+
+ // Check for a *working* Object.defineProperty, fallback to
+ // simple assignment.
+ defineProperty = definePropertyWorks()
+ ? Object.defineProperty
+ : function(obj, prop, descriptor) {
+ obj[prop] = descriptor.value;
+ };
+
+ objectCreate = Object.create ||
+ (function() {
+ function F() {}
+ return function(proto) {
+ F.prototype = proto;
+ var instance = new F();
+ F.prototype = null;
+ return instance;
+ };
+ }());
+
+ iterators = {
+ // Before uses reverse iteration
+ before: forEachReverse,
+ around: false
+ };
+
+ // All other advice types use forward iteration
+ // Around is a special case that uses recursion rather than
+ // iteration. See Advisor._callAroundAdvice
+ iterators.on
+ = iterators.afterReturning
+ = iterators.afterThrowing
+ = iterators.after
+ = forEach;
+
function forEach(array, func) {
for (var i = 0, len = array.length; i < len; i++) {
func(array[i]);
@@ -529,12 +572,27 @@ define(function () {
}
}
-});
-})(typeof define == 'function'
- ? define
- : function (factory) { typeof module != 'undefined'
- ? (module.exports = factory())
- : (this.meld = factory());
+ function joinpoint() {
+ return currentJoinpoint;
+ }
+
+ function pushJoinpoint(newJoinpoint) {
+ joinpointStack.push(currentJoinpoint);
+ return currentJoinpoint = newJoinpoint;
+ }
+
+ function popJoinpoint() {
+ return currentJoinpoint = joinpointStack.pop();
}
- // Boilerplate for AMD, Node, and browser global
+
+ function definePropertyWorks() {
+ try {
+ return 'x' in Object.defineProperty({}, 'x', {});
+ } catch (e) { /* return falsey */ }
+ }
+
+ return meld;
+
+});
+})(typeof define == 'function' && define.amd ? define : function (factory) { module.exports = factory(); }
);
diff --git a/package.json b/package.json
index b1449d2..aa985ab 100644
--- a/package.json
+++ b/package.json
@@ -1,22 +1,31 @@
{
"name": "meld",
- "version": "0.7.2",
+ "version": "1.3.2",
"description": "AOP for JS with before, around, on, afterReturning, afterThrowing, after advice, and pointcut support",
"keywords": ["aop", "aspect", "cujo"],
+ "homepage": "http://cujojs.com/",
"licenses": [
{
"type": "MIT",
"url": "http://www.opensource.org/licenses/mit-license.php"
}
],
- "repositories": [
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/cujojs/meld"
+ },
+ "bugs": "https://github.com/cujojs/meld/issues",
+ "maintainers": [
+ {
+ "name": "Brian Cavalier",
+ "web": "http://hovercraftstudios.com/"
+ },
{
- "type": "git",
- "url": "https://github.com/cujojs/meld"
+ "name": "John Hann",
+ "web": "http://unscriptable.com/"
}
],
- "bugs": "https://github.com/cujojs/meld/issues",
- "maintainers": [
+ "contributors": [
{
"name": "Brian Cavalier",
"web": "http://hovercraftstudios.com/"
@@ -24,16 +33,20 @@
{
"name": "John Hann",
"web": "http://unscriptable.com/"
+ },
+ {
+ "name": "Scott Andrews"
}
],
"devDependencies": {
- "buster": "~0.6"
+ "buster": "~0.7",
+ "jshint": "~2"
},
"main": "meld",
"directories": {
"test": "test"
},
"scripts": {
- "test": "buster test -e node"
+ "test": "jshint . && buster-test -e node"
}
-}
\ No newline at end of file
+}
diff --git a/test/after.js b/test/after.js
index b400387..769417d 100644
--- a/test/after.js
+++ b/test/after.js
@@ -1,12 +1,9 @@
-(function(buster, aop) {
-"use strict";
+(function(buster, meld) {
+'use strict';
-var assert, refute;
+var assert = buster.assert;
-assert = buster.assert;
-refute = buster.refute;
-
-var arg = "foo"; // const
+var arg = 'foo'; // const
// Test fixture
function Fixture(shouldThrow) {
@@ -15,7 +12,7 @@ function Fixture(shouldThrow) {
}
Fixture.prototype = {
- method: function(a) {
+ method: function() {
this.val++;
if(this.shouldThrow) {
throw new Error('testing after advice with throw');
@@ -32,7 +29,7 @@ buster.testCase('after', {
// Starting value
assert.equals(0, target.val);
- aop.after(target, 'method', function afterAdvice(a) {
+ meld.after(target, 'method', function afterAdvice(a) {
// this should be the advised object
assert.equals(target, this);
@@ -60,7 +57,7 @@ buster.testCase('after', {
// Starting value
assert.equals(0, target.val);
- aop.after(target, 'method', function afterAdvice(a) {
+ meld.after(target, 'method', function afterAdvice(a) {
// this should be the advised object
assert.equals(target, this);
diff --git a/test/afterReturning.js b/test/afterReturning.js
index a33fb94..3c2e8b5 100644
--- a/test/afterReturning.js
+++ b/test/afterReturning.js
@@ -1,13 +1,10 @@
-(function(buster, aop) {
-"use strict";
+(function(buster, meld) {
+'use strict';
-var assert, refute, fail;
+var assert = buster.assert;
+var refute = buster.refute;
-assert = buster.assert;
-refute = buster.refute;
-fail = buster.assertions.fail;
-
-var arg = "foo"; // const
+var arg = 'foo'; // const
// Test fixture
function Fixture() {
@@ -15,7 +12,7 @@ function Fixture() {
}
Fixture.prototype = {
- method: function(a) {
+ method: function() {
this.val++;
return this.val;
}
@@ -28,7 +25,7 @@ buster.testCase('afterReturning', {
// Starting value
assert.equals(0, target.val);
- aop.afterReturning(target, 'method', function afterAdvice(a) {
+ meld.afterReturning(target, 'method', function afterAdvice(a) {
// this should be the advised object
assert.equals(target, this);
@@ -61,7 +58,7 @@ buster.testCase('afterReturning', {
// Starting value
assert.equals(0, target.val);
- aop.afterReturning(target, 'method', function afterReturning0(a) {
+ meld.afterReturning(target, 'method', function afterReturning0(a) {
// this should be the advised object
assert.equals(target, this);
@@ -81,7 +78,7 @@ buster.testCase('afterReturning', {
count++;
});
- aop.afterReturning(target, 'method', function afterReturning1(a) {
+ meld.afterReturning(target, 'method', function afterReturning1(a) {
// this should be the advised object
assert.equals(target, this);
@@ -98,7 +95,7 @@ buster.testCase('afterReturning', {
count++;
});
- aop.afterReturning(target, 'method', function afterReturning2(a) {
+ meld.afterReturning(target, 'method', function afterReturning2(a) {
// this should be the advised object
assert.equals(target, this);
@@ -130,7 +127,7 @@ buster.testCase('afterReturning', {
spy = this.spy();
- aop.afterReturning(target, 'method', spy);
+ meld.afterReturning(target, 'method', spy);
assert.exception(function() {
target.method(arg);
diff --git a/test/afterThrowing.js b/test/afterThrowing.js
index f0d83a6..c56db8f 100644
--- a/test/afterThrowing.js
+++ b/test/afterThrowing.js
@@ -1,13 +1,10 @@
-(function(buster, aop) {
-"use strict";
+(function(buster, meld) {
+'use strict';
-var assert, refute, fail;
+var assert = buster.assert;
+var refute = buster.refute;
-assert = buster.assert;
-refute = buster.refute;
-fail = buster.assertions.fail;
-
-var arg = "foo"; // const
+var arg = 'foo'; // const
// Test fixture
function Fixture() {
@@ -15,7 +12,7 @@ function Fixture() {
}
Fixture.prototype = {
- method: function(a) {
+ method: function() {
this.val++;
throw new Error('testing afterThrowing');
}
@@ -28,7 +25,7 @@ buster.testCase('afterThrowing', {
// Starting value
assert.equals(0, target.val);
- aop.afterThrowing(target, 'method', function afterReturning1(a) {
+ meld.afterThrowing(target, 'method', function afterReturning1(a) {
// this should be the advised object
assert.equals(target, this);
@@ -58,7 +55,7 @@ buster.testCase('afterThrowing', {
// Starting value
assert.equals(0, target.val);
- aop.afterThrowing(target, 'method', function afterReturning0(a) {
+ meld.afterThrowing(target, 'method', function afterReturning0(a) {
// this should be the advised object
assert.equals(target, this);
@@ -78,7 +75,7 @@ buster.testCase('afterThrowing', {
count++;
});
- aop.afterThrowing(target, 'method', function afterReturning1(a) {
+ meld.afterThrowing(target, 'method', function afterReturning1(a) {
// this should be the advised object
assert.equals(target, this);
@@ -95,7 +92,7 @@ buster.testCase('afterThrowing', {
count++;
});
- aop.afterThrowing(target, 'method', function afterReturning2(a) {
+ meld.afterThrowing(target, 'method', function afterReturning2(a) {
// this should be the advised object
assert.equals(target, this);
@@ -125,7 +122,7 @@ buster.testCase('afterThrowing', {
spy = this.spy();
- aop.afterThrowing(target, 'method', spy);
+ meld.afterThrowing(target, 'method', spy);
refute.exception(function() {
target.method(arg);
diff --git a/test/around.js b/test/around.js
index 79713f0..f9c07c4 100644
--- a/test/around.js
+++ b/test/around.js
@@ -1,12 +1,10 @@
-(function(buster, aop) {
-"use strict";
+(function(buster, meld) {
+'use strict';
-var assert, refute;
+var assert = buster.assert;
+var refute = buster.refute;
-assert = buster.assert;
-refute = buster.refute;
-
-var arg = "foo"; // const
+var arg = 'foo'; // const
// Test fixture
function Fixture() {
@@ -14,7 +12,7 @@ function Fixture() {
}
Fixture.prototype = {
- method: function(a) {
+ method: function() {
return (++this.val);
}
};
@@ -26,7 +24,7 @@ buster.testCase('around', {
// Starting value
assert.equals(0, target.val);
- aop.around(target, 'method', function aroundAdvice(joinpoint) {
+ meld.around(target, 'method', function aroundAdvice(joinpoint) {
// this should be the advised object
assert.equals(target, this);
assert.equals(target, joinpoint.target);
@@ -59,7 +57,7 @@ buster.testCase('around', {
'should allow multiple calls to proceed()': function() {
var target = new Fixture();
- aop.around(target, 'method', function aroundAdvice(joinpoint) {
+ meld.around(target, 'method', function aroundAdvice(joinpoint) {
// Calling joinpoint.proceed() multiple times is allowed
assert.same(0, joinpoint.proceedCount());
refute.exception(joinpoint.proceed);
@@ -72,21 +70,20 @@ buster.testCase('around', {
},
'should be invoked from most recently added to least recently added': function() {
- var inner, outer, target;
+ var inner, target;
inner = this.spy();
- outer = this.spy();
target = new Fixture();
// Inner
- aop.around(target, 'method', function aroundAdviceInner(joinpoint) {
+ meld.around(target, 'method', function aroundAdviceInner(joinpoint) {
inner();
// This will proceed to the original method
joinpoint.proceed();
});
// Outer
- aop.around(target, 'method', function aroundAdviceOuter(joinpoint) {
+ meld.around(target, 'method', function aroundAdviceOuter(joinpoint) {
refute.calledOnce(inner);
// This will proceed to the inner around
joinpoint.proceed();
@@ -108,7 +105,7 @@ buster.testCase('around', {
// Starting value
assert.equals(0, target.val);
- aop.around(target, 'method', function aroundAdvice(joinpoint) {
+ meld.around(target, 'method', function aroundAdvice(joinpoint) {
// arg should be the return value from the orig method
assert.equals(1, joinpoint.args.length);
assert.equals(arg, joinpoint.args[0]);
@@ -133,7 +130,7 @@ buster.testCase('around', {
// Starting value
assert.equals(0, target.val);
- aop.around(target, 'method', function aroundAdvice(joinpoint) {
+ meld.around(target, 'method', function aroundAdvice(joinpoint) {
// arg should be the return value from the orig method
assert.equals(1, joinpoint.args.length);
assert.equals(arg, joinpoint.args[0]);
@@ -153,7 +150,7 @@ buster.testCase('around', {
var target = new Fixture();
target.method = this.stub().returns(0);
- aop.around(target, 'method', function aroundAdvice(joinpoint) {
+ meld.around(target, 'method', function aroundAdvice(joinpoint) {
joinpoint.proceed();
return 1;
});
@@ -167,7 +164,7 @@ buster.testCase('around', {
target = new Fixture();
spy = target.method = this.spy();
- aop.around(target, 'method', function aroundAdvice(joinpoint) {
+ meld.around(target, 'method', function aroundAdvice(/*joinpoint*/) {
// Don't proceed to original method
// joinpoint.proceed();
return 1;
diff --git a/test/aspect/cache-test.js b/test/aspect/cache-test.js
new file mode 100644
index 0000000..60344dd
--- /dev/null
+++ b/test/aspect/cache-test.js
@@ -0,0 +1,95 @@
+(function(buster, meld, createCache) {
+'use strict';
+
+var assert = buster.assert;
+var refute = buster.refute;
+
+var param = { foo: 'bar' };
+var sentinel = {};
+
+function noop() {}
+
+buster.testCase('aspect/cache', {
+
+ 'should call original method when key not found': function() {
+ var spy, advised, cache, key;
+
+ spy = this.stub();
+ advised = { method: spy };
+ cache = {
+ has: this.stub().returns(false),
+ set: noop
+ };
+
+ meld(advised, 'method', createCache(cache));
+
+ advised.method(param);
+
+ key = JSON.stringify([param]);
+ assert.calledOnceWith(cache.has, key);
+ assert.calledWith(spy, param);
+ },
+
+ 'should not call original method when key has been cached': function() {
+ var spy, advised, cache;
+
+ spy = this.spy();
+ advised = { method: spy };
+ cache = {
+ has: this.stub().returns(true),
+ get: this.stub().returns(sentinel)
+ };
+
+ meld(advised, 'method', createCache(cache));
+
+ assert.same(advised.method(param), sentinel);
+
+ assert.calledOnce(cache.get);
+ refute.called(spy);
+ },
+
+ 'should place new items into the cache': function() {
+ var stub, advised, cache, key;
+
+ stub = this.stub().returns(sentinel);
+ advised = { method: stub };
+ cache = {
+ has: this.stub().returns(false),
+ set: this.spy()
+ };
+
+ meld(advised, 'method', createCache(cache));
+
+ advised.method(param);
+
+ key = JSON.stringify([param]);
+ assert.calledWith(cache.set, key, sentinel);
+ },
+
+ 'should use provided keyGenerator': function() {
+ var stubKeyGenerator, advised, spy, cache;
+
+ stubKeyGenerator = this.stub().returns(sentinel);
+ spy = this.stub();
+ advised = { method: spy };
+ cache = {
+ has: this.stub().returns(true),
+ set: noop,
+ get: this.stub()
+ };
+
+ meld(advised, 'method', createCache(cache, stubKeyGenerator));
+
+ advised.method(param, 1);
+
+ assert.calledWith(stubKeyGenerator, [param, 1]);
+ assert.calledWith(cache.has, sentinel);
+ }
+
+});
+
+})(
+ require('buster'),
+ require('../../meld'),
+ require('../../aspect/cache')
+);
\ No newline at end of file
diff --git a/test/aspect/memoize-test.js b/test/aspect/memoize-test.js
new file mode 100644
index 0000000..693d84d
--- /dev/null
+++ b/test/aspect/memoize-test.js
@@ -0,0 +1,71 @@
+(function(buster, meld, createMemoizer) {
+'use strict';
+
+var assert = buster.assert;
+var refute = buster.refute;
+
+var param = { foo: 'bar' };
+
+buster.testCase('aspect/memoize', {
+
+ 'should call original method when key not found': function() {
+ var spy, advised;
+
+ spy = this.spy();
+ advised = { method: spy };
+
+ meld(advised, 'method', createMemoizer());
+
+ advised.method(param);
+
+ assert.calledWith(spy, param);
+ },
+
+ 'should not call original method when key has been memoized': function() {
+ var spy, advised;
+
+ spy = this.spy();
+ advised = { method: spy };
+
+ meld(advised, 'method', createMemoizer());
+
+ advised.method(param);
+ advised.method(param);
+
+ assert.calledOnceWith(spy, param);
+ refute.calledTwice(spy);
+ },
+
+ 'default keyGenerator should consider all params when memoizing': function() {
+ var spy, advised;
+
+ spy = this.spy();
+ advised = { method: spy };
+
+ meld(advised, 'method', createMemoizer());
+
+ advised.method(param, 1);
+ advised.method(param, 2);
+
+ assert.calledTwice(spy);
+ },
+
+ 'should use provided keyGenerator': function() {
+ var stubKeyGenerator, advised;
+
+ stubKeyGenerator = this.stub().returns('the key');
+ advised = { method: function(/*x, y*/) {} };
+
+ meld(advised, 'method', createMemoizer(stubKeyGenerator));
+
+ advised.method(param, 1);
+
+ assert.calledWith(stubKeyGenerator, [param, 1]);
+ }
+
+});
+})(
+ require('buster'),
+ require('../../meld'),
+ require('../../aspect/memoize')
+);
\ No newline at end of file
diff --git a/test/aspect/trace-test.js b/test/aspect/trace-test.js
new file mode 100644
index 0000000..8fb1445
--- /dev/null
+++ b/test/aspect/trace-test.js
@@ -0,0 +1,66 @@
+(function(buster, meld, createTracer) {
+'use strict';
+
+var assert = buster.assert;
+var refute = buster.refute;
+
+var sentinel = {};
+
+buster.testCase('aspect/trace', {
+
+ 'should call enter upon entering advised method': function() {
+ var spy, advised, reporter;
+
+ spy = this.spy();
+ advised = { method: spy };
+ reporter = { onCall: this.spy() };
+
+ meld(advised, 'method', createTracer(reporter));
+
+ advised.method(sentinel);
+
+ assert.calledOnceWith(spy, sentinel);
+ assert.calledOnce(reporter.onCall);
+ },
+
+ 'should call success upon returning from advised method': function() {
+ var advised, reporter;
+
+ advised = {
+ method: function() { refute.called(reporter.onReturn); }
+ };
+ reporter = { onReturn: this.spy() };
+
+ meld(advised, 'method', createTracer(reporter));
+
+ advised.method();
+
+ assert.calledOnce(reporter.onReturn);
+ },
+
+ 'should call fail upon throwing from advised method': function() {
+ var advised, reporter;
+
+ advised = {
+ method: function() {
+ refute.called(reporter.onThrow);
+ throw sentinel;
+ }
+ };
+ reporter = { onThrow: this.spy() };
+
+ meld(advised, 'method', createTracer(reporter));
+
+ assert.exception(function() {
+ advised.method();
+ assert.calledOnce(reporter.onThrow);
+ });
+ }
+
+});
+
+})(
+ require('buster'),
+ require('../../meld'),
+ require('../../aspect/trace')
+);
\ No newline at end of file
diff --git a/test/before.js b/test/before.js
index 0c5a6dd..d61a88c 100644
--- a/test/before.js
+++ b/test/before.js
@@ -1,12 +1,9 @@
-(function(buster, aop) {
-"use strict";
+(function(buster, meld) {
+'use strict';
-var assert, refute;
+var assert = buster.assert;
-assert = buster.assert;
-refute = buster.refute;
-
-var arg = "foo"; // const
+var arg = 'foo'; // const
// Test fixture
function Fixture() {
@@ -14,7 +11,7 @@ function Fixture() {
}
Fixture.prototype = {
- method: function(a) {
+ method: function() {
return (++this.val);
}
};
@@ -23,7 +20,7 @@ buster.testCase('before', {
'should invoke advice before advised method': function() {
var target = new Fixture();
- aop.before(target, 'method', function before1(a) {
+ meld.before(target, 'method', function before1(a) {
// this should be the advised object
assert.equals(target, this);
@@ -44,7 +41,7 @@ buster.testCase('before', {
assert.equals(ret, target.val);
},
-
+
'should invoke most recently added advices first': function() {
var target = new Fixture();
var beforeCount = 0;
@@ -52,7 +49,7 @@ buster.testCase('before', {
// Add 3 before advices and test their invocation order,
// args, and return value
- aop.before(target, 'method', function before0(a) {
+ meld.before(target, 'method', function before0(a) {
assert.equals(target, this);
assert.equals(arg, a);
@@ -66,7 +63,7 @@ buster.testCase('before', {
assert.equals(2, beforeCount);
});
- aop.before(target, 'method', function before1(a) {
+ meld.before(target, 'method', function before1(a) {
assert.equals(target, this);
assert.equals(arg, a);
@@ -84,7 +81,7 @@ buster.testCase('before', {
beforeCount++;
});
- aop.before(target, 'method', function before2(a) {
+ meld.before(target, 'method', function before2(a) {
assert.equals(target, this);
assert.equals(arg, a);
diff --git a/test/buster.js b/test/buster.js
index f8e1725..32dcb98 100644
--- a/test/buster.js
+++ b/test/buster.js
@@ -1,21 +1,4 @@
-(function() {
-
- var config = {};
-
- config['meld:node'] = {
- environment: 'node',
- tests: ['*.js']
- };
-
- config['meld:browser'] = {
- environment: 'browser',
- rootPath: '../',
- sources: ['meld.js'],
- tests: ['test/*.js']
- };
-
- if(typeof module != 'undefined') {
- module.exports = config;
- }
-
-})();
\ No newline at end of file
+exports['meld:node'] = {
+ environment: 'node',
+ tests: ['**/*.js']
+};
diff --git a/test/constructors.js b/test/constructors.js
index ccb5b5c..c7736ee 100644
--- a/test/constructors.js
+++ b/test/constructors.js
@@ -1,12 +1,9 @@
(function(buster, meld) {
-"use strict";
-var assert, refute, arg;
+var assert = buster.assert;
+var refute = buster.refute;
-assert = buster.assert;
-refute = buster.refute;
-
-arg = {};
+var arg = {};
function method() {}
// Test fixture
@@ -26,6 +23,7 @@ function FixtureThrows(a) {
buster.testCase('constructors', {
'should provide correct context to advice types': function() {
+ /*jshint nonew:false*/
var Advised, count;
Advised = Fixture;
@@ -52,6 +50,7 @@ buster.testCase('constructors', {
},
'should provide correct context to afterThrowing': function() {
+ /*jshint nonew:false*/
var Advised;
Advised = meld.afterThrowing(FixtureThrows, verifyContext);
diff --git a/test/functions.js b/test/functions.js
index a5fa817..dba2246 100644
--- a/test/functions.js
+++ b/test/functions.js
@@ -1,10 +1,7 @@
(function(buster, meld) {
-"use strict";
+'use strict';
-var assert, refute;
-
-assert = buster.assert;
-refute = buster.refute;
+var assert = buster.assert;
// Test fixture
function f(x) {
@@ -32,7 +29,7 @@ buster.testCase('functions', {
spyAround = this.spy();
spyAfter = this.spy();
- advised = meld.add(f, {
+ advised = meld(f, {
before: spyBefore,
around: function(call) {
var ret = call.proceed();
@@ -47,6 +44,22 @@ buster.testCase('functions', {
assert.calledOnceWith(spyBefore, 1);
assert.calledOnceWith(spyAround, 2);
assert.calledOnceWith(spyAfter, 3);
+ },
+
+ 'should advise a method on a function': function() {
+ var before, method;
+
+ function target() {}
+ method = target.method = this.spy();
+ before = this.spy();
+
+ // Advise the function method
+ meld.before(target, 'method', before);
+
+ target.method();
+
+ assert.calledOnce(before);
+ assert.calledOnce(method);
}
diff --git a/test/joinpoint.js b/test/joinpoint.js
new file mode 100644
index 0000000..1ad9437
--- /dev/null
+++ b/test/joinpoint.js
@@ -0,0 +1,117 @@
+(function(buster, meld) {
+'use strict';
+
+var assert = buster.assert;
+var refute = buster.refute;
+
+buster.testCase('joinpoint', {
+ 'should be undefined before entering advised function stack': function() {
+ refute.defined(meld.joinpoint());
+ },
+
+ 'should be valid within an advised function stack': function() {
+ var target, expected;
+
+ target = {
+ method: function() {}
+ };
+
+ expected = [1, 2, 3];
+
+ meld.before(target, 'method', verifyJoinpoint);
+ meld.on(target, 'method', verifyJoinpoint);
+ meld.afterReturning(target, 'method', verifyJoinpoint);
+ meld.after(target, 'method', verifyJoinpoint);
+
+ target.method.apply(target, expected);
+
+ refute.defined(meld.joinpoint());
+
+ function verifyJoinpoint() {
+ var jp = meld.joinpoint();
+ assert.equals(jp.target, target);
+ assert.equals(jp.method, 'method');
+ assert.equals(jp.args, expected);
+ }
+ },
+
+ 'should be valid within an advised function stack when throwing': function() {
+ var target, expected;
+
+ target = {
+ method: function() { throw new Error(); }
+ };
+
+ expected = [1, 2, 3];
+
+ meld.before(target, 'method', verifyJoinpoint);
+ meld.on(target, 'method', verifyJoinpoint);
+ meld.afterThrowing(target, 'method', verifyJoinpoint);
+ meld.after(target, 'method', verifyJoinpoint);
+
+ assert.exception(function() {
+ target.method.apply(target, expected);
+ });
+
+ refute.defined(meld.joinpoint());
+
+ function verifyJoinpoint() {
+ var jp = meld.joinpoint();
+ assert.equals(jp.target, target);
+ assert.equals(jp.method, 'method');
+ assert.equals(jp.args, expected);
+ }
+ },
+
+ 'should follow nesting for around advice': function() {
+ var inner, target, expected;
+
+ inner = this.spy();
+ target = {
+ method: function() {}
+ };
+ expected = [1, 2, 3];
+
+ // Inner
+ meld.around(target, 'method', function aroundAdviceInner(passedJoinpoint) {
+ var joinpoint = meld.joinpoint();
+ verifyJoinpoint(joinpoint, passedJoinpoint);
+
+ inner();
+
+ // This will proceed to the original method
+ joinpoint.proceed();
+ });
+
+ // Outer
+ meld.around(target, 'method', function aroundAdviceOuter(passedJoinpoint) {
+ var joinpoint = meld.joinpoint();
+ verifyJoinpoint(joinpoint, passedJoinpoint);
+
+ refute.calledOnce(inner);
+
+ // This will proceed to the inner around
+ joinpoint.proceed();
+
+ // Verify that the inner around around has been called
+ assert.calledOnce(inner);
+ });
+
+ target.method.apply(target, expected);
+
+ function verifyJoinpoint(meldJoinpoint, passedJoinpoint) {
+ assert.same(meldJoinpoint, passedJoinpoint);
+
+ assert.equals(meldJoinpoint.target, target);
+ assert.equals(meldJoinpoint.method, 'method');
+ assert.equals(meldJoinpoint.args, expected);
+ }
+
+ }
+
+});
+
+})(
+ this.buster || require('buster'),
+ this.meld || require('../meld')
+);
\ No newline at end of file
diff --git a/test/on.js b/test/on.js
index 2d325b1..4ce24b1 100644
--- a/test/on.js
+++ b/test/on.js
@@ -1,12 +1,10 @@
-(function(buster, aop) {
-"use strict";
+(function(buster, meld) {
+'use strict';
-var assert, refute;
+var assert = buster.assert;
+var refute = buster.refute;
-assert = buster.assert;
-refute = buster.refute;
-
-var arg = "foo"; // const
+var arg = 'foo'; // const
// Test fixture
function Fixture() {
@@ -14,7 +12,7 @@ function Fixture() {
}
Fixture.prototype = {
- method: function(a) {
+ method: function() {
this.val++;
return this.val;
}
@@ -29,7 +27,7 @@ buster.testCase('on', {
// Starting value
assert.equals(0, target.val);
- aop.on(target, 'method', function on(a) {
+ meld.on(target, 'method', function on(a) {
// this should be the advised object
assert.equals(target, this);
@@ -57,7 +55,7 @@ buster.testCase('on', {
target.method = this.stub().throws(new Error());
spy = this.spy();
- aop.on(target, 'method', spy);
+ meld.on(target, 'method', spy);
assert.exception(function() {
target.method(arg);
diff --git a/test/pointcut.js b/test/pointcut.js
index 17eb1d3..e0fa8e9 100644
--- a/test/pointcut.js
+++ b/test/pointcut.js
@@ -1,10 +1,8 @@
-(function(buster, aop) {
-"use strict";
+(function(buster, meld) {
+'use strict';
-var assert, refute;
-
-assert = buster.assert;
-refute = buster.refute;
+var assert = buster.assert;
+var refute = buster.refute;
buster.testCase('pointcuts', {
@@ -18,7 +16,7 @@ buster.testCase('pointcuts', {
before = this.spy();
- aop.add(target, 'method1', {
+ meld(target, 'method1', {
before: before
});
@@ -43,7 +41,7 @@ buster.testCase('pointcuts', {
before = this.spy();
- aop.add(target, ['method1', 'method3'], {
+ meld(target, ['method1', 'method3'], {
before: before
});
@@ -73,7 +71,7 @@ buster.testCase('pointcuts', {
before = this.spy();
- aop.add(target, /method[13]/, {
+ meld(target, /method[13]/, {
before: before
});
@@ -103,8 +101,8 @@ buster.testCase('pointcuts', {
before = this.spy();
- aop.add(target,
- function(target) {
+ meld(target,
+ function() {
return ['method1', 'method3'];
},
{
diff --git a/test/prototype.js b/test/prototype.js
index e679fb0..c7264b8 100644
--- a/test/prototype.js
+++ b/test/prototype.js
@@ -1,14 +1,12 @@
-(function(buster, aop) {
-"use strict";
+(function(buster, meld) {
+'use strict';
-var assert, refute;
-
-assert = buster.assert;
-refute = buster.refute;
+var assert = buster.assert;
+var refute = buster.refute;
buster.testCase('prototype', {
- 'should apply aspects to the prototype when given a constructor': function() {
+ 'should apply aspects to the prototype when given a prototype': function() {
var before, method, target;
function Fixture() {}
@@ -16,13 +14,16 @@ buster.testCase('prototype', {
before = this.spy();
// Advise the prototype method
- aop.before(Fixture, 'method', before);
+ meld.before(Fixture.prototype, 'method', before);
target = new Fixture();
target.method();
assert.calledOnce(before);
assert.calledOnce(method);
+
+ // This is quite obviously impossible, but refute it anyway :)
+ refute(target.hasOwnProperty('method'));
}
});
diff --git a/test/remove.js b/test/remove.js
index 6a84062..7fefab3 100644
--- a/test/remove.js
+++ b/test/remove.js
@@ -1,10 +1,8 @@
-(function(buster, aop) {
-"use strict";
+(function(buster, meld) {
+'use strict';
-var assert, refute;
-
-assert = buster.assert;
-refute = buster.refute;
+var assert = buster.assert;
+var refute = buster.refute;
function Fixture() {}
@@ -13,15 +11,16 @@ buster.testCase('remove', {
'should remove advisor': function() {
// Just test that the advisor is added, and then
// removed when the final aspect is removed
- var fixture, ref, advice;
+ var fixture, ref, advice, advised;
fixture = new Fixture();
fixture.method = function() {};
advice = this.spy();
- ref = aop.before(fixture, 'method', advice);
+ ref = meld.before(fixture, 'method', advice);
assert.defined(fixture.method._advisor);
+ advised = fixture.method;
fixture.method();
assert.calledOnce(advice);
@@ -29,6 +28,7 @@ buster.testCase('remove', {
ref.remove();
refute('_advisor' in fixture.method);
+ refute('_advisor' in advised);
fixture.method();
refute.calledTwice(advice);
@@ -42,7 +42,7 @@ buster.testCase('remove', {
fixture.method2 = function() {};
advice = this.spy();
- ref = aop.before(fixture, /method[12]/, advice);
+ ref = meld.before(fixture, /method[12]/, advice);
assert.defined(fixture.method1._advisor);
assert.defined(fixture.method2._advisor);
@@ -68,7 +68,7 @@ buster.testCase('remove', {
spy = fixture.method = this.spy();
advice = this.spy();
- ref = aop.around(fixture, 'method', advice);
+ ref = meld.around(fixture, 'method', advice);
fixture.method();
@@ -96,7 +96,7 @@ buster.testCase('remove', {
fixture = new Fixture();
fixture.method = this.spy();
- ref = aop.add(fixture, 'method', aspect);
+ ref = meld(fixture, 'method', aspect);
fixture.method();