diff --git a/incubation/container/container/lib/src/container.dart b/incubation/container/container/lib/src/container.dart index fc845ad..84dcc8a 100644 --- a/incubation/container/container/lib/src/container.dart +++ b/incubation/container/container/lib/src/container.dart @@ -24,6 +24,12 @@ class Container { final Map _factories = {}; final Map _namedSingletons = {}; + /// The container's type aliases + final Map _aliases = {}; + + /// The container's service extenders + final Map> _extenders = {}; + /// The container's contextual bindings final Map> _contextual = {}; @@ -135,6 +141,9 @@ class Container { return false; } + // Check if the type is aliased + t2 = getAlias(t2); + Container? search = this; while (search != null) { if (search._singletons.containsKey(t2)) { @@ -253,6 +262,7 @@ class Container { /// This method is central to the dependency injection mechanism, allowing for /// flexible object creation and dependency resolution within the container hierarchy. T make([Type? type]) { + // Get the original type Type t2 = T; if (type != null) { t2 = type; @@ -324,6 +334,7 @@ class Container { } if (instance != null) { + instance = _applyExtenders(t2, instance); var typedInstance = instance as T; _fireResolvingCallbacks(typedInstance); _fireAfterResolvingCallbacks(typedInstance); @@ -354,6 +365,7 @@ class Container { } if (instance != null) { + instance = _applyExtenders(t2, instance); var typedInstance = instance as T; _fireResolvingCallbacks(typedInstance); _fireAfterResolvingCallbacks(typedInstance); @@ -361,19 +373,22 @@ class Container { } } - // Check for singleton or factory + // Check for singleton or factory, resolving aliases if no contextual binding was found Container? search = this; + var resolvedType = contextualConcrete == null ? getAlias(t2) : t2; while (search != null) { - if (search._singletons.containsKey(t2)) { - var instance = search._singletons[t2] as T; + if (search._singletons.containsKey(resolvedType)) { + var instance = search._singletons[resolvedType]; + instance = _applyExtenders(resolvedType, instance); _fireResolvingCallbacks(instance); _fireAfterResolvingCallbacks(instance); - return instance; - } else if (search._factories.containsKey(t2)) { - var instance = search._factories[t2]!(this) as T; + return instance as T; + } else if (search._factories.containsKey(resolvedType)) { + var instance = search._factories[resolvedType]!(this); + instance = _applyExtenders(resolvedType, instance); _fireResolvingCallbacks(instance); _fireAfterResolvingCallbacks(instance); - return instance; + return instance as T; } else { search = search._parent; } @@ -443,11 +458,12 @@ class Container { var instance = reflectedType.newInstance( isDefault(constructor.name) ? '' : constructor.name, positional, - named, []).reflectee as T; + named, []).reflectee; + instance = _applyExtenders(t2, instance); _fireResolvingCallbacks(instance); _fireAfterResolvingCallbacks(instance); - return instance; + return instance as T; } else { throw BindingResolutionException( '$t2 is not a class, and therefore cannot be instantiated.'); @@ -722,8 +738,16 @@ class Container { while (search != null) { var building = _buildStack.last; var contextMap = search._contextual[building]; - if (contextMap != null && contextMap.containsKey(abstract)) { - return contextMap[abstract]; + if (contextMap != null) { + // First try to find a binding for the original type + if (contextMap.containsKey(abstract)) { + return contextMap[abstract]; + } + // Then try to find a binding for the aliased type + var aliasedType = getAlias(abstract); + if (aliasedType != abstract && contextMap.containsKey(aliasedType)) { + return contextMap[aliasedType]; + } } search = search._parent; } @@ -768,8 +792,16 @@ class Container { while (search != null) { var building = _buildStack.last; var contextMap = search._contextual[building]; - if (contextMap != null && contextMap.containsKey(type)) { - return true; + if (contextMap != null) { + // First check for binding of original type + if (contextMap.containsKey(type)) { + return true; + } + // Then check for binding of aliased type + var aliasedType = getAlias(type); + if (aliasedType != type && contextMap.containsKey(aliasedType)) { + return true; + } } search = search._parent; } @@ -777,6 +809,80 @@ class Container { return false; } + /// Register an alias for an abstract type. + /// + /// This allows you to alias an abstract type to a concrete implementation. + /// For example, you might alias an interface to its default implementation: + /// ```dart + /// container.alias(ConsoleLogger); + /// ``` + void alias(Type concrete) { + _aliases[T] = concrete; + } + + /// Get the concrete type that an abstract type is aliased to. + /// + /// If the type is not aliased in this container or any parent container, + /// returns the type itself. + Type getAlias(Type abstract) { + Container? search = this; + while (search != null) { + if (search._aliases.containsKey(abstract)) { + return search._aliases[abstract]!; + } + search = search._parent; + } + return abstract; + } + + /// Check if a type is aliased to another type in this container or any parent container. + bool isAlias(Type type) { + Container? search = this; + while (search != null) { + if (search._aliases.containsKey(type)) { + return true; + } + search = search._parent; + } + return false; + } + + /// Extend a service after it is resolved. + /// + /// This allows you to modify a service after it has been resolved from the container. + /// The callback receives the resolved instance and the container, and should return + /// the modified instance. + /// + /// ```dart + /// container.extend((logger, container) { + /// logger.level = LogLevel.debug; + /// return logger; + /// }); + /// ``` + void extend( + dynamic Function(dynamic instance, Container container) callback) { + _extenders.putIfAbsent(T, () => []).add(callback); + } + + /// Apply any registered extenders to an instance. + dynamic _applyExtenders(Type type, dynamic instance) { + // Collect all extenders from parent to child + var extenders = []; + Container? search = this; + while (search != null) { + if (search._extenders.containsKey(type)) { + extenders.insertAll(0, search._extenders[type]!); + } + search = search._parent; + } + + // Apply extenders in order (parent to child) + for (var extender in extenders) { + instance = extender(instance, this); + } + return instance; + } + /// Check if we're in danger of a circular dependency. void _checkCircularDependency(Type type) { if (_buildStack.contains(type)) { diff --git a/incubation/container/container/lib/src/contextual_binding_builder.dart b/incubation/container/container/lib/src/contextual_binding_builder.dart index 77186f6..07f69e2 100644 --- a/incubation/container/container/lib/src/contextual_binding_builder.dart +++ b/incubation/container/container/lib/src/contextual_binding_builder.dart @@ -56,8 +56,7 @@ class ContextualImplementationBuilder { /// Specify the implementation that should be used void give() { for (var concreteType in concrete) { - container.addContextualBinding( - concreteType, abstract, (Container c) => c.make()); + container.addContextualBinding(concreteType, abstract, T); } } diff --git a/incubation/container/container/test/alias_test.dart b/incubation/container/container/test/alias_test.dart new file mode 100644 index 0000000..9b48751 --- /dev/null +++ b/incubation/container/container/test/alias_test.dart @@ -0,0 +1,230 @@ +import 'package:platformed_container/container.dart'; +import 'package:test/test.dart'; +import 'common.dart'; + +// Test interfaces and implementations +abstract class Logger { + void log(String message); +} + +class ConsoleLogger implements Logger { + @override + void log(String message) => print('Console: $message'); +} + +class FileLogger implements Logger { + final String filename; + FileLogger(this.filename); + @override + void log(String message) => print('File($filename): $message'); +} + +class LoggerClient { + final Logger logger; + LoggerClient(this.logger); +} + +class MockReflector extends Reflector { + @override + String? getName(Symbol symbol) => null; + + @override + ReflectedClass? reflectClass(Type clazz) => null; + + @override + ReflectedType? reflectType(Type type) { + if (type == LoggerClient) { + return MockReflectedClass( + 'LoggerClient', + [], + [], + [ + MockConstructor([MockParameter('logger', Logger)]) + ], + [], + type, + (name, positional, named, typeArgs) => LoggerClient(positional[0]), + ); + } else if (type == FileLogger) { + return MockReflectedClass( + 'FileLogger', + [], + [], + [ + MockConstructor([MockParameter('filename', String)]) + ], + [], + type, + (name, positional, named, typeArgs) => FileLogger(positional[0]), + ); + } + return null; + } + + @override + ReflectedInstance? reflectInstance(Object? instance) => null; + + @override + ReflectedFunction? reflectFunction(Function function) => null; + + @override + ReflectedType reflectFutureOf(Type type) => throw UnimplementedError(); +} + +class MockReflectedClass extends ReflectedClass { + final Function instanceBuilder; + + MockReflectedClass( + String name, + List typeParameters, + List annotations, + List constructors, + List declarations, + Type reflectedType, + this.instanceBuilder, + ) : super(name, typeParameters, annotations, constructors, declarations, + reflectedType); + + @override + ReflectedInstance newInstance( + String constructorName, List positionalArguments, + [Map namedArguments = const {}, + List typeArguments = const []]) { + var instance = instanceBuilder( + constructorName, positionalArguments, namedArguments, typeArguments); + return MockReflectedInstance(this, instance); + } + + @override + bool isAssignableTo(ReflectedType? other) { + if (other == null) return false; + return reflectedType == other.reflectedType; + } +} + +class MockReflectedInstance extends ReflectedInstance { + MockReflectedInstance(ReflectedClass clazz, Object? reflectee) + : super(clazz, clazz, reflectee); + + @override + ReflectedInstance getField(String name) { + throw UnimplementedError(); + } +} + +class MockConstructor extends ReflectedFunction { + final List params; + + MockConstructor(this.params) + : super('', [], [], params, false, false, + returnType: MockReflectedType('void', [], dynamic)); + + @override + ReflectedInstance invoke(Invocation invocation) { + throw UnimplementedError(); + } +} + +class MockParameter extends ReflectedParameter { + MockParameter(String name, Type type) + : super(name, [], MockReflectedType(type.toString(), [], type), true, + false); +} + +class MockReflectedType extends ReflectedType { + MockReflectedType(String name, List typeParameters, + Type reflectedType) + : super(name, typeParameters, reflectedType); + + @override + ReflectedInstance newInstance( + String constructorName, List positionalArguments, + [Map namedArguments = const {}, + List typeArguments = const []]) { + throw UnimplementedError(); + } + + @override + bool isAssignableTo(ReflectedType? other) { + if (other == null) return false; + return reflectedType == other.reflectedType; + } +} + +void main() { + late Container container; + + setUp(() { + container = Container(MockReflector()); + }); + + group('Alias Tests', () { + test('alias resolves to concrete type', () { + container.registerSingleton(ConsoleLogger()); + container.alias(ConsoleLogger); + + var logger = container.make(); + expect(logger, isA()); + }); + + test('isAlias returns true for aliased type', () { + container.alias(ConsoleLogger); + expect(container.isAlias(Logger), isTrue); + }); + + test('isAlias returns false for non-aliased type', () { + expect(container.isAlias(ConsoleLogger), isFalse); + }); + + test('getAlias returns concrete type for aliased type', () { + container.alias(ConsoleLogger); + expect(container.getAlias(Logger), equals(ConsoleLogger)); + }); + + test('getAlias returns same type for non-aliased type', () { + expect(container.getAlias(ConsoleLogger), equals(ConsoleLogger)); + }); + + test('alias works with contextual bindings', () { + // Register both logger implementations + container.registerSingleton(ConsoleLogger()); + container.registerSingleton(FileLogger('test.log')); + + // Set up the alias + container.alias(ConsoleLogger); + + // Set up contextual binding for the interface + container.when(LoggerClient).needs().give(); + + var logger = container.make(); + expect(logger, isA()); + + var client = container.make(); + expect(client.logger, isA()); + }); + + test('child container inherits parent aliases', () { + container.registerSingleton(ConsoleLogger()); + container.alias(ConsoleLogger); + + var child = container.createChild(); + var logger = child.make(); + expect(logger, isA()); + }); + + test('child container can override parent aliases', () { + container.registerSingleton(ConsoleLogger()); + container.registerSingleton(FileLogger('test.log')); + container.alias(ConsoleLogger); + + var child = container.createChild(); + child.alias(FileLogger); + + var parentLogger = container.make(); + expect(parentLogger, isA()); + + var childLogger = child.make(); + expect(childLogger, isA()); + }); + }); +} diff --git a/incubation/container/container/test/extend_test.dart b/incubation/container/container/test/extend_test.dart new file mode 100644 index 0000000..62c01be --- /dev/null +++ b/incubation/container/container/test/extend_test.dart @@ -0,0 +1,113 @@ +import 'package:platformed_container/container.dart'; +import 'package:test/test.dart'; + +class MockReflector extends Reflector { + @override + String? getName(Symbol symbol) => null; + + @override + ReflectedClass? reflectClass(Type clazz) => null; + + @override + ReflectedType? reflectType(Type type) => null; + + @override + ReflectedInstance? reflectInstance(Object? instance) => null; + + @override + ReflectedFunction? reflectFunction(Function function) => null; + + @override + ReflectedType reflectFutureOf(Type type) => throw UnimplementedError(); +} + +abstract class Logger { + LogLevel get level; + set level(LogLevel value); + void log(String message); +} + +enum LogLevel { debug, info, warning, error } + +class ConsoleLogger implements Logger { + LogLevel _level = LogLevel.info; + + @override + LogLevel get level => _level; + + @override + set level(LogLevel value) => _level = value; + + @override + void log(String message) => print('Console: $message'); +} + +void main() { + late Container container; + + setUp(() { + container = Container(MockReflector()); + }); + + group('Service Extender Tests', () { + test('can extend a service after resolution', () { + container.registerSingleton(ConsoleLogger()); + container.extend((logger, container) { + logger.level = LogLevel.debug; + return logger; + }); + + var logger = container.make(); + expect(logger.level, equals(LogLevel.debug)); + }); + + test('can apply multiple extenders in order', () { + container.registerSingleton(ConsoleLogger()); + + container.extend((logger, container) { + logger.level = LogLevel.debug; + return logger; + }); + + container.extend((logger, container) { + logger.level = LogLevel.error; + return logger; + }); + + var logger = container.make(); + expect(logger.level, equals(LogLevel.error)); + }); + + test('child container inherits parent extenders', () { + container.registerSingleton(ConsoleLogger()); + container.extend((logger, container) { + logger.level = LogLevel.debug; + return logger; + }); + + var child = container.createChild(); + var logger = child.make(); + expect(logger.level, equals(LogLevel.debug)); + }); + + test('child container can add its own extenders', () { + container.registerSingleton(ConsoleLogger()); + container.extend((logger, container) { + logger.level = LogLevel.debug; + return logger; + }); + + var child = container.createChild(); + child.extend((logger, container) { + logger.level = LogLevel.error; + return logger; + }); + + var parentLogger = container.make(); + expect(parentLogger.level, equals(LogLevel.debug)); + + var childLogger = child.make(); + expect(childLogger.level, equals(LogLevel.error)); + }); + }); +}