Skip to content

Commit

Permalink
Revert "Revert "Implement di.injectMany() as way to inject multiple r…
Browse files Browse the repository at this point in the history
…egistered injectables for an injection token""

This reverts commit 0dd2251.
  • Loading branch information
Iku-turso committed Jan 21, 2022
1 parent fc67c38 commit f5d0e01
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 56 deletions.
7 changes: 7 additions & 0 deletions packages/injectable/ogre-tools-injectable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ declare module '@ogre-tools/injectable' {
>
): TInjectionToken['template'];

injectMany<TInjectionToken extends InjectionToken<unknown, unknown>>(
injectionToken: TInjectionToken,
...instantiationParameter: TentativeTuple<
TInjectionToken['instantiationParameter']
>
): TInjectionToken['template'][];

purge: (injectableKey: Injectable<any, any, any>) => void;

runSetups: () => Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import tap from 'lodash/fp/tap';
import conforms from 'lodash/fp/conforms';
import filter from 'lodash/fp/filter';
import find from 'lodash/fp/find';
import findLast from 'lodash/fp/findLast';
Expand All @@ -21,14 +20,14 @@ export default (...listOfGetRequireContexts) => {
const instanceMap = new Map();

const getLifecycle = alias =>
getInjectable({
getRelatedInjectable({
injectables,
alias,
}).lifecycle;

const privateDi = {
inject: (alias, instantiationParameter, context = []) => {
const originalInjectable = getInjectable({
const originalInjectable = getRelatedInjectable({
injectables,
alias,
});
Expand Down Expand Up @@ -70,6 +69,15 @@ export default (...listOfGetRequireContexts) => {
});
},

injectMany: (alias, instantiationParameter, context = []) =>
pipeline(
getRelatedInjectables({ injectables, alias }),

map(injectable =>
privateDi.inject(injectable, instantiationParameter, context),
),
),

register: injectable => {
if (!injectable.module) {
throw new Error('Tried to register injectable without module.');
Expand All @@ -78,12 +86,6 @@ export default (...listOfGetRequireContexts) => {
injectables.push({
...injectable,

aliases: [
injectable,
...(injectable.injectionToken ? [injectable.injectionToken] : []),
...(injectable.aliases || []),
],

lifecycle: injectable.lifecycle || lifecycleEnum.singleton,

permitSideEffects: function () {
Expand All @@ -95,12 +97,12 @@ export default (...listOfGetRequireContexts) => {
override: (alias, instantiateStub) => {
const originalInjectable = pipeline(
injectables,
find(getRelatedInjectables(alias)),
find(isRelatedTo(alias)),
);

if (!originalInjectable) {
throw new Error(
`Tried to override "${alias.toString()}" which is not registered.`,
`Tried to override "${alias.module.filename}" which is not registered.`,
);
}

Expand All @@ -114,7 +116,7 @@ export default (...listOfGetRequireContexts) => {
unoverride: alias => {
overridingInjectables = pipeline(
overridingInjectables,
reject(getRelatedInjectables(alias)),
reject(isRelatedTo(alias)),
);
},

Expand Down Expand Up @@ -158,18 +160,18 @@ export default (...listOfGetRequireContexts) => {
},

permitSideEffects: alias => {
getInjectable({ injectables, alias }).permitSideEffects();
getRelatedInjectable({ injectables, alias }).permitSideEffects();
},

getLifecycle,

purge: injectableKey => {
const injectable = getInjectable({
purge: alias => {
const injectable = getRelatedInjectable({
injectables,
alias: injectableKey,
alias,
});

getLifecycle(injectableKey).purge({ injectable, instanceMap });
getLifecycle(alias).purge({ injectable, instanceMap });
},
};

Expand All @@ -180,44 +182,37 @@ export default (...listOfGetRequireContexts) => {
const publicDi = {
...privateDi,
inject: (alias, parameter) => privateDi.inject(alias, parameter),
injectMany: (alias, parameter) => privateDi.injectMany(alias, parameter),
};

return publicDi;
};

const getRelatedInjectables = alias => conforms({ aliases: includes(alias) });

const autoRegisterInjectables = ({ getRequireContextForInjectables, di }) => {
const requireContextForInjectables = getRequireContextForInjectables();

pipeline(
requireContextForInjectables,
invoke('keys'),
map(key => {
const injectableExport = requireContextForInjectables(key).default;

return {
id: key,
...injectableExport,
aliases: [injectableExport],
};
}),

forEach(injectable => di.register(injectable)),
map(requireContextForInjectables),
map('default'),
forEach(di.register),
);
};

const getInjectable = ({ injectables, alias }) => {
const relatedInjectables = pipeline(
injectables,
filter(getRelatedInjectables(alias)),
);
const isRelatedTo = alias => injectable =>
injectable.module === alias.module ||
(injectable.injectionToken && injectable.injectionToken === alias);

const getRelatedInjectable = ({ injectables, alias }) => {
const relatedInjectables = getRelatedInjectables({ injectables, alias });

if (relatedInjectables.length === 0) {
throw new Error(
`Tried to inject non-registered injectable "${alias.module.filename}".`,
);
}

if (relatedInjectables.length > 1) {
throw new Error(
`Tried to inject single injectable for injection token "${
Expand All @@ -231,5 +226,8 @@ const getInjectable = ({ injectables, alias }) => {
return first(relatedInjectables);
};

const getRelatedInjectables = ({ injectables, alias }) =>
pipeline(injectables, filter(isRelatedTo(alias)));

const getOverridingInjectable = ({ overridingInjectables, alias }) =>
pipeline(overridingInjectables, findLast(getRelatedInjectables(alias)));
pipeline(overridingInjectables, findLast(isRelatedTo(alias)));
Original file line number Diff line number Diff line change
Expand Up @@ -365,22 +365,14 @@ describe('createContainer', () => {
it('when overriding non-registered injectable, throws', () => {
const di = getDi();

expect(() => {
di.override('some-non-existing-injectable', () => 'irrelevant');
}).toThrow(
'Tried to override "some-non-existing-injectable" which is not registered.',
);
});

it('when overriding non-registered injectable using a symbol, throws', () => {
const di = getDi();

const symbol = Symbol('some-symbol');
const injectable = getInjectable({
module: { filename: 'some-non-registered-injectable-filename' },
});

expect(() => {
di.override(symbol, () => 'irrelevant');
di.override(injectable, () => 'irrelevant');
}).toThrow(
'Tried to override "Symbol(some-symbol)" which is not registered.',
'Tried to override "some-non-registered-injectable-filename" which is not registered.',
);
});

Expand Down Expand Up @@ -461,19 +453,15 @@ describe('createContainer', () => {
let instanceFromSetup;

const someSetuppable = getInjectable({
module: { filename: 'irrelevant' },
module: { filename: 'some-setuppable-filename' },

setup: di => {
instanceFromSetup = di.inject(
someInjectable,
'some-parameter',
'irrelevant',
);
instanceFromSetup = di.inject(someInjectable, 'some-parameter');
},
});

const someInjectable = getInjectable({
module: { filename: 'irrelevant' },
module: { filename: 'some-injectable-filename' },
lifecycle: lifecycleEnum.transient,
instantiate: (di, parameter) => `some-instance: "${parameter}"`,
});
Expand Down Expand Up @@ -635,6 +623,141 @@ describe('createContainer', () => {
);
});

it('given multiple sync injectables with shared injection token, when injecting many using the token, injects all injectables with the shared token', () => {
const someSharedInjectionToken = getInjectionToken({
module: { filename: 'some-injection-token-filename' },
});

const someInjectable = getInjectable({
module: { filename: 'some-injectable-filename' },
injectionToken: someSharedInjectionToken,
instantiate: () => 'some-instance',
});

const someOtherInjectable = getInjectable({
module: { filename: 'some-other-injectable-filename' },
injectionToken: someSharedInjectionToken,
instantiate: () => 'some-other-instance',
});

const someUnrelatedInjectable = getInjectable({
module: { filename: 'some-unrelated-injectable-filename' },
instantiate: () => 'some-other-instance',
});

const di = getDi(
someInjectable,
someOtherInjectable,
someUnrelatedInjectable,
);

const actual = di.injectMany(someSharedInjectionToken);

expect(actual).toEqual(['some-instance', 'some-other-instance']);
});

it('given multiple sync and async injectables with shared injection token, when injecting many using the token, injects all injectables with the shared token', async () => {
const someSharedInjectionToken = getInjectionToken({
module: { filename: 'some-injection-token-filename' },
});

const someSyncInjectable = getInjectable({
module: { filename: 'some-injectable-filename' },
injectionToken: someSharedInjectionToken,
instantiate: () => 'some-instance',
});

const someAsyncInjectable = getInjectable({
module: { filename: 'some-other-injectable-filename' },
injectionToken: someSharedInjectionToken,
instantiate: async () => 'some-other-instance',
});

const someUnrelatedInjectable = getInjectable({
module: { filename: 'some-other-injectable-filename' },
instantiate: () => 'some-other-instance',
});

const di = getDi(
someSyncInjectable,
someAsyncInjectable,
someUnrelatedInjectable,
);

const actual = await di.injectMany(someSharedInjectionToken);

expect(actual).toEqual(['some-instance', 'some-other-instance']);
});

it('given multiple transient injectables, when injecting many with an instantiation parameter, injects the injectables using the instantiation parameter', () => {
const someSharedInjectionToken = getInjectionToken({
module: { filename: 'some-injection-token-filename' },
});

const someInjectable = getInjectable({
module: { filename: 'some-injectable-filename' },
injectionToken: someSharedInjectionToken,
lifecycle: lifecycleEnum.transient,

instantiate: (di, instantiationParameter) =>
`some-instance: "${instantiationParameter}"`,
});

const di = getDi(someInjectable);

const actual = di.injectMany(
someSharedInjectionToken,
'some-instantiation-parameter',
);

expect(actual).toEqual(['some-instance: "some-instantiation-parameter"']);
});

it('given no injectables, when injecting many, injects no instances', async () => {
const someSharedInjectionToken = getInjectionToken({
module: { filename: 'some-injection-token-filename' },
});

const di = getDi();

const actual = await di.injectMany(
someSharedInjectionToken,
'some-instantiation-parameter',
);

expect(actual).toEqual([]);
});

it('given injectables with a dependency cycle, when injecting many, throws', () => {
const someInjectionToken = getInjectionToken({
module: { filename: 'some-injection-token-filename' },
});

const someOtherInjectionToken = getInjectionToken({
module: { filename: 'some-injection-token-filename' },
});

const childInjectable = getInjectable({
module: { filename: 'some-child-injectable' },
injectionToken: someOtherInjectionToken,
instantiate: di => di.injectMany(parentInjectable),
});

const parentInjectable = getInjectable({
module: { filename: 'some-parent-injectable' },
injectionToken: someInjectionToken,
instantiate: di => di.injectMany(childInjectable),
});

const di = getDi(parentInjectable, childInjectable);

expect(() => {
di.injectMany(parentInjectable, undefined, ['some-bogus-context']);
}).toThrow(
'Cycle of injectables encountered: "some-parent-injectable" -> "some-child-injectable" -> "some-parent-injectable"',
);
});

it('given in side effects are not prevented, when injecting injectable which causes side effects, does not throw', () => {
const someInjectable = getInjectable({
module: { filename: 'irrelevant' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const getInstance = ({

const minimalDi = {
inject: (alias, parameter) => di.inject(alias, parameter, newContext),

injectMany: (alias, parameter) =>
di.injectMany(alias, parameter, newContext),
};

return injectable.instantiate(
Expand Down

0 comments on commit f5d0e01

Please sign in to comment.