Skip to content

Commit

Permalink
feature(injector): PartialFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
timvandam committed Oct 1, 2023
1 parent a47111c commit a72234d
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 2 deletions.
61 changes: 60 additions & 1 deletion packages/injector/src/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ function createTransientInjectionTarget(destination: Destination | undefined) {
return new TransientInjectionTarget(destination.token);
}


let CircularDetector: any[] = [];
let CircularDetectorResets: (() => void)[] = [];

Expand Down Expand Up @@ -225,6 +224,12 @@ export class TransientInjectionTarget {
}
}

/**
* A factory function for some class.
* All properties that are not provided will be resolved using the injector that was used to create the factory.
*/
export type PartialFactory<C> = (args: Partial<{ [K in keyof C]: C[K] }>) => C;

/**
* This is the actual dependency injection container.
* Every module has its own injector.
Expand Down Expand Up @@ -763,6 +768,16 @@ export class Injector implements InjectorInterface {
return new type.classType(args);
}

if (type.kind === ReflectionKind.function && type.typeName === 'PartialFactory') {
const token = type.typeArguments && type.typeArguments[0];

if (!token || token.kind !== ReflectionKind.class) {
throw new Error('PartialFactory only supports classes');
}

return () => partialFactory(token.classType, this);
}

if (isWithAnnotations(type)) {
if (type.kind === ReflectionKind.objectLiteral) {
const pickArguments = getPickArguments(type);
Expand Down Expand Up @@ -973,3 +988,47 @@ export function injectedFunction<T extends (...args: any) => any>(fn: T, injecto
}
return fn;
}

export function partialFactory<T>(
classType: ClassType<T>,
injector: Injector,
) {
const reflectionClass = ReflectionClass.from(classType);
const constructor = reflectionClass.getMethodOrUndefined('constructor');
const args: { name: keyof T & string; resolve: () => ReturnType<Resolver<any>> }[] = [];
const properties = new Map<keyof T, () => ReturnType<Resolver<any>>>();

function createLazyResolver(type: Type, scope?: Scope, label?: string): Resolver<any> {
let _resolver: Resolver<any> | undefined = undefined;
return () => {
if (!_resolver) {
_resolver = injector.createResolver(type, scope, label);
}
return _resolver();
}
}

if (constructor) {
for (const parameter of constructor.getParameters()) {
args.push({
name: parameter.name as keyof T & string,
resolve: createLazyResolver(parameter.getType() as Type, undefined, parameter.name),
});
}
}

for (const property of reflectionClass.getProperties()) {
const tokenType = getInjectOptions(property.type);
if (!tokenType) continue;

properties.set(property.getName() as keyof T, createLazyResolver(tokenType, undefined, property.name));
}

return (partial: Partial<{ [K in keyof T]: T[K] }>) => {
const instance = new classType(...(args.map((v) => partial[v.name] !== undefined ? partial[v.name] : v.resolve())));
for (const [property, resolve] of properties.entries()) {
instance[property] = partial[property] ?? resolve();
}
return instance;
};
}
52 changes: 51 additions & 1 deletion packages/injector/tests/injector.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { expect, test } from '@jest/globals';
import { CircularDependencyError, DependenciesUnmetError, injectedFunction, Injector, InjectorContext, TransientInjectionTarget } from '../src/injector.js';
import {
CircularDependencyError, DependenciesUnmetError, injectedFunction, Injector, InjectorContext, partialFactory,
PartialFactory, TransientInjectionTarget
} from '../src/injector.js';
import { InjectorModule } from '../src/module.js';
import { ReflectionClass, ReflectionKind, ReflectionParameter, ReflectionProperty } from '@deepkit/type';
import { Inject } from '../src/types.js';
Expand Down Expand Up @@ -855,3 +858,50 @@ test('TransientInjectionTarget', () => {
expect(a.b.target.token).toBe(A);
}
});

test('PartialFactory', () => {
{
class A {
constructor (public readonly b: B, public readonly num: number) {
}
}

class B {
public b = 'b';

constructor() {
}
}

const injector = Injector.from([B]);
const factory = injector.get<PartialFactory<A>>();

const a = factory({
num: 5,
});

expect(a.b).toBeInstanceOf(B);
expect(a.num).toBe(5);
}

{
class A {
public readonly num!: Inject<number>;
public readonly b!: Inject<B>;
}

class B {
public b = 'b';
}

const injector = Injector.from([B]);
const factory = injector.get<PartialFactory<A>>();

const a = factory({
num: 6,
});

expect(a.b).toBeInstanceOf(B);
expect(a.num).toBe(6);
}
});

0 comments on commit a72234d

Please sign in to comment.