Skip to content
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

Add more strict types and better inference #11

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/TsAspectContainer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Advice } from './advice.enum';
import { Aspect } from './aspect.interface';

type AdviceAspectMap = Map<Advice, Aspect[]>;
type MethodContainer = {
type AdviceAspectMap<Data, Args> = Map<Advice, Aspect<Data, Args>[]>;
type MethodContainer<Data, Args> = {
originalMethod: any;
adviceAspectMap: AdviceAspectMap;
adviceAspectMap: AdviceAspectMap<Data, Args>;
};
type TsAspectContainer = Record<string, MethodContainer>;
type TsAspectContainer = Record<string, MethodContainer<any, any>>;

export { TsAspectContainer, MethodContainer, AdviceAspectMap };
9 changes: 7 additions & 2 deletions src/addAspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { Aspect } from './aspect.interface';
import { proxyFunc, asyncProxyFunc } from './proxyFunc';
import { setTsAspectProp, getTsAspectProp } from './TsAspectProperty';

export function addAspect(target: any, methodName: string, advice: Advice, aspect: Aspect): void {
export function addAspect<Data, Args>(
target: any,
methodName: string,
advice: Advice,
aspect: Aspect<Data, Args>,
): void {
let tsAspectProp = getTsAspectProp(target);
if (!tsAspectProp) {
tsAspectProp = {};
Expand All @@ -16,7 +21,7 @@ export function addAspect(target: any, methodName: string, advice: Advice, aspec

tsAspectProp[methodName] = {
originalMethod,
adviceAspectMap: new Map<Advice, Aspect[]>(),
adviceAspectMap: new Map<Advice, Aspect<Data, Args>[]>(),
};

const wrapperFunc = function (...args: any): any {
Expand Down
4 changes: 2 additions & 2 deletions src/addAspectToPointcut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { Advice } from './advice.enum';
import { Aspect } from './aspect.interface';
import { getPointcutMethods } from './getPointcutMethods';

export function addAspectToPointcut(
export function addAspectToPointcut<Data, Args>(
target: any,
pointcut: string,
advice: Advice,
aspect: Aspect,
aspect: Aspect<Data, Args>,
): void {
const methods = getPointcutMethods(target, pointcut);
methods.forEach(method => {
Expand Down
10 changes: 5 additions & 5 deletions src/aspect.interface.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export interface Aspect {
execute(ctx: AspectContext): any;
export interface Aspect<Data = any, Args = any> {
execute(ctx: AspectContext<Data, Args>): any;
}

export type AspectContext = {
export type AspectContext<Data, Args> = {
target: any;
methodName: string;
functionParams: any[];
returnValue: any;
functionParams: Args;
returnValue: Data | null;
error: any;
};
19 changes: 12 additions & 7 deletions src/decorator/UseAspect.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { types } from 'util';
import { getTsAspectProp, setTsAspectProp } from '../TsAspectProperty';
import { Advice } from '../advice.enum';
import { Aspect } from '../aspect.interface';
import { proxyFunc, asyncProxyFunc } from '../proxyFunc';
import { getTsAspectProp, setTsAspectProp } from '../TsAspectProperty';
import { asyncProxyFunc, proxyFunc } from '../proxyFunc';

export function UseAspect(advice: Advice, aspect: Aspect | (new () => Aspect)): MethodDecorator {
return function (target, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
export function UseAspect<T extends (...args: any[]) => any>(
advice: Advice,
aspect:
| Aspect<Awaited<ReturnType<T>>, Parameters<T>>
| (new () => Aspect<Awaited<ReturnType<T>>, Parameters<T>>),
) {
return (target: any, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => {
let tsAspectProp = getTsAspectProp(target);
if (!tsAspectProp) {
tsAspectProp = {};
Expand All @@ -19,7 +24,7 @@ export function UseAspect(advice: Advice, aspect: Aspect | (new () => Aspect)):

tsAspectProp[propertyKeyString] = {
originalMethod,
adviceAspectMap: new Map<Advice, Aspect[]>(),
adviceAspectMap: new Map<Advice, Aspect<Awaited<ReturnType<T>>, Parameters<T>>[]>(),
};

descriptor.value = function (...args: any): any {
Expand All @@ -41,8 +46,8 @@ export function UseAspect(advice: Advice, aspect: Aspect | (new () => Aspect)):
);
}
}
return originalMethod(...args);
};
return originalMethod?.(...args);
} as T;
}

const { adviceAspectMap } = tsAspectProp[propertyKeyString];
Expand Down
24 changes: 12 additions & 12 deletions src/proxyFunc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { Advice } from './advice.enum';
import { AspectContext } from './aspect.interface';
import { AdviceAspectMap, MethodContainer } from './TsAspectContainer';

export async function asyncProxyFunc(
export async function asyncProxyFunc<Data, Args>(
target: any,
methodName: string,
methodContainer: MethodContainer,
methodContainer: MethodContainer<Data, Args>,
...args: any
): Promise<any> {
const { originalMethod, adviceAspectMap } = methodContainer;
const aspectCtx: AspectContext = {
const aspectCtx: AspectContext<Data, Args> = {
target: target,
methodName: methodName,
functionParams: args,
Expand Down Expand Up @@ -43,14 +43,14 @@ export async function asyncProxyFunc(
return aspectCtx.returnValue;
}

export function proxyFunc(
export function proxyFunc<Data, Args>(
target: any,
methodName: string,
methodContainer: MethodContainer,
methodContainer: MethodContainer<Data, Args>,
...args: any
): any {
const { originalMethod, adviceAspectMap } = methodContainer;
const aspectCtx: AspectContext = {
const aspectCtx: AspectContext<Data, Args> = {
target: target,
methodName: methodName,
functionParams: args,
Expand Down Expand Up @@ -84,9 +84,9 @@ export function proxyFunc(
return aspectCtx.returnValue;
}

function applyPreExecutionAspects(
aspectCtx: AspectContext,
adviceAspectMap: AdviceAspectMap,
function applyPreExecutionAspects<Data, Args>(
aspectCtx: AspectContext<Data, Args>,
adviceAspectMap: AdviceAspectMap<Data, Args>,
): void {
if (adviceAspectMap.has(Advice.Before)) {
adviceAspectMap.get(Advice.Before)?.forEach(aspect => {
Expand All @@ -100,9 +100,9 @@ function applyPreExecutionAspects(
}
}

function applyPostExecutionAspects(
aspectCtx: AspectContext,
adviceAspectMap: AdviceAspectMap,
function applyPostExecutionAspects<Data, Args>(
aspectCtx: AspectContext<Data, Args>,
adviceAspectMap: AdviceAspectMap<Data, Args>,
): void {
if (adviceAspectMap.has(Advice.Around)) {
adviceAspectMap.get(Advice.Around)?.forEach(aspect => {
Expand Down
23 changes: 13 additions & 10 deletions test/addAspect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { CalculatorCls } from './samples/CalculatorCls.sample';

describe('addAspect', () => {
let calculator: CalculatorCls;
const aspect = mock<Aspect>();
const aspect = mock<Aspect<number, [number, number]>>();

beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -63,7 +63,7 @@ describe('addAspect', () => {

calculator.add(1, 2);

const expectedCtx: AspectContext = {
const expectedCtx: AspectContext<number, [number, number]> = {
target: calculator,
methodName: 'add',
functionParams: [1, 2],
Expand All @@ -81,7 +81,7 @@ describe('addAspect', () => {

expect(aspect.execute).toHaveBeenCalledTimes(1);

const expectedCtx: AspectContext = {
const expectedCtx: AspectContext<number, [number, number]> = {
target: calculator,
methodName: 'divide',
functionParams: [1, 0],
Expand All @@ -92,7 +92,7 @@ describe('addAspect', () => {
});

it('should pass the returned value to the injected aspect for Advice.AfterReturn', () => {
aspect.execute.mockImplementationOnce((ctx: AspectContext) => {
aspect.execute.mockImplementationOnce((ctx: AspectContext<number, [number, number]>) => {
return ctx.returnValue;
});

Expand All @@ -102,7 +102,7 @@ describe('addAspect', () => {

expect(aspect.execute).toHaveBeenCalledTimes(1);

const expectedCtx: AspectContext = {
const expectedCtx: AspectContext<number, [number, number]> = {
target: calculator,
methodName: 'add',
functionParams: [1, 2],
Expand All @@ -113,8 +113,11 @@ describe('addAspect', () => {
});

it('should return the returned value manipulated by the injected aspect for Advice.AfterReturn', () => {
aspect.execute.mockImplementationOnce((ctx: AspectContext) => {
aspect.execute.mockImplementationOnce((ctx: AspectContext<number, [number, number]>) => {
const returnValue = ctx.returnValue;
if (returnValue === null) {
return null;
}
return returnValue * 42;
});

Expand All @@ -126,8 +129,8 @@ describe('addAspect', () => {
});

it('should execute all injected aspects for the same advice and pointcut', () => {
const secondAspect = mock<Aspect>();
const thirdAspect = mock<Aspect>();
const secondAspect = mock<Aspect<number, [number, number]>>();
const thirdAspect = mock<Aspect<number, [number, number]>>();

addAspect(calculator, 'add', Advice.Before, aspect);

Expand Down Expand Up @@ -160,8 +163,8 @@ describe('addAspect', () => {
}
}

const aspect = mock<Aspect>();
aspect.execute.mockImplementation((ctx: AspectContext) => {
const aspect = mock<Aspect<number, [number, number]>>();
aspect.execute.mockImplementation((ctx: AspectContext<number, [number, number]>) => {
return 42;
});

Expand Down
2 changes: 1 addition & 1 deletion test/addAspectToPointcut.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { CalculatorCls } from './samples/CalculatorCls.sample';

describe('addAspectToPointcut', () => {
let calculator: CalculatorCls;
const aspect = mock<Aspect>();
const aspect = mock<Aspect<number, [number, number]>>();

beforeEach(() => {
jest.clearAllMocks();
Expand Down
31 changes: 20 additions & 11 deletions test/decorator/UseAspect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,17 @@ import { Advice } from '../../src/advice.enum';
import { Aspect, AspectContext } from '../../src/aspect.interface';
import { UseAspect } from '../../src/decorator/UseAspect';

const beforeAspect = mock<Aspect>();
const afterAspect = mock<Aspect>();
const beforeAspect = mock<Aspect<number, []>>();
const afterAspect = mock<Aspect<void, [number]>>();

const throwBeforeAspect = mock<Aspect<void, []>>();
const throwAfterAspect = mock<Aspect<void, []>>();

class TestAspect implements Aspect {
execute(ctx: AspectContext<void, [number]>) {
throw new Error('Method not implemented.');
}
}

class SampleClass {
public constructor(private sampleId: number) {}
Expand All @@ -22,8 +31,8 @@ class SampleClass {
this.sampleId = sampleId;
}

@UseAspect(Advice.Before, beforeAspect)
@UseAspect(Advice.After, afterAspect)
@UseAspect(Advice.Before, throwBeforeAspect)
@UseAspect(Advice.After, throwAfterAspect)
public throwError(): void {
throw new Error('this is expected!');
}
Expand All @@ -43,7 +52,7 @@ describe('UseAspect', () => {

expect(beforeAspect.execute).toHaveBeenCalledTimes(1);

const expectedCtx: AspectContext = {
const expectedCtx: AspectContext<number, []> = {
target: sample,
methodName: 'getSampleId',
functionParams: [],
Expand All @@ -55,8 +64,8 @@ describe('UseAspect', () => {

it('should instantiate a new object of the aspect class passed as parameter', () => {
let executeHasBeenCalled = false;
class SomeAspect implements Aspect {
execute(ctx: AspectContext) {
class SomeAspect implements Aspect<void, []> {
execute(ctx: AspectContext<void, []>) {
executeHasBeenCalled = true;
}
}
Expand All @@ -79,13 +88,13 @@ describe('UseAspect', () => {

it('should not execute the aspect annotated with Advice.After if an error is thrown in method', () => {
expect(() => sample.throwError()).toThrow(Error);
expect(beforeAspect.execute).toHaveBeenCalledTimes(1);
expect(afterAspect.execute).toHaveBeenCalledTimes(0);
expect(throwBeforeAspect.execute).toHaveBeenCalledTimes(1);
expect(throwAfterAspect.execute).toHaveBeenCalledTimes(0);
});

describe('for async functions', () => {
const aspect = mock<Aspect>();
aspect.execute.mockImplementation((ctx: AspectContext) => {
const aspect = mock<Aspect<number, []>>();
aspect.execute.mockImplementation((ctx: AspectContext<number, []>) => {
return 42;
});

Expand Down
2 changes: 1 addition & 1 deletion test/resetAllAspects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { CalculatorCls } from './samples/CalculatorCls.sample';

describe('resetAllAspects', () => {
let calculator: CalculatorCls;
const aspect = mock<Aspect>();
const aspect = mock<Aspect<number, [number, number]>>();

beforeEach(() => {
jest.clearAllMocks();
Expand Down
41 changes: 19 additions & 22 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"allowJs": true,
"declaration": true,
"outDir": "build",
"rootDir": "src",
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"typeRoots" : ["./node_modules/@types", "./src/@types"],
"experimentalDecorators": true,
},
"include": [
"src", "test"
],
"exclude": [
"node_modules", "test"
]
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"allowJs": true,
"declaration": true,
"outDir": "build",
"rootDir": "src",
"strict": true,
"noImplicitAny": true,
"noImplicitThis": false,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": ["./node_modules/@types", "./src/@types"],
"experimentalDecorators": true
},
"include": ["src", "test"],
"exclude": ["node_modules", "test"]
}