Skip to content

Feature/add on exception join point #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 1, 2017
Merged
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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)
})
})