Setup eslint and enforce rules
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2021-06-02 22:36:25 -05:00
parent 82e7a1f299
commit 1d5056b753
149 changed files with 6104 additions and 3114 deletions

View File

@@ -1,14 +1,14 @@
import {DependencyKey, InstanceRef, Instantiable, isInstantiable, StaticClass} from "./types";
import {AbstractFactory} from "./factory/AbstractFactory";
import {collect, Collection, globalRegistry, logIfDebugging} from "../util";
import {Factory} from "./factory/Factory";
import {DuplicateFactoryKeyError} from "./error/DuplicateFactoryKeyError";
import {ClosureFactory} from "./factory/ClosureFactory";
import NamedFactory from "./factory/NamedFactory";
import SingletonFactory from "./factory/SingletonFactory";
import {InvalidDependencyKeyError} from "./error/InvalidDependencyKeyError";
import {DependencyKey, InstanceRef, Instantiable, isInstantiable} from './types'
import {AbstractFactory} from './factory/AbstractFactory'
import {collect, Collection, globalRegistry, logIfDebugging} from '../util'
import {Factory} from './factory/Factory'
import {DuplicateFactoryKeyError} from './error/DuplicateFactoryKeyError'
import {ClosureFactory} from './factory/ClosureFactory'
import NamedFactory from './factory/NamedFactory'
import SingletonFactory from './factory/SingletonFactory'
import {InvalidDependencyKeyError} from './error/InvalidDependencyKeyError'
export type MaybeFactory = AbstractFactory | undefined
export type MaybeFactory<T> = AbstractFactory<T> | undefined
export type MaybeDependency = any | undefined
export type ResolvedDependency = { paramIndex: number, key: DependencyKey, resolved: any }
@@ -34,7 +34,7 @@ export class Container {
* Collection of factories registered with this container.
* @type Collection<AbstractFactory>
*/
protected factories: Collection<AbstractFactory> = new Collection<AbstractFactory>()
protected factories: Collection<AbstractFactory<unknown>> = new Collection<AbstractFactory<unknown>>()
/**
* Collection of singleton instances produced by this container.
@@ -51,12 +51,14 @@ export class Container {
* Register a basic instantiable class as a standard Factory with this container.
* @param {Instantiable} dependency
*/
register(dependency: Instantiable<any>) {
if ( this.resolve(dependency) )
register(dependency: Instantiable<any>): this {
if ( this.resolve(dependency) ) {
throw new DuplicateFactoryKeyError(dependency)
}
const factory = new Factory(dependency)
this.factories.push(factory)
return this
}
/**
@@ -64,12 +66,14 @@ export class Container {
* @param {string} name - unique name to identify the factory in the container
* @param {function} producer - factory to produce a value
*/
registerProducer(name: string | StaticClass<any, any>, producer: () => any) {
if ( this.resolve(name) )
registerProducer(name: DependencyKey, producer: () => any): this {
if ( this.resolve(name) ) {
throw new DuplicateFactoryKeyError(name)
}
const factory = new ClosureFactory(name, producer)
this.factories.push(factory)
return this
}
/**
@@ -78,12 +82,14 @@ export class Container {
* @param {string} name - unique name to identify the factory in the container
* @param {Instantiable} dependency
*/
registerNamed(name: string, dependency: Instantiable<any>) {
if ( this.resolve(name) )
registerNamed(name: string, dependency: Instantiable<any>): this {
if ( this.resolve(name) ) {
throw new DuplicateFactoryKeyError(name)
}
const factory = new NamedFactory(name, dependency)
this.factories.push(factory)
return this
}
/**
@@ -92,11 +98,13 @@ export class Container {
* @param {string} key - unique name to identify the singleton in the container
* @param value
*/
registerSingleton(key: string, value: any) {
if ( this.resolve(key) )
registerSingleton<T>(key: DependencyKey, value: T): this {
if ( this.resolve(key) ) {
throw new DuplicateFactoryKeyError(key)
}
this.factories.push(new SingletonFactory(value, key))
this.factories.push(new SingletonFactory(key, value))
return this
}
/**
@@ -105,24 +113,30 @@ export class Container {
* @param staticClass
* @param instance
*/
registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T) {
if ( this.resolve(staticClass) )
registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T): this {
if ( this.resolve(staticClass) ) {
throw new DuplicateFactoryKeyError(staticClass)
}
this.register(staticClass)
this.instances.push({
key: staticClass,
value: instance,
})
return this
}
/**
* Register a given factory with the container.
* @param {AbstractFactory} factory
*/
registerFactory(factory: AbstractFactory) {
if ( !this.factories.includes(factory) )
registerFactory(factory: AbstractFactory<unknown>): this {
if ( !this.factories.includes(factory) ) {
this.factories.push(factory)
}
return this
}
/**
@@ -138,7 +152,7 @@ export class Container {
* @param {DependencyKey} key
*/
hasKey(key: DependencyKey): boolean {
return !!this.resolve(key)
return Boolean(this.resolve(key))
}
/**
@@ -147,17 +161,22 @@ export class Container {
*/
getExistingInstance(key: DependencyKey): MaybeDependency {
const instances = this.instances.where('key', '=', key)
if ( instances.isNotEmpty() ) return instances.first()
if ( instances.isNotEmpty() ) {
return instances.first()
}
}
/**
* Find the factory for the given key, if one is registered with this container.
* @param {DependencyKey} key
*/
resolve(key: DependencyKey): MaybeFactory {
resolve(key: DependencyKey): MaybeFactory<unknown> {
const factory = this.factories.firstWhere(item => item.match(key))
if ( factory ) return factory
else logIfDebugging('extollo.di.injector', 'unable to resolve factory', factory, this.factories)
if ( factory ) {
return factory
} else {
logIfDebugging('extollo.di.injector', 'unable to resolve factory', factory, this.factories)
}
}
/**
@@ -172,22 +191,25 @@ export class Container {
// If we've already instantiated this, just return that
const instance = this.getExistingInstance(key)
logIfDebugging('extollo.di.injector', 'resolveAndCreate existing instance?', instance)
if ( typeof instance !== 'undefined' ) return instance.value
if ( typeof instance !== 'undefined' ) {
return instance.value
}
// Otherwise, attempt to create it
const factory = this.resolve(key)
logIfDebugging('extollo.di.injector', 'resolveAndCreate factory', factory)
if ( !factory )
if ( !factory ) {
throw new InvalidDependencyKeyError(key)
}
// Produce and store a new instance
const new_instance = this.produceFactory(factory, parameters)
const newInstance = this.produceFactory(factory, parameters)
this.instances.push({
key,
value: new_instance,
value: newInstance,
})
return new_instance
return newInstance
}
/**
@@ -196,7 +218,7 @@ export class Container {
* @param {AbstractFactory} factory
* @param {array} parameters
*/
protected produceFactory(factory: AbstractFactory, parameters: any[]) {
protected produceFactory<T>(factory: AbstractFactory<T>, parameters: any[]): T {
// Create the dependencies for the factory
const keys = factory.getDependencyKeys().filter(req => this.hasKey(req.key))
const dependencies = keys.map<ResolvedDependency>(req => {
@@ -210,20 +232,23 @@ export class Container {
// Build the arguments for the factory, using dependencies in the
// correct paramIndex positions, or parameters of we don't have
// the dependency.
const construction_args = []
let params = collect(parameters).reverse()
const constructorArguments = []
const params = collect(parameters).reverse()
for ( let i = 0; i <= dependencies.max('paramIndex'); i++ ) {
const dep = dependencies.firstWhere('paramIndex', '=', i)
if ( dep ) construction_args.push(dep.resolved)
else construction_args.push(params.pop())
if ( dep ) {
constructorArguments.push(dep.resolved)
} else {
constructorArguments.push(params.pop())
}
}
// Produce a new instance
const inst = factory.produce(construction_args, params.reverse().all())
const inst = factory.produce(constructorArguments, params.reverse().all())
factory.getInjectedProperties().each(dependency => {
if ( dependency.key && inst ) {
inst[dependency.property] = this.resolveAndCreate(dependency.key)
(inst as any)[dependency.property] = this.resolveAndCreate(dependency.key)
}
})
@@ -240,12 +265,13 @@ export class Container {
* @param {...any} parameters
*/
make<T>(target: DependencyKey, ...parameters: any[]): T {
if ( this.hasKey(target) )
if ( this.hasKey(target) ) {
return this.resolveAndCreate(target, ...parameters)
else if ( typeof target !== 'string' && isInstantiable(target) )
} else if ( typeof target !== 'string' && isInstantiable(target) ) {
return this.produceFactory(new Factory(target), parameters)
else
} else {
throw new TypeError(`Invalid or unknown make target: ${target}`)
}
}
/**
@@ -255,8 +281,9 @@ export class Container {
getDependencies(target: DependencyKey): Collection<DependencyKey> {
const factory = this.resolve(target)
if ( !factory )
if ( !factory ) {
throw new InvalidDependencyKeyError(target)
}
return factory.getDependencyKeys().pluck('key')
}
@@ -265,8 +292,9 @@ export class Container {
* Given a different container, copy the factories and instances from this container over to it.
* @param container
*/
cloneTo(container: Container) {
cloneTo(container: Container): this {
container.factories = this.factories.clone()
container.instances = this.instances.clone()
return this
}
}

View File

@@ -1,5 +1,5 @@
import {Container, MaybeDependency, MaybeFactory} from "./Container"
import {DependencyKey} from "./types"
import {Container, MaybeDependency, MaybeFactory} from './Container'
import {DependencyKey} from './types'
/**
* A container that uses some parent container as a base, but
@@ -26,8 +26,8 @@ export class ScopedContainer extends Container {
* Create a new scoped container based on a parent container instance.
* @param container
*/
public static fromParent(container: Container) {
return new ScopedContainer(container);
public static fromParent(container: Container): ScopedContainer {
return new ScopedContainer(container)
}
constructor(
@@ -47,15 +47,19 @@ export class ScopedContainer extends Container {
getExistingInstance(key: DependencyKey): MaybeDependency {
const inst = super.getExistingInstance(key)
if ( inst ) return inst;
if ( inst ) {
return inst
}
return this.parentContainer.getExistingInstance(key);
return this.parentContainer.getExistingInstance(key)
}
resolve(key: DependencyKey): MaybeFactory {
const factory = super.resolve(key);
if ( factory ) return factory;
resolve(key: DependencyKey): MaybeFactory<any> {
const factory = super.resolve(key)
if ( factory ) {
return factory
}
return this.parentContainer?.resolve(key);
return this.parentContainer?.resolve(key)
}
}

View File

@@ -1,5 +1,5 @@
import 'reflect-metadata'
import {collect, Collection} from "../../util";
import {collect, Collection} from '../../util'
import {
DependencyKey,
DependencyRequirement,
@@ -9,16 +9,16 @@ import {
InjectionType,
DEPENDENCY_KEYS_SERVICE_TYPE_KEY,
PropertyDependency,
} from "../types";
import {Container} from "../Container";
} from '../types'
import {Container} from '../Container'
/**
* Get a collection of dependency requirements for the given target object.
* @param {Object} target
* @return Collection<DependencyRequirement>
*/
function initDependencyMetadata(target: Object): Collection<DependencyRequirement> {
const paramTypes = Reflect.getMetadata('design:paramtypes', target)
function initDependencyMetadata(target: unknown): Collection<DependencyRequirement> {
const paramTypes = Reflect.getMetadata('design:paramtypes', target as any)
return collect<DependencyKey>(paramTypes).map<DependencyRequirement>((type, idx) => {
return {
paramIndex: idx,
@@ -37,32 +37,32 @@ export const Injectable = (): ClassDecorator => {
return (target) => {
const meta = initDependencyMetadata(target)
const existing = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target)
const new_meta = new Collection<DependencyRequirement>()
const newMetadata = new Collection<DependencyRequirement>()
if ( existing ) {
const max_new = meta.max('paramIndex')
const max_existing = existing.max('paramIndex')
for ( let i = 0; i <= Math.max(max_new, max_existing); i++ ) {
const existing_dr = existing.firstWhere('paramIndex', '=', i)
const new_dr = meta.firstWhere('paramIndex', '=', i)
const maxNew = meta.max('paramIndex')
const maxExisting = existing.max('paramIndex')
for ( let i = 0; i <= Math.max(maxNew, maxExisting); i++ ) {
const existingDR = existing.firstWhere('paramIndex', '=', i)
const newDR = meta.firstWhere('paramIndex', '=', i)
if ( existing_dr && !new_dr ) {
new_meta.push(existing_dr)
} else if ( new_dr && !existing_dr ) {
new_meta.push(new_dr)
} else if ( new_dr && existing_dr ) {
if ( existing_dr.overridden ) {
new_meta.push(existing_dr)
if ( existingDR && !newDR ) {
newMetadata.push(existingDR)
} else if ( newDR && !existingDR ) {
newMetadata.push(newDR)
} else if ( newDR && existingDR ) {
if ( existingDR.overridden ) {
newMetadata.push(existingDR)
} else {
new_meta.push(new_dr)
newMetadata.push(newDR)
}
}
}
} else {
new_meta.concat(meta)
newMetadata.concat(meta)
}
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, new_meta, target)
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, newMetadata, target)
}
}
@@ -82,14 +82,17 @@ export const Inject = (key?: DependencyKey): PropertyDecorator => {
}
const type = Reflect.getMetadata('design:type', target, property)
if ( !key && type ) key = type
if ( !key && type ) {
key = type
}
if ( key ) {
const existing = propertyMetadata.firstWhere('property', '=', property)
if ( existing ) {
existing.key = key
} else {
propertyMetadata.push({ property, key })
propertyMetadata.push({ property,
key })
}
}
@@ -118,7 +121,7 @@ export const InjectParam = (key: DependencyKey): ParameterDecorator => {
meta.push({
paramIndex,
key,
overridden: true
overridden: true,
})
}
@@ -135,7 +138,7 @@ export const Singleton = (name?: string): ClassDecorator => {
if ( isInstantiable(target) ) {
const injectionType: InjectionType = {
type: name ? 'named' : 'singleton',
...(name ? { name } : {})
...(name ? { name } : {}),
}
Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target)

View File

@@ -1,4 +1,4 @@
import {DependencyKey} from "../types";
import {DependencyKey} from '../types'
/**
* Error thrown when a factory is registered with a duplicate dependency key.

View File

@@ -1,4 +1,4 @@
import {DependencyKey} from "../types";
import {DependencyKey} from '../types'
/**
* Error thrown when a dependency key that has not been registered is passed to a resolver.

View File

@@ -1,11 +1,11 @@
import {DependencyRequirement, PropertyDependency} from "../types";
import { Collection } from "../../util";
import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
import { Collection } from '../../util'
/**
* Abstract base class for dependency container factories.
* @abstract
*/
export abstract class AbstractFactory {
export abstract class AbstractFactory<T> {
protected constructor(
/**
* Token that was registered for this factory. In most cases, this is the static
@@ -13,7 +13,7 @@ export abstract class AbstractFactory {
* @var
* @protected
*/
protected token: any
protected token: DependencyKey,
) {}
/**
@@ -21,14 +21,14 @@ export abstract class AbstractFactory {
* @param {Array} dependencies - the resolved dependencies, in order
* @param {Array} parameters - the bound constructor parameters, in order
*/
abstract produce(dependencies: any[], parameters: any[]): any
abstract produce(dependencies: any[], parameters: any[]): T
/**
* Should return true if the given identifier matches the token for this factory.
* @param something
* @return boolean
*/
abstract match(something: any): boolean
abstract match(something: unknown): boolean
/**
* Get the dependency requirements required by this factory's token.

View File

@@ -1,6 +1,6 @@
import {AbstractFactory} from "./AbstractFactory";
import {DependencyRequirement, PropertyDependency, StaticClass} from "../types";
import {Collection} from "../../util";
import {AbstractFactory} from './AbstractFactory'
import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
import {Collection} from '../../util'
/**
* A factory whose token is produced by calling a function.
@@ -17,19 +17,19 @@ import {Collection} from "../../util";
* fact.produce([], []) // => 4
* ```
*/
export class ClosureFactory extends AbstractFactory {
export class ClosureFactory<T> extends AbstractFactory<T> {
constructor(
protected readonly name: string | StaticClass<any, any>,
protected readonly token: () => any,
protected readonly name: DependencyKey,
protected readonly token: () => T,
) {
super(token)
}
produce(dependencies: any[], parameters: any[]): any {
produce(): any {
return this.token()
}
match(something: any) {
match(something: unknown): boolean {
return something === this.name
}

View File

@@ -1,12 +1,12 @@
import {AbstractFactory} from "./AbstractFactory";
import {AbstractFactory} from './AbstractFactory'
import {
DEPENDENCY_KEYS_METADATA_KEY,
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY,
DependencyRequirement,
Instantiable,
PropertyDependency
} from "../types";
import {Collection} from "../../util";
PropertyDependency,
} from '../types'
import {Collection} from '../../util'
import 'reflect-metadata'
/**
@@ -29,9 +29,9 @@ import 'reflect-metadata'
* fact.produce([myServiceInstance], []) // => A { myService: myServiceInstance }
* ```
*/
export class Factory extends AbstractFactory {
export class Factory<T> extends AbstractFactory<T> {
constructor(
protected readonly token: Instantiable<any>
protected readonly token: Instantiable<T>,
) {
super(token)
}
@@ -40,13 +40,15 @@ export class Factory extends AbstractFactory {
return new this.token(...dependencies, ...parameters)
}
match(something: any) {
match(something: unknown): boolean {
return something === this.token // || (something?.name && something.name === this.token.name)
}
getDependencyKeys(): Collection<DependencyRequirement> {
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.token)
if ( meta ) return meta
if ( meta ) {
return meta
}
return new Collection<DependencyRequirement>()
}
@@ -56,7 +58,9 @@ export class Factory extends AbstractFactory {
do {
const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken)
if ( loadedMeta ) meta.concat(loadedMeta)
if ( loadedMeta ) {
meta.concat(loadedMeta)
}
currentToken = Object.getPrototypeOf(currentToken)
} while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype)

View File

@@ -1,12 +1,12 @@
import {Factory} from "./Factory";
import {Instantiable} from "../types";
import {Factory} from './Factory'
import {Instantiable} from '../types'
/**
* Container factory that produces an instance of the token, however the token
* is identified by a string name rather than a class reference.
* @extends Factory
*/
export default class NamedFactory extends Factory {
export default class NamedFactory<T> extends Factory<T> {
constructor(
/**
* The name identifying this factory in the container.
@@ -18,12 +18,12 @@ export default class NamedFactory extends Factory {
* The token to be instantiated.
* @type {Instantiable}
*/
protected token: Instantiable<any>,
protected token: Instantiable<T>,
) {
super(token)
}
match(something: any) {
match(something: unknown): boolean {
return something === this.name
}
}

View File

@@ -1,6 +1,6 @@
import { Factory } from './Factory'
import { Collection } from '../../util'
import {DependencyRequirement, PropertyDependency} from "../types";
import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
/**
* Container factory which returns its token as its value, without attempting
@@ -19,29 +19,23 @@ import {DependencyRequirement, PropertyDependency} from "../types";
*
* @extends Factory
*/
export default class SingletonFactory extends Factory {
export default class SingletonFactory<T> extends Factory<T> {
constructor(
/**
* Instantiated value of this factory.
* @type FunctionConstructor
* Token identifying this singleton.
*/
protected token: FunctionConstructor,
protected token: DependencyKey,
/**
* String name of this singleton identifying it in the container.
* @type string
* The value of this singleton.
*/
protected key: string,
protected value: T,
) {
super(token)
}
produce(dependencies: any[], parameters: any[]) {
return this.token
}
match(something: any) {
return something === this.key
produce(): T {
return this.value
}
getDependencyKeys(): Collection<DependencyRequirement> {

View File

@@ -1,6 +1,6 @@
export const DEPENDENCY_KEYS_METADATA_KEY = 'extollo:di:dependencies:ctor';
export const DEPENDENCY_KEYS_PROPERTY_METADATA_KEY = 'extollo:di:dependencies:properties';
export const DEPENDENCY_KEYS_SERVICE_TYPE_KEY = 'extollo:di:service_type';
export const DEPENDENCY_KEYS_METADATA_KEY = 'extollo:di:dependencies:ctor'
export const DEPENDENCY_KEYS_PROPERTY_METADATA_KEY = 'extollo:di:dependencies:properties'
export const DEPENDENCY_KEYS_SERVICE_TYPE_KEY = 'extollo:di:service_type'
/**
* Interface that designates a particular value as able to be constructed.
@@ -13,20 +13,26 @@ export interface Instantiable<T> {
* Returns true if the given value is instantiable.
* @param what
*/
export function isInstantiable<T>(what: any): what is Instantiable<T> {
return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function'
export function isInstantiable<T>(what: unknown): what is Instantiable<T> {
return (
Boolean(what)
&& (typeof what === 'object' || typeof what === 'function')
&& (what !== null)
&& 'constructor' in what
&& typeof what.constructor === 'function'
)
}
/**
* Type that identifies a value as a static class, even if it is not instantiable.
*/
export type StaticClass<T, T2> = Function & {prototype: T} & T2
export type StaticClass<T, T2> = Function & {prototype: T} & T2 // eslint-disable-line @typescript-eslint/ban-types
/**
* Returns true if the parameter is a static class.
* @param something
*/
export function isStaticClass<T, T2>(something: any): something is StaticClass<T, T2> {
export function isStaticClass<T, T2>(something: unknown): something is StaticClass<T, T2> {
return typeof something === 'function' && typeof something.prototype !== 'undefined'
}