Skip to content

Commit

Permalink
Feature/add on exception join point (#4)
Browse files Browse the repository at this point in the history
* feat(advices): Add onException joinPoint

    The joinPoint will trigger after all "beforeMethod" advices, preventing the original method to
    trigger because is triggered by itself. For now, this takes into account that only one @onException
    will be used per method

    Closes #2

* build: ensure remove compiled folder

* onException should have access to the captured exception

* no need for a flag $$exception

* Update IAdviceParamInjector.ts

* save exception and group up for async afters

* Update decorators.ts

* add exception prop

* added $$error advice container

* pass $$error container value to CallStackIterator
  • Loading branch information
alexjoverm authored and k1r0s committed Jun 1, 2017
1 parent 7f6865c commit 36c55a9
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 36 deletions.
3 changes: 1 addition & 2 deletions package.json
Expand Up @@ -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",
Expand Down
39 changes: 27 additions & 12 deletions src/core/CallStackIterator.ts
Expand Up @@ -17,34 +17,42 @@ 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()
}

/**
* 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
}
}
Expand All @@ -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
*
Expand Down
2 changes: 1 addition & 1 deletion src/core/bootstrapFn.ts
Expand Up @@ -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

Expand Down
19 changes: 19 additions & 0 deletions src/decorators.ts
Expand Up @@ -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
}
}
3 changes: 2 additions & 1 deletion src/interface/IFakeMethodReplacement.ts
Expand Up @@ -3,5 +3,6 @@ import { IStackEntry } from "./IStackEntry"
export interface IFakeMethodReplacement {
(...args: any[]): any
$$before: IStackEntry[],
$$after: IStackEntry[]
$$after: IStackEntry[],
$$error: IStackEntry
}
1 change: 1 addition & 0 deletions src/interface/IMetadata.ts
Expand Up @@ -3,6 +3,7 @@ export interface IMetadata {
target: any,
// target: Object | Function,
propertyKey: string,
exception: Error,
rawMethod: () => any,
args: any[],
result: any
Expand Down
3 changes: 2 additions & 1 deletion src/kaop-ts.ts
Expand Up @@ -7,5 +7,6 @@ export {
adviceParam,
afterInstance,
beforeInstance,
beforeMethod
beforeMethod,
onException
} from "./decorators"
81 changes: 62 additions & 19 deletions 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)
})
})

0 comments on commit 36c55a9

Please sign in to comment.