├── .bowerrc ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmignore ├── .travis.yml ├── Gruntfile.js ├── README.md ├── bower.json ├── build ├── angular-aop.js └── angular-aop.min.js ├── demo ├── index.html └── src │ └── demo.js ├── docs └── README.markdown ├── index.js ├── karma.conf.js ├── package.json ├── src ├── angular-aop.js └── aspects │ ├── aspect.js │ └── jointpoints │ ├── after.js │ ├── afterresolve.js │ ├── around-async.js │ ├── around.js │ ├── before-async.js │ ├── before.js │ ├── onreject.js │ ├── onresolve.js │ └── onthrow.js └── test ├── angular-aop.spec.js └── joint-points ├── after.spec.js ├── afterresolve.spec.js ├── around-async.spec.js ├── around.spec.js ├── before-async.spec.js ├── before.spec.js ├── common-tests.js ├── onresolve.spec.js └── onthrow.spec.js /.bowerrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/angular-aop/3ad9066a798ed01ad1d094318aa53d37e41787e2/.bowerrc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | npm-debug.log -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": ["else", "for", "while", "do", "try", "catch"], 3 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch", "function"], 4 | "requireSpacesInFunctionExpression": { 5 | "beforeOpeningCurlyBrace": true 6 | }, 7 | "disallowMultipleVarDecl": true, 8 | "requireSpacesInsideObjectBrackets": "allButNested", 9 | "disallowSpacesInsideArrayBrackets": true, 10 | "disallowSpacesInsideParentheses": true, 11 | "disallowSpaceAfterObjectKeys": true, 12 | "disallowQuotedKeysInObjects": true, 13 | "requireSpaceBeforeBinaryOperators": ["?", "+", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 14 | "disallowSpaceAfterBinaryOperators": ["!"], 15 | "requireSpaceAfterBinaryOperators": ["?", ",", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 16 | "disallowSpaceBeforeBinaryOperators": [","], 17 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 18 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 19 | "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], 20 | "requireSpaceAfterBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], 21 | "disallowImplicitTypeConversion": ["numeric", "binary", "string"], 22 | "disallowKeywords": ["with", "eval"], 23 | "disallowMultipleLineBreaks": true, 24 | "disallowKeywordsOnNewLine": ["else"], 25 | "requireLineFeedAtFileEnd": true, 26 | "disallowTrailingWhitespace": true, 27 | "excludeFiles": ["node_modules/**", "bower_components/**"], 28 | "validateIndentation": 2 29 | } -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "jquery": true, 4 | "node": true, 5 | "esnext": true, 6 | "bitwise": true, 7 | "camelcase": true, 8 | "curly": true, 9 | "eqeqeq": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "noarg": true, 13 | "newcap": false, 14 | "quotmark": "single", 15 | "unused": true, 16 | "strict": true, 17 | "trailing": true, 18 | "smarttabs": true, 19 | "white": true, 20 | "freeze": true, 21 | "immed": true, 22 | "noempty": true, 23 | "plusplus": true, 24 | "undef": true, 25 | "laxbreak": true, 26 | "maxdepth": 3, 27 | "loopfunc": true, 28 | "maxcomplexity": 9, 29 | "maxlen": 80, 30 | "maxparams": 4 31 | } 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | demo 3 | src 4 | test 5 | .*.swp 6 | ._* 7 | .DS_Store 8 | .git 9 | .hg 10 | .npmrc 11 | .lock-wscript 12 | .svn 13 | .wafpickle-* 14 | config.gypi 15 | CVS 16 | npm-debug.log 17 | index.js 18 | Gruntfile.js 19 | bower.json 20 | karma.conf.js 21 | .bowerrc 22 | .gitignore 23 | .jscsrc 24 | .jshintrc 25 | .travis.yml 26 | .npmignore 27 | 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | - npm install -g grunt-cli 9 | - npm install -g bower 10 | script: bower install && grunt build 11 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | 'use strict'; 4 | 5 | grunt.loadNpmTasks('grunt-contrib-uglify'); 6 | grunt.loadNpmTasks('grunt-contrib-concat'); 7 | grunt.loadNpmTasks('grunt-karma'); 8 | grunt.loadNpmTasks('grunt-jscs'); 9 | 10 | grunt.initConfig({ 11 | pkg: grunt.file.readJSON('package.json'), 12 | uglify: { 13 | dist: { 14 | files: { 15 | 'build/angular-aop.min.js': [ 16 | './src/aspects/aspect.js', 17 | './src/angular-aop.js', 18 | './src/aspects/jointpoints/*.js' 19 | ] 20 | }, 21 | options: { 22 | wrap: true 23 | } 24 | } 25 | }, 26 | concat: { 27 | dist: { 28 | files: { 29 | 'build/angular-aop.js': [ 30 | './src/aspects/aspect.js', 31 | './src/angular-aop.js', 32 | './src/aspects/jointpoints/*.js' 33 | ] 34 | }, 35 | options: { 36 | banner: '(function () {\n\'use strict\';\n', 37 | footer: '\n}());', 38 | process: function (src) { 39 | return src 40 | .replace(/(^|\n)[ \t]*('use strict'|"use strict");?\s*/g, '$1'); 41 | } 42 | } 43 | } 44 | }, 45 | karma: { 46 | unit: { 47 | configFile: 'karma.conf.js', 48 | singleRun: true 49 | }, 50 | devunit: { 51 | configFile: 'karma.conf.js', 52 | singleRun: false 53 | } 54 | }, 55 | jscs: { 56 | src: './src/**/*.js', 57 | options: { 58 | config: '.jscsrc' 59 | } 60 | } 61 | }); 62 | 63 | grunt.registerTask('dev', 'karma:devunit'); 64 | 65 | grunt.registerTask('test', 'karma:unit'); 66 | grunt.registerTask('buildStaging', ['test', 'jscs', 'concat']); 67 | grunt.registerTask('build', ['test', 'jscs', 'uglify', 'concat']); 68 | 69 | grunt.registerTask('default', 'build'); 70 | }; 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Travis CI](https://travis-ci.org/mgechev/angular-aop.svg?branch=master) [![Join the chat at https://gitter.im/mgechev/angular-aop](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/mgechev/angular-aop?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | 3 | # Online demo 4 | 5 | 6 | If you prefer learning by doing (trial and error), come [right this way](http://plnkr.co/edit/0ThG5wpmWByIXY1db9m2?p=preview). 7 | 8 | # API 9 | 10 | AngularAOP allows you to apply aspect-oriented programming in your AngularJS applications. 11 | 12 | If you notice that you have cross-cutting concerns you can isolate them in individual services and apply them as follows: 13 | 14 | ```javascript 15 | myModule.config(function ($provide, executeProvider) { 16 | executeProvider.annotate($provide, { 17 | ServiceToWove: [{ 18 | jointPoint: JOINT_POINT, 19 | advice: ADVICE_NAME, 20 | methodPattern: /Special/ 21 | argsPattern: [/arg1/, /arg2/] 22 | }, { 23 | jointPoint: JOINT_POINT 24 | advice: ADVICE_NAME 25 | }] 26 | }); 27 | }); 28 | ``` 29 | 30 | The joint-points supported in the current version of the framework are: 31 | 32 | - `after` - the advice will be invoked after the target method 33 | - `afterResolveOf` - the advice will be invoked after the promise returned by target method has been resolved and after the resolve callback attached to the promise is invoked 34 | - `aroundAsync` - the advice will be invoked before the target method was invoked and after the promise returned by it was resolved 35 | - `around` - the advice will be invoked before the target method was invoked and after it was invoked 36 | - `beforeAsync` - the target method will be called after the promise returned by the advice was resolved 37 | - `before` - the target method will be invoked after the advice 38 | - `onRejectOf` - the advice will be invoked when the promise returned by the target method was rejected 39 | - `onResolveOf` - the advice will be invoked after the promise returned by the target method was resolved but before the resolve callback attached to the promise is invoked 40 | - `onThrowOf` - the advice will be called if the target method throws an error 41 | 42 | For additional information about Aspect-Oriented Programming and AngularAOP visit the [project's documentation](https://github.com/mgechev/angular-aop/tree/master/docs) and [this blog post](http://blog.mgechev.com/2013/08/07/aspect-oriented-programming-with-javascript-angularjs/). 43 | 44 | # Change log 45 | 46 | ## v0.1.0 47 | 48 | New way of annotating. Now you can annotate in your config callback: 49 | 50 | ```js 51 | DemoApp.config(function ($provide, executeProvider) { 52 | executeProvider.annotate($provide, { 53 | ArticlesCollection: { 54 | jointPoint: 'before', 55 | advice: 'Logger', 56 | methodPattern: /Special/, 57 | argsPatterns: [/arg1/, /arg2/, ..., /argn/] 58 | } 59 | }); 60 | }); 61 | ``` 62 | 63 | ## v0.1.1 64 | 65 | Multiple aspects can be applied to single service through the new way of annotation: 66 | 67 | ```js 68 | DemoApp.config(function ($provide, executeProvider) { 69 | executeProvider.annotate($provide, { 70 | ArticlesCollection: [{ 71 | jointPoint: 'before', 72 | advice: 'Logger', 73 | }, { 74 | //aspect 2 75 | }, { 76 | //aspect 3 77 | }, ... , { 78 | //aspect n 79 | }] 80 | }); 81 | }); 82 | ``` 83 | 84 | **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. 85 | 86 | ## v0.2.0 87 | 88 | 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"). 89 | 90 | Issues fixed: 91 | 92 | - 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. 93 | 94 | ## v0.2.1 95 | 96 | Added tests for: 97 | 98 | - Before async joint-point 99 | - On resolve joint-point 100 | 101 | Add JSCS and update Gruntfile.js 102 | 103 | ## v0.3.1 104 | 105 | - `deep` config property, which allows adding wrappers to prototype methods 106 | - Fix `forceObject` 107 | 108 | ## v0.3.1 109 | 110 | - Wrap the non-minified code in build in IIFE ([Issue 15](https://github.com/mgechev/angular-aop/pull/15)) 111 | - Single `'use strict';` at the top of the IIFE 112 | 113 | ## v0.4.0 114 | 115 | - Add the joint-point names as constants to the `executeProvider`, so now the following code is valid: 116 | ```javascript 117 | myModule.config(function ($provide, executeProvider) { 118 | executeProvider.annotate($provide, { 119 | ServiceToWove: [{ 120 | jointPoint: executeProvider.ON_THROW, 121 | advice: ADVICE_NAME, 122 | methodPattern: /Special/ 123 | argsPattern: [/arg1/, /arg2/] 124 | }, { 125 | jointPoint: executeProvider.BEFORE, 126 | advice: ADVICE_NAME 127 | }] 128 | }); 129 | }); 130 | ``` 131 | 132 | - Add more tests 133 | 134 | ## v0.4.1 135 | 136 | - Special polyfill for IE9 of `Object.setPrototypeOf`. 137 | 138 | # Roadmap 139 | 140 | 1. [`joinpoint.proceed()`](https://github.com/mgechev/angular-aop/issues/19) 141 | 2. *Use proper execution context inside the target services. This will fix the issue of invoking non-woven internal methods.* 142 | 3. More flexible way of defining pointcuts (patching `$provide.provider` might be required) 143 | 144 | # Known issues 145 | 146 | ## Circular dependency 147 | 148 | This is not issue in AngularAOP but something which should be considered when using Dependency Injection. 149 | 150 | 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. 151 | 152 | # Contributors 153 | 154 | [mgechev](https://github.com/mgechev) |[david-gang](https://github.com/david-gang) |[Wizek](https://github.com/Wizek) |[slobo](https://github.com/slobo) |[christianacca](https://github.com/christianacca) |[peernohell](https://github.com/peernohell) | 155 | :---: |:---: |:---: |:---: |:---: |:---: | 156 | [mgechev](https://github.com/mgechev) |[david-gang](https://github.com/david-gang) |[Wizek](https://github.com/Wizek) |[slobo](https://github.com/slobo) |[christianacca](https://github.com/christianacca) |[peernohell](https://github.com/peernohell) | 157 | 158 | # License 159 | 160 | AngularAOP is distributed under the terms of the MIT license. 161 | 162 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-aop", 3 | "main": "./build/angular-aop.js", 4 | "version": "0.4.1", 5 | "homepage": "https://github.com/mgechev/angular-aop", 6 | "authors": [ 7 | "mgechev " 8 | ], 9 | "description": "Micro-framework for aspect-oriented programming with AngularJS", 10 | "keywords": [ 11 | "aspect-oriented", 12 | "programming", 13 | "aop", 14 | "AngularJS" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "app/bower_components", 22 | "test", 23 | "tests" 24 | ], 25 | "dependencies": { 26 | "angular": ">=1.2.0" 27 | }, 28 | "devDependencies": { 29 | "angular-mocks": "~1.2.0", 30 | "jasmine": "~2.2.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /build/angular-aop.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 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 | 29 | /* global angular */ 30 | /** 31 | * Framework for aspect-oriented programming with AngularJS 32 | * 33 | * @author Minko Gechev (@mgechev) 34 | * @version <%= version => 35 | * @license http://opensource.org/licenses/MIT MIT 36 | */ 37 | 38 | var AngularAop = angular.module('AngularAOP', []); 39 | 40 | //Contains all aspects (pointcut + advice) 41 | var Aspects = {}; 42 | 43 | //Defines all joint points 44 | var JOINT_POINTS = { 45 | BEFORE: 'before', 46 | BEFORE_ASYNC: 'beforeAsync', 47 | AFTER: 'after', 48 | AROUND: 'around', 49 | AROUND_ASYNC: 'aroundAsync', 50 | ON_THROW: 'onThrow', 51 | ON_RESOLVE: 'onResolve', 52 | AFTER_RESOLVE: 'afterResolve', 53 | ON_REJECT: 'onReject' 54 | }; 55 | 56 | var MaybeQ = null; 57 | 58 | if (!Object.setPrototypeOf) { 59 | Object.setPrototypeOf = (function (Object, magic) { 60 | var set; 61 | function checkArgs(O, proto) { 62 | if (!(/object|function/).test(typeof O) || O === null) { 63 | throw new TypeError('can not set prototype on a non-object'); 64 | } 65 | if (!(/object|function/).test(typeof proto) && proto !== null) { 66 | throw new TypeError('can only set prototype to an object or null'); 67 | } 68 | } 69 | function setPrototypeOf(O, proto) { 70 | checkArgs(O, proto); 71 | set.call(O, proto); 72 | return O; 73 | } 74 | try { 75 | // this works already in Firefox and Safari 76 | set = Object.getOwnPropertyDescriptor(Object.prototype, magic).set; 77 | set.call({}, null); 78 | } catch (oO) { 79 | if ( 80 | // IE < 11 cannot be shimmed 81 | Object.prototype !== {}[magic] || 82 | // neither can any browser that actually 83 | // implemented __proto__ correctly 84 | // (all but old V8 will return here) 85 | /* jshint proto: true */ 86 | { __proto__: null }.__proto__ === void 0 87 | // this case means null objects cannot be passed 88 | // through setPrototypeOf in a reliable way 89 | // which means here a **Sham** is needed instead 90 | ) { 91 | return; 92 | } 93 | // nodejs 0.8 and 0.10 are (buggy and..) fine here 94 | // probably Chrome or some old Mobile stock browser 95 | set = function (proto) { 96 | this[magic] = proto; 97 | }; 98 | // please note that this will **not** work 99 | // in those browsers that do not inherit 100 | // __proto__ by mistake from Object.prototype 101 | // in these cases we should probably throw an error 102 | // or at least be informed about the issue 103 | setPrototypeOf.polyfill = setPrototypeOf( 104 | setPrototypeOf({}, null), 105 | Object.prototype 106 | ) instanceof Object; 107 | // setPrototypeOf.polyfill === true means it works as meant 108 | // setPrototypeOf.polyfill === false means it's not 100% reliable 109 | // setPrototypeOf.polyfill === undefined 110 | // or 111 | // setPrototypeOf.polyfill == null means it's not a polyfill 112 | // which means it works as expected 113 | // we can even delete Object.prototype.__proto__; 114 | } 115 | return setPrototypeOf; 116 | }(Object, '__proto__')); 117 | } 118 | // Last chance to pollyfil it... 119 | Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) { 120 | obj.__proto__ = proto; 121 | return obj; 122 | }; 123 | 124 | /** 125 | * Service which give access to the pointcuts. 126 | */ 127 | AngularAop.provider('execute', function executeProvider() { 128 | 129 | //Default regular expression for matching arguments and method names 130 | var defaultRule = /.*/; 131 | 132 | var slice = Array.prototype.slice; 133 | 134 | //Cross-browser trim function 135 | var trim = (function () { 136 | var trimFunction; 137 | if (typeof String.prototype.trim === 'function') { 138 | trimFunction = String.prototype.trim; 139 | } else { 140 | if (this === null) { 141 | return ''; 142 | } 143 | var strVal = this.toString(); 144 | trimFunction = function () { 145 | return strVal 146 | .replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); 147 | }; 148 | } 149 | return trimFunction; 150 | }()); 151 | 152 | //Builds specified aspect 153 | var AspectBuilder = { 154 | createAspectFactory: function (advice, jointPoint) { 155 | var self = this; 156 | return function (target, rules) { 157 | if (typeof target === 'function' && !rules.forceObject) { 158 | return self._getFunctionAspect(target, jointPoint, advice); 159 | } else if (target) { 160 | return self._getObjectAspect(target, rules || {}, 161 | jointPoint, advice); 162 | } 163 | }; 164 | }, 165 | _getFunctionAspect: function (method, jointPoint, advice, methodName) { 166 | methodName = methodName || this._getMethodName(method); 167 | var aspect = new Aspects[jointPoint](advice); 168 | var wrapper = function __angularAOPWrapper__() { 169 | var args = slice.call(arguments); 170 | args = { 171 | args: args, 172 | context: this, 173 | method: method, 174 | methodName: methodName 175 | }; 176 | return aspect._wrapper.call(aspect, args); 177 | }; 178 | wrapper.originalMethod = method; 179 | Object.setPrototypeOf(wrapper, method); 180 | aspect.setWrapper(wrapper); 181 | return wrapper; 182 | }, 183 | _getMethodName: function (method) { 184 | while (method.originalMethod) { 185 | method = method.originalMethod; 186 | } 187 | return (/function\s+(.*?)\s*\(/).exec(method.toString())[1]; 188 | }, 189 | _getObjectAspect: function (obj, rules, jointPoint, advice) { 190 | for (var prop in obj) { 191 | if ((obj.hasOwnProperty(prop) || rules.deep) && 192 | typeof obj[prop] === 'function' && 193 | this._matchRules(obj, prop, rules)) { 194 | obj[prop] = 195 | this._getFunctionAspect(obj[prop], jointPoint, advice, prop); 196 | } 197 | } 198 | return obj; 199 | }, 200 | _matchRules: function (obj, prop, rules) { 201 | var methodPattern = rules.methodPattern || defaultRule; 202 | var argsPatterns = rules.argsPatterns || []; 203 | var method = obj[prop]; 204 | var tokens = this._parseMethod(method, prop); 205 | while (tokens.when === '__angularAOPWrapper__') { 206 | method = method.originalMethod; 207 | tokens = this._parseMethod(method, prop); 208 | } 209 | return methodPattern.test(tokens.method) && 210 | this._matchArguments(argsPatterns, tokens); 211 | }, 212 | _matchArguments: function (argsPatterns, tokens) { 213 | if (tokens.args.length < argsPatterns.length) { 214 | return false; 215 | } 216 | var passed = true; 217 | angular.forEach(tokens.args, function (arg, idx) { 218 | var rule = argsPatterns[idx] || defaultRule; 219 | if (!rule.test(arg)) { 220 | passed = false; 221 | return; 222 | } 223 | }); 224 | return passed; 225 | }, 226 | _parseMethod: function (method, prop) { 227 | var result = { method: prop }; 228 | var parts = method.toString() 229 | //stripping the comments 230 | .replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, '') 231 | .match(/function\s+([^\(]*)\s*\(([^\)]*)\)/) || []; 232 | if (parts && parts[2]) { 233 | result.args = []; 234 | angular.forEach(parts[2].split(','), function (arg) { 235 | result.args.push(trim.call(arg)); 236 | }); 237 | } else { 238 | result.args = []; 239 | } 240 | result.when = parts[1]; 241 | return result; 242 | } 243 | }; 244 | 245 | /** 246 | * Defines and implements the different advices. 247 | * 248 | * @constructor 249 | * @private 250 | * @param {Function} advice The advice which should be 251 | * applied in the specified joint-point(s) 252 | */ 253 | function AspectCollection(advice) { 254 | if (typeof advice !== 'function') { 255 | throw new Error('The advice should be a function'); 256 | } 257 | this[JOINT_POINTS.BEFORE] = 258 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.BEFORE); 259 | this[JOINT_POINTS.BEFORE_ASYNC] = 260 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.BEFORE_ASYNC); 261 | this[JOINT_POINTS.AFTER] = 262 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.AFTER); 263 | this[JOINT_POINTS.AROUND] = 264 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.AROUND); 265 | this[JOINT_POINTS.AROUND_ASYNC] = 266 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.AROUND_ASYNC); 267 | this[JOINT_POINTS.ON_THROW] = 268 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.ON_THROW); 269 | this[JOINT_POINTS.ON_RESOLVE] = 270 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.ON_RESOLVE); 271 | this[JOINT_POINTS.AFTER_RESOLVE] = 272 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.AFTER_RESOLVE); 273 | this[JOINT_POINTS.ON_REJECT] = 274 | AspectBuilder.createAspectFactory(advice, JOINT_POINTS.ON_REJECT); 275 | } 276 | 277 | function applyAspects($provide, target, aspects) { 278 | angular.forEach(aspects, function (aspect) { 279 | decorate($provide, target, aspect); 280 | }); 281 | } 282 | 283 | function decorate($provide, target, annotation) { 284 | $provide.decorator(target, ['$q', '$injector', '$delegate', 285 | function ($q, $injector, $delegate) { 286 | var advice = (typeof annotation.advice === 'string') ? 287 | $injector.get(annotation.advice) : annotation.advice; 288 | var jointPoint = annotation.jointPoint; 289 | var methodPattern = annotation.methodPattern; 290 | var argsPatterns = annotation.argsPattern; 291 | var aspect = new AspectCollection(advice); 292 | MaybeQ = $q; 293 | if (typeof aspect[jointPoint] !== 'function') { 294 | throw new Error('No such joint-point ' + jointPoint); 295 | } 296 | return aspect[jointPoint]($delegate, { 297 | methodPattern: methodPattern, 298 | argsPatterns: argsPatterns, 299 | forceObject: annotation.forceObject, 300 | deep: annotation.deep 301 | }); 302 | }]); 303 | } 304 | 305 | var api = { 306 | 307 | annotate: function ($provide, annotations) { 308 | var aspects; 309 | for (var target in annotations) { 310 | aspects = annotations[target]; 311 | if (!angular.isArray(aspects)) { 312 | aspects = [aspects]; 313 | } 314 | applyAspects($provide, target, aspects); 315 | } 316 | }, 317 | 318 | $get: ['$q', function ($q) { 319 | MaybeQ = $q; 320 | return function (advice) { 321 | return new AspectCollection(advice); 322 | }; 323 | }] 324 | }; 325 | 326 | angular.extend(api, JOINT_POINTS); 327 | 328 | return api; 329 | }); 330 | 331 | /* global Aspects, JOINT_POINTS, Aspect */ 332 | Aspects[JOINT_POINTS.AFTER] = function () { 333 | Aspect.apply(this, arguments); 334 | this.when = JOINT_POINTS.AFTER; 335 | }; 336 | 337 | Aspects[JOINT_POINTS.AFTER].prototype = Object.create(Aspect.prototype); 338 | 339 | Aspects[JOINT_POINTS.AFTER].prototype._wrapper = function (params) { 340 | var context = params.context; 341 | var result = params.method.apply(context, params.args); 342 | params.result = result; 343 | return this.invoke(params).result || result; 344 | }; 345 | 346 | /* global Aspects, JOINT_POINTS, Aspect, MaybeQ */ 347 | Aspects[JOINT_POINTS.AFTER_RESOLVE] = function () { 348 | Aspect.apply(this, arguments); 349 | this.when = JOINT_POINTS.AFTER_RESOLVE; 350 | }; 351 | 352 | Aspects[JOINT_POINTS.AFTER_RESOLVE].prototype = 353 | Object.create(Aspect.prototype); 354 | 355 | Aspects[JOINT_POINTS.AFTER_RESOLVE].prototype._wrapper = function (params) { 356 | var args = params.args; 357 | var context = params.context; 358 | var method = params.method; 359 | var deferred = MaybeQ.defer(); 360 | var innerPromise = deferred.promise; 361 | var promise = method.apply(context, args); 362 | var self = this; 363 | if (!promise || typeof promise.then !== 'function') { 364 | throw new Error('The woven method doesn\'t return a promise'); 365 | } 366 | promise.then(function () { 367 | params.resolveArgs = arguments; 368 | innerPromise.then(function () { 369 | self.invoke(params); 370 | }); 371 | deferred.resolve.apply(deferred, arguments); 372 | }, function () { 373 | deferred.reject.apply(innerPromise, arguments); 374 | }); 375 | return innerPromise; 376 | }; 377 | 378 | /* global Aspects, JOINT_POINTS, Aspect, MaybeQ */ 379 | Aspects[JOINT_POINTS.AROUND_ASYNC] = function () { 380 | Aspect.apply(this, arguments); 381 | this.when = JOINT_POINTS.AROUND_ASYNC; 382 | }; 383 | 384 | Aspects[JOINT_POINTS.AROUND_ASYNC].prototype = 385 | Object.create(Aspect.prototype); 386 | 387 | Aspects[JOINT_POINTS.AROUND_ASYNC].prototype._wrapper = function (params) { 388 | var context = params.context; 389 | var method = params.method; 390 | var aspectData = this.invoke(params); 391 | var self = this; 392 | var result; 393 | 394 | function afterBefore() { 395 | result = method.apply(context, aspectData.result); 396 | params.result = result; 397 | return self.invoke(params).result || result; 398 | } 399 | 400 | return MaybeQ.when(aspectData.result) 401 | .then(afterBefore, afterBefore); 402 | }; 403 | 404 | /* global Aspects, JOINT_POINTS, Aspect */ 405 | Aspects[JOINT_POINTS.AROUND] = function () { 406 | Aspect.apply(this, arguments); 407 | this.when = JOINT_POINTS.AROUND; 408 | }; 409 | 410 | Aspects[JOINT_POINTS.AROUND].prototype = Object.create(Aspect.prototype); 411 | 412 | Aspects[JOINT_POINTS.AROUND].prototype._wrapper = function (params) { 413 | var context = params.context; 414 | var method = params.method; 415 | var result; 416 | result = method.apply(context, this.invoke(params).args); 417 | params.result = result; 418 | return this.invoke(params).result || result; 419 | }; 420 | 421 | /* global Aspects, JOINT_POINTS, Aspect, MaybeQ */ 422 | Aspects[JOINT_POINTS.BEFORE_ASYNC] = function () { 423 | Aspect.apply(this, arguments); 424 | this.when = JOINT_POINTS.BEFORE_ASYNC; 425 | }; 426 | 427 | Aspects[JOINT_POINTS.BEFORE_ASYNC].prototype = 428 | Object.create(Aspect.prototype); 429 | 430 | Aspects[JOINT_POINTS.BEFORE_ASYNC].prototype._wrapper = function (params) { 431 | var aspectData = this.invoke(params); 432 | return MaybeQ.when(aspectData.result) 433 | .then(function () { 434 | return params.method.apply(params.context, aspectData.args); 435 | }, function () { 436 | return params.method.apply(params.context, aspectData.args); 437 | }); 438 | }; 439 | 440 | /* global Aspects, JOINT_POINTS, Aspect */ 441 | Aspects[JOINT_POINTS.BEFORE] = function () { 442 | Aspect.apply(this, arguments); 443 | this.when = JOINT_POINTS.BEFORE; 444 | }; 445 | 446 | Aspects[JOINT_POINTS.BEFORE].prototype = Object.create(Aspect.prototype); 447 | 448 | Aspects[JOINT_POINTS.BEFORE].prototype._wrapper = function (params) { 449 | return params.method.apply(params.context, this.invoke(params).args); 450 | }; 451 | 452 | /* global Aspects, JOINT_POINTS, Aspect */ 453 | Aspects[JOINT_POINTS.ON_REJECT] = function () { 454 | Aspect.apply(this, arguments); 455 | this.when = JOINT_POINTS.ON_REJECT; 456 | }; 457 | 458 | Aspects[JOINT_POINTS.ON_REJECT].prototype = Object.create(Aspect.prototype); 459 | 460 | Aspects[JOINT_POINTS.ON_REJECT].prototype._wrapper = function (params) { 461 | var args = params.args; 462 | var context = params.context; 463 | var method = params.method; 464 | var promise = method.apply(context, args); 465 | var self = this; 466 | if (promise && typeof promise.then === 'function') { 467 | promise.then(undefined, function () { 468 | params.rejectArgs = arguments; 469 | self.invoke(params); 470 | }); 471 | } 472 | return promise; 473 | }; 474 | 475 | /* global Aspects, JOINT_POINTS, Aspect */ 476 | Aspects[JOINT_POINTS.ON_RESOLVE] = function () { 477 | Aspect.apply(this, arguments); 478 | this.when = JOINT_POINTS.ON_RESOLVE; 479 | }; 480 | 481 | Aspects[JOINT_POINTS.ON_RESOLVE].prototype = 482 | Object.create(Aspect.prototype); 483 | 484 | Aspects[JOINT_POINTS.ON_RESOLVE].prototype._wrapper = function (params) { 485 | var args = params.args; 486 | var context = params.context; 487 | var method = params.method; 488 | var promise = method.apply(context, args); 489 | var self = this; 490 | if (promise && typeof promise.then === 'function') { 491 | promise.then(function () { 492 | params.resolveArgs = arguments; 493 | self.invoke(params); 494 | }); 495 | } 496 | return promise; 497 | }; 498 | 499 | /* global Aspects, JOINT_POINTS, Aspect */ 500 | Aspects[JOINT_POINTS.ON_THROW] = function () { 501 | Aspect.apply(this, arguments); 502 | this.when = JOINT_POINTS.ON_THROW; 503 | }; 504 | 505 | Aspects[JOINT_POINTS.ON_THROW].prototype = Object.create(Aspect.prototype); 506 | 507 | Aspects[JOINT_POINTS.ON_THROW].prototype._wrapper = function (params) { 508 | var args = params.args; 509 | var result; 510 | try { 511 | result = params.method.apply(params.context, args); 512 | } catch (e) { 513 | params.exception = e; 514 | this.invoke(params); 515 | } 516 | return result; 517 | }; 518 | 519 | }()); -------------------------------------------------------------------------------- /build/angular-aop.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"use strict";function c(a){this._advice=a,this._wrapperFunc=null}b["true"]=a,c.prototype.setWrapper=function(a){this._wrapperFunc=a},c.prototype._wrapper=function(){throw"Not implemented"},c.prototype.invoke=function(a){var b={};return b.when=this.when,b.method=a.methodName,b.args=a.args,b.exception=a.exception,b.result=a.result,b.resolveArgs=a.resolveArgs,b.rejectArgs=a.rejectArgs,b.result=this._advice.call(a.context,b),b};var d=angular.module("AngularAOP",[]),e={},f={BEFORE:"before",BEFORE_ASYNC:"beforeAsync",AFTER:"after",AROUND:"around",AROUND_ASYNC:"aroundAsync",ON_THROW:"onThrow",ON_RESOLVE:"onResolve",AFTER_RESOLVE:"afterResolve",ON_REJECT:"onReject"},g=null;Object.setPrototypeOf||(Object.setPrototypeOf=function(a,b){function c(a,b){if(!/object|function/.test(typeof a)||null===a)throw new TypeError("can not set prototype on a non-object");if(!/object|function/.test(typeof b)&&null!==b)throw new TypeError("can only set prototype to an object or null")}function d(a,b){return c(a,b),e.call(a,b),a}var e;try{e=a.getOwnPropertyDescriptor(a.prototype,b).set,e.call({},null)}catch(f){if(a.prototype!=={}[b]||void 0==={__proto__:null}.__proto__)return;e=function(a){this[b]=a},d.polyfill=d(d({},null),a.prototype)instanceof a}return d}(Object,"__proto__")),Object.setPrototypeOf=Object.setPrototypeOf||function(a,b){return a.__proto__=b,a},d.provider("execute",function(){function a(a){if("function"!=typeof a)throw new Error("The advice should be a function");this[f.BEFORE]=j.createAspectFactory(a,f.BEFORE),this[f.BEFORE_ASYNC]=j.createAspectFactory(a,f.BEFORE_ASYNC),this[f.AFTER]=j.createAspectFactory(a,f.AFTER),this[f.AROUND]=j.createAspectFactory(a,f.AROUND),this[f.AROUND_ASYNC]=j.createAspectFactory(a,f.AROUND_ASYNC),this[f.ON_THROW]=j.createAspectFactory(a,f.ON_THROW),this[f.ON_RESOLVE]=j.createAspectFactory(a,f.ON_RESOLVE),this[f.AFTER_RESOLVE]=j.createAspectFactory(a,f.AFTER_RESOLVE),this[f.ON_REJECT]=j.createAspectFactory(a,f.ON_REJECT)}function b(a,b,d){angular.forEach(d,function(d){c(a,b,d)})}function c(b,c,d){b.decorator(c,["$q","$injector","$delegate",function(b,c,e){var f="string"==typeof d.advice?c.get(d.advice):d.advice,h=d.jointPoint,i=d.methodPattern,j=d.argsPattern,k=new a(f);if(g=b,"function"!=typeof k[h])throw new Error("No such joint-point "+h);return k[h](e,{methodPattern:i,argsPatterns:j,forceObject:d.forceObject,deep:d.deep})}])}var d=/.*/,h=Array.prototype.slice,i=function(){var a;if("function"==typeof String.prototype.trim)a=String.prototype.trim;else{if(null===this)return"";var b=this.toString();a=function(){return b.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}}return a}(),j={createAspectFactory:function(a,b){var c=this;return function(d,e){return"function"!=typeof d||e.forceObject?d?c._getObjectAspect(d,e||{},b,a):void 0:c._getFunctionAspect(d,b,a)}},_getFunctionAspect:function(a,b,c,d){d=d||this._getMethodName(a);var f=new e[b](c),g=function(){var b=h.call(arguments);return b={args:b,context:this,method:a,methodName:d},f._wrapper.call(f,b)};return g.originalMethod=a,Object.setPrototypeOf(g,a),f.setWrapper(g),g},_getMethodName:function(a){for(;a.originalMethod;)a=a.originalMethod;return/function\s+(.*?)\s*\(/.exec(a.toString())[1]},_getObjectAspect:function(a,b,c,d){for(var e in a)(a.hasOwnProperty(e)||b.deep)&&"function"==typeof a[e]&&this._matchRules(a,e,b)&&(a[e]=this._getFunctionAspect(a[e],c,d,e));return a},_matchRules:function(a,b,c){for(var e=c.methodPattern||d,f=c.argsPatterns||[],g=a[b],h=this._parseMethod(g,b);"__angularAOPWrapper__"===h.when;)g=g.originalMethod,h=this._parseMethod(g,b);return e.test(h.method)&&this._matchArguments(f,h)},_matchArguments:function(a,b){if(b.args.length 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 | [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) | 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 | --------------------------------------------------------------------------------