diff --git a/packages/injectable/src/dependency-injection-container/createContainer.js b/packages/injectable/src/dependency-injection-container/createContainer.js index dd783a2a..3ea0ea70 100644 --- a/packages/injectable/src/dependency-injection-container/createContainer.js +++ b/packages/injectable/src/dependency-injection-container/createContainer.js @@ -1,10 +1,12 @@ import tap from 'lodash/fp/tap'; import conforms from 'lodash/fp/conforms'; +import every from 'lodash/fp/every'; import filter from 'lodash/fp/filter'; import find from 'lodash/fp/find'; import findLast from 'lodash/fp/findLast'; import first from 'lodash/fp/first'; import forEach from 'lodash/fp/forEach'; +import get from 'lodash/fp/get'; import includes from 'lodash/fp/includes'; import invoke from 'lodash/fp/invoke'; import lifecycleEnum from './lifecycleEnum'; @@ -20,10 +22,11 @@ export default (...listOfGetRequireContexts) => { const instanceMap = new Map(); - const getLifecycle = alias => + const getLifecycle = injectableKey => getInjectable({ injectables, - alias, + alias: injectableKey, + di: privateDi, }).lifecycle; const privateDi = { @@ -31,6 +34,7 @@ export default (...listOfGetRequireContexts) => { const originalInjectable = getInjectable({ injectables, alias, + di: privateDi, }); const overriddenInjectable = getOverridingInjectable({ @@ -158,7 +162,7 @@ export default (...listOfGetRequireContexts) => { }, permitSideEffects: alias => { - getInjectable({ injectables, alias }).permitSideEffects(); + getInjectable({ injectables, alias, di: privateDi }).permitSideEffects(); }, getLifecycle, @@ -166,6 +170,7 @@ export default (...listOfGetRequireContexts) => { purge: injectableKey => { const injectable = getInjectable({ injectables, + di: privateDi, alias: injectableKey, }); @@ -207,7 +212,7 @@ const autoRegisterInjectables = ({ getRequireContextForInjectables, di }) => { ); }; -const getInjectable = ({ injectables, alias }) => { +const getInjectable = ({ injectables, alias, di }) => { const relatedInjectables = pipeline( injectables, filter(getRelatedInjectables(alias)), @@ -219,14 +224,47 @@ const getInjectable = ({ injectables, alias }) => { ); } - if (relatedInjectables.length > 1) { + if (relatedInjectables.length > 1 && !viabilityIsOk(relatedInjectables)) { throw new Error( - `Tried to inject injectable with ambiguous alias: "${alias.toString()}".`, + `Tried to inject one of multiple injectables with no way to demonstrate viability for "${relatedInjectables + .map(get('id')) + .join('", "')}"`, ); } - return first(relatedInjectables); + const viableInjectables = pipeline( + relatedInjectables, + filter(injectable => + injectable.viability ? injectable.viability(di) : true, + ), + ); + + if (relatedInjectables.length === 1 && viableInjectables.length === 0) { + throw new Error( + `Tried to inject injectable with no viability for "${relatedInjectables[0].id}"`, + ); + } + + if (viableInjectables.length === 0) { + throw new Error( + `Tried to inject one of multiple injectables with no viability within "${relatedInjectables + .map(get('id')) + .join('", "')}"`, + ); + } + + if (viableInjectables.length !== 1) { + throw new Error( + `Tried to inject one of multiple injectables with non-singular viability within "${relatedInjectables + .map(get('id')) + .join('", "')}"`, + ); + } + + return first(viableInjectables); }; +const viabilityIsOk = every('viability'); + const getOverridingInjectable = ({ overridingInjectables, alias }) => pipeline(overridingInjectables, findLast(getRelatedInjectables(alias))); diff --git a/packages/injectable/src/dependency-injection-container/createContainer.test.js b/packages/injectable/src/dependency-injection-container/createContainer.test.js index ae0860b6..a2f4bae5 100644 --- a/packages/injectable/src/dependency-injection-container/createContainer.test.js +++ b/packages/injectable/src/dependency-injection-container/createContainer.test.js @@ -680,23 +680,161 @@ describe('createContainer', () => { ); }); - it('when injecting ambiguous injectable, throws', () => { + it('given multiple injectables with same alias, but no way to demonstrate viability, when injected, throws', () => { const someInjectable = getInjectable({ - id: 'some-ambiguous-injectable-id', + id: 'some-injectable-id', + aliases: ['some-alias'], + viability: undefined, instantiate: () => 'irrelevant', }); const someOtherInjectable = getInjectable({ - id: 'some-ambiguous-injectable-id', + id: 'some-other-injectable-id', + aliases: ['some-alias'], + viability: () => {}, instantiate: () => 'irrelevant', }); const di = getDi(someInjectable, someOtherInjectable); expect(() => { - di.inject('some-ambiguous-injectable-id'); + di.inject('some-alias'); + }).toThrow( + 'Tried to inject one of multiple injectables with no way to demonstrate viability for "some-injectable-id", "some-other-injectable-id"', + ); + }); + + it('given multiple injectables with same alias, one of which is viable, when injected, injects viable instance', () => { + const someInjectable = getInjectable({ + aliases: ['some-alias'], + viability: () => false, + instantiate: () => 'irrelevant', + }); + + const someOtherInjectable = getInjectable({ + aliases: ['some-alias'], + viability: () => true, + instantiate: () => 'viable-instance', + }); + + const di = getDi(someInjectable, someOtherInjectable); + + const actual = di.inject('some-alias'); + + expect(actual).toBe('viable-instance'); + }); + + it('given multiple injectables with same alias, one of which is viable, given overridden, when injected, injects overridden instance', () => { + const someInjectable = getInjectable({ + aliases: ['some-alias'], + viability: () => false, + instantiate: () => 'irrelevant', + }); + + const someOtherInjectable = getInjectable({ + aliases: ['some-alias'], + viability: () => true, + instantiate: () => 'irrelevant', + }); + + const di = getDi(someInjectable, someOtherInjectable); + + di.override('some-alias', () => 'overridden-instance'); + + const actual = di.inject('some-alias'); + + expect(actual).toBe('overridden-instance'); + }); + + it('given multiple injectables with same alias, one of which is viable by considering a third injectable, injects viable instance', () => { + const someThirdInjectable = getInjectable({ + aliases: ['third-injectable-alias'], + instantiate: () => 'third-injectable-instance', + }); + + const someInjectable = getInjectable({ + aliases: ['some-alias'], + viability: di => + di.inject('third-injectable-alias') !== 'third-injectable-instance', + instantiate: () => 'irrelevant', + }); + + const someOtherInjectable = getInjectable({ + aliases: ['some-alias'], + viability: di => + di.inject('third-injectable-alias') === 'third-injectable-instance', + instantiate: () => 'viable-instance', + }); + + const di = getDi(someInjectable, someOtherInjectable, someThirdInjectable); + + const actual = di.inject('some-alias'); + + expect(actual).toBe('viable-instance'); + }); + + it('given multiple injectables with same alias, all of which are unviable, when injected, throws', () => { + const someInjectable = getInjectable({ + id: 'some-injectable-id', + aliases: ['some-alias'], + viability: () => false, + instantiate: () => 'irrelevant', + }); + + const someOtherInjectable = getInjectable({ + id: 'some-other-injectable-id', + aliases: ['some-alias'], + viability: () => false, + instantiate: () => 'irrelevant', + }); + + const di = getDi(someInjectable, someOtherInjectable); + + expect(() => { + di.inject('some-alias'); + }).toThrow( + 'Tried to inject one of multiple injectables with no viability within "some-injectable-id", "some-other-injectable-id"', + ); + }); + + it('given multiple injectables with same alias, all of which are viable, when injected, throws', () => { + const someInjectable = getInjectable({ + id: 'some-injectable-id', + aliases: ['some-alias'], + viability: () => true, + instantiate: () => 'irrelevant', + }); + + const someOtherInjectable = getInjectable({ + id: 'some-other-injectable-id', + aliases: ['some-alias'], + viability: () => true, + instantiate: () => 'irrelevant', + }); + + const di = getDi(someInjectable, someOtherInjectable); + + expect(() => { + di.inject('some-alias'); + }).toThrow( + 'Tried to inject one of multiple injectables with non-singular viability within "some-injectable-id", "some-other-injectable-id"', + ); + }); + + it('given single injectable, but unviable, when injected, throws', () => { + const someInjectable = getInjectable({ + id: 'some-injectable-id', + aliases: ['some-alias'], + viability: () => false, + instantiate: () => 'irrelevant', + }); + + const di = getDi(someInjectable); + + expect(() => { + di.inject('some-alias'); }).toThrow( - `Tried to inject injectable with ambiguous alias: "some-ambiguous-injectable-id"`, + 'Tried to inject injectable with no viability for "some-injectable-id"', ); });