diff --git a/src/container.ts b/src/container.ts index 33ffb33..2c74c46 100644 --- a/src/container.ts +++ b/src/container.ts @@ -22,18 +22,26 @@ import { getMetadata, getParamMetadata, isClass, + isFunction, isPrimitiveFunction, isUndefined, recursiveGetMetadata, } from './util'; -import { NotFoundError, NoTypeError, NoHandlerError } from './error'; + +import { + NotFoundError, + NoTypeError, + NoHandlerError, + NoIdentifierError, + InjectionError, +} from './error'; export default class Container implements ContainerType { private registry: Map; private tags: Map>; // @ts-ignore protected name: string; - protected handlerMap: Map; + protected handlerMap: Map; constructor(name: string) { this.name = name; @@ -72,40 +80,42 @@ export default class Container implements ContainerType { } const { type, id, scope } = this.getDefinedMetaData(options); - const args = getMetadata(CLASS_CONSTRUCTOR_ARGS, type) as ReflectMetadataType[]; - const props = recursiveGetMetadata(CLASS_PROPERTY, type) as ReflectMetadataType[]; - const initMethodMd = getMetadata(CLASS_ASYNC_INIT_METHOD, type) as ReflectMetadataType; - const handlerArgs = getMetadata(INJECT_HANDLER_ARGS, type) as ReflectMetadataType[]; - const handlerProps = recursiveGetMetadata( - INJECT_HANDLER_PROPS, - type - ) as ReflectMetadataType[]; - const md: InjectableMetadata = { ...options, id, type, scope, - constructorArgs: (args ?? []).concat(handlerArgs ?? []), - properties: (props ?? []).concat(handlerProps ?? []), - initMethod: initMethodMd?.propertyName ?? 'init', }; + if (type) { + const args = getMetadata(CLASS_CONSTRUCTOR_ARGS, type) as ReflectMetadataType[]; + const props = recursiveGetMetadata(CLASS_PROPERTY, type) as ReflectMetadataType[]; + const initMethodMd = getMetadata(CLASS_ASYNC_INIT_METHOD, type) as ReflectMetadataType; + const handlerArgs = getMetadata(INJECT_HANDLER_ARGS, type) as ReflectMetadataType[]; + const handlerProps = recursiveGetMetadata( + INJECT_HANDLER_PROPS, + type + ) as ReflectMetadataType[]; + + md.constructorArgs = (args ?? []).concat(handlerArgs ?? []); + md.properties = (props ?? []).concat(handlerProps ?? []); + md.initMethod = initMethodMd?.propertyName ?? 'init'; + /** + * compatible with inject type identifier when identifier is string + */ + if (md.id !== type) { + md[MAP_TYPE] = type; + this.registry.set(type, md); + } - /** - * compatible with inject type identifier when identifier is string - */ - if (md.id !== type) { - md[MAP_TYPE] = type; - this.registry.set(type, md); + this.handleTag(type); } - this.registry.set(md.id, md); + this.registry.set(md.id, md); if (md.eager && md.scope !== ScopeEnum.TRANSIENT) { + // TODO: handle async this.get(md.id); } - this.handleTag(type); - return this; } @@ -128,11 +138,11 @@ export default class Container implements ContainerType { return Promise.all(clazzes.map(clazz => this.getAsync(clazz))); } - public registerHandler(name: string, handler: HandlerFunction) { + public registerHandler(name: string | symbol, handler: HandlerFunction) { this.handlerMap.set(name, handler); } - public getHandler(name: string) { + public getHandler(name: string | symbol) { return this.handlerMap.get(name); } @@ -146,10 +156,18 @@ export default class Container implements ContainerType { if (!isUndefined(md.value)) { return md.value; } - const clazz = md.type!; - const params = this.resolveParams(clazz, md.constructorArgs); - const value = new clazz(...params); - this.handleProps(value, md.properties ?? []); + let value; + if (md.factory) { + value = md.factory(md.id, this); + } + + if (!value && md.type) { + const clazz = md.type!; + const params = this.resolveParams(clazz, md.constructorArgs); + value = new clazz(...params); + this.handleProps(value, md.properties ?? []); + } + if (md.scope === ScopeEnum.SINGLETON) { md.value = value; } @@ -160,10 +178,18 @@ export default class Container implements ContainerType { if (!isUndefined(md.value)) { return md.value; } - const clazz = md.type!; - const params = await this.resolveParamsAsync(clazz, md.constructorArgs); - const value = new clazz(...params); - await this.handlePropsAsync(value, md.properties ?? []); + let value; + if (md.factory) { + value = await md.factory(md.id, this); + } + + if (!value && md.type) { + const clazz = md.type!; + const params = await this.resolveParamsAsync(clazz, md.constructorArgs); + value = new clazz(...params); + await this.handlePropsAsync(value, md.properties ?? []); + } + if (md.scope === ScopeEnum.SINGLETON) { md.value = value; } @@ -179,26 +205,36 @@ export default class Container implements ContainerType { } private getDefinedMetaData(options: Partial): { - type: Constructable; id: Identifier; scope: ScopeEnum; + type?: Constructable | null; } { - let type = options.type; + let { type, id, scope = ScopeEnum.SINGLETON, factory } = options; if (!type) { - if (options.id && isClass(options.id)) { - type = options.id as Constructable; + if (id && isClass(id)) { + type = id as Constructable; } } - if (!type) { - throw new NoTypeError('type is required'); + if (!type && !factory) { + throw new NoTypeError(`injectable ${id?.toString()}`); } - const targetMd = (getMetadata(CLASS_CONSTRUCTOR, type) as ReflectMetadataType) || {}; - const id = targetMd.id ?? options.id ?? type; - const scope = targetMd.scope ?? options.scope ?? ScopeEnum.SINGLETON; + if (factory && !isFunction(factory)) { + throw new InjectionError('factory option must be function'); + } - return { type, id, scope }; + if (type) { + const targetMd = (getMetadata(CLASS_CONSTRUCTOR, type) as ReflectMetadataType) || {}; + id = targetMd.id ?? id ?? type; + scope = targetMd.scope ?? scope; + } + + if (!id && factory) { + throw new NoIdentifierError(`injectable with factory option`); + } + + return { type, id: id!, scope }; } private resolveParams(clazz: any, args?: ReflectMetadataType[]): any[] { @@ -280,12 +316,13 @@ export default class Container implements ContainerType { }); } - private resolveHandler(handlerName: string, id?: Identifier): any { + private resolveHandler(handlerName: string | symbol, id?: Identifier): any { const handler = this.getHandler(handlerName); if (!handler) { - throw new NoHandlerError(handlerName); + throw new NoHandlerError(handlerName.toString()); } - return handler(id, this); + + return id ? handler(id, this) : handler(this); } } diff --git a/src/decorator/handler.ts b/src/decorator/handler.ts index 102a2e0..106de8f 100644 --- a/src/decorator/handler.ts +++ b/src/decorator/handler.ts @@ -2,20 +2,22 @@ import { INJECT_HANDLER_ARGS, INJECT_HANDLER_PROPS } from '../constant'; import { ReflectMetadataType } from '../types'; import { isUndefined, isObject, setMetadata, getMetadata } from '../util'; -export function InjectHandler(handlerName: string, id) { +export function InjectHandler(handlerName: string | symbol, id) { return function (target: any, key: string, index?: number) { if (isObject(target)) { target = target.constructor; } if (!isUndefined(index)) { - const metadatas = (getMetadata(INJECT_HANDLER_ARGS, target) || []) as ReflectMetadataType[]; + const metadatas = (getMetadata(INJECT_HANDLER_ARGS, target) || + []) as ReflectMetadataType[]; metadatas.push({ handler: handlerName, id, index }); setMetadata(INJECT_HANDLER_ARGS, metadatas, target); return; } - const metadatas = (getMetadata(INJECT_HANDLER_PROPS, target) || []) as ReflectMetadataType[]; + const metadatas = (getMetadata(INJECT_HANDLER_PROPS, target) || + []) as ReflectMetadataType[]; metadatas.push({ handler: handlerName, id, propertyName: key }); setMetadata(INJECT_HANDLER_PROPS, metadatas, target); - } -} \ No newline at end of file + }; +} diff --git a/src/error.ts b/src/error.ts index a42d197..ac0b12f 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,15 +1,13 @@ import { Constructable, Identifier } from './types'; import { createErrorClass } from './base_error'; -export class CannotInjectValueError - extends createErrorClass('CannotInjectValueError') { - constructor( - target: Constructable, - propertyName: string | symbol, - ) { - super(() => ( - `[@artus/injection] Cannot inject value into "` + - `${target.name}.${String(propertyName)}". `)); +export class CannotInjectValueError extends createErrorClass('CannotInjectValueError') { + constructor(target: Constructable, propertyName: string | symbol) { + super( + () => + `[@artus/injection] Cannot inject value into "` + + `${target.name}.${String(propertyName)}". ` + ); } } @@ -21,13 +19,15 @@ export class NoTypeError extends createErrorClass('NoTypeError') { export class NotFoundError extends createErrorClass('NotFoundError') { constructor(identifier: Identifier) { - const normalizedIdentifier = typeof identifier === 'string' ? - identifier : - (identifier?.name ?? 'Unknown'); + const normalizedIdentifier = + typeof identifier === 'function' + ? identifier.name + : (identifier ?? 'Unknown').toString(); super(() => { return ( `[@artus/injection] with "${normalizedIdentifier}" ` + - `identifier was not found in the container. `); + `identifier was not found in the container. ` + ); }); } } @@ -35,8 +35,19 @@ export class NotFoundError extends createErrorClass('NotFoundError') { export class NoHandlerError extends createErrorClass('NoHandlerError') { constructor(handler: string) { super(() => { - return ( - `[@artus/injection] "${handler}" handler was not found in the container.`); + return `[@artus/injection] "${handler}" handler was not found in the container.`; }); } } + +export class NoIdentifierError extends createErrorClass('NoIdentifierError') { + constructor(message: string) { + super(`[@artus/injection] id is required: ${message}`); + } +} + +export class InjectionError extends createErrorClass('InjectionError') { + constructor(message: string) { + super(`[@artus/injection] ${message}`); + } +} diff --git a/src/types.ts b/src/types.ts index 56959fb..1362896 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ export type Constructable = new (...args: any[]) => T; export type AbstractConstructable = NewableFunction & { prototype: T }; -export type Identifier = AbstractConstructable | Constructable | string; +export type Identifier = AbstractConstructable | Constructable | string | symbol; export enum ScopeEnum { SINGLETON = 'singleton', @@ -24,6 +24,7 @@ export interface InjectableDefinition { * By default the registered classes are only instantiated when they are requested from the container. */ eager?: boolean; + factory?: CallableFunction; } export interface InjectableMetadata extends InjectableDefinition { @@ -37,7 +38,7 @@ export interface ReflectMetadataType { scope?: ScopeEnum; index?: number; propertyName?: string | symbol; - handler?: string; + handler?: string | symbol; } export interface ContainerType { @@ -51,4 +52,8 @@ export interface ContainerType { getHandler(name: string): HandlerFunction | undefined; } -export type HandlerFunction = (handlerKey: any, instance?: any) => any; +/** + * A function that is used to handle a property + * last parameter is the instance of the Container + */ +export type HandlerFunction = CallableFunction; diff --git a/src/util.ts b/src/util.ts index b312a07..307d343 100644 --- a/src/util.ts +++ b/src/util.ts @@ -2,14 +2,23 @@ import { ReflectMetadataType } from './types'; import { CLASS_TAG } from './constant'; const functionPrototype = Object.getPrototypeOf(Function); -export function getMetadata(metadataKey: string | symbol, target: any, propertyKey?: string | symbol): ReflectMetadataType | ReflectMetadataType[] { +export function getMetadata( + metadataKey: string | symbol, + target: any, + propertyKey?: string | symbol +): ReflectMetadataType | ReflectMetadataType[] { if (propertyKey) { return Reflect.getOwnMetadata(metadataKey, target, propertyKey); } return Reflect.getOwnMetadata(metadataKey, target); } -export function setMetadata(metadataKey: string | symbol, value: ReflectMetadataType | ReflectMetadataType[], target: any, propertyKey?: string | symbol) { +export function setMetadata( + metadataKey: string | symbol, + value: ReflectMetadataType | ReflectMetadataType[], + target: any, + propertyKey?: string | symbol +) { if (propertyKey) { Reflect.defineMetadata(metadataKey, value, target, propertyKey); } else { @@ -19,12 +28,16 @@ export function setMetadata(metadataKey: string | symbol, value: ReflectMetadata /** * recursive get class and super class metadata - * @param metadataKey - * @param target - * @param propertyKey - * @returns + * @param metadataKey + * @param target + * @param propertyKey + * @returns */ -export function recursiveGetMetadata(metadataKey: any, target: any, propertyKey?: string | symbol): ReflectMetadataType[] { +export function recursiveGetMetadata( + metadataKey: any, + target: any, + propertyKey?: string | symbol +): ReflectMetadataType[] { let metadatas: any[] = []; const metadata = getMetadata(metadataKey, target, propertyKey); if (metadata) { @@ -44,8 +57,8 @@ export function recursiveGetMetadata(metadataKey: any, target: any, propertyKey? /** * get constructor parameter types - * @param clazz - * @returns + * @param clazz + * @returns */ export function getParamMetadata(clazz) { return Reflect.getMetadata('design:paramtypes', clazz); @@ -53,9 +66,9 @@ export function getParamMetadata(clazz) { /** * get the property type - * @param clazz - * @param property - * @returns + * @param clazz + * @param property + * @returns */ export function getDesignTypeMetadata(clazz: any, property: string | symbol) { return Reflect.getMetadata('design:type', clazz, property); @@ -80,9 +93,7 @@ export function isClass(clazz: any) { return ( fnStr.substring(0, 5) === 'class' || Boolean(~fnStr.indexOf('classCallCheck(')) || - Boolean( - ~fnStr.indexOf('TypeError("Cannot call a class as a function")') - ) + Boolean(~fnStr.indexOf('TypeError("Cannot call a class as a function")')) ); } @@ -98,7 +109,10 @@ export function isObject(value) { return typeof value === 'object'; } +export function isFunction(value) { + return typeof value === 'function'; +} export function isPrimitiveFunction(value) { return ['String', 'Boolean', 'Number', 'Object'].includes(value.name); -} \ No newline at end of file +} diff --git a/test/container.test.ts b/test/container.test.ts index dd6c70d..23e02b6 100644 --- a/test/container.test.ts +++ b/test/container.test.ts @@ -44,7 +44,7 @@ describe('container', () => { expect(person.email).toBe('artus@artusjs.com'); }); - it('should set throw error without value or type', () => { + it('should set throw error without value or type or factory', () => { expect(() => { container.set({ id: 'hello' }); }).toThrow('type is required'); @@ -272,3 +272,58 @@ describe('hasValue', () => { expect(container.hasValue({ id: ClassA })).toBeTruthy(); }); }); + +describe('container#factory', () => { + let container: Container; + beforeAll(() => { + container = new Container('factory'); + }); + + it('should set not throw error with factory and no type', () => { + expect(() => { + container.set({ id: 'demo', factory: () => {} }); + }).not.toThrow(); + }); + it('should set not throw error with factory and type', () => { + expect(() => { + container.set({ factory: () => {}, type: Foo }); + }).not.toThrow(); + }); + + it('should set throw error with factory and no id', () => { + expect(() => { + container.set({ factory: () => {} }); + }).toThrow('id is required'); + }); + + it('should set throw error when factory is not function', () => { + expect(() => { + container.set({ factory: {} as any, id: 'noFunction' }); + }).toThrow('factory option must be function'); + }); + + it('should use factory instance', async () => { + container.set({ + id: 'hello', + factory: () => { + return 'world'; + }, + }); + container.set({ + id: 'asyncHello', + factory: () => { + return Promise.resolve('world'); + }, + }); + + expect(container.get('hello')).toBe('world'); + expect(await container.getAsync('asyncHello')).toBe('world'); + }); + + it('should priority use factory when factory and type all provide', () => { + container.set({ factory: () => ({ hello: 'world' }), type: Phone }); + const phone = container.get(Phone); + expect(phone).toEqual({ hello: 'world' }); + expect(phone).not.toBeInstanceOf(Phone); + }); +});