@miracledevs/paradigm-web-di
Version:
A minimal dependency injection library for the web
151 lines (120 loc) • 5.55 kB
text/typescript
import { ObjectType, getObjectTypeName } from "./object-type";
import { DependencyLifeTime } from "./dependency-life-time";
import { DependencyDescriptor } from "./dependency-descriptor";
import { DependencyContainer } from "./dependency-container";
export class DependencyCollection
{
private static _globalCollection: DependencyCollection;
public static get globalCollection(): DependencyCollection
{
if (!this._globalCollection)
this._globalCollection = new DependencyCollection();
return this._globalCollection;
}
private readonly _registeredTypes: Map<ObjectType, DependencyDescriptor>;
constructor()
{
this._registeredTypes = new Map<ObjectType, DependencyDescriptor>();
this.register(DependencyContainer as any, DependencyLifeTime.Transient);
}
register<T = any>(objectType: ObjectType<T>, lifeTime: DependencyLifeTime, dependencies?: ObjectType[], instance?: T): void
{
if (this.contains(objectType))
throw new Error(`The type ${getObjectTypeName(objectType)} is already registered.`);
if (instance && lifeTime !== DependencyLifeTime.Singleton)
throw new Error(`Only the singletons can be registered with an existing instance.`);
this._registeredTypes.set(objectType, new DependencyDescriptor(lifeTime || DependencyLifeTime.Transient, dependencies || [], instance || null));
}
registerTransient<T = any>(objectType: ObjectType<T>, dependencies?: ObjectType[], ): void
{
this.register(objectType, DependencyLifeTime.Transient, dependencies);
}
registerScoped<T = any>(objectType: ObjectType<T>, dependencies?: ObjectType[], ): void
{
this.register(objectType, DependencyLifeTime.Scoped, dependencies);
}
registerSingleton<T = any>(objectType: ObjectType<T>, dependencies?: ObjectType[], instance?: T): void
{
this.register(objectType, DependencyLifeTime.Singleton, dependencies, instance);
}
get(objectType: ObjectType): DependencyDescriptor
{
if (!this._registeredTypes.has(objectType))
throw new Error(`The type ${getObjectTypeName(objectType)} is not registered.`);
return this._registeredTypes.get(objectType) as DependencyDescriptor;
}
contains(objectType: ObjectType): boolean
{
return this._registeredTypes.has(objectType);
}
buildContainer(validate: boolean = false): DependencyContainer
{
if (validate)
{
let errors = '';
for (const registeredType of this._registeredTypes.keys())
{
try
{
this.validateCircularDependency(registeredType);
this.validateDependencyRegistration(registeredType);
this.validateScopedOnSingletons(registeredType);
}
catch (e)
{
errors += ` - ${(e instanceof Error? e.message : e)}\n`;
}
}
if (errors.length > 0)
{
throw new Error("Errors found on the dependency configuration:\n" + errors);
}
}
return DependencyContainer.createFromCollection(this, "root");
}
private validateDependencyRegistration<T>(objectType: ObjectType<T>): void
{
const descriptor = this.get(objectType);
for (const dependencyType of descriptor.dependencies)
{
if (!this.contains(dependencyType))
{
throw new Error(`The type '${getObjectTypeName(objectType)}' depends on the type '${getObjectTypeName(dependencyType)}' but is not registered.`);
}
this.validateDependencyRegistration(dependencyType);
}
}
private validateCircularDependency<T>(objectType: ObjectType<T>, hierarchy?: ObjectType[]): void
{
if (!hierarchy)
hierarchy = [];
const descriptor = this.get(objectType);
const dependencies = descriptor.dependencies;
for (const dependencyType of dependencies)
{
if (!this.contains(dependencyType))
continue;
if (hierarchy.find(x => x === dependencyType))
{
throw new Error(`Circular dependency found in service type '${getObjectTypeName(hierarchy[0])}': ${hierarchy.map(x => getObjectTypeName(x)).join(" -> ")} -> ${getObjectTypeName(dependencyType)}.`);
}
const newHierarchy = [...hierarchy];
newHierarchy.push(dependencyType);
this.validateCircularDependency(dependencyType, newHierarchy);
}
}
private validateScopedOnSingletons<T>(objectType: ObjectType<T>): void
{
const descriptor = this.get(objectType);
for (const dependencyType of descriptor.dependencies)
{
const dependencyDescriptor = this.get(dependencyType);
if (descriptor.lifeTime === DependencyLifeTime.Singleton &&
dependencyDescriptor.lifeTime === DependencyLifeTime.Scoped)
{
throw new Error(`Cannot consume scoped type '${getObjectTypeName(dependencyType)}' from singleton '${getObjectTypeName(objectType)}'.`);
}
this.validateScopedOnSingletons(dependencyType);
}
}
}