@knowark/injectarkjs
Version:
Dependency Injector for Javascript
437 lines (351 loc) • 10.5 kB
JavaScript
import { Injectark } from './injectark.js'
import { Factory } from './factory.js'
class A { }
class B { }
class C {
/** @param {A} a @param {B} b */
constructor (a, b) {
this.a = a
this.b = b
}
}
class D {
/** @param {B} b @param {C} c */
constructor (b, c) {
this.b = b
this.c = c
}
}
class StandardFactory extends Factory {
constructor (config) {
super(config)
this._standardC.dependencies = ['A', 'B']
this._standardD.dependencies = ['B', 'C']
}
extract (method) {
return this[`_${method}`]
}
_standardA () {
return new A()
}
_standardB () {
return new B()
}
_standardC (a, b) {
if (!(a && b)) throw new Error('Supply dependencies.')
return new C(a, b)
}
_standardD (b, c) {
return new D(b, c)
}
}
class DefaultFactory extends Factory {
lazy = ['dataService']
constructor (config) {
super(config)
this.c.dependencies = ['A', 'B']
}
a () {
return new A()
}
b () {
return new B()
}
c (a, b) {
if (!(a && b)) throw new Error('Supply dependencies.')
return new C(a, b)
}
d (b, c) {
return new D(b, c)
}
dataService () {
throw new Error('Not implemented service.')
}
}
const standardStrategy = {
A: {
method: 'standardA'
},
B: {
method: 'standardB'
},
C: {
method: 'standardC'
},
D: {
method: 'standardD'
}
}
describe('Injectark default', () => {
let injector = null
beforeEach(() => {
injector = new Injectark()
})
it('can be instantiated', () => {
expect(injector).toBeTruthy()
})
it('has a null parent', () => {
expect(injector.parent).toBeNull()
})
it('has an empty registry', () => {
expect(injector.registry).toEqual({})
})
it('has an empty strategy object', () => {
expect(injector.strategy).toEqual({})
})
})
describe('Injectark params', () => {
const parent = new Injectark()
const factory = new StandardFactory()
const strategy = standardStrategy
let injector = null
beforeEach(() => {
injector = new Injectark({
strategy,
factory,
parent
})
})
it('has the given objects as attributes', () => {
expect(injector.parent).toBe(parent)
expect(injector.strategy).toBe(strategy)
expect(injector.factory).toBe(factory)
})
describe('Injectark resolve', () => {
it('resolves a resource with no dependencies', () => {
let instance = injector.resolve('A')
expect(instance).toEqual(expect.any(A))
instance = injector.resolve('B')
expect(instance).toEqual(expect.any(B))
expect(Object.keys(injector.registry).length).toEqual(4)
})
it('serves an already instantiated resource from its registry', () => {
const instance = injector.resolve('A')
expect(instance).toEqual(expect.any(A))
const registryInstance = injector.resolve('A')
expect(registryInstance).toBe(instance)
expect(Object.keys(injector.registry).length).toEqual(4)
})
it("generates and injects a resource's dependencies on resolve", () => {
const instance = injector.resolve('C')
expect(instance).toEqual(expect.any(C))
expect(instance.a).toEqual(expect.any(A))
expect(instance.b).toEqual(expect.any(B))
expect(Object.keys(injector.registry).length).toEqual(4)
expect(injector.resolve('A')).toBe(instance.a)
expect(injector.resolve('B')).toBe(instance.b)
})
it('serves a resource given its constructor class', () => {
const instance = injector.resolve(C)
expect(instance).toEqual(expect.any(C))
expect(instance.a).toEqual(expect.any(A))
expect(instance.b).toEqual(expect.any(B))
expect(Object.keys(injector.registry).length).toEqual(4)
expect(injector.resolve('A')).toBe(instance.a)
expect(injector.resolve('B')).toBe(instance.b)
})
it("doesn't persist ephemeral dependencies on the registry", () => {
const strategy = {
A: {
method: 'standardA',
ephemeral: true
}
}
injector = new Injectark({
strategy,
factory,
parent
})
const instance = injector.resolve('A')
expect(instance).toEqual(expect.any(A))
expect(Object.keys(injector.registry).length).toEqual(0)
})
it('preemptively instantiates all its dependencies', () => {
expect(Object.keys(injector.registry).length).toEqual(4)
})
})
})
describe('Injectark forge', () => {
class X {
}
class Y {
constructor (x) {
this.x = x
}
}
class CoreFactory extends Factory {
constructor (config) {
super(config)
this._coreY.dependencies = ['X']
}
extract (method) {
return this[`_${method}`]
}
_coreX () {
return new X()
}
_coreY (x) {
return new Y(x)
}
}
const coreStrategy = {
X: {
method: 'coreX',
second: 'yes'
},
Y: {
method: 'coreY'
}
}
let parent = null
let injector = null
let factory = null
let strategy = null
describe('Injectark hierarchical resolve', () => {
beforeEach(() => {
parent = new Injectark({
strategy: coreStrategy,
factory: new CoreFactory()
})
parent.registry.X = new X()
factory = new StandardFactory()
strategy = Object.assign({}, standardStrategy)
injector = parent.forge({
strategy,
factory
})
})
it('forges a new injector from a parent one', () => {
expect(injector.parent).toBe(parent)
expect(injector.strategy).toBe(strategy)
expect(injector.factory).toBe(factory)
})
it('resolves a resource owned by its parent registry', () => {
const instance = injector.resolve('X')
expect(instance).toEqual(expect.any(X))
expect(instance).toBe(parent.registry.X)
expect(Object.keys(parent.registry).length).toEqual(2)
expect(Object.keys(injector.registry).length).toEqual(4)
})
it('resolves a resource its parent knows how to build', () => {
const instance = injector.resolve('Y')
expect(instance).toEqual(injector.parent.registry.Y)
expect(Object.keys(injector.parent.registry).length).toEqual(2)
expect(Object.keys(injector.registry).length).toEqual(4)
})
it('returns a unique resource if "unique" is true', () => {
injector.strategy.Y = {
method: 'coreY',
unique: true
}
const instance = injector._registryFetch('Y')
expect(instance).toBe(false)
})
it('might forge a subinjector without arguments', () => {
const subInjector = injector.forge()
expect(subInjector.parent).toBe(injector)
expect(subInjector.strategy).toEqual({})
expect(subInjector.factory).toBe(null)
})
it('resolves from its own registry if there is no parent', () => {
injector.parent = null
const instance = injector.resolve('Y')
expect(instance).toEqual(undefined)
})
})
})
describe('Injectark optional strategy', () => {
const parent = new Injectark({ factory: new Factory() })
const config = { key: 'value' }
const factory = new DefaultFactory(config)
let injector = null
beforeEach(() => {
injector = new Injectark({
factory,
parent
})
})
it('has a strategy that defaults to an empty object', () => {
expect(injector.parent).toBe(parent)
expect(injector.factory).toBe(factory)
expect(injector.strategy).toEqual({})
})
it('allows setting a custom resource instance given its name', () => {
class CustomA {}
const originalA = injector.resolve('A')
expect(originalA instanceof A).toBeTruthy()
injector.set('A', new CustomA())
const customA = injector.resolve('A')
expect(customA instanceof CustomA).toBeTruthy()
})
it('allows setting a custom resource instance given its class', () => {
class CustomA {}
const originalA = injector.resolve(A)
expect(originalA instanceof A).toBeTruthy()
injector.set(A, new CustomA())
const customA = injector.resolve(A)
expect(customA instanceof CustomA).toBeTruthy()
})
it('allows to manually replace a dependency using "set" method', () => {
expect(injector.config).toBe(injector.factory.config)
})
it('returns null on unresolved resources both locally and its parent', () => {
expect(injector.resolve('MISSING')).toBe(undefined)
})
describe('Injectark default factory resolvers', () => {
it('resolves a resource by its camelCase name by default', () => {
let instance = injector.resolve('A')
expect(instance).toEqual(expect.any(A))
instance = injector.resolve('B')
expect(instance).toEqual(expect.any(B))
instance = injector.resolve('C')
expect(instance).toEqual(expect.any(C))
instance = injector.resolve('D')
expect(instance).toEqual(expect.any(D))
expect(Object.keys(injector.registry).length).toEqual(4)
})
it('resolves TitleCase to camelCase by default', () => {
try {
injector.resolve('DataService')
} catch (error) {
expect(error.message).toEqual('Not implemented service.')
}
})
})
})
describe('Injectark strict and restricted access', () => {
const config = { key: 'value' }
const factory = new DefaultFactory(config)
let injector = null
beforeEach(() => {
injector = new Injectark({
factory
})
})
it('resolves in strict mode raising an error on missings', () => {
expect(() => injector.resolve('X', true)).toThrow(
'The "X" resource could not be resolved.')
})
it('restricts access on getting non-public factory resources', () => {
factory.allowed = ['C', 'D']
expect(() => injector.get('A')).toThrow(
'Direct access to "A" is not allowed.')
expect(() => injector.get('B')).toThrow(
'Direct access to "B" is not allowed.')
let instance = injector.get(C)
expect(instance instanceof C).toBeTruthy()
instance = injector.get(D)
expect(instance instanceof D).toBeTruthy()
})
it('restricts access based on a regular expression', () => {
factory.allowed = ['[CD]']
expect(() => injector.get('A')).toThrow(
'Direct access to "A" is not allowed.')
expect(() => injector.get('B')).toThrow(
'Direct access to "B" is not allowed.')
let instance = injector.get(C)
expect(instance instanceof C).toBeTruthy()
instance = injector.get(D)
expect(instance instanceof D).toBeTruthy()
})
})