Skip to content

Commit

Permalink
feat: Introduce early-override to permit override of injectable befor…
Browse files Browse the repository at this point in the history
…e registered
  • Loading branch information
Iku-turso committed Oct 17, 2023
1 parent c3ed69a commit 6e7f5ea
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 40 deletions.
28 changes: 14 additions & 14 deletions packages/injectable/core/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
/// <reference types="jest" />

export type Override = <
InjectionInstance extends InjectionTokenInstance,
InjectionTokenInstance,
InstantiationParam,
>(
injectable:
| InjectionToken<InjectionInstance, InstantiationParam>
| Injectable<InjectionInstance, InjectionTokenInstance, InstantiationParam>,
instantiateStub: Instantiate<InjectionInstance, InstantiationParam>,
) => void;

export interface DiContainer extends DiContainerForInjection {
purge: (injectableKey: Injectable<any, any, any>) => void;

permitSideEffects: (
injectableKey: InjectionToken<any, any> | Injectable<any, any, any>,
) => void;

override<
InjectionInstance extends InjectionTokenInstance,
InjectionTokenInstance,
InstantiationParam,
>(
injectable:
| InjectionToken<InjectionInstance, InstantiationParam>
| Injectable<
InjectionInstance,
InjectionTokenInstance,
InstantiationParam
>,
instantiateStub: Instantiate<InjectionInstance, InstantiationParam>,
): void;
override: Override;
earlyOverride: Override;

unoverride(alias: InjectionToken<any, any> | Injectable<any, any, any>): void;

Expand Down
12 changes: 12 additions & 0 deletions packages/injectable/core/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,18 @@ expectError(
})),
);

// given injectable, when early-overriding with matching instantiate, typing is OK
expectType<void>(
di.earlyOverride(someInjectableForOverrides, () => ({ someProperty: 84 })),
);

// given injectable, when early-overriding with not matching instantiate, typing is not OK
expectError(
di.earlyOverride(someInjectableForOverrides, () => ({
someProperty: 'some-not-number',
})),
);

