Skip to content

Commit

Permalink
feat: support factory (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
JerrysShan authored Jul 6, 2022
1 parent 732062a commit c52a680
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 86 deletions.
129 changes: 83 additions & 46 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Identifier, InjectableMetadata>;
private tags: Map<string, Set<any>>;
// @ts-ignore
protected name: string;
protected handlerMap: Map<string, HandlerFunction>;
protected handlerMap: Map<string | symbol, HandlerFunction>;

constructor(name: string) {
this.name = name;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}

Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -179,26 +205,36 @@ export default class Container implements ContainerType {
}

private getDefinedMetaData(options: Partial<InjectableDefinition>): {
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[] {
Expand Down Expand Up @@ -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);
}
}
12 changes: 7 additions & 5 deletions src/decorator/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
};
}
41 changes: 26 additions & 15 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { Constructable, Identifier } from './types';
import { createErrorClass } from './base_error';

export class CannotInjectValueError
extends createErrorClass('CannotInjectValueError') {
constructor(
target: Constructable<unknown>,
propertyName: string | symbol,
) {
super(() => (
`[@artus/injection] Cannot inject value into "` +
`${target.name}.${String(propertyName)}". `));
export class CannotInjectValueError extends createErrorClass('CannotInjectValueError') {
constructor(target: Constructable<unknown>, propertyName: string | symbol) {
super(
() =>
`[@artus/injection] Cannot inject value into "` +
`${target.name}.${String(propertyName)}". `
);
}
}

Expand All @@ -21,22 +19,35 @@ 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. `
);
});
}
}

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}`);
}
}
11 changes: 8 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type Constructable<T = unknown> = new (...args: any[]) => T;
export type AbstractConstructable<T> = NewableFunction & { prototype: T };
export type Identifier<T = unknown> = AbstractConstructable<T> | Constructable<T> | string;
export type Identifier<T = unknown> = AbstractConstructable<T> | Constructable<T> | string | symbol;

export enum ScopeEnum {
SINGLETON = 'singleton',
Expand All @@ -24,6 +24,7 @@ export interface InjectableDefinition<T = unknown> {
* By default the registered classes are only instantiated when they are requested from the container.
*/
eager?: boolean;
factory?: CallableFunction;
}

export interface InjectableMetadata<T = any> extends InjectableDefinition<T> {
Expand All @@ -37,7 +38,7 @@ export interface ReflectMetadataType {
scope?: ScopeEnum;
index?: number;
propertyName?: string | symbol;
handler?: string;
handler?: string | symbol;
}

export interface ContainerType {
Expand All @@ -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;
Loading

0 comments on commit c52a680

Please sign in to comment.