From 75573e8e83b1b274225b15d2e32bceeec2b3c104 Mon Sep 17 00:00:00 2001 From: Dan Kadera Date: Sun, 17 Nov 2024 17:40:03 +0100 Subject: [PATCH] add support for injecting tuples --- core/cli/package.json | 2 +- core/cli/src/analysis/autowiring.ts | 54 +++++++++++++++----- core/cli/src/analysis/results/arguments.ts | 7 +++ core/cli/src/analysis/serviceAnalyser.ts | 12 +++-- core/cli/src/compiler/serviceCompiler.ts | 14 +++++ core/cli/src/definitions/types.ts | 11 ++++ core/cli/src/extensions/servicesExtension.ts | 25 ++++++--- core/cli/src/utils/typeHelper.ts | 18 +++++-- core/cli/tests/implicit/definitions.ts | 16 +++++- core/cli/tests/implicit/expectedContainer.ts | 21 +++++--- package-lock.json | 2 +- 11 files changed, 145 insertions(+), 37 deletions(-) diff --git a/core/cli/package.json b/core/cli/package.json index 74e8e82..211150b 100644 --- a/core/cli/package.json +++ b/core/cli/package.json @@ -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", diff --git a/core/cli/src/analysis/autowiring.ts b/core/cli/src/analysis/autowiring.ts index 0d40182..f9cc58d 100644 --- a/core/cli/src/analysis/autowiring.ts +++ b/core/cli/src/analysis/autowiring.ts @@ -2,7 +2,6 @@ import { ServiceScope } from 'dicc'; import { ContainerBuilder } from '../container'; import { AccessorType, - ArgumentDefinition, InjectableType, InjectorType, IterableType, @@ -12,6 +11,8 @@ import { ReturnType, ScopedRunnerType, ServiceDefinition, + TupleType, + ValueType, } from '../definitions'; import { AutowiringError, ContainerContext } from '../errors'; import { getFirst, mapMap } from '../utils'; @@ -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) { @@ -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, @@ -98,8 +128,8 @@ export class Autowiring { private resolveInjection( ctx: ContainerContext, - arg: ArgumentDefinition, injectable: InjectableType, + options: ArgumentInjectionOptions, candidates: Set, scope: ServiceScope, ): InjectedArgument { @@ -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 { diff --git a/core/cli/src/analysis/results/arguments.ts b/core/cli/src/analysis/results/arguments.ts index d6b54ae..86e814f 100644 --- a/core/cli/src/analysis/results/arguments.ts +++ b/core/cli/src/analysis/results/arguments.ts @@ -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'; }; @@ -86,6 +92,7 @@ export type InjectedArgument = | IterableInjectedArgument | AccessorInjectedArgument | InjectorInjectedArgument + | TupleInjectedArgument | ScopedRunnerInjectedArgument; export type Argument = diff --git a/core/cli/src/analysis/serviceAnalyser.ts b/core/cli/src/analysis/serviceAnalyser.ts index 41178b6..6159102 100644 --- a/core/cli/src/analysis/serviceAnalyser.ts +++ b/core/cli/src/analysis/serviceAnalyser.ts @@ -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' }); @@ -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 = result): boolean { + for (const arg of args) { + if ( + hasAsyncMode(arg) && arg.async === 'await' + || + arg.kind === 'injected' && arg.mode === 'tuple' && hasAsyncArg(arg.values) + ) { return true; } } diff --git a/core/cli/src/compiler/serviceCompiler.ts b/core/cli/src/compiler/serviceCompiler.ts index f1fb782..43e3005 100644 --- a/core/cli/src/compiler/serviceCompiler.ts +++ b/core/cli/src/compiler/serviceCompiler.ts @@ -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})`)); diff --git a/core/cli/src/definitions/types.ts b/core/cli/src/definitions/types.ts index d22387a..3f1d220 100644 --- a/core/cli/src/definitions/types.ts +++ b/core/cli/src/definitions/types.ts @@ -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, @@ -161,6 +171,7 @@ export type ValueType = | IterableType | AccessorType | InjectorType + | TupleType | ScopedRunnerType; export type InjectableType = diff --git a/core/cli/src/extensions/servicesExtension.ts b/core/cli/src/extensions/servicesExtension.ts index fa0cb0b..bc7bbca 100644 --- a/core/cli/src/extensions/servicesExtension.ts +++ b/core/cli/src/extensions/servicesExtension.ts @@ -1,9 +1,7 @@ import { - ArrowFunction, ClassDeclaration, Expression, FunctionDeclaration, - FunctionExpression, InterfaceDeclaration, Node, ObjectLiteralExpression, @@ -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); } } @@ -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) { diff --git a/core/cli/src/utils/typeHelper.ts b/core/cli/src/utils/typeHelper.ts index 0dd4b4d..88bc36d 100644 --- a/core/cli/src/utils/typeHelper.ts +++ b/core/cli/src/utils/typeHelper.ts @@ -23,7 +23,7 @@ import { PromiseType, ReturnType, ScopedRunnerType, - SingleType, + SingleType, TupleType, ValueType, } from '../definitions'; import { DefinitionError, UnsupportedError, UserCodeContext } from '../errors'; @@ -166,10 +166,18 @@ export class TypeHelper { .find((node) => Node.isClassDeclaration(node) || Node.isInterfaceDeclaration(node)); } - resolveFactory(type: Type, ctx: UserCodeContext): FactoryDefinition { + resolveFactory( + 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); } @@ -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); diff --git a/core/cli/tests/implicit/definitions.ts b/core/cli/tests/implicit/definitions.ts index afaac7b..0926892 100644 --- a/core/cli/tests/implicit/definitions.ts +++ b/core/cli/tests/implicit/definitions.ts @@ -83,7 +83,7 @@ export class TestAsyncDependency { ) {} } -class Entrypoint { +class TestTupleInjection { constructor( readonly testSingle: TestSingleDependency, readonly testMultiple: TestMultipleDependencies, @@ -95,6 +95,20 @@ class Entrypoint { ) {} } +function testTupleInjectionFactory any>( + ctor: C, +): (...args: ConstructorParameters) => InstanceType { + return (...args: ConstructorParameters) => new ctor(...args); +} + +export const testTupleInjection = testTupleInjectionFactory(TestTupleInjection); + +class Entrypoint { + constructor( + readonly testTupleInjection: TestTupleInjection, + ) {} +} + export const entrypoint = { factory: Entrypoint, } satisfies ServiceDefinition; diff --git a/core/cli/tests/implicit/expectedContainer.ts b/core/cli/tests/implicit/expectedContainer.ts index f6b118e..4a863e2 100644 --- a/core/cli/tests/implicit/expectedContainer.ts +++ b/core/cli/tests/implicit/expectedContainer.ts @@ -22,6 +22,7 @@ interface AnonymousServices { '#TestMultipleDependencies0.0': definitions0.TestMultipleDependencies; '#TestNoDependencies0.0': definitions0.TestNoDependencies; '#TestSingleDependency0.0': definitions0.TestSingleDependency; + '#TestTupleInjection0.0': Promise>; } export class TestContainer extends Container { @@ -29,13 +30,7 @@ export class TestContainer extends Container 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, }, @@ -88,6 +83,18 @@ export class TestContainer extends Container 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, + }, }); } } diff --git a/package-lock.json b/package-lock.json index c2aba9b..f26e388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ }, "core/cli": { "name": "dicc-cli", - "version": "1.0.0-rc.0", + "version": "1.0.0-rc.1", "license": "MIT", "dependencies": { "@debugr/console": "^3.0.0-rc.10",