Skip to content

Commit

Permalink
Merge pull request #46 from kitesjs/dev-ioc
Browse files Browse the repository at this point in the history
Dev ioc
  • Loading branch information
vunb authored Apr 5, 2020
2 parents 8a2a7f3 + 50eb177 commit b4272ca
Show file tree
Hide file tree
Showing 40 changed files with 1,141 additions and 314 deletions.
368 changes: 256 additions & 112 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kites/core",
"version": "1.1.8",
"version": "1.2.0",
"displayName": "Kites",
"description": "Template-based Web Application Framework",
"main": "src/main.js",
Expand All @@ -13,6 +13,7 @@
"start:dev": "ts-node app.ts",
"todo": "ts-node sample/01-todo-app/app.ts",
"test": "mocha \"./packages/**/*.spec.ts\" --reporter spec --retries 3 --require ts-node/register --require node_modules/reflect-metadata/Reflect.js --exit",
"test:common": "mocha \"./packages/common/**/*.spec.ts\" --reporter spec --require ts-node/register --require node_modules/reflect-metadata/Reflect.js --exit",
"clean": "gulp clean:bundle",
"build": "npm run clean && gulp build",
"prebuild:dev": "rimraf node_modules/@kites",
Expand Down
33 changes: 0 additions & 33 deletions packages/common/constants.ts

This file was deleted.

7 changes: 7 additions & 0 deletions packages/common/constants/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as interfaces from '../interfaces';

export const TargetTypeEnum: interfaces.TargetTypeEnum = {
ClassProperty: 'ClassProperty',
ConstructorArgument: 'ConstructorArgument',
Variable: 'Variable'
};
15 changes: 15 additions & 0 deletions packages/common/constants/error.messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const STACK_OVERFLOW = 'Maximum call stack size exceeded';
export const DUPLICATED_INJECTABLE_DECORATOR = 'Duplicated injectable decorator';
export const DUPLICATED_METADATA = 'Metadata key was used more than once in a parameter';
export const NULL_ARGUMENT = 'NULL argument';
export const KEY_NOT_FOUND = 'Key Not Found';
export const MISSING_INJECTABLE_ANNOTATION = 'Missing required @Injectable annotation in:';
export const MISSING_INJECT_ANNOTATION = 'Missing required @Inject or @MultiInject annotation in:';

export const UNDEFINED_INJECT_ANNOTATION = (name: string) =>
`@Inject called with undefined this could mean that the class ${name} has ` +
'a circular dependency problem. You can use a LazyServiceIdentifer to ' +
'overcome this limitation.';

export const INVALID_DECORATOR_OPERATION = 'The @Inject @Tagged and @Named decorators ' +
'must be applied to the parameters of a class constructor or a class property.';
1 change: 1 addition & 0 deletions packages/common/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './metadata.keys';
39 changes: 39 additions & 0 deletions packages/common/constants/metadata.keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export const METADATA = {
CONTROLLERS: 'controllers',
EXPORTS: 'exports',
IMPORTS: 'imports',
PROVIDERS: 'providers',
};

export const PATH_METADATA = 'path';
export const METHOD_METADATA = 'method';
export const RENDER_METADATA = '__renderTemplate__';

// Used for named bindings
export const NAMED_TAG = 'named';

// The name of the target at design time
export const NAME_TAG = 'name';

// The for unmanaged injections (in base classes when using inheritance)
export const UNMANAGED_TAG = 'unmanaged';

// The type of the binding at design time
export const INJECT_TAG = 'inject';

// The type of the binding at design type for multi-injections
export const MULTI_INJECT_TAG = 'multi_inject';

// used to store constructor arguments tags
export const TAGGED = 'kites:tagged';

// used to store class properties tags
export const TAGGED_PROP = 'kites:tagged_props';

// used to access design time types
export const DESIGN_PARAM_TYPES = 'design:paramtypes';

// used to store types to be injected
export const PARAM_TYPES = 'kites:paramtypes';

export const INJECT_METADATA_KEY = Symbol('INJECT_KEY');
79 changes: 64 additions & 15 deletions packages/common/decorators/core/inject.decorator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,84 @@
import 'reflect-metadata';

import { expect } from 'chai';
import { INJECT_METADATA_KEY, SELF_DECLARED_DEPS_METADATA } from '../../constants';
import { UNDEFINED_INJECT_ANNOTATION } from '../../constants/error.messages';
import * as METADATA_KEY from '../../constants/metadata.keys';
import { InjectionToken } from '../../interfaces';
import { Inject } from './inject.decorator';
import * as interfaces from '../../interfaces';
import { Decorate } from '../decorate';
import { Inject, LazyServiceIdentifer } from './inject.decorator';

