Skip to content

Commit

Permalink
add support for injecting tuples
Browse files Browse the repository at this point in the history
  • Loading branch information
jahudka committed Nov 17, 2024
1 parent eb49563 commit 75573e8
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 37 deletions.
2 changes: 1 addition & 1 deletion core/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"ioc",
"inversion of control"
],
"version": "1.0.0-rc.0",
"version": "1.0.0-rc.1",
"license": "MIT",
"author": {
"name": "Dan Kadera",
Expand Down
54 changes: 42 additions & 12 deletions core/cli/src/analysis/autowiring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { ServiceScope } from 'dicc';
import { ContainerBuilder } from '../container';
import {
AccessorType,
ArgumentDefinition,
InjectableType,
InjectorType,
IterableType,
Expand All @@ -12,6 +11,8 @@ import {
ReturnType,
ScopedRunnerType,
ServiceDefinition,
TupleType,
ValueType,
} from '../definitions';
import { AutowiringError, ContainerContext } from '../errors';
import { getFirst, mapMap } from '../utils';
Expand All @@ -35,18 +36,45 @@ export interface AutowiringFactory {
create(serviceAnalyser: ServiceAnalyser): Autowiring;
}

export type ArgumentInjectionOptions = {
optional?: boolean;
rest?: boolean;
};

export class Autowiring {
constructor(
private readonly reflector: ContainerReflector,
private readonly serviceAnalyser: ServiceAnalyser,
) {}

resolveArgumentInjection(ctx: ContainerContext, arg: ArgumentDefinition, scope: ServiceScope): Argument | undefined {
if (arg.type instanceof ScopedRunnerType) {
resolveArgumentInjection(
ctx: ContainerContext,
type: ValueType,
options: ArgumentInjectionOptions,
scope: ServiceScope,
): Argument | undefined {
if (type instanceof TupleType) {
return {
kind: 'injected',
mode: 'tuple',
values: type.values.map((elemType, idx) => {
const elem = this.resolveArgumentInjection(ctx, elemType, { optional: true }, scope);

if (!elem) {
throw new AutowiringError(`Unable to autowire tuple element #${idx}`, ctx);
}

return elem;
}),
spread: options.rest ?? false,
};
}

if (type instanceof ScopedRunnerType) {
return { kind: 'injected', mode: 'scoped-runner' };
}

for (const injectable of arg.type.getInjectableTypes()) {
for (const injectable of type.getInjectableTypes()) {
const candidates = ctx.builder.findByType(injectable.serviceType);

if (!candidates.size) {
Expand All @@ -65,17 +93,19 @@ export class Autowiring {
return this.resolveInjector(ctx, getFirst(candidates));
}

return this.resolveInjection(ctx, arg, injectable, candidates, scope);
return this.resolveInjection(ctx, injectable, options, candidates, scope);
}

if (arg.optional) {
if (options.optional) {
return undefined;
} else if (arg.type.nullable) {
} else if (type.nullable) {
return { kind: 'literal', source: 'undefined', async: 'none' };
}

console.log(type.type.getText());

throw new AutowiringError(
arg.type instanceof InjectorType
type instanceof InjectorType
? 'Unknown service type in injector'
: 'Unable to autowire non-optional argument',
ctx,
Expand All @@ -98,8 +128,8 @@ export class Autowiring {

private resolveInjection(
ctx: ContainerContext,
arg: ArgumentDefinition,
injectable: InjectableType,
options: ArgumentInjectionOptions,
candidates: Set<ServiceDefinition>,
scope: ServiceScope,
): InjectedArgument {
Expand Down Expand Up @@ -146,11 +176,11 @@ export class Autowiring {
if (argIsAccessor) {
return this.accessor(injectable.returnType, alias, () => async.some((cb) => cb()));
} else if (argIsIterable) {
return this.iterable(arg.rest, alias, getAsyncCb(injectable.async));
return this.iterable(options.rest ?? false, alias, getAsyncCb(injectable.async));
} else if ((argIsPromise ? injectable.value : injectable) instanceof ListType) {
return this.list(arg.rest, alias, getAsyncCb());
return this.list(options.rest ?? false, alias, getAsyncCb());
} else {
return this.single(injectable, arg.optional, alias, getAsyncCb());
return this.single(injectable, options.optional ?? false, alias, getAsyncCb());
}

function getAsyncCb(targetIsAsync: boolean = argIsPromise): () => AsyncMode {
Expand Down
7 changes: 7 additions & 0 deletions core/cli/src/analysis/results/arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ export type InjectorInjectedArgument = InjectedArgumentOptions & {
id: string;
};

export type TupleInjectedArgument = InjectedArgumentOptions & {
mode: 'tuple';
values: Argument[];
spread: boolean;
};

export type ScopedRunnerInjectedArgument = InjectedArgumentOptions & {
mode: 'scoped-runner';
};
Expand All @@ -86,6 +92,7 @@ export type InjectedArgument =
| IterableInjectedArgument
| AccessorInjectedArgument
| InjectorInjectedArgument
| TupleInjectedArgument
| ScopedRunnerInjectedArgument;

export type Argument =
Expand Down
12 changes: 8 additions & 4 deletions core/cli/src/analysis/serviceAnalyser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ export class ServiceAnalyser {
continue;
}

const argument = this.autowiring.resolveArgumentInjection({ ...ctx, argument: name }, arg, scope);
const argument = this.autowiring.resolveArgumentInjection({ ...ctx, argument: name }, arg.type, arg, scope);

if (!argument) {
undefs.push({ kind: 'literal', source: 'undefined', async: 'none' });
Expand All @@ -410,9 +410,13 @@ export class ServiceAnalyser {

return withAsync(hasAsyncArg, result);

function hasAsyncArg(): boolean {
for (const arg of result) {
if (hasAsyncMode(arg) && arg.async === 'await') {
function hasAsyncArg(args: Iterable<Argument> = result): boolean {
for (const arg of args) {
if (
hasAsyncMode(arg) && arg.async === 'await'
||
arg.kind === 'injected' && arg.mode === 'tuple' && hasAsyncArg(arg.values)
) {
return true;
}
}
Expand Down
14 changes: 14 additions & 0 deletions core/cli/src/compiler/serviceCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,20 @@ export class ServiceCompiler {
return `${asyncKw(arg)}() => di.${method(arg.target)}('${arg.alias}'${needKw(arg)})`;
}

if (arg.mode === 'tuple') {
const values = arg.values.map((value) => this.compileArg(value)).join(',\n');

if (arg.spread) {
return values;
}

const writer = this.writerFactory.create();
writer.write('[');
writer.indent(() => writer.write(`${values},`));
writer.write(']');
return writer.toString();
}

const need = arg.mode === 'single' ? needKw(arg) : '';
const asyncMode = arg.mode === 'iterable' ? withIterableMode : withAsyncMode;
return withSpread(arg, asyncMode(arg, `di.${method(arg.mode)}('${arg.alias}'${need})`));
Expand Down
11 changes: 11 additions & 0 deletions core/cli/src/definitions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,16 @@ export class InjectorType {
}
}

export class TupleType {
readonly values: ValueType[];

constructor(
...values: ValueType[]
) {
this.values = values;
}
}

export class ScopedRunnerType {
constructor(
public readonly nullable: boolean = false,
Expand All @@ -161,6 +171,7 @@ export type ValueType =
| IterableType
| AccessorType
| InjectorType
| TupleType
| ScopedRunnerType;

export type InjectableType =
Expand Down
25 changes: 17 additions & 8 deletions core/cli/src/extensions/servicesExtension.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {
ArrowFunction,
ClassDeclaration,
Expression,
FunctionDeclaration,
FunctionExpression,
InterfaceDeclaration,
Node,
ObjectLiteralExpression,
Expand Down Expand Up @@ -51,10 +49,10 @@ export class ServicesExtension extends CompilerExtension {
this.scanClassDeclaration(node, ctx);
} else if (Node.isInterfaceDeclaration(node)) {
this.scanInterfaceDeclaration(node, ctx);
} else if (Node.isFunctionDeclaration(node) || Node.isFunctionExpression(node) || Node.isArrowFunction(node)) {
this.scanFunctionDeclaration(node, ctx);
} else if (Node.isSatisfiesExpression(node)) {
this.scanSatisfiesExpression(node, ctx);
} else if (Node.isFunctionDeclaration(node) || Node.isExpression(node)) {
this.scanExpression(node, ctx);
}
}

Expand Down Expand Up @@ -89,15 +87,26 @@ export class ServicesExtension extends CompilerExtension {
});
}

private scanFunctionDeclaration(
node: FunctionDeclaration | FunctionExpression | ArrowFunction,
private scanExpression(
node: FunctionDeclaration | Expression,
ctx: UserCodeContext,
): void {
if (node.getTypeParameters().length) {
const nodeIsExpression = Node.isExpression(node);

if (!nodeIsExpression && node.getTypeParameters().length) {
return;
}

const factory = this.typeHelper.resolveFactory(node.getType(), ctx);
const factory = this.typeHelper.resolveFactory(node.getType(), ctx, !nodeIsExpression);

if (!factory) {
if (nodeIsExpression) {
return;
}

throw new DefinitionError('Unable to resolve factory signature', ctx);
}

let rtn = factory.returnType;

if (!rtn) {
Expand Down
18 changes: 15 additions & 3 deletions core/cli/src/utils/typeHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
PromiseType,
ReturnType,
ScopedRunnerType,
SingleType,
SingleType, TupleType,
ValueType,
} from '../definitions';
import { DefinitionError, UnsupportedError, UserCodeContext } from '../errors';
Expand Down Expand Up @@ -166,10 +166,18 @@ export class TypeHelper {
.find((node) => Node.isClassDeclaration(node) || Node.isInterfaceDeclaration(node));
}

resolveFactory(type: Type, ctx: UserCodeContext): FactoryDefinition {
resolveFactory<Need extends boolean | undefined = undefined>(
type: Type,
ctx: UserCodeContext,
need?: Need,
): Need extends false ? FactoryDefinition | undefined : FactoryDefinition {
const [signature, method] = this.resolveFactorySignature(type, ctx);

if (!signature) {
if (need === false) {
return undefined as any;
}

throw new DefinitionError(`Unable to resolve factory signature`, ctx);
}

Expand Down Expand Up @@ -295,7 +303,11 @@ export class TypeHelper {
resolveValueType(rawType: Type, node: Node): ValueType {
const [type, nullable] = this.unwrapNullable(rawType);

if (this.refs.isType(type, 'ScopedRunner')) {
if (type.isTuple()) {
return new TupleType(
...type.getTupleElements().map((elem) => this.resolveValueType(elem, node)),
);
} else if (this.refs.isType(type, 'ScopedRunner')) {
return new ScopedRunnerType(nullable);
} else if (this.isIterable(type)) {
return new IterableType(type, this.resolveSingleType(getFirst(type.getTypeArguments())), nullable);
Expand Down
16 changes: 15 additions & 1 deletion core/cli/tests/implicit/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class TestAsyncDependency {
) {}
}

class Entrypoint {
class TestTupleInjection {
constructor(
readonly testSingle: TestSingleDependency,
readonly testMultiple: TestMultipleDependencies,
Expand All @@ -95,6 +95,20 @@ class Entrypoint {
) {}
}

function testTupleInjectionFactory<C extends new (...args: any[]) => any>(
ctor: C,
): (...args: ConstructorParameters<C>) => InstanceType<C> {
return (...args: ConstructorParameters<C>) => new ctor(...args);
}

export const testTupleInjection = testTupleInjectionFactory(TestTupleInjection);

class Entrypoint {
constructor(
readonly testTupleInjection: TestTupleInjection,
) {}
}

export const entrypoint = {
factory: Entrypoint,
} satisfies ServiceDefinition<Entrypoint>;
21 changes: 14 additions & 7 deletions core/cli/tests/implicit/expectedContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,15 @@ interface AnonymousServices {
'#TestMultipleDependencies0.0': definitions0.TestMultipleDependencies;
'#TestNoDependencies0.0': definitions0.TestNoDependencies;
'#TestSingleDependency0.0': definitions0.TestSingleDependency;
'#TestTupleInjection0.0': Promise<ServiceType<typeof definitions0.testTupleInjection>>;
}

export class TestContainer extends Container<PublicServices, DynamicServices, AnonymousServices> {
constructor() {
super({
'entrypoint': {
factory: async (di) => new definitions0.entrypoint.factory(
di.get('#TestSingleDependency0.0'),
di.get('#TestMultipleDependencies0.0'),
di.get('#TestListDependency0.0'),
di.get('#TestInjectionModes0.0'),
await di.get('#TestAsyncFactoryMethod0.0'),
await di.get('#TestAsyncFactoryMethod0.0'),
await di.get('#TestAsyncDependency0.0'),
await di.get('#TestTupleInjection0.0'),
),
async: true,
},
Expand Down Expand Up @@ -88,6 +83,18 @@ export class TestContainer extends Container<PublicServices, DynamicServices, An
di.get('#TestNoDependencies0.0'),
),
},
'#TestTupleInjection0.0': {
factory: async (di) => definitions0.testTupleInjection(
di.get('#TestSingleDependency0.0', false),
di.get('#TestMultipleDependencies0.0', false),
di.get('#TestListDependency0.0', false),
di.get('#TestInjectionModes0.0', false),
await di.get('#TestAsyncFactoryMethod0.0', false),
await di.get('#TestAsyncFactoryMethod0.0', false),
await di.get('#TestAsyncDependency0.0', false),
),
async: true,
},
});
}
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 75573e8

Please sign in to comment.