import { DIKey, Callable } from '@/distage/model/DIKey'; import { AnyBinding, Bindings } from '@/distage/model/Binding'; import { BindingTags, Axis, AxisPoint } from '@/distage/model/Activation'; import { Functoid } from '@/distage/core/Functoid'; import { getConstructorTypes } from '@/distage/model/Reflected'; /** * Helper type to extract instance types from a tuple of constructor types. * Maps [typeof Database, typeof Config] -> [Database, Config] */ type InstanceTypes = T extends readonly [infer First, ...infer Rest] ? [ First extends abstract new (...args: any[]) => infer R ? R : First, ...InstanceTypes ] : []; /** * Builder for specifying the source of a binding (like izumi-chibi-py's .using()) */ export class BindingFromBuilder { constructor( private readonly bindingBuilder: BindingBuilder, private readonly constructorTypes?: any[], ) {} /** * Bind to a specific class type (will be instantiated via constructor injection) */ type(implementation: new (...args: any[]) => T): ModuleDef { return this.bindingBuilder.finalize((key, tags) => { const binding = Bindings.class(key, implementation, tags); // If we have explicit types from .withDeps(), use them if (this.constructorTypes && this.constructorTypes.length > 0) { binding.factory.withTypes(this.constructorTypes); } // Note: We don't need to call withTypes() for @Reflected/@Injectable here, // because Functoid.fromConstructor() already handles both @Reflected types // and @Id decorators when creating the binding. return binding; }); } /** * Bind to a specific value instance */ value(instance: T): ModuleDef { return this.bindingBuilder.finalize((key, tags) => Bindings.instance(key, instance, tags) ); } /** * Bind to a factory function or Functoid */ factory(factory: (...args: any[]) => R): ModuleDef; factory(functoid: Functoid): ModuleDef; factory(factoryOrFunctoid: ((...args: any[]) => R) | Functoid): ModuleDef { return this.bindingBuilder.finalize((key, tags) => { const functoid = factoryOrFunctoid instanceof Functoid ? factoryOrFunctoid : Functoid.fromFunctionUnsafe(factoryOrFunctoid); return Bindings.factory(key, functoid, tags); }); } /** * Bind to a pre-constructed Functoid * * Example: * const myFunctoid = Functoid.fromFunction([Database, Config], (db, cfg) => ...); * module.make(UserService).from().functoid(myFunctoid) */ functoid(functoid: Functoid): ModuleDef { return this.bindingBuilder.finalize((key, tags) => { return Bindings.factory(key, functoid, tags); }); } /** * Bind to a type-safe factory function with explicit parameter types. * This is a shorthand for Functoid.fromFunction(). * * Supports both synchronous and asynchronous factories. * * Example: * // Synchronous * module.make(UserService).from().func( * [Database, Config], * (db, cfg) => new UserService(db, cfg) * ) * * // Asynchronous * module.make(Database).from().func( * [], * async () => { * const db = new Database(); * await db.connect(); * return db; * } * ) */ func any)[], R extends T>( types: Args, fn: (...params: InstanceTypes) => R | Promise ): ModuleDef { return this.bindingBuilder.finalize((key, tags) => { const functoid = Functoid.fromFunction(types, fn); return Bindings.factory(key, functoid, tags); }); } /** * Create an alias to another binding * Can accept either a type with optional ID, or a DIKey directly */ alias(target: Callable | DIKey, targetId?: string): ModuleDef { return this.bindingBuilder.finalize((key, tags) => { const targetKey = target instanceof DIKey ? target : (targetId ? DIKey.named(target, targetId) : DIKey.of(target)); return Bindings.alias(key, targetKey, tags); }); } /** * Create an assisted factory binding (for runtime parameters + DI) */ assistedFactory(assistedParams: string[] = []): ModuleDef { return this.bindingBuilder.finalize((key, tags) => { const implementation = this.bindingBuilder['type'] as new (...args: any[]) => T; const functoid = Functoid.fromConstructor(implementation); // If we have explicit types from .withDeps(), use them if (this.constructorTypes && this.constructorTypes.length > 0) { functoid.withTypes(this.constructorTypes); } else { // Otherwise, try to auto-detect from @Injectable decorator const injectableTypes = getConstructorTypes(implementation); if (injectableTypes && injectableTypes.length > 0) { functoid.withTypes(injectableTypes); } } return Bindings.assistedFactory(key, functoid, assistedParams, tags); }); } } /** * Builder for creating a single binding with fluent API */ export class BindingBuilder { private currentId?: string; private currentTags: BindingTags = BindingTags.empty(); private constructorTypes?: any[]; constructor( private readonly type: Callable | symbol, private readonly module: ModuleDef, ) {} /** * Add a named identifier to this binding */ named(id: string): this { this.currentId = id; return this; } /** * Specify constructor parameter types explicitly (required without reflect-metadata) * * Example: * module.make(UserService).withDeps([Database, Config]).from().type(UserService) */ withDeps(types: any[]): this { this.constructorTypes = types; return this; } /** * Tag this binding with an axis point for conditional selection */ tagged(axis: Axis, choice: string): this; tagged(point: AxisPoint): this; tagged(axisOrPoint: Axis | AxisPoint, choice?: string): this { if (axisOrPoint instanceof AxisPoint) { this.currentTags = this.currentTags.withTag(axisOrPoint.axis, axisOrPoint.choice); } else if (choice !== undefined) { this.currentTags = this.currentTags.withTag(axisOrPoint, choice); } else { throw new Error('Either provide an AxisPoint or both axis and choice'); } return this; } /** * Start specifying where the binding comes from (izumi-chibi-py style) */ from(): BindingFromBuilder { return new BindingFromBuilder(this, this.constructorTypes); } /** * Get the DIKey for this binding */ private getKey(): DIKey { if (typeof this.type === 'symbol') { return this.currentId ? DIKey.namedToken(this.type, this.currentId) : DIKey.token(this.type); } else { return this.currentId ? DIKey.named(this.type as Callable, this.currentId) : DIKey.of(this.type as Callable); } } /** * Internal method to finalize the binding * @internal */ finalize(createBinding: (key: DIKey, tags: BindingTags) => AnyBinding): ModuleDef { const key = this.getKey(); const binding = createBinding(key, this.currentTags); this.module.addBinding(binding); return this.module; } } /** * Builder for specifying the source of a set element binding */ export class SetBindingFromBuilder { constructor( private readonly setBuilder: SetBindingBuilder, ) {} /** * Add a class type to the set (will be instantiated) */ type(implementation: new (...args: any[]) => T): ModuleDef { return this.setBuilder.finalizeElement((setKey, elementKey, tags, weak) => { const element = Bindings.class(elementKey, implementation, tags); return Bindings.set(setKey, elementKey, element, weak, tags); }); } /** * Add a value instance to the set */ value(instance: T): ModuleDef { return this.setBuilder.finalizeElement((setKey, elementKey, tags, weak) => { const element = Bindings.instance(elementKey, instance, tags); return Bindings.set(setKey, elementKey, element, weak, tags); }); } /** * Add a factory to the set */ factory(factory: (...args: any[]) => R): ModuleDef; factory(functoid: Functoid): ModuleDef; factory(factoryOrFunctoid: ((...args: any[]) => R) | Functoid): ModuleDef { return this.setBuilder.finalizeElement((setKey, elementKey, tags, weak) => { const functoid = factoryOrFunctoid instanceof Functoid ? factoryOrFunctoid : Functoid.fromFunctionUnsafe(factoryOrFunctoid); const element = Bindings.factory(elementKey, functoid, tags); return Bindings.set(setKey, elementKey, element, weak, tags); }); } /** * Add a pre-constructed Functoid to the set * * Example: * const myFunctoid = Functoid.fromFunction([Database], (db) => new Plugin(db)); * module.many(Plugin).from().functoid(myFunctoid) */ functoid(functoid: Functoid): ModuleDef { return this.setBuilder.finalizeElement((setKey, elementKey, tags, weak) => { const element = Bindings.factory(elementKey, functoid, tags); return Bindings.set(setKey, elementKey, element, weak, tags); }); } /** * Add a type-safe factory function to the set with explicit parameter types. * This is a shorthand for Functoid.fromFunction(). * * Example: * module.many(Plugin).from().func( * [Database], * (db) => new AuthPlugin(db) * ) */ func any)[], R extends T>( types: Args, fn: (...params: InstanceTypes) => R ): ModuleDef { return this.setBuilder.finalizeElement((setKey, elementKey, tags, weak) => { const functoid = Functoid.fromFunction(types, fn); const element = Bindings.factory(elementKey, functoid, tags); return Bindings.set(setKey, elementKey, element, weak, tags); }); } } /** * Builder for set bindings */ export class SetBindingBuilder { private currentId?: string; private currentTags: BindingTags = BindingTags.empty(); private weak: boolean = false; constructor( private readonly elementType: Callable | symbol, private readonly module: ModuleDef, ) {} /** * Add a named identifier to this set binding */ named(id: string): this { this.currentId = id; return this; } /** * Tag this set binding */ tagged(axis: Axis, choice: string): this; tagged(point: AxisPoint): this; tagged(axisOrPoint: Axis | AxisPoint, choice?: string): this { if (axisOrPoint instanceof AxisPoint) { this.currentTags = this.currentTags.withTag(axisOrPoint.axis, axisOrPoint.choice); } else if (choice !== undefined) { this.currentTags = this.currentTags.withTag(axisOrPoint, choice); } else { throw new Error('Either provide an AxisPoint or both axis and choice'); } return this; } /** * Mark this as a weak set binding (only included if dependencies are satisfied) */ makeWeak(): this { this.weak = true; return this; } /** * Start specifying where the set element comes from */ from(): SetBindingFromBuilder { return new SetBindingFromBuilder(this); } private getSetKey(): DIKey> { if (typeof this.elementType === 'symbol') { return this.currentId ? DIKey.namedSetToken(this.elementType, this.currentId) : DIKey.setToken(this.elementType); } else { return this.currentId ? DIKey.namedSet(this.elementType as Callable, this.currentId) : DIKey.set(this.elementType as Callable); } } private getElementKey(): DIKey { if (typeof this.elementType === 'symbol') { return this.currentId ? DIKey.namedToken(this.elementType, this.currentId) : DIKey.token(this.elementType); } else { return this.currentId ? DIKey.named(this.elementType as Callable, this.currentId) : DIKey.of(this.elementType as Callable); } } /** * Internal method to finalize a set element binding * @internal */ finalizeElement( createBinding: ( setKey: DIKey>, elementKey: DIKey, tags: BindingTags, weak: boolean, ) => AnyBinding ): ModuleDef { const setKey = this.getSetKey(); const elementKey = this.getElementKey(); const binding = createBinding(setKey, elementKey, this.currentTags, this.weak); this.module.addBinding(binding); return this.module; } } /** * Main DSL for defining dependency injection modules. * Provides a fluent API for declaring bindings, inspired by izumi-chibi-py. * * Example: * const module = new ModuleDef() * .make(Database).from().type(PostgresDatabase) * .make(UserService).from().type(UserService) * .make(Config).named('db').from().value(dbConfig) * .many(Plugin).from().type(AuthPlugin) * .many(Plugin).from().type(LoggingPlugin); * * The .from() method returns a builder that supports: * - .type(Class) - bind to a class (constructor injection) * - .value(instance) - bind to a specific instance * - .factory(fn) - bind to a factory function * - .alias(TargetClass) - create an alias to another binding */ export class ModuleDef { private bindings: AnyBinding[] = []; /** * Start defining a binding for a type or symbol token */ make(type: Callable | symbol): BindingBuilder { return new BindingBuilder(type, this); } /** * Start defining a set binding for a type or symbol token */ many(elementType: Callable | symbol): SetBindingBuilder { return new SetBindingBuilder(elementType, this); } /** * Add a binding directly (internal use) */ addBinding(binding: AnyBinding): void { this.bindings.push(binding); } /** * Get all bindings defined in this module */ getBindings(): readonly AnyBinding[] { return this.bindings; } /** * Merge this module with another, combining bindings */ append(other: ModuleDef): ModuleDef { const merged = new ModuleDef(); merged.bindings = [...this.bindings, ...other.bindings]; return merged; } /** * Override bindings in this module with those from another module. * Later bindings take precedence. */ overriddenBy(other: ModuleDef): ModuleDef { const merged = new ModuleDef(); // Group bindings by key const bindingMap = new Map(); for (const binding of this.bindings) { const key = binding.key.toMapKey(); if (!bindingMap.has(key)) { bindingMap.set(key, []); } bindingMap.get(key)!.push(binding); } for (const binding of other.bindings) { const key = binding.key.toMapKey(); if (!bindingMap.has(key)) { bindingMap.set(key, []); } bindingMap.get(key)!.push(binding); } // For each key, use the last binding for (const bindings of bindingMap.values()) { merged.bindings.push(bindings[bindings.length - 1]); } return merged; } /** * Get the number of bindings in this module */ size(): number { return this.bindings.length; } }