describe('@Inject', () => {
const USER_STRING_TOKEN = new InjectionToken('user-identifier');

class ABasicClass {
class PingService {
constructor(public x: number) { }
}

class ServiceTest {
class DomainService { }
class DiagnosticService { }

const lazyDiagnosticId = new LazyServiceIdentifer(() => 'Diagnostic');

class DecoratedServiceTest {
constructor(
@Inject(USER_STRING_TOKEN) param1,
@Inject(ABasicClass) param2,
// @Inject('Test') param3,
@Inject(PingService) private svPing: PingService,
@Inject('Domain') private svDomain: DomainService,
@Inject(lazyDiagnosticId) private svDiagnostic: DiagnosticService,
) { }
}

it('should enhance class with expected constructor params metadata', () => {
const metadata = Reflect.getMetadata(INJECT_METADATA_KEY, ServiceTest);
class InvalidDecoratorUsageService {
private svDomain: DomainService;
private svPing: PingService;

constructor(
svDomain: DomainService,
svPing: PingService,
) {
this.svDomain = svDomain;
this.svPing = svPing;
}
}

it('should enhance class with expected constructor params metadata using named parameters', () => {
const metadata = Reflect.getMetadata(METADATA_KEY.TAGGED, DecoratedServiceTest);

const expectedMetadata = [
{ index: 1, param: USER_STRING_TOKEN.injectionIdentifier },
{ index: 0, param: ABasicClass.name },
];
expect(metadata).to.be.an('object');

// assert metadata for first argument
expect(metadata['0']).to.be.instanceOf(Array);
const arg1: interfaces.Metadata = metadata['0'][0];
expect(arg1.key).to.be.eq(METADATA_KEY.INJECT_TAG);
expect(arg1.value).to.be.eql(PingService);
expect(metadata['0'][1]).to.be.eq(undefined);

// assert metadata for second argument
expect(metadata['1']).to.be.instanceOf(Array);
const arg2: interfaces.Metadata = metadata['1'][0];
expect(arg2.key).to.be.eq(METADATA_KEY.INJECT_TAG);
expect(arg2.value).to.be.eql('Domain');
expect(metadata['1'][1]).to.be.eq(undefined);

// assert metadata for third argument
expect(metadata['2']).to.be.instanceOf(Array);
const arg3: interfaces.Metadata = metadata['2'][0];
expect(arg3.key).to.be.eq(METADATA_KEY.INJECT_TAG);
expect(arg3.value).to.be.eql(lazyDiagnosticId);
expect(metadata['2'][1]).to.be.eq(undefined);

// no more metadata should be available
expect(metadata['3']).to.be.eq(undefined);

});

// console.log('AAAAA', metadata, '123', expectedMetadata, '456');
it('should throw when applied with undefined token', () => {
// this can be happen when there is a circluar dependency between tokens
const useDecoratorWithUndefinedToken = function () {
Decorate(Inject(undefined as any) as any, InvalidDecoratorUsageService, 0);
};

// expect(metadata, 'Get metadata of ServiceTest').to.be.eql(expectedMetadata);
const errMsg = `${UNDEFINED_INJECT_ANNOTATION('InvalidDecoratorUsageService')}`;
expect(useDecoratorWithUndefinedToken).to.throw(errMsg);
});
});
42 changes: 35 additions & 7 deletions packages/common/decorators/core/inject.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
import { INJECT_METADATA_KEY } from '../../constants';
import { UNDEFINED_INJECT_ANNOTATION } from '../../constants/error.messages';
import * as METADATA_KEY from '../../constants/metadata.keys';
import { Token } from '../../interfaces/provider.interface';
import { Metadata } from '../metadata';
import { tagParameter, tagProperty } from './tag.decorator';

export function Inject(token: Token<any>) {
return function (target: any, _: string | symbol, index: number) {
Reflect.defineMetadata(INJECT_METADATA_KEY, token, target, `index-${index}`);
export type TokenIdentifierOrFunc = Token<any> | LazyServiceIdentifer;

export class LazyServiceIdentifer<T = any> {
private _cb: () => Token<T>;
public constructor(cb: () => Token<T>) {
this._cb = cb;
}

public unwrap() {
return this._cb();
}
}

export function Inject(token: TokenIdentifierOrFunc) {
return function (target: any, targetKey: string, index?: number) {

if (token === undefined) {
throw new Error(UNDEFINED_INJECT_ANNOTATION(target.name));
}

// Reflect.defineMetadata(INJECT_METADATA_KEY, token, target, `index-${index}`);
const metadata = new Metadata(METADATA_KEY.INJECT_TAG, token);

if (typeof index === 'number') {
tagParameter(target, targetKey, index, metadata);
} else {
tagProperty(target, targetKey, metadata);
}
return target;
};
}

export function getInjectionToken(target: any, index: number) {
return Reflect.getMetadata(INJECT_METADATA_KEY, target, `index-${index}`) as Token<any> | undefined;
}
// export function getInjectionToken(target: any, index: number) {
// return Reflect.getMetadata(INJECT_METADATA_KEY, target, `index-${index}`) as Token<any> | undefined;
// }
41 changes: 36 additions & 5 deletions packages/common/decorators/core/injectable.decorator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import 'reflect-metadata';

import { expect } from 'chai';
import * as ERRORS_MSGS from '../../constants/error.messages';
import * as METADATA_KEY from '../../constants/metadata.keys';
import { Decorate } from '../decorate';
import { Injectable, isInjectable } from './injectable.decorator';

describe('@Injectable', () => {
interface AbstractInterface { }
class StandardClass { }

@Injectable()
class TestMiddleware {
constructor(param: number, test: string) { }
constructor
(
param: number,
test: string,
svA: AbstractInterface,
svB: StandardClass,
) { }
}

class StandardClass { }

it('recognize injectable class', () => {
it('should recognize injectable class', () => {
const injectable = isInjectable(TestMiddleware);
expect(injectable).to.be.eq(true);
});
Expand All @@ -23,11 +33,32 @@ describe('@Injectable', () => {

it('should enhance provider with "design:paramtypes" metadata', () => {
const constructorParams = Reflect.getMetadata(
'design:paramtypes',
METADATA_KEY.PARAM_TYPES,
TestMiddleware,
);

// meta data info
expect(constructorParams).to.be.instanceof(Array);

// constructor params info
expect(constructorParams[0]).to.be.eql(Number);
expect(constructorParams[1]).to.be.eql(String);
expect(constructorParams[2]).to.be.eql(Object);
expect(constructorParams[3]).to.be.eql(StandardClass);
expect(constructorParams[4]).to.eq(undefined);
});

it('should throw when applied multiple times', () => {

@Injectable()
class Test { }

const useDecoratorMoreThanOnce = function () {
Decorate(Injectable(), Test);
Decorate(Injectable(), Test);
};

expect(useDecoratorMoreThanOnce).to.throw(ERRORS_MSGS.DUPLICATED_INJECTABLE_DECORATOR);
});

});
17 changes: 9 additions & 8 deletions packages/common/decorators/core/injectable.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { INJECTABLE_METADATA_KEY, PARAMTYPES_METADATA, SCOPE_OPTIONS_METADATA } from '../../constants';
import { DESIGN_PARAM_TYPES, PARAM_TYPES } from '../../constants';
import { DUPLICATED_INJECTABLE_DECORATOR } from '../../constants/error.messages';
import { ScopeOptions } from '../../interfaces/scope-options.interface';
import { Type } from '../../interfaces/type.interface';
import { Newable } from '../../interfaces/type.interface';

export interface InjectableOptions extends ScopeOptions { }

Expand All @@ -12,18 +13,18 @@ export interface InjectableOptions extends ScopeOptions { }
export function Injectable() {
return (target: any) => {

if (Reflect.hasOwnMetadata(INJECTABLE_METADATA_KEY, target)) {
throw new Error('Duplicated injectable decorator');
if (Reflect.hasOwnMetadata(PARAM_TYPES, target)) {
throw new Error(DUPLICATED_INJECTABLE_DECORATOR);
}

const types = Reflect.getMetadata(PARAMTYPES_METADATA, target) || [];
const types = Reflect.getMetadata(DESIGN_PARAM_TYPES, target) || [];
Reflect.defineMetadata(PARAM_TYPES, types, target);

Reflect.defineMetadata(INJECTABLE_METADATA_KEY, types, target);
return target;
};
}

export function isInjectable<T>(target: Type<T>) {
export function isInjectable<T>(target: Newable<T>) {
// return Reflect.getMetadata(INJECTABLE_METADATA_KEY, target) === true;
return Reflect.hasOwnMetadata(INJECTABLE_METADATA_KEY, target);
return Reflect.hasOwnMetadata(PARAM_TYPES, target);
}
Loading

0 comments on commit b4272ca

Please sign in to comment.