Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(di): ability to use newInstance() with interface #1767

Merged
merged 1 commit into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 43 additions & 5 deletions packages/__tests__/1-kernel/di.get.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,15 +554,31 @@ describe('1-kernel/di.get.spec.ts', function () {
assert.throws(() => container.get(newInstanceOf(I)), `No registration for interface: 'I'`);
});

it('throws when there is NOT factory registration for an instance', function () {
it('instantiates when there is an constructable registration for an interface', function () {
const container = DI.createContainer();
const I = DI.createInterface('I');
class IImpl {}
container.register(Registration.singleton(I, IImpl));
assert.throws(() => container.get(newInstanceOf(I)), `No registration for interface: 'I'`);
class Impl {}
container.register(Registration.singleton(I, Impl));
assert.instanceOf(container.get(newInstanceOf(I)), Impl);
});

it('jit-registers and instantiates when there is a default impl for an interface', function () {
const container = DI.createContainer();
class Impl {}
const I = DI.createInterface('I', x => x.singleton(Impl));
assert.instanceOf(container.get(newInstanceOf(I)), Impl);
});

it('does not register instance when retrieved through interface', function () {
const container = DI.createContainer();
const I = DI.createInterface('I');
class Impl {}
container.register(Registration.singleton(I, Impl));
assert.notStrictEqual(container.get(newInstanceOf(I)), container.get(I));
assert.strictEqual(container.getAll(I).length, 1);
});

it('does not throw when there is factory registration for an instance', function () {
it('does not throw when there is factory registration for an interface', function () {
const container = DI.createContainer();
const I = DI.createInterface('I');
let iCallCount = 0;
Expand Down Expand Up @@ -647,6 +663,28 @@ describe('1-kernel/di.get.spec.ts', function () {
});

describe('@newInstanceForScope', function () {
it('jit-registers and instantiates when there is a default impl for an interface', function () {
const container = DI.createContainer();
class Impl {}
const I = DI.createInterface('I', x => x.singleton(Impl));
assert.instanceOf(container.get(newInstanceForScope(I)), Impl);
assert.strictEqual(container.getAll(I).length, 2);
});

it('jit-registers resolver and instance in child', function () {
const container = DI.createContainer();
const child = container.createChild();
class Impl {}
const I = DI.createInterface('I', x => x.singleton(Impl));
const instance = child.get(newInstanceForScope(I));
assert.instanceOf(instance, Impl);
assert.strictEqual(child.getAll(I).length, 1);
assert.strictEqual(child.getAll(I)[0], instance);
// has resolver, no instance
// has resolver because createNewInstance/scope auto-registers a resolver
// no instance because new instance forScope doesn't register on handler
assert.strictEqual(container.has(I, false), true);
});
// the following test tests a more common, expected scenario,
// where some instance is scoped to a child container,
// instead of the container registering the interface itself.
Expand Down
4 changes: 4 additions & 0 deletions packages/kernel/src/di.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,10 @@ export class Container implements IContainer {
}
}

public hasFactory<T extends Constructable>(key: T): boolean {
return this._factories.has(key);
}

public getFactory<K extends Constructable>(Type: K): IFactory<K> {
let factory = this._factories.get(Type);
if (factory === void 0) {
Expand Down
24 changes: 24 additions & 0 deletions packages/kernel/src/di.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export interface IContainer extends IServiceLocator, IDisposable {
getResolver<K extends Key, T = K>(key: K | Key, autoRegister?: boolean): IResolver<T> | null;
registerFactory<T extends Constructable>(key: T, factory: IFactory<T>): void;
invoke<T extends {}, TDeps extends unknown[] = unknown[]>(key: Constructable<T>, dynamicDependencies?: TDeps): T;
hasFactory<T extends Constructable>(key: any): boolean;
getFactory<T extends Constructable>(key: T): IFactory<T>;
createChild(config?: IContainerConfiguration): IContainer;
disposeResolvers(): void;
Expand Down Expand Up @@ -731,6 +732,23 @@ export type INewInstanceResolver<T> = IResolver<T> & {
};

const createNewInstance = (key: any, handler: IContainer, requestor: IContainer) => {
// 1. if there's a factory registration for the key
if (handler.hasFactory(key)) {
return handler.getFactory(key).construct(requestor);
}
// 2. if key is an interface
if (isInterface(key)) {
const hasDefault = isFunction((key as unknown as IRegistry).register);
const resolver = handler.getResolver(key, hasDefault) as IResolver<Constructable<typeof key>>;
const factory = resolver?.getFactory?.(handler);
// 2.1 and has factory
if (factory != null) {
return factory.construct(requestor);
}
// 2.2 cannot instantiate a dummy interface
throw cannotInstantiateInterfaceError(key);
}
// 3. jit factory, in case of newInstanceOf(SomeClass)
return handler.getFactory(key).construct(requestor);
};

Expand Down Expand Up @@ -984,6 +1002,12 @@ export class InstanceProvider<K extends Key> implements IDisposableResolver<K |
}
}

const isInterface = <K>(key: any): key is InterfaceSymbol<K> => isFunction(key) && key.$isInterface === true;
const cannotInstantiateInterfaceError = (key: InterfaceSymbol) =>
__DEV__
? createError(`AURxxxx: Failed to instantiate ${key}, there's no registration and no default implementation.`)
: createError(`AURxxxx: ${key}`);

const noInstanceError = (name?: string) => {
if (__DEV__) {
return createError(`AUR0013: Cannot call resolve ${name} before calling prepare or after calling dispose.`);
Expand Down