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

feature(injector): PartialFactory #487

Merged
merged 1 commit into from
Oct 2, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to change this to support creating factories for interfaces without having to Inject<.> every property. Interface properties always have a property.type.indexAccessOrigin set, which triggers config lookup. Without this change it would always return undefined (which was undesirable behavior in the first place I think)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this broke some stuff. Fixed in c4ed83a

}
}

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);
}
});
Loading