diff --git a/package.json b/package.json index eeb6d51d..382b400d 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,7 @@ "scripts": { "lint": "tslint -t codeFrame src/**/*.ts test/**/*.ts", "prebuild": "rimraf dist", - "build": "tsc && rollup -c && typedoc --out dist/docs --target es6 --theme minimal src", - "postbuild": "rimraf compiled", + "build": "tsc && rollup -c && rimraf compiled && typedoc --out dist/docs --target es6 --theme minimal src", "start": "tsc-watch --onSuccess 'rollup -c'", "test": "jest", "test:watch": "jest --watch", diff --git a/src/core/CallStackIterator.ts b/src/core/CallStackIterator.ts index 3bbdc8aa..eb14ac7d 100644 --- a/src/core/CallStackIterator.ts +++ b/src/core/CallStackIterator.ts @@ -17,7 +17,7 @@ export class CallStackIterator { * call stack * @param {IMetadata} metadata current metadata for this stack */ - constructor (private metadata: IMetadata, private stack: IStackEntry[]) { + constructor (private metadata: IMetadata, private stack: IStackEntry[], private exceptionEntry?: IStackEntry) { this.next() } @@ -25,26 +25,34 @@ export class CallStackIterator { * next - this method will resolve by calling the next advice in the call stack * or calling the main method */ - next () { + next() { this.index++ let currentEntry = this.stack[this.index] - if (currentEntry === undefined) { + if(currentEntry === undefined) { return } - if (this.proceed && currentEntry === null) { - this.invokeOriginal() - this.next() - return - } - - if (currentEntry) { - currentEntry.advice.apply({ next: this.next.bind(this), stop: this.stop.bind(this) }, this.transformArguments(currentEntry)) - if (!this.isAsync(currentEntry.advice)) { + if(this.proceed && currentEntry === null) { + if(!this.exceptionEntry) { + this.invokeOriginal() this.next() + return + } else { + try { + this.invokeOriginal() + this.next() + } catch (err) { + this.metadata.exception = err + this.executeAdvice(this.exceptionEntry) + return + } } + } + + if(currentEntry) { + this.executeAdvice(currentEntry) return } } @@ -57,6 +65,13 @@ export class CallStackIterator { this.proceed = false } + private executeAdvice (currentEntry: IStackEntry) { + currentEntry.advice.apply({ next: this.next.bind(this), stop: this.stop.bind(this) }, this.transformArguments(currentEntry)) + if(!this.isAsync(currentEntry.advice)) { + this.next() + } + } + /** * @private invokeOriginal * diff --git a/src/core/bootstrapFn.ts b/src/core/bootstrapFn.ts index 76400463..f69e22e1 100644 --- a/src/core/bootstrapFn.ts +++ b/src/core/bootstrapFn.ts @@ -28,7 +28,7 @@ export function bootstrap (target: Object, propertyKey: string, rawMethod: () => let stack = [].concat(fakeReplacement.$$before, [null], fakeReplacement.$$after) /* tslint:disable-next-line */ - new CallStackIterator(metadata, stack) + new CallStackIterator(metadata, stack, fakeReplacement.$$error) return metadata.result } as IFakeMethodReplacement diff --git a/src/decorators.ts b/src/decorators.ts index 1fada119..07080b66 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -113,3 +113,22 @@ export function beforeMethod (adviceFn: (...args) => void, ...args: any[]): IAdv return descriptor } } + +/** + * Triggers an aspect when an exception occurs + */ +export function onException (adviceFn: (...args) => void, ...args: any[]): IAdviceSignature { + return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) { + // If descriptor hasn't been initializated + if (!descriptor.value.$$error) { + let rawMethod = descriptor.value + descriptor.value = bootstrap(target, propertyKey, rawMethod) + } + const advice = adviceFn as IAdviceParamInjector + const stackEntry: IStackEntry = { advice, args } + + // Place it at the end of the $$before stack + descriptor.value.$$error = stackEntry + return descriptor + } +} diff --git a/src/interface/IFakeMethodReplacement.ts b/src/interface/IFakeMethodReplacement.ts index 9253ff88..f1a398ec 100644 --- a/src/interface/IFakeMethodReplacement.ts +++ b/src/interface/IFakeMethodReplacement.ts @@ -3,5 +3,6 @@ import { IStackEntry } from "./IStackEntry" export interface IFakeMethodReplacement { (...args: any[]): any $$before: IStackEntry[], - $$after: IStackEntry[] + $$after: IStackEntry[], + $$error: IStackEntry } diff --git a/src/interface/IMetadata.ts b/src/interface/IMetadata.ts index f0a3986d..a94a602e 100644 --- a/src/interface/IMetadata.ts +++ b/src/interface/IMetadata.ts @@ -3,6 +3,7 @@ export interface IMetadata { target: any, // target: Object | Function, propertyKey: string, + exception: Error, rawMethod: () => any, args: any[], result: any diff --git a/src/kaop-ts.ts b/src/kaop-ts.ts index b194c653..e9585cfa 100644 --- a/src/kaop-ts.ts +++ b/src/kaop-ts.ts @@ -7,5 +7,6 @@ export { adviceParam, afterInstance, beforeInstance, - beforeMethod + beforeMethod, + onException } from "./decorators" diff --git a/test/demos/onExceptionAdvice.spec.ts b/test/demos/onExceptionAdvice.spec.ts index 9be5a4c3..a5970c32 100644 --- a/test/demos/onExceptionAdvice.spec.ts +++ b/test/demos/onExceptionAdvice.spec.ts @@ -1,28 +1,71 @@ -import { AdvicePool, IMetadata, beforeMethod, adviceMetadata } from "../../src/kaop-ts" - -class MyAdvicePool extends AdvicePool { - static onException (@adviceMetadata meta: IMetadata) { - this.stop() - try { - meta.result = meta.rawMethod.apply(meta.scope, meta.args) - } catch (err) { - console.log(`There was an error in ${meta.propertyKey}(): -> ${err.message}`) +import { AdvicePool, IMetadata, beforeMethod, adviceMetadata, adviceParam, onException } from "../../src/kaop-ts" + +describe("kaop-ts demo -> onException join point", () => { + + let exceptionSpy = jest.fn() + let noopSpy = jest.fn() + let methodSpy = jest.fn() + + let orderArr = [] + let capturedException = null + + class MyAdvicePool extends AdvicePool { + static handleException (@adviceMetadata meta: IMetadata, @adviceParam(0) order) { + orderArr.push(order) + capturedException = meta.exception + exceptionSpy() + } + + static noop (@adviceParam(0) order) { + orderArr.push(order) + noopSpy() } } -} -class ExceptionTest { + class ExceptionTest { + + @onException(MyAdvicePool.handleException) + static wrongMethod (callback: any) { + callback() + } - @beforeMethod(MyAdvicePool.onException) - // static wrongMethod (callback: number | Function) { - static wrongMethod (callback: any) { - callback() + @beforeMethod(MyAdvicePool.noop, 0) + @onException(MyAdvicePool.handleException, 1) + @beforeMethod(MyAdvicePool.noop, 2) + static orderTest (cb: any) { + cb() + } } -} -describe("kaop-ts demo -> exception join point", () => { - it("advices are callback driven, advice stack will be executed when this.next is invoked", (done) => { + beforeEach(() => { + exceptionSpy.mockClear() + methodSpy.mockClear() + noopSpy.mockClear() + orderArr = [] + }) + + it("throws an exception and thus calls MyAdvicePool.handleException", () => { ExceptionTest.wrongMethod(2) - ExceptionTest.wrongMethod(done) + ExceptionTest.wrongMethod(methodSpy) + + expect(capturedException instanceof Error).toBe(true) + expect(exceptionSpy).toHaveBeenCalledTimes(1) + expect(methodSpy).toHaveBeenCalledTimes(1) + }) + + it("onException must be called after the last beforeMethod, regardless of the order", () => { + ExceptionTest.orderTest(4) + expect(orderArr).toEqual([0, 2, 1]) + }) + + it("prevents the original function from triggering twice", () => { + ExceptionTest.orderTest(() => { + methodSpy() + throw Error() + }) + + expect(noopSpy).toHaveBeenCalledTimes(2) + expect(exceptionSpy).toHaveBeenCalledTimes(1) + expect(methodSpy).toHaveBeenCalledTimes(1) }) })