2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Open your browser console
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/demo/src/demo.js:
--------------------------------------------------------------------------------
1 | DemoApp = angular.module('DemoApp', ['AngularAOP']);
2 |
3 |
4 | DemoApp.controller('ArticlesListCtrl', function ($scope, ArticlesCollection) {
5 | ArticlesCollection.getSpecialArticles();
6 | ArticlesCollection.loadArticles().then(function () {
7 | try {
8 | var article = ArticlesCollection.getArticleById(0);
9 | } catch (e) {
10 | console.error(e.message);
11 | }
12 | });
13 | });
14 |
15 | DemoApp.factory('Authorization', function (User) {
16 | return function () {
17 | if (User.getUsername() !== 'foo' &&
18 | User.getPassword() !== 'bar') {
19 | throw new Error('Not authorized');
20 | }
21 | };
22 | });
23 |
24 | DemoApp.provider('Logger', function () {
25 | return {
26 | $get: function () {
27 | return function (args) {
28 | if (args.exception) {
29 | console.log('%cException: ' + args.exception.message + '. ' + args.method + ' called before proper authorization.',
30 | 'color: red; text-weight: bold; font-size: 1.2em;');
31 | }
32 | var throwData = (args.exception) ? ' and threw: ' + args.exception.message : '';
33 | console.log('Method: ' + args.method + ', Pointcut: ' + args.when + ', with arguments: ' +
34 | angular.toJson(args.args) + throwData + ' and resolve data: ' +
35 | angular.toJson(args.resolveArgs) + ', reject data: ' + angular.toJson(args.rejectArgs));
36 | };
37 | }
38 | }
39 | });
40 |
41 |
42 | DemoApp.provider('LoggerAsync', function () {
43 | return {
44 | $get: function ($timeout) {
45 | return function (args) {
46 | return $timeout(function () {
47 | console.log('Async logger', args);
48 | }, 1000);
49 | };
50 | }
51 | }
52 | });
53 |
54 | DemoApp.service('User', function () {
55 |
56 | this._username = null;
57 | this._password = null;
58 |
59 | this.setUsername = function (user) {
60 | this._username = user;
61 | };
62 |
63 | this.setPassword = function (pass) {
64 | this._password = pass;
65 | };
66 |
67 | this.getUsername = function () {
68 | return this._username;
69 | };
70 |
71 | this.getPassword = function () {
72 | return this._password;
73 | };
74 | });
75 |
76 | DemoApp.provider('ArticlesCollection', function () {
77 | return {
78 | $get: function ($q, $timeout, execute, Logger, Authorization) {
79 | var sampleArticles = [
80 | { id: 0, title: 'Title 1', content: 'Content 1' },
81 | { id: 1, title: 'Title 2', content: 'Content 2' },
82 | { id: 2, title: 'Title 3', content: 'Content 3' }
83 | ],
84 | privateArticles = [
85 | { id: 3, title: 'Title 4', content: 'Content 4' },
86 | { id: 4, title: 'Title 5', content: 'Content 5' }
87 | ],
88 | api = {
89 | loadArticles: function () {
90 | var deferred = $q.defer();
91 | $timeout(function () {
92 | deferred.resolve(sampleArticles);
93 | }, 1000);
94 | return deferred.promise;
95 | },
96 | getArticleById: function (id) {
97 | for (var i = 0; i < sampleArticles.length; i += 1) {
98 | if (sampleArticles[i].id === id) {
99 | return sampleArticles[i];
100 | }
101 | }
102 | return undefined;
103 | },
104 | getSpecialArticles: function () {
105 | return privateArticles;
106 | }
107 | };
108 | return api;
109 | }
110 | };
111 | });
112 |
113 | DemoApp.config(function ($provide, executeProvider) {
114 | executeProvider.annotate($provide, {
115 | 'ArticlesCollection': [{
116 | jointPoint: 'aroundAsync',
117 | advice: 'LoggerAsync',
118 | methodPattern: /Special/
119 | }]
120 | });
121 | });
--------------------------------------------------------------------------------
/docs/README.markdown:
--------------------------------------------------------------------------------
1 |
2 |
3 | Table of Contents
4 | ========
5 | * [About AngularAOP](#about-angularaop)
6 | * [Online demo](#online-demo)
7 | * [Usage](#usage)
8 | * [Known issues](#known-issues)
9 | * [Circular dependency](#circular-dependency)
10 | * [Change log](#change-log)
11 | * [v0.1.0](#v010)
12 | * [v0.1.1](#v011)
13 | * [v0.2.0](#v020)
14 | * [Roadmap](#roadmap)
15 | * [License](#license)
16 |
17 | About AngularAOP
18 | ===========
19 |
20 | *AngularAOP* is simple framework for Aspect-Oriented Programming with AngularJS.
21 | AOP fits great with AngularJS because of the framework architecture and also because it solves many cross-cutting concerns.
22 |
23 | AngularAOP allows the usage of different aspects on a single method of given service or applying given aspect to all service's methods.
24 |
25 | Few sample usages of AOP with AngularJS are:
26 |
27 | * Logging
28 | * Forcing authorization policies
29 | * Caching
30 | * Applying exception handling policies
31 | * Instrumentation to gather performance statistics
32 | * Retry logic, circuit breakers
33 |
34 | Some of these use cases are suggested by [Christian Crowhurst](https://github.com/christianacca).
35 |
36 | This micro-framework is only 1.5KB (minified and gzipped).
37 |
38 | Online demo
39 | ============
40 |
41 | If you prefer learning by doing (trial and error), come [right this way](http://plnkr.co/edit/R9juR0oe4xT5AHQs5uDF?p=preview).
42 |
43 | Usage
44 | ======
45 |
46 | For using AngularAOP you need to load the `AngularAOP` module:
47 | ```js
48 | angular.module('myModule', ['AngularAOP']);
49 | ```
50 |
51 | Your cross-cutting concerns can be defined in separate services. For example here is a definition of logging service which logs the method calls and thrown exception:
52 |
53 | ```js
54 | DemoApp.factory('Logger', function () {
55 | return function (args) {
56 | if (args.exception) {
57 | console.log('%cException: ' + args.exception.message + '. '
58 | + args.method + ' called before proper authorization.',
59 | 'color: red; text-weight: bold; font-size: 1.2em;');
60 | }
61 | var throwData = (args.exception) ? ' and threw: ' + args.exception.message : '';
62 | console.log('Method: ' + args.method + ', Pointcut: ' + args.when + ', with arguments: ' +
63 | angular.toJson(args.args) + throwData);
64 | };
65 | });
66 | ```
67 |
68 | The definition of that service doesn't differ from the usual service definition.
69 |
70 | Let's look closer at the `args` argument of the logging service.
71 | It has few properties which we use for logging:
72 |
73 | * `result` - The result returned by the user function (when the joint point permit this).
74 | * `exception` - `Error` object thrown inside the method to which the aspect was applied.
75 | * `method` - The name of the method to which the aspect was applied.
76 | * `when` - When the advice was applied i.e. when the actual logging was occurred.
77 | * `arguments` - The arguments of the method to which the advice was applied.
78 | * `resolveArgs` - The arguments passed to the resolve callback, when promise related aspects are used
79 | * `rejectArgs` - The arguments passed to the reject callback, when promise related aspects are used
80 |
81 |
82 | Let's look at one more declaration of aspect:
83 |
84 | ```js
85 | DemoApp.factory('Authorization', function (User) {
86 | return function () {
87 | if (User.getUsername() !== 'foo' &&
88 | User.getPassword() !== 'bar') {
89 | throw new Error('Not authorized');
90 | }
91 | };
92 | });
93 | ```
94 |
95 | This is another common example for using AOP - authorization. The given service just checks whether user's user name and password are equal respectively to `foo` and `bar`, if they are not equal to these values the service throws an `Error('Not authorized')`.
96 |
97 | We may want to apply authorization for reading news:
98 |
99 | ```js
100 | DemoApp.service('ArticlesCollection', function ($q, $timeout, execute, Logger, Authorization) {
101 |
102 | var sampleArticles = [
103 | { id: 0, title: 'Title 1', content: 'Content 1' },
104 | { id: 1, title: 'Title 2', content: 'Content 2' },
105 | { id: 2, title: 'Title 3', content: 'Content 3' }
106 | ],
107 | privateArticles = [
108 | { id: 3, title: 'Title 4', content: 'Content 4' },
109 | { id: 4, title: 'Title 5', content: 'Content 5' }
110 | ],
111 | api = {
112 | loadArticles: function () {
113 | var deferred = $q.defer();
114 | $timeout(function () {
115 | deferred.resolve(sampleArticles);
116 | }, 1000);
117 | return deferred.promise;
118 | },
119 | getArticleById: function (id) {
120 | for (var i = 0; i < sampleArticles.length; i += 1) {
121 | if (sampleArticles[i].id === id) {
122 | return sampleArticles[i];
123 | }
124 | }
125 | return undefined;
126 | },
127 | getPrivateArticles: function () {
128 | return privateArticles;
129 | }
130 | };
131 | return api;
132 | });
133 | ```
134 |
135 | This is simple service which contains two kinds of articles (simple object literals): `sampleArticles` and `privateArticles`.
136 | The `api` object is the actual service public interface.
137 |
138 | We may want to apply authorization to the private articles, before the `getPrivateArticles` method return its result.
139 | The usual way to do it is:
140 |
141 | ```js
142 | getPrivateArticles: function () {
143 | Authorization();
144 | return privateArticles;
145 | }
146 | ```
147 |
148 | We may also want to apply authorization to the `getArticleById` method, so:
149 |
150 | ```js
151 | getArticleById: function (id) {
152 | Authorization();
153 | for (var i = 0; i < sampleArticles.length; i += 1) {
154 | if (sampleArticles[i].id === id) {
155 | return sampleArticles[i];
156 | }
157 | }
158 | return undefined;
159 | }
160 | ```
161 |
162 | We have two duplicate lines of code. At this moment it's not a big deal but we may want to add logging and see special error message in the console when `Error` is thrown:
163 |
164 |
165 | ```js
166 | //...
167 | getPrivateArticles: function () {
168 | try {
169 | Authorization();
170 | return privateArticles;
171 | } catch (e) {
172 | console.log('%cException: ' + e.message + '. getPrivateArticles called before proper authorization.',
173 | 'color: red; text-weight: bold; font-size: 1.2em;');
174 | }
175 | },
176 | getArticleById: function (id) {
177 | try {
178 | Authorization();
179 | for (var i = 0; i < sampleArticles.length; i += 1) {
180 | if (sampleArticles[i].id === id) {
181 | return sampleArticles[i];
182 | }
183 | }
184 | } catch (e) {
185 | console.log('%cException: ' + e.message + '. getArticleById called before proper authorization.',
186 | 'color: red; text-weight: bold; font-size: 1.2em;');
187 | }
188 | return undefined;
189 | }
190 | //...
191 | ```
192 |
193 | Now we have a lot of duplicates and if we want to change something in the code which authorizes the user and logs the error we should change it in both places. We may have service with large interface which requires logging and authorization (or something else) in all of its methods or big part of them. In this case we need something more powerful and the Aspect-Oriented Programming gives us the tools for that.
194 |
195 | We can achieve the same effect as in the code above just by applying `Authorization` and `Logger` service to the `api` object:
196 |
197 | ```js
198 | return execute(Logger).onThrowOf(execute(Authorization).before(api, {
199 | methodPattern: /Special|getArticleById/
200 | }));
201 | ```
202 |
203 | This code will invoke the `Authorization` service before executing the methods which match the pattern: `/Special|getArticleById/` when an `Error` is thrown the `Logger` will log it with detailed information.
204 | Notice that `onThrowOf`, `before` and all the methods listed bellow return object with the same methods so chaining is possible.
205 | We can also match the methods not only by their names but also by their arguments:
206 |
207 |
208 | ```js
209 | return execute(Logger).onThrowOf(execute(Authorization).before(api, {
210 | methodPattern: /Special|getArticleById/,
211 | argsPatterns: [/^user$/, /^[Ii]d(_num)?$/]
212 | }));
213 | ```
214 |
215 | Now the aspects will be applied only to the methods which match both the `methodPattern` and `argsPatterns` rules.
216 |
217 | Currently `execute` supports the following pointcuts:
218 |
219 | * `before` - executes given service before the matched methods are invoked.
220 | * `after` - executes given service after the matched methods are invoked.
221 | * `around` - executes given service before and after the matched methods are invoked.
222 | * `onThrowOf` - executes when an `Error` is thrown by method from the given set of matched methods.
223 | * `onResolveOf` - executes after promise returned by a method from the given set of matched methods is resolved but before the resolve callback is invoked.
224 | * `afterResolveOf` - executes after promise returned by a method from the given set of matched methods is resolved but after the resolve callback is invoked.
225 | * `onRejectOf` - executes after promise returned by a method from the given set of matched methods is rejected.
226 |
227 | Aspects can be applied not only to objects but also to functions:
228 |
229 | ```js
230 | DemoApp.factory('ArticlesCollection', function ($q, $timeout, execute, Logger, Authorization) {
231 | return execute(Logger).before(function () {
232 | //body
233 | });
234 | });
235 | ```
236 |
237 | # Known issues
238 |
239 | ## Circular dependency
240 |
241 | This is not issue in AngularAOP but something which should be considered when using Dependency Injection.
242 |
243 | Note that if the `$injector` tries to get a service that depends on itself, either directly or indirectly you will get error "Circular dependency". To fix this, construct your dependency chain such that there are no circular dependencies. Check the [following article](http://misko.hevery.com/2008/08/01/circular-dependency-in-constructors-and-dependency-injection/), it can give you a basic idea how to procceed.
244 |
245 | Change log
246 | =========
247 |
248 | ##v0.1.0
249 |
250 | New way of annotating. Now you can annotate in your config callback:
251 |
252 | ```js
253 | DemoApp.config(function ($provide, executeProvider) {
254 | executeProvider.annotate($provide, {
255 | ArticlesCollection: {
256 | jointPoint: 'before',
257 | advice: 'Logger',
258 | methodPattern: /Special/,
259 | argsPatterns: [/arg1/, /arg2/, ..., /argn/]
260 | }
261 | });
262 | });
263 | ```
264 |
265 | ##v0.1.1
266 |
267 | Multiple aspects can be applied to single service through the new way of annotation:
268 |
269 | ```js
270 | DemoApp.config(function ($provide, executeProvider) {
271 | executeProvider.annotate($provide, {
272 | ArticlesCollection: [{
273 | jointPoint: 'before',
274 | advice: 'Logger',
275 | }, {
276 | //aspect 2
277 | }, {
278 | //aspect 3
279 | }, ... , {
280 | //aspect n
281 | }]
282 | });
283 | });
284 | ```
285 |
286 | **Note:** In this way you won't couple your target methods/objects with the aspect at all but your target service must be defined as provider.
287 |
288 | ##v0.2.0
289 |
290 | Added `forceObject` property to the rules. This way issues like [#12](https://github.com/mgechev/angular-aop/issues/12) will not be reproducable since we can force the framework to wrap the target's method, insted of the target itself (in case the target is a function with "static" methods").
291 |
292 | Issues fixed:
293 |
294 | - Once a function is wrapped into an aspect its methods are preserved. We add the target to be prototype of the wrapper, this way using the prototype chain the required methods could be found.
295 |
296 | ##v0.2.1
297 |
298 | Added tests for:
299 |
300 | - Before async joint-point
301 | - On resolve joint-point
302 |
303 | Add JSCS and update Gruntfile.js
304 |
305 | Roadmap
306 | =======
307 |
308 | 1. *Use proper execution context inside the target services. This will fix the issue of invoking non-woven internal methods.*
309 | 3. Write solid amount of tests
310 | 4. More flexible way of defining pointcuts (patching `$provide.provider` might be required)
311 |
312 | Contributors
313 | ============
314 |
315 | [
](https://github.com/mgechev) |[
](https://github.com/Wizek) |[
](https://github.com/slobo) |[
](https://github.com/bitdeli-chef) |[
](https://github.com/christianacca) |[
](https://github.com/peernohell) |
316 | :---: |:---: |:---: |:---: |:---: |:---: |
317 | [mgechev](https://github.com/mgechev) |[Wizek](https://github.com/Wizek) |[slobo](https://github.com/slobo) |[bitdeli-chef](https://github.com/bitdeli-chef) |[christianacca](https://github.com/christianacca) |[peernohell](https://github.com/peernohell) |
318 |
319 |
320 | License
321 | =======
322 |
323 | AngularAOP is distributed under the terms of the MIT license.
324 |
325 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "require angular"
2 | "require ./src/angular-aop.js"
3 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Wed Dec 25 2013 13:10:00 GMT+0200 (EET)
3 |
4 | module.exports = function (config) {
5 |
6 | 'use strict';
7 |
8 | config.set({
9 |
10 | // base path, that will be used to resolve files and exclude
11 | basePath: '',
12 |
13 |
14 | // frameworks to use
15 | frameworks: ['jasmine'],
16 |
17 |
18 | // list of files / patterns to load in the browser
19 | files: [
20 | 'bower_components/angular/angular.js',
21 | 'bower_components/angular-mocks/angular-mocks.js',
22 | 'src/angular-aop.js',
23 | 'src/aspects/aspect.js',
24 | 'src/**/*.js',
25 | 'test/joint-points/common-tests.js',
26 | 'test/**/*spec.js'
27 | ],
28 |
29 |
30 | // list of files to exclude
31 | exclude: [
32 |
33 | ],
34 |
35 |
36 | // test results reporter to use
37 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
38 | reporters: ['progress'],
39 |
40 |
41 | // web server port
42 | port: 9876,
43 |
44 |
45 | // enable / disable colors in the output (reporters and logs)
46 | colors: true,
47 |
48 |
49 | // level of logging
50 | // possible values: config.LOG_DISABLE || config.LOG_ERROR ||
51 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
52 | logLevel: config.LOG_INFO,
53 |
54 |
55 | // enable / disable watching file and executing
56 | // tests whenever any file changes
57 | autoWatch: true,
58 |
59 |
60 | // Start these browsers, currently available:
61 | // - Chrome
62 | // - ChromeCanary
63 | // - Firefox
64 | // - Opera
65 | // - Safari (only Mac)
66 | // - PhantomJS
67 | // - IE (only Windows)
68 | browsers: ['PhantomJS'],
69 |
70 |
71 | // If browser does not capture in given timeout [ms], kill it
72 | captureTimeout: 60000,
73 |
74 |
75 | // Continuous Integration mode
76 | // if true, it capture browsers, run tests and exit
77 | singleRun: false
78 | });
79 | };
80 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-aop",
3 | "version": "0.4.5",
4 | "description": "Framework for aspect-oriented programming with AngularJS",
5 | "author": "Minko Gechev ",
6 | "main": "./build/angular-aop.js",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/mgechev/angular-aop"
10 | },
11 | "dependencies": {
12 | "angular": ">=1.2.0"
13 | },
14 | "devDependencies": {
15 | "grunt": "~0.4.1",
16 | "grunt-contrib-concat": "^0.5.0",
17 | "grunt-contrib-uglify": "~0.2.2",
18 | "grunt-jscs": "~1.5.0",
19 | "grunt-karma": "^0.10.1",
20 | "jasmine-core": "^2.2.0",
21 | "karma": "^0.12.31",
22 | "karma-chrome-launcher": "^0.1.7",
23 | "karma-jasmine": "^0.3.5",
24 | "karma-phantomjs-launcher": "^0.1.4",
25 | "phantom-jasmine": "^0.3.0"
26 | },
27 | "license": "MIT"
28 | }
29 |
--------------------------------------------------------------------------------
/src/angular-aop.js:
--------------------------------------------------------------------------------
1 | /* global angular */
2 | /**
3 | * Framework for aspect-oriented programming with AngularJS
4 | *
5 | * @author Minko Gechev (@mgechev)
6 | * @version <%= version =>
7 | * @license http://opensource.org/licenses/MIT MIT
8 | */
9 |
10 | 'use strict';
11 |
12 | var AngularAop = angular.module('AngularAOP', []);
13 |
14 | //Contains all aspects (pointcut + advice)
15 | var Aspects = {};
16 |
17 | //Defines all joint points
18 | var JOINT_POINTS = {
19 | BEFORE: 'before',
20 | BEFORE_ASYNC: 'beforeAsync',
21 | AFTER: 'after',
22 | AROUND: 'around',
23 | AROUND_ASYNC: 'aroundAsync',
24 | ON_THROW: 'onThrow',
25 | ON_RESOLVE: 'onResolve',
26 | AFTER_RESOLVE: 'afterResolve',
27 | ON_REJECT: 'onReject'
28 | };
29 |
30 | var MaybeQ = null;
31 |
32 | if (!Object.setPrototypeOf) {
33 | Object.setPrototypeOf = (function (Object, magic) {
34 | var set;
35 | function checkArgs(O, proto) {
36 | if (!(/object|function/).test(typeof O) || O === null) {
37 | throw new TypeError('can not set prototype on a non-object');
38 | }
39 | if (!(/object|function/).test(typeof proto) && proto !== null) {
40 | throw new TypeError('can only set prototype to an object or null');
41 | }
42 | }
43 | function setPrototypeOf(O, proto) {
44 | checkArgs(O, proto);
45 | set.call(O, proto);
46 | return O;
47 | }
48 | try {
49 | // this works already in Firefox and Safari
50 | set = Object.getOwnPropertyDescriptor(Object.prototype, magic).set;
51 | set.call({}, null);
52 | } catch (oO) {
53 | if (
54 | // IE < 11 cannot be shimmed
55 | Object.prototype !== {}[magic] ||
56 | // neither can any browser that actually
57 | // implemented __proto__ correctly
58 | // (all but old V8 will return here)
59 | /* jshint proto: true */
60 | { __proto__: null }.__proto__ === void 0
61 | // this case means null objects cannot be passed
62 | // through setPrototypeOf in a reliable way
63 | // which means here a **Sham** is needed instead
64 | ) {
65 | return;
66 | }
67 | // nodejs 0.8 and 0.10 are (buggy and..) fine here
68 | // probably Chrome or some old Mobile stock browser
69 | set = function (proto) {
70 | this[magic] = proto;
71 | };
72 | // please note that this will **not** work
73 | // in those browsers that do not inherit
74 | // __proto__ by mistake from Object.prototype
75 | // in these cases we should probably throw an error
76 | // or at least be informed about the issue
77 | setPrototypeOf.polyfill = setPrototypeOf(
78 | setPrototypeOf({}, null),
79 | Object.prototype
80 | ) instanceof Object;
81 | // setPrototypeOf.polyfill === true means it works as meant
82 | // setPrototypeOf.polyfill === false means it's not 100% reliable
83 | // setPrototypeOf.polyfill === undefined
84 | // or
85 | // setPrototypeOf.polyfill == null means it's not a polyfill
86 | // which means it works as expected
87 | // we can even delete Object.prototype.__proto__;
88 | }
89 | return setPrototypeOf;
90 | }(Object, '__proto__'));
91 | }
92 | // Last chance to pollyfil it...
93 | Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {
94 | obj.__proto__ = proto;
95 | return obj;
96 | };
97 |
98 | /**
99 | * Service which give access to the pointcuts.
100 | */
101 | AngularAop.provider('execute', function executeProvider() {
102 |
103 | //Default regular expression for matching arguments and method names
104 | var defaultRule = /.*/;
105 |
106 | var slice = Array.prototype.slice;
107 |
108 | //Cross-browser trim function
109 | var trim = (function () {
110 | var trimFunction;
111 | if (typeof String.prototype.trim === 'function') {
112 | trimFunction = String.prototype.trim;
113 | } else {
114 | if (this === null) {
115 | return '';
116 | }
117 | var strVal = this.toString();
118 | trimFunction = function () {
119 | return strVal
120 | .replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
121 | };
122 | }
123 | return trimFunction;
124 | }());
125 |
126 | //Builds specified aspect
127 | var AspectBuilder = {
128 | createAspectFactory: function (advice, jointPoint) {
129 | var self = this;
130 | return function (target, rules) {
131 | if (typeof target === 'function' && !rules.forceObject) {
132 | return self._getFunctionAspect(target, jointPoint, advice);
133 | } else if (target) {
134 | return self._getObjectAspect(target, rules || {},
135 | jointPoint, advice);
136 | }
137 | };
138 | },
139 | _getFunctionAspect: function (method, jointPoint, advice, methodName) {
140 | methodName = methodName || this._getMethodName(method);
141 | var aspect = new Aspects[jointPoint](advice);
142 | var wrapper = function __angularAOPWrapper__() {
143 | var args = slice.call(arguments);
144 | args = {
145 | args: args,
146 | context: this,
147 | method: method,
148 | methodName: methodName
149 | };
150 | return aspect._wrapper.call(aspect, args);
151 | };
152 | wrapper.originalMethod = method;
153 | Object.setPrototypeOf(wrapper, method);
154 | aspect.setWrapper(wrapper);
155 | return wrapper;
156 | },
157 | _getMethodName: function (method) {
158 | while (method.originalMethod) {
159 | method = method.originalMethod;
160 | }
161 | return (/function\s+(.*?)\s*\(/).exec(method.toString())[1];
162 | },
163 | _getObjectAspect: function (obj, rules, jointPoint, advice) {
164 | for (var prop in obj) {
165 | if ((obj.hasOwnProperty(prop) || rules.deep) &&
166 | typeof obj[prop] === 'function' &&
167 | this._matchRules(obj, prop, rules)) {
168 | obj[prop] =
169 | this._getFunctionAspect(obj[prop], jointPoint, advice, prop);
170 | }
171 | }
172 | return obj;
173 | },
174 | _matchRules: function (obj, prop, rules) {
175 | var methodPattern = rules.methodPattern || defaultRule;
176 | var argsPatterns = rules.argsPatterns || [];
177 | var method = obj[prop];
178 | var tokens = this._parseMethod(method, prop);
179 | while (tokens.when === '__angularAOPWrapper__') {
180 | method = method.originalMethod;
181 | tokens = this._parseMethod(method, prop);
182 | }
183 | return methodPattern.test(tokens.method) &&
184 | this._matchArguments(argsPatterns, tokens);
185 | },
186 | _matchArguments: function (argsPatterns, tokens) {
187 | if (tokens.args.length < argsPatterns.length) {
188 | return false;
189 | }
190 | var passed = true;
191 | angular.forEach(tokens.args, function (arg, idx) {
192 | var rule = argsPatterns[idx] || defaultRule;
193 | if (!rule.test(arg)) {
194 | passed = false;
195 | return;
196 | }
197 | });
198 | return passed;
199 | },
200 | _parseMethod: function (method, prop) {
201 | var result = { method: prop };
202 | var parts = method.toString()
203 | //stripping the comments
204 | .replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, '')
205 | .match(/function\s+([^\(]*)\s*\(([^\)]*)\)/) || [];
206 | if (parts && parts[2]) {
207 | result.args = [];
208 | angular.forEach(parts[2].split(','), function (arg) {
209 | result.args.push(trim.call(arg));
210 | });
211 | } else {
212 | result.args = [];
213 | }
214 | result.when = parts[1];
215 | return result;
216 | }
217 | };
218 |
219 | /**
220 | * Defines and implements the different advices.
221 | *
222 | * @constructor
223 | * @private
224 | * @param {Function} advice The advice which should be
225 | * applied in the specified joint-point(s)
226 | */
227 | function AspectCollection(advice) {
228 | if (typeof advice !== 'function') {
229 | throw new Error('The advice should be a function');
230 | }
231 | this[JOINT_POINTS.BEFORE] =
232 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.BEFORE);
233 | this[JOINT_POINTS.BEFORE_ASYNC] =
234 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.BEFORE_ASYNC);
235 | this[JOINT_POINTS.AFTER] =
236 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.AFTER);
237 | this[JOINT_POINTS.AROUND] =
238 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.AROUND);
239 | this[JOINT_POINTS.AROUND_ASYNC] =
240 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.AROUND_ASYNC);
241 | this[JOINT_POINTS.ON_THROW] =
242 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.ON_THROW);
243 | this[JOINT_POINTS.ON_RESOLVE] =
244 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.ON_RESOLVE);
245 | this[JOINT_POINTS.AFTER_RESOLVE] =
246 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.AFTER_RESOLVE);
247 | this[JOINT_POINTS.ON_REJECT] =
248 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.ON_REJECT);
249 | }
250 |
251 | function applyAspects($provide, target, aspects) {
252 | angular.forEach(aspects, function (aspect) {
253 | decorate($provide, target, aspect);
254 | });
255 | }
256 |
257 | function decorate($provide, target, annotation) {
258 | $provide.decorator(target, ['$q', '$injector', '$delegate',
259 | function ($q, $injector, $delegate) {
260 | var advice = (typeof annotation.advice === 'string') ?
261 | $injector.get(annotation.advice) : annotation.advice;
262 | var jointPoint = annotation.jointPoint;
263 | var methodPattern = annotation.methodPattern;
264 | var argsPatterns = annotation.argsPattern;
265 | var aspect = new AspectCollection(advice);
266 | MaybeQ = $q;
267 | if (typeof aspect[jointPoint] !== 'function') {
268 | throw new Error('No such joint-point ' + jointPoint);
269 | }
270 | return aspect[jointPoint]($delegate, {
271 | methodPattern: methodPattern,
272 | argsPatterns: argsPatterns,
273 | forceObject: annotation.forceObject,
274 | deep: annotation.deep
275 | });
276 | }]);
277 | }
278 |
279 | var api = {
280 |
281 | annotate: function ($provide, annotations) {
282 | var aspects;
283 | for (var target in annotations) {
284 | aspects = annotations[target];
285 | if (!angular.isArray(aspects)) {
286 | aspects = [aspects];
287 | }
288 | applyAspects($provide, target, aspects);
289 | }
290 | },
291 |
292 | $get: ['$q', function ($q) {
293 | MaybeQ = $q;
294 | return function (advice) {
295 | return new AspectCollection(advice);
296 | };
297 | }]
298 | };
299 |
300 | angular.extend(api, JOINT_POINTS);
301 |
302 | return api;
303 | });
304 |
--------------------------------------------------------------------------------
/src/aspects/aspect.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function Aspect(advice) {
4 | this._advice = advice;
5 | this._wrapperFunc = null;
6 | }
7 |
8 | Aspect.prototype.setWrapper = function (w) {
9 | this._wrapperFunc = w;
10 | };
11 |
12 | Aspect.prototype._wrapper = function () {
13 | throw 'Not implemented';
14 | };
15 |
16 | Aspect.prototype.invoke = function (params) {
17 | var aspectData = {};
18 | aspectData.when = this.when;
19 | aspectData.method = params.methodName;
20 | aspectData.args = params.args;
21 | aspectData.exception = params.exception;
22 | aspectData.result = params.result;
23 | aspectData.resolveArgs = params.resolveArgs;
24 | aspectData.rejectArgs = params.rejectArgs;
25 | aspectData.result = this._advice.call(params.context, aspectData);
26 | return aspectData;
27 | };
28 |
--------------------------------------------------------------------------------
/src/aspects/jointpoints/after.js:
--------------------------------------------------------------------------------
1 | /* global Aspects, JOINT_POINTS, Aspect */
2 | 'use strict';
3 |
4 | Aspects[JOINT_POINTS.AFTER] = function () {
5 | Aspect.apply(this, arguments);
6 | this.when = JOINT_POINTS.AFTER;
7 | };
8 |
9 | Aspects[JOINT_POINTS.AFTER].prototype = Object.create(Aspect.prototype);
10 |
11 | Aspects[JOINT_POINTS.AFTER].prototype._wrapper = function (params) {
12 | var context = params.context;
13 | var result = params.method.apply(context, params.args);
14 | params.result = result;
15 | return this.invoke(params).result || result;
16 | };
17 |
--------------------------------------------------------------------------------
/src/aspects/jointpoints/afterresolve.js:
--------------------------------------------------------------------------------
1 | /* global Aspects, JOINT_POINTS, Aspect, MaybeQ */
2 | 'use strict';
3 |
4 | Aspects[JOINT_POINTS.AFTER_RESOLVE] = function () {
5 | Aspect.apply(this, arguments);
6 | this.when = JOINT_POINTS.AFTER_RESOLVE;
7 | };
8 |
9 | Aspects[JOINT_POINTS.AFTER_RESOLVE].prototype =
10 | Object.create(Aspect.prototype);
11 |
12 | Aspects[JOINT_POINTS.AFTER_RESOLVE].prototype._wrapper = function (params) {
13 | var args = params.args;
14 | var context = params.context;
15 | var method = params.method;
16 | var deferred = MaybeQ.defer();
17 | var innerPromise = deferred.promise;
18 | var promise = method.apply(context, args);
19 | var self = this;
20 | if (!promise || typeof promise.then !== 'function') {
21 | throw new Error('The woven method doesn\'t return a promise');
22 | }
23 | promise.then(function () {
24 | params.resolveArgs = arguments;
25 | innerPromise.then(function () {
26 | self.invoke(params);
27 | });
28 | deferred.resolve.apply(deferred, arguments);
29 | }, function () {
30 | deferred.reject.apply(innerPromise, arguments);
31 | });
32 | return innerPromise;
33 | };
34 |
--------------------------------------------------------------------------------
/src/aspects/jointpoints/around-async.js:
--------------------------------------------------------------------------------
1 | /* global Aspects, JOINT_POINTS, Aspect, MaybeQ */
2 | 'use strict';
3 |
4 | Aspects[JOINT_POINTS.AROUND_ASYNC] = function () {
5 | Aspect.apply(this, arguments);
6 | this.when = JOINT_POINTS.AROUND_ASYNC;
7 | };
8 |
9 | Aspects[JOINT_POINTS.AROUND_ASYNC].prototype =
10 | Object.create(Aspect.prototype);
11 |
12 | Aspects[JOINT_POINTS.AROUND_ASYNC].prototype._wrapper = function (params) {
13 | var context = params.context;
14 | var method = params.method;
15 | var aspectData = this.invoke(params);
16 | var self = this;
17 | var result;
18 |
19 | function afterBefore() {
20 | result = method.apply(context, aspectData.result);
21 | params.result = result;
22 | return self.invoke(params).result || result;
23 | }
24 |
25 | return MaybeQ.when(aspectData.result)
26 | .then(afterBefore, afterBefore);
27 | };
28 |
--------------------------------------------------------------------------------
/src/aspects/jointpoints/around.js:
--------------------------------------------------------------------------------
1 | /* global Aspects, JOINT_POINTS, Aspect */
2 | 'use strict';
3 |
4 | Aspects[JOINT_POINTS.AROUND] = function () {
5 | Aspect.apply(this, arguments);
6 | this.when = JOINT_POINTS.AROUND;
7 | };
8 |
9 | Aspects[JOINT_POINTS.AROUND].prototype = Object.create(Aspect.prototype);
10 |
11 | Aspects[JOINT_POINTS.AROUND].prototype._wrapper = function (params) {
12 | var context = params.context;
13 | var method = params.method;
14 | var result;
15 | result = method.apply(context, this.invoke(params).args);
16 | params.result = result;
17 | return this.invoke(params).result || result;
18 | };
19 |
--------------------------------------------------------------------------------
/src/aspects/jointpoints/before-async.js:
--------------------------------------------------------------------------------
1 | /* global Aspects, JOINT_POINTS, Aspect, MaybeQ */
2 | 'use strict';
3 |
4 | Aspects[JOINT_POINTS.BEFORE_ASYNC] = function () {
5 | Aspect.apply(this, arguments);
6 | this.when = JOINT_POINTS.BEFORE_ASYNC;
7 | };
8 |
9 | Aspects[JOINT_POINTS.BEFORE_ASYNC].prototype =
10 | Object.create(Aspect.prototype);
11 |
12 | Aspects[JOINT_POINTS.BEFORE_ASYNC].prototype._wrapper = function (params) {
13 | var aspectData = this.invoke(params);
14 | return MaybeQ.when(aspectData.result)
15 | .then(function () {
16 | return params.method.apply(params.context, aspectData.args);
17 | }, function () {
18 | return params.method.apply(params.context, aspectData.args);
19 | });
20 | };
21 |
--------------------------------------------------------------------------------
/src/aspects/jointpoints/before.js:
--------------------------------------------------------------------------------
1 | /* global Aspects, JOINT_POINTS, Aspect */
2 | 'use strict';
3 |
4 | Aspects[JOINT_POINTS.BEFORE] = function () {
5 | Aspect.apply(this, arguments);
6 | this.when = JOINT_POINTS.BEFORE;
7 | };
8 |
9 | Aspects[JOINT_POINTS.BEFORE].prototype = Object.create(Aspect.prototype);
10 |
11 | Aspects[JOINT_POINTS.BEFORE].prototype._wrapper = function (params) {
12 | return params.method.apply(params.context, this.invoke(params).args);
13 | };
14 |
--------------------------------------------------------------------------------
/src/aspects/jointpoints/onreject.js:
--------------------------------------------------------------------------------
1 | /* global Aspects, JOINT_POINTS, Aspect */
2 | 'use strict';
3 |
4 | Aspects[JOINT_POINTS.ON_REJECT] = function () {
5 | Aspect.apply(this, arguments);
6 | this.when = JOINT_POINTS.ON_REJECT;
7 | };
8 |
9 | Aspects[JOINT_POINTS.ON_REJECT].prototype = Object.create(Aspect.prototype);
10 |
11 | Aspects[JOINT_POINTS.ON_REJECT].prototype._wrapper = function (params) {
12 | var args = params.args;
13 | var context = params.context;
14 | var method = params.method;
15 | var promise = method.apply(context, args);
16 | var self = this;
17 | if (promise && typeof promise.then === 'function') {
18 | promise.then(undefined, function () {
19 | params.rejectArgs = arguments;
20 | self.invoke(params);
21 | });
22 | }
23 | return promise;
24 | };
25 |
--------------------------------------------------------------------------------
/src/aspects/jointpoints/onresolve.js:
--------------------------------------------------------------------------------
1 | /* global Aspects, JOINT_POINTS, Aspect */
2 | 'use strict';
3 |
4 | Aspects[JOINT_POINTS.ON_RESOLVE] = function () {
5 | Aspect.apply(this, arguments);
6 | this.when = JOINT_POINTS.ON_RESOLVE;
7 | };
8 |
9 | Aspects[JOINT_POINTS.ON_RESOLVE].prototype =
10 | Object.create(Aspect.prototype);
11 |
12 | Aspects[JOINT_POINTS.ON_RESOLVE].prototype._wrapper = function (params) {
13 | var args = params.args;
14 | var context = params.context;
15 | var method = params.method;
16 | var promise = method.apply(context, args);
17 | var self = this;
18 | if (promise && typeof promise.then === 'function') {
19 | promise.then(function () {
20 | params.resolveArgs = arguments;
21 | self.invoke(params);
22 | });
23 | }
24 | return promise;
25 | };
26 |
--------------------------------------------------------------------------------
/src/aspects/jointpoints/onthrow.js:
--------------------------------------------------------------------------------
1 | /* global Aspects, JOINT_POINTS, Aspect */
2 | 'use strict';
3 |
4 | Aspects[JOINT_POINTS.ON_THROW] = function () {
5 | Aspect.apply(this, arguments);
6 | this.when = JOINT_POINTS.ON_THROW;
7 | };
8 |
9 | Aspects[JOINT_POINTS.ON_THROW].prototype = Object.create(Aspect.prototype);
10 |
11 | Aspects[JOINT_POINTS.ON_THROW].prototype._wrapper = function (params) {
12 | var args = params.args;
13 | var result;
14 | try {
15 | result = params.method.apply(params.context, args);
16 | } catch (e) {
17 | params.exception = e;
18 | this.invoke(params);
19 | }
20 | return result;
21 | };
22 |
--------------------------------------------------------------------------------
/test/angular-aop.spec.js:
--------------------------------------------------------------------------------
1 | /* global describe,it,expect,spyOn,afterEach,
2 | document,angular,beforeEach,JOINT_POINTS */
3 |
4 | describe('Angular AOP', function () {
5 | 'use strict';
6 |
7 | it('should define AngularAOP module', function () {
8 | var module;
9 | expect(function () {
10 | module = angular.module('AngularAOP');
11 | }).not.toThrow();
12 | expect(typeof module).toBe('object');
13 | });
14 |
15 | it('should define appropriate interface of the provider', function () {
16 | var api = [
17 | '$get', 'annotate'
18 | ];
19 | api = api.concat(Object.keys(JOINT_POINTS));
20 | angular.module('demo', ['ng', 'AngularAOP'])
21 | .config(function (executeProvider) {
22 | api.forEach(function (key) {
23 | expect(executeProvider[key]).not.toBeUndefined();
24 | });
25 | });
26 | angular.bootstrap({}, ['demo']);
27 | });
28 |
29 | it('should define service called execute with dependencies in "ng"',
30 | function () {
31 | var injector = angular.injector(['ng', 'AngularAOP']);
32 | var execute;
33 | expect(function () {
34 | execute = injector.get('execute');
35 | }).not.toThrow();
36 | expect(typeof execute).toBe('function');
37 | });
38 |
39 | describe('annotation', function () {
40 | var module;
41 | var dummyServiceSpyActiveMethod;
42 | var dummyServiceSpyInactiveMethod;
43 | var a1Spy;
44 | var a2Spy;
45 | var advices;
46 |
47 | beforeEach(function () {
48 | module = angular.module('Test', ['AngularAOP']);
49 | advices = {
50 | a1: function () {},
51 | a2: function () {}
52 | };
53 |
54 | a1Spy = spyOn(advices, 'a1');
55 | a2Spy = spyOn(advices, 'a2');
56 |
57 | module.factory('A1', function () {
58 | return advices.a1;
59 | });
60 |
61 | module.factory('A2', function () {
62 | return advices.a2;
63 | });
64 |
65 | module.factory('DummyService', function () {
66 | var api = {
67 | active: function (simpleArg) {
68 | return simpleArg;
69 | },
70 | inactive: function () {}
71 | };
72 | dummyServiceSpyActiveMethod =
73 | spyOn(api, 'active');
74 | dummyServiceSpyInactiveMethod =
75 | spyOn(api, 'inactive');
76 | return api;
77 | });
78 | });
79 |
80 | it('should be able to annotate services in the config callback',
81 | function () {
82 | module.config(function (executeProvider, $provide) {
83 | executeProvider.annotate($provide, {
84 | DummyService: [{
85 | jointPoint: 'before',
86 | advice: 'A1'
87 | }]
88 | });
89 | });
90 | var ds = angular.injector(['ng', 'Test']).get('DummyService');
91 | ds.active();
92 | expect(dummyServiceSpyActiveMethod).toHaveBeenCalled();
93 | expect(a1Spy).toHaveBeenCalled();
94 | });
95 |
96 | it('should be able to filter methods based on' +
97 | 'pattern matching the method name',
98 | function () {
99 |
100 | module.config(function (executeProvider, $provide) {
101 | executeProvider.annotate($provide, {
102 | DummyService: [{
103 | jointPoint: 'before',
104 | advice: 'A1',
105 | methodPattern: /^a/
106 | }]
107 | });
108 | });
109 |
110 | var ds = angular.injector(['ng', 'Test']).get('DummyService');
111 | ds.inactive();
112 | expect(dummyServiceSpyInactiveMethod).toHaveBeenCalled();
113 | expect(a1Spy).not.toHaveBeenCalled();
114 |
115 | ds.active();
116 | expect(dummyServiceSpyActiveMethod).toHaveBeenCalled();
117 | expect(a1Spy).toHaveBeenCalled();
118 | });
119 |
120 | // Cannot test with spys
121 | it('should be able to filter methods based on ' +
122 | 'pattern matching the method args',
123 | function () {
124 |
125 | module.config(function (executeProvider, $provide) {
126 | executeProvider.annotate($provide, {
127 | DummyService: [{
128 | jointPoint: 'before',
129 | advice: 'A1',
130 | argsPatterns: [/^simple/]
131 | }]
132 | });
133 | });
134 |
135 | var ds = angular.injector(['ng', 'Test']).get('DummyService');
136 | ds.inactive();
137 | expect(dummyServiceSpyInactiveMethod).toHaveBeenCalled();
138 | // expect(a1Spy).not.toHaveBeenCalled();
139 |
140 | ds.active();
141 | expect(dummyServiceSpyActiveMethod).toHaveBeenCalled();
142 | expect(a1Spy).toHaveBeenCalled();
143 |
144 | });
145 |
146 | afterEach(function () {
147 | angular.bootstrap(document, ['Test']);
148 | });
149 |
150 | });
151 |
152 | describe('The API', function () {
153 |
154 | var module;
155 | beforeEach(function () {
156 | module = angular.module('Test', ['AngularAOP']);
157 | });
158 |
159 | describe('forceObject', function () {
160 | it('should not wrap function\'s methods if "forceObject" ' +
161 | 'property is set to false', function () {
162 | var injector = angular.injector(['ng', 'AngularAOP']);
163 | var execute = injector.get('execute');
164 | var target = function () {
165 | targetCalled = true;
166 | };
167 | var targetCalled = false;
168 | target.method = function () {
169 | };
170 | target.anotherMethod = function () {
171 | };
172 | var parentObj = {};
173 | parentObj.advice = function () {
174 | };
175 | var adviceSpy = spyOn(parentObj, 'advice');
176 | var aspect = execute(parentObj.advice).around(target, {
177 | forceObject: false
178 | });
179 | aspect.method();
180 | expect(adviceSpy).not.toHaveBeenCalled();
181 | expect(targetCalled).toBeFalsy();
182 | aspect();
183 | expect(adviceSpy).toHaveBeenCalled();
184 | expect(targetCalled).toBeTruthy();
185 | });
186 |
187 | it('should wrap function\'s methods if "forceObject" ' +
188 | 'property is set to true', function () {
189 |
190 | var injector = angular.injector(['ng', 'AngularAOP']);
191 | var execute = injector.get('execute');
192 | var target = function () {
193 | };
194 | var targetCalled = false;
195 | target.method = function () {
196 | targetCalled = true;
197 | };
198 | target.anotherMethod = function () {
199 | };
200 | var parentObj = {};
201 | parentObj.advice = function () {
202 | };
203 | var adviceSpy = spyOn(parentObj, 'advice');
204 | var aspect = execute(parentObj.advice).around(target, {
205 | forceObject: true
206 | });
207 | aspect();
208 | expect(adviceSpy).not.toHaveBeenCalled();
209 | expect(targetCalled).toBeFalsy();
210 | aspect.method();
211 | expect(adviceSpy).toHaveBeenCalled();
212 | expect(targetCalled).toBeTruthy();
213 | });
214 |
215 | // To refactor with spies
216 | it('should allow wrapping prototype methods when "deep" is specified',
217 | function () {
218 | var app = angular.module('demo', ['AngularAOP', 'ng']);
219 | var loggerCalled = false;
220 | var wovenCalled = false;
221 | app.factory('Logger', function () {
222 | return function () {
223 | loggerCalled = true;
224 | };
225 | });
226 | function DummyService() {}
227 | DummyService.prototype.foo = function () {
228 | wovenCalled = true;
229 | };
230 | app.service('DummyService', DummyService);
231 | app.config(function ($provide, executeProvider) {
232 | executeProvider.annotate($provide, {
233 | DummyService: [{
234 | jointPoint: 'after',
235 | advice: 'Logger',
236 | deep: true
237 | }]
238 | });
239 | });
240 | var injector = angular.injector(['demo']);
241 | var Dummy = injector.get('DummyService');
242 | Dummy.foo();
243 | expect(wovenCalled).toBeTruthy();
244 | expect(loggerCalled).toBeTruthy();
245 | });
246 |
247 | });
248 |
249 | });
250 |
251 | });
252 |
--------------------------------------------------------------------------------
/test/joint-points/after.spec.js:
--------------------------------------------------------------------------------
1 | describe('After joint-point', function () {
2 | 'use strict';
3 |
4 | commonJointpointTests(JOINT_POINTS.AFTER);
5 |
6 | it('should invoke the advice after the method', function () {
7 | var after = new Aspects[JOINT_POINTS.AFTER](function () {
8 | adviceCalled = true;
9 | expect(methodCalled).toBeTruthy();
10 | }),
11 | params = {
12 | method: function () {
13 | methodCalled = true;
14 | expect(adviceCalled).toBeFalsy();
15 | },
16 | context: {}
17 | },
18 | adviceCalled = false,
19 | methodCalled = false;
20 | after._wrapper(params);
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/test/joint-points/afterresolve.spec.js:
--------------------------------------------------------------------------------
1 | describe('After resolve joint-point', function () {
2 | 'use strict';
3 |
4 | commonJointpointTests(JOINT_POINTS.AFTER_RESOLVE);
5 |
6 | it('should invoke the advice after the method was resolved',
7 | function (done) {
8 | var onResolve = new Aspects[JOINT_POINTS.AFTER_RESOLVE](function () {
9 | adviceCalled = true;
10 | expect(methodCalled).toBeTruthy();
11 | done();
12 | });
13 | var params = {
14 | method: function () {
15 | var res = MaybeQ.when(1);
16 | methodCalled = true;
17 | expect(adviceCalled).toBeFalsy();
18 | return res;
19 | },
20 | context: {}
21 | };
22 | var adviceCalled = false;
23 | var methodCalled = false;
24 | onResolve._wrapper(params);
25 | });
26 |
27 | it('should invoke the advice before the attached to promise' +
28 | 'method was invoked',
29 | function (done) {
30 | var onResolve = new Aspects[JOINT_POINTS.AFTER_RESOLVE](function () {
31 | adviceCalled = true;
32 | expect(methodCalled).toBeTruthy();
33 | expect(resolvedPoitcut).toBeTruthy();
34 | done();
35 | });
36 | var params = {
37 | method: function () {
38 | var res = MaybeQ.when(1);
39 | methodCalled = true;
40 | expect(adviceCalled).toBeFalsy();
41 | expect(resolvedPoitcut).toBeFalsy();
42 | return res;
43 | },
44 | context: {}
45 | };
46 | var adviceCalled = false;
47 | var methodCalled = false;
48 | var resolvedPoitcut = false;
49 | onResolve._wrapper(params)
50 | .then(function () {
51 | expect(adviceCalled).toBeFalsy();
52 | expect(methodCalled).toBeTruthy();
53 | resolvedPoitcut = true;
54 | });
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/test/joint-points/around-async.spec.js:
--------------------------------------------------------------------------------
1 | describe('Around async joint-point', function () {
2 | 'use strict';
3 |
4 | commonJointpointTests(JOINT_POINTS.AROUND_ASYNC);
5 |
6 | it('should invoke the advice after the method was resolved',
7 | function (done) {
8 | var onResolve = new Aspects[JOINT_POINTS.AROUND_ASYNC](function () {
9 | adviceCalled += 1;
10 | if (adviceCalled === 2) {
11 | expect(methodCalled).toBeTruthy();
12 | done();
13 | }
14 | });
15 | var params = {
16 | method: function () {
17 | var res = MaybeQ.when(1);
18 | methodCalled = true;
19 | expect(adviceCalled).toBe(1);
20 | return res;
21 | },
22 | context: {}
23 | };
24 | var adviceCalled = 0;
25 | var methodCalled = false;
26 | onResolve._wrapper(params);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/test/joint-points/around.spec.js:
--------------------------------------------------------------------------------
1 | describe('Around joint-point', function () {
2 | 'use strict';
3 |
4 | commonJointpointTests(JOINT_POINTS.AROUND);
5 |
6 | it('should invoke the advice around the method', function () {
7 | var around = new Aspects[JOINT_POINTS.AROUND](function () {
8 | adviceCalled += 1;
9 | if (adviceCalled === 2) {
10 | expect(methodCalled).toBeTruthy();
11 | } else {
12 | expect(methodCalled).toBeFalsy();
13 | }
14 | }),
15 | params = {
16 | method: function () {
17 | methodCalled = true;
18 | expect(adviceCalled).toBe(1);
19 | },
20 | context: {}
21 | },
22 | adviceCalled = 0,
23 | methodCalled = false;
24 | around._wrapper(params);
25 | });
26 |
27 | });
28 |
--------------------------------------------------------------------------------
/test/joint-points/before-async.spec.js:
--------------------------------------------------------------------------------
1 | describe('Before async joint-point', function () {
2 | 'use strict';
3 |
4 | commonJointpointTests(JOINT_POINTS.BEFORE_ASYNC);
5 |
6 | it('should invoke the method after the advice\'s result was resolved',
7 | function (done) {
8 | var resolved = MaybeQ.when(1);
9 | var beforeAsync = new Aspects[JOINT_POINTS.BEFORE_ASYNC](function () {
10 | adviceCalled = true;
11 | expect(methodCalled).toBeFalsy();
12 | return resolved;
13 | });
14 | var params = {
15 | method: function () {
16 | methodCalled = true;
17 | expect(adviceCalled).toBeTruthy();
18 | done();
19 | },
20 | context: {}
21 | };
22 | var adviceCalled = false;
23 | var methodCalled = false;
24 | beforeAsync._wrapper(params);
25 | });
26 |
27 | it('should invoke the method after the advice\'s result was rejected',
28 | function (done) {
29 | var rejected = MaybeQ.reject(1);
30 | var beforeAsync = new Aspects[JOINT_POINTS.BEFORE_ASYNC](function () {
31 | adviceCalled = true;
32 | expect(methodCalled).toBeFalsy();
33 | return rejected;
34 | });
35 | var params = {
36 | method: function () {
37 | methodCalled = true;
38 | expect(adviceCalled).toBeTruthy();
39 | done();
40 | },
41 | context: {}
42 | };
43 | var adviceCalled = false;
44 | var methodCalled = false;
45 | beforeAsync._wrapper(params);
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/test/joint-points/before.spec.js:
--------------------------------------------------------------------------------
1 | describe('Before joint-point', function () {
2 | 'use strict';
3 |
4 | commonJointpointTests(JOINT_POINTS.BEFORE);
5 |
6 | it('should invoke the advice before the method', function () {
7 | var before = new Aspects[JOINT_POINTS.BEFORE](function () {
8 | adviceCalled = true;
9 | expect(methodCalled).toBeFalsy();
10 | }),
11 | params = {
12 | method: function () {
13 | methodCalled = true;
14 | expect(adviceCalled).toBeTruthy();
15 | },
16 | context: {}
17 | },
18 | adviceCalled = false,
19 | methodCalled = false;
20 | before._wrapper(params);
21 | });
22 | });
--------------------------------------------------------------------------------
/test/joint-points/common-tests.js:
--------------------------------------------------------------------------------
1 | function commonJointpointTests(jointPoint) {
2 | 'use strict';
3 |
4 | var KEYS = ['when', 'method', 'args', 'exception',
5 | 'result', 'resolveArgs', 'rejectArgs'];
6 |
7 | it('should be defined', function () {
8 | expect(typeof Aspects[jointPoint]).toBe('function');
9 | });
10 |
11 | it('should extend Aspect', function () {
12 | var after = new Aspects[jointPoint](42);
13 | expect(after instanceof Aspect).toBeTruthy();
14 | });
15 |
16 | it('should set appropriate value to when', function () {
17 | var after = new Aspects[jointPoint](42);
18 | expect(after.when).toBe(jointPoint);
19 | });
20 |
21 | it('should invoke the advice with the appropriate context', function () {
22 | var after = new Aspects[jointPoint](function () {}),
23 | params = {
24 | method: function () {
25 | expect(this).toBe(params.context);
26 | return MaybeQ.when(null);
27 | },
28 | context: {}
29 | };
30 | expect(after._wrapper(params));
31 | });
32 |
33 | it('should invoke the advice with appropriate object', function () {
34 | var after = new Aspects[jointPoint](function (args) {
35 | var keys = Object.keys(args);
36 | KEYS.forEach(function (k) {
37 | expect(keys.indexOf(k) >= 0).toBeTruthy();
38 | });
39 | }),
40 | params = {
41 | method: function () {
42 | expect(this).toBe(params.context);
43 | return MaybeQ.when(null);
44 | },
45 | context: {}
46 | };
47 | expect(after._wrapper(params));
48 | });
49 | }
50 |
--------------------------------------------------------------------------------
/test/joint-points/onresolve.spec.js:
--------------------------------------------------------------------------------
1 | describe('On resolve joint-point', function () {
2 | 'use strict';
3 |
4 | commonJointpointTests(JOINT_POINTS.ON_RESOLVE);
5 |
6 | it('should invoke the advice after the method was resolved',
7 | function (done) {
8 | var onResolve = new Aspects[JOINT_POINTS.ON_RESOLVE](function () {
9 | adviceCalled = true;
10 | expect(methodCalled).toBeTruthy();
11 | done();
12 | });
13 | var params = {
14 | method: function () {
15 | var res = MaybeQ.when(1);
16 | methodCalled = true;
17 | expect(adviceCalled).toBeFalsy();
18 | return res;
19 | },
20 | context: {}
21 | };
22 | var adviceCalled = false;
23 | var methodCalled = false;
24 | onResolve._wrapper(params);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/test/joint-points/onthrow.spec.js:
--------------------------------------------------------------------------------
1 | describe('On throw joint-point', function () {
2 | 'use strict';
3 |
4 | commonJointpointTests(JOINT_POINTS.ON_THROW);
5 |
6 | it('should invoke the advice after throw of error', function () {
7 | var onThrow = new Aspects[JOINT_POINTS.ON_THROW](function () {
8 | adviceCalled = true;
9 | expect(methodCalled).toBeTruthy();
10 | }),
11 | params = {
12 | method: function () {
13 | methodCalled = true;
14 | expect(adviceCalled).toBeFalsy();
15 | throw 'Error';
16 | },
17 | context: {}
18 | },
19 | adviceCalled = false,
20 | methodCalled = false;
21 | onThrow._wrapper(params);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------