// given injectable, when overriding with a more specific matching instantiate, typing is OK
expectType<void>(
di.override(someInjectableForOverrides, () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { setDependeeFor } from './setDependeeFor';
import { checkForSideEffectsFor } from './checkForSideEffectsFor';
import { getRelatedInjectablesFor } from './getRelatedInjectablesFor';
import { noop } from 'lodash/fp';
import { earlyOverrideFor } from './early-override';

export default (containerId, { detectCycles = true } = {}) => {
const injectableSet = new Set();
Expand Down Expand Up @@ -160,12 +161,17 @@ export default (containerId, { detectCycles = true } = {}) => {
injectMany: nonDecoratedPrivateInjectMany,
});

const override = overrideFor({
const earlyOverride = earlyOverrideFor({
getRelatedInjectables,
alreadyInjected,
overridingInjectables,
});

const override = overrideFor({
getRelatedInjectables,
earlyOverride,
});

const unoverride = unoverrideFor({
overridingInjectables,
getRelatedInjectables,
Expand All @@ -187,6 +193,7 @@ export default (containerId, { detectCycles = true } = {}) => {
decorate,
decorateFunction,
override,
earlyOverride,
unoverride,

reset: () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import isInjectionToken from '../getInjectionToken/isInjectionToken';

export const earlyOverrideFor =
({ getRelatedInjectables, alreadyInjected, overridingInjectables }) =>
(alias, instantiateStub) => {
const relatedInjectables = getRelatedInjectables(alias);

if (relatedInjectables.length > 1) {
throw new Error(
`Tried to override single implementation of injection token "${
alias.id
}", but found multiple registered implementations: "${relatedInjectables
.map(x => x.id)
.join('", "')}".`,
);
}

if (relatedInjectables.length === 0 && isInjectionToken(alias)) {
throw new Error(
`Tried to early-override an injection token "${alias.id}", but that is currently not supported.`,
);
}

if (alreadyInjected.has(alias)) {
throw new Error(
`Tried to override injectable "${alias.id}", but it was already injected.`,
);
}

const originalInjectable = relatedInjectables[0] || alias;

overridingInjectables.set(originalInjectable, {
...originalInjectable,
overriddenInjectable: originalInjectable,
causesSideEffects: false,
instantiate: instantiateStub,
});
};
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { earlyOverrideFor } from './early-override';

export const overrideFor =
({ getRelatedInjectables, alreadyInjected, overridingInjectables }) =>
({ getRelatedInjectables, earlyOverride }) =>
(alias, instantiateStub) => {
const relatedInjectables = getRelatedInjectables(alias);

if (relatedInjectables.length > 1) {
throw new Error(
`Tried to override single implementation of injection token "${
alias.id
}", but found multiple registered implementations: "${relatedInjectables
.map(x => x.id)
.join('", "')}".`,
);
}

if (relatedInjectables.length === 0) {
if (alias.aliasType === 'injection-token') {
throw new Error(
Expand All @@ -25,20 +17,7 @@ export const overrideFor =
);
}

if (alreadyInjected.has(alias)) {
throw new Error(
`Tried to override injectable "${alias.id}", but it was already injected.`,
);
}

const originalInjectable = relatedInjectables[0];

overridingInjectables.set(originalInjectable, {
...originalInjectable,
overriddenInjectable: originalInjectable,
causesSideEffects: false,
instantiate: instantiateStub,
});
earlyOverride(alias, instantiateStub);
};

export const unoverrideFor =
Expand Down
107 changes: 107 additions & 0 deletions packages/injectable/core/src/scenarios/early-override.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import getInjectable from '../getInjectable/getInjectable';
import createContainer from '../dependency-injection-container/createContainer';
import getInjectionToken from '../getInjectionToken/getInjectionToken';

describe('early-override', () => {
describe('given a still non-registered injectable', () => {
let di;
let someInjectable;
let someInjectionToken;

beforeEach(() => {
di = createContainer('irrelevant');

someInjectionToken = getInjectionToken({ id: 'some-token-id' });
});

it('when early-overriding an injection token, throws', () => {
expect(() => {
di.earlyOverride(someInjectionToken, () => 'irrevant');
}).toThrow(
'Tried to early-override an injection token "some-token-id", but that is currently not supported.',
);
});

describe('when early-overriding it using injectable', () => {
beforeEach(() => {
someInjectable = getInjectable({
id: 'some-injectable',
instantiate: () => 'some-instance',
injectionToken: someInjectionToken,
});

di.earlyOverride(someInjectable, () => 'some-overridden-instance');
});

describe('given the injectable is still non-registered', () => {
it('when injected, throws as normal', () => {
expect(() => {
di.inject(someInjectable);
}).toThrow(
'Tried to inject non-registered injectable "irrelevant" -> "some-injectable".',
);
});

it('when injected using token, throws as normal', () => {
expect(() => {
di.inject(someInjectionToken);
}).toThrow(
'Tried to inject non-registered injectable "irrelevant" -> "some-token-id".',
);
});
});

describe('given the injectable is registered', () => {
beforeEach(() => {
di.register(someInjectable);
});

it('when injected, returns the overridden instance', () => {
const actual = di.inject(someInjectable);

expect(actual).toBe('some-overridden-instance');
});

it('when injected using token, returns the overridden instance', () => {
const actual = di.inject(someInjectionToken);

expect(actual).toBe('some-overridden-instance');
});

it('when injecting many using token, returns the overridden instance', () => {
const actual = di.injectMany(someInjectionToken);

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

it('given injected, when the early-overridden again, throws', () => {
di.inject(someInjectionToken);

expect(() => {
di.earlyOverride(someInjectable);
}).toThrow(
'Tried to override injectable "some-injectable", but it was already injected.',
);
});

describe('given unoverridden', () => {
beforeEach(() => {
di.unoverride(someInjectable);
});

it('when injected, returns the original instance', () => {
const actual = di.inject(someInjectable);

expect(actual).toBe('some-instance');
});

it('when injected using token, returns the original instance', () => {
const actual = di.inject(someInjectionToken);

expect(actual).toBe('some-instance');
});
});
});
});
});
});

0 comments on commit 6e7f5ea

Please sign in to comment.