Skip to content

Commit

Permalink
feature(injector): PartialFactory (#487)
Browse files Browse the repository at this point in the history
  • Loading branch information
timvandam committed Oct 2, 2023
1 parent 9eafe2d commit c730aed
Show file tree
Hide file tree
Showing 2 changed files with 209 additions and 6 deletions.
108 changes: 103 additions & 5 deletions packages/injector/src/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,19 @@ import {
TagRegistry,
Token
} from './provider.js';
import { AbstractClassType, ClassType, CompilerContext, CustomError, getClassName, getPathValue, isArray, isClass, isFunction, isPrototypeOfBase } from '@deepkit/core';
import { findModuleForConfig, getScope, InjectorModule, PreparedProvider } from './module.js';
import {
AbstractClassType,
ClassType,
CompilerContext,
CustomError,
getClassName,
getPathValue,
isArray,
isClass,
isFunction,
isPrototypeOfBase
} from '@deepkit/core';
import {findModuleForConfig, getScope, InjectorModule, PreparedProvider} from './module.js';
import {
hasTypeInformation,
isExtendable,
Expand Down Expand Up @@ -111,7 +122,6 @@ function createTransientInjectionTarget(destination: Destination | undefined) {
return new TransientInjectionTarget(destination.token);
}


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

Expand Down Expand Up @@ -225,14 +235,20 @@ 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.
*
* @reflection never
*/
export class Injector implements InjectorInterface {
private resolver?: (token: any, scope?: Scope) => any;
private resolver?: (token: any, scope?: Scope, destination?: Destination) => any;
private setter?: (token: any, value: any, scope?: Scope) => any;
private instantiations?: (token: any, scope?: string) => number;

Expand Down Expand Up @@ -612,6 +628,13 @@ export class Injector implements InjectorInterface {
return `new ${tokenVar}(${resolvedVar} || (${resolvedVar} = [${args.join(', ')}]))`;
}

if (options.type.kind === ReflectionKind.function && options.type.typeName === 'PartialFactory') {
const type = options.type.typeArguments?.[0];
const factory = partialFactory(type, this);
const factoryVar = compiler.reserveConst(factory, 'factory');
return `${factoryVar}(scope)`;
}

if (options.type.kind === ReflectionKind.objectLiteral) {
const pickArguments = getPickArguments(options.type);
if (pickArguments) {
Expand Down Expand Up @@ -763,6 +786,12 @@ export class Injector implements InjectorInterface {
return new type.classType(args);
}

if (type.kind === ReflectionKind.function && type.typeName === 'PartialFactory') {
const factoryType = type.typeArguments?.[0];
const factory = partialFactory(factoryType, this);
return (scopeIn?: Scope) => factory(scopeIn);
}

if (isWithAnnotations(type)) {
if (type.kind === ReflectionKind.objectLiteral) {
const pickArguments = getPickArguments(type);
Expand Down Expand Up @@ -808,7 +837,8 @@ export class Injector implements InjectorInterface {
}
current = current.indexAccessOrigin.container;
}
return () => config;

if (config) return () => config;
}
}

Expand Down Expand Up @@ -973,3 +1003,71 @@ export function injectedFunction<T extends (...args: any) => any>(fn: T, injecto
}
return fn;
}

export function partialFactory(
type: Type | undefined,
injector: Injector,
) {
if (!type) throw new Error('Can not create partial factory for undefined type');

// must be lazy because creating resolvers for types that are never resolved & unresolvable will throw
function createLazyResolver(type: Type, label?: string): Resolver<any> {
let resolver: Resolver<any> | undefined = undefined;
return (scope?: Scope) => {
if (!resolver) {
resolver = injector.createResolver(type, scope, label);
}
return resolver(scope);
};
}

if (type.kind === ReflectionKind.class) {
const classType = type.classType;
const reflectionClass = ReflectionClass.from(classType);

const args: { name: string; resolve: (scope?: Scope) => ReturnType<Resolver<any>> }[] = [];
const constructor = reflectionClass.getMethodOrUndefined('constructor');
if (constructor) {
for (const parameter of constructor.getParameters()) {
args.push({
name: parameter.name,
resolve: createLazyResolver(parameter.getType() as Type, parameter.name),
});
}
}

const properties = new Map<keyof any, (scope?: Scope) => ReturnType<Resolver<any>>>();
for (const property of reflectionClass.getProperties()) {
const tokenType = getInjectOptions(property.type);
if (!tokenType) continue;

properties.set(property.getName(), createLazyResolver(tokenType, property.name));
}

return (scope?: Scope) => <T>(partial: Partial<{ [K in keyof T]: T[K] }>) => {
const instance = new classType(...(args.map((v) => partial[v.name as keyof T] ?? v.resolve(scope))));
for (const [property, resolve] of properties.entries()) {
instance[property] ??= partial[property as keyof T] ?? resolve(scope);
}
return instance as T;
};
}

if (type.kind === ReflectionKind.objectLiteral) {
const properties = new Map<keyof any, (scope?: Scope) => ReturnType<Resolver<any>>>();
for (const property of type.types) {
if (property.kind !== ReflectionKind.propertySignature) continue;
properties.set(property.name, createLazyResolver(property, String(property.name)));
}

return (scope?: Scope) => <T>(partial: Partial<{ [K in keyof T]: T[K] }>) => {
const obj: any = {};
for (const [property, resolve] of properties.entries()) {
obj[property] = partial[property as keyof T] ?? resolve(scope);
}
return obj as T;
};
}

throw new Error(`Can not create partial factory for ${stringifyType(type, { showFullDefinition: false })}`);
}
107 changes: 106 additions & 1 deletion packages/injector/tests/injector.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { expect, test } from '@jest/globals';
import { CircularDependencyError, DependenciesUnmetError, injectedFunction, Injector, InjectorContext, TransientInjectionTarget } from '../src/injector.js';
import {
CircularDependencyError,
DependenciesUnmetError,
injectedFunction,
Injector,
InjectorContext,
TransientInjectionTarget,
PartialFactory,
} 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 +863,100 @@ test('TransientInjectionTarget', () => {
expect(a.b.target.token).toBe(A);
}
});

test('PartialFactory', () => {
{
class A {
constructor (public b: B, public 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 num!: Inject<number>;
public 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);
}

{
class A {
constructor(public factory: PartialFactory<B>) {
}
}

class B {
constructor(public num: number, public c: C) {
}
}

class C {
public c = 'c';
}

const injector = Injector.from([A, C]);

const a = injector.get(A);
const b = a.factory({ num: 5 });

expect(a.factory).toBeInstanceOf(Function);
expect(b).toBeInstanceOf(B);
expect(b.num).toBe(5);
expect(b.c).toBeInstanceOf(C);
}

{
class A {
public b: B;
public num: number;

constructor(public factory: PartialFactory<{ b: B; num: number }>) {
const { b, num } = factory({ num: 5 });
this.b = b;
this.num = num;
}
}

class B {
public b = 'b';
}

const injector = Injector.from([A, B]);
const a = injector.get(A);

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

0 comments on commit c730aed

Please sign in to comment.