From 0de4f37270e212cb80466dd685d1d86178199d36 Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Fri, 27 Dec 2024 09:03:34 -0700 Subject: [PATCH] refactor: add factory() for deferred resolution working on wrap() --- .../container/lib/src/container.dart | 80 +++++ .../container/test/factory_test.dart | 209 ++++++++++++ .../container/container/test/wrap_test.dart | 306 ++++++++++++++++++ 3 files changed, 595 insertions(+) create mode 100644 incubation/container/container/test/factory_test.dart create mode 100644 incubation/container/container/test/wrap_test.dart diff --git a/incubation/container/container/lib/src/container.dart b/incubation/container/container/lib/src/container.dart index 8218c40..76d21d4 100644 --- a/incubation/container/container/lib/src/container.dart +++ b/incubation/container/container/lib/src/container.dart @@ -1157,6 +1157,73 @@ class Container { bind(abstract).to(concrete); } + /// Wrap a closure with dependency injection + /// + /// This allows you to wrap a closure so its dependencies will be injected when executed. + /// The wrapped closure will receive injected dependencies first, followed by any + /// additional parameters passed to the wrapper. + /// + /// ```dart + /// var wrapped = container.wrap((Logger logger, String message) { + /// logger.log(message); + /// }); + /// wrapped('Hello world'); // Logger will be injected automatically + /// ``` + Function wrap(T callback) { + return ([dynamic arg1]) { + // Get the function's parameter types through reflection + var reflectedFunction = reflector.reflectFunction(callback); + if (reflectedFunction == null) { + throw BindingResolutionException( + 'Could not reflect function parameters for dependency injection'); + } + + // Resolve dependencies for each parameter + var resolvedArgs = []; + var namedArgs = {}; + + // First resolve all container-injectable parameters + var injectedTypes = {}; + for (var param in reflectedFunction.parameters) { + var paramType = param.type.reflectedType; + if (has(paramType)) { + if (param.isNamed) { + namedArgs[Symbol(param.name)] = make(paramType); + } else { + resolvedArgs.add(make(paramType)); + } + injectedTypes.add(paramType); + } + } + + // Then add any provided arguments for remaining parameters + var providedArgs = []; + if (arg1 != null) providedArgs.add(arg1); + + // Process remaining parameters in order + var paramIndex = 0; + var providedIndex = 0; + for (var param in reflectedFunction.parameters) { + if (param.isNamed) continue; + if (injectedTypes.contains(param.type.reflectedType)) { + paramIndex++; + continue; + } + + if (providedIndex < providedArgs.length) { + resolvedArgs.add(providedArgs[providedIndex++]); + } else if (param.isRequired) { + throw BindingResolutionException( + 'No value provided for required parameter ${param.name} of type ${param.type.reflectedType}'); + } + paramIndex++; + } + + // Call the function with resolved arguments + return Function.apply(callback, resolvedArgs, namedArgs); + }; + } + /// Register a binding if it hasn't already been registered void bindIf(dynamic concrete, {bool singleton = false}) { if (!has()) { @@ -1181,6 +1248,19 @@ class Container { bindIf(concrete, singleton: true); } + /// Create a factory binding for deferred resolution + /// + /// This method allows you to create a factory binding that will be resolved + /// only when the dependency is requested. This is useful for lazy loading + /// and circular dependency resolution. + /// + /// ```dart + /// container.factory(() => ConsoleLogger()); + /// ``` + void factory(T Function() concrete) { + registerFactory((container) => concrete()); + } + /// Register all attribute-based bindings for a type void registerAttributeBindings(Type type) { var annotations = reflector.getAnnotations(type); diff --git a/incubation/container/container/test/factory_test.dart b/incubation/container/container/test/factory_test.dart new file mode 100644 index 0000000..8a486d6 --- /dev/null +++ b/incubation/container/container/test/factory_test.dart @@ -0,0 +1,209 @@ +import 'package:platformed_container/container.dart'; +import 'package:test/test.dart'; + +abstract class Logger { + void log(String message); +} + +class ConsoleLogger implements Logger { + @override + void log(String message) => print('Console: $message'); +} + +void main() { + late Container container; + + setUp(() { + container = Container(MockReflector()); + }); + + group('Factory Tests', () { + test('factory creates deferred binding', () { + var created = false; + container.factory(() { + created = true; + return ConsoleLogger(); + }); + + // Verify binding is not created yet + expect(created, isFalse); + + // Resolve the binding + var logger = container.make(); + + // Verify binding was created + expect(created, isTrue); + expect(logger, isA()); + }); + + test('factory creates new instance each time', () { + container.factory(() => ConsoleLogger()); + + var logger1 = container.make(); + var logger2 = container.make(); + + expect(logger1, isNot(same(logger2))); + }); + + test('factory throws when already bound', () { + container.factory(() => ConsoleLogger()); + + expect(() => container.factory(() => ConsoleLogger()), + throwsA(isA())); + }); + + test('factory works with interfaces', () { + container.factory(() => ConsoleLogger()); + + var logger = container.make(); + expect(logger, isA()); + expect(logger, isA()); + }); + + test('factory preserves parameter overrides', () { + var paramValue = ''; + container.factory(() { + paramValue = container.getParameterOverride('level') as String; + return ConsoleLogger(); + }); + + container.withParameters({'level': 'debug'}, () { + container.make(); + }); + + expect(paramValue, equals('debug')); + }); + }); +} + +class MockReflector extends Reflector { + @override + String? getName(Symbol symbol) => null; + + @override + ReflectedClass? reflectClass(Type clazz) { + if (clazz == ConsoleLogger) { + return MockReflectedClass( + 'ConsoleLogger', [], [], [MockConstructor('', [])], ConsoleLogger); + } + return null; + } + + @override + ReflectedType? reflectType(Type type) { + return reflectClass(type); + } + + @override + ReflectedInstance? reflectInstance(Object? instance) => null; + + @override + ReflectedFunction? reflectFunction(Function function) => null; + + @override + ReflectedType reflectFutureOf(Type type) => throw UnimplementedError(); + + @override + Type? findTypeByName(String name) => null; + + @override + ReflectedFunction? findInstanceMethod(Object instance, String methodName) => + null; + + @override + List getAnnotations(Type type) => []; + + @override + List getParameterAnnotations( + Type type, String constructorName, String parameterName) => + []; +} + +class MockConstructor implements ReflectedFunction { + final String constructorName; + final List constructorParameters; + + MockConstructor(this.constructorName, this.constructorParameters); + + @override + List get annotations => []; + + @override + bool get isGetter => false; + + @override + bool get isSetter => false; + + @override + String get name => constructorName; + + @override + List get parameters => constructorParameters; + + @override + ReflectedType? get returnType => null; + + @override + List get typeParameters => []; + + @override + ReflectedInstance invoke(Invocation invocation) => throw UnimplementedError(); +} + +class MockReflectedClass extends ReflectedType implements ReflectedClass { + @override + final List annotations; + @override + final List constructors; + @override + final List declarations; + + MockReflectedClass( + String name, + List typeParameters, + this.annotations, + this.constructors, + Type reflectedType, + ) : declarations = [], + super(reflectedType.toString(), typeParameters, reflectedType); + + @override + ReflectedInstance newInstance( + String constructorName, List positionalArguments, + [Map namedArguments = const {}, + List typeArguments = const []]) { + if (reflectedType == ConsoleLogger) { + return MockReflectedInstance(ConsoleLogger()); + } + throw UnsupportedError('Unknown type: $reflectedType'); + } + + @override + bool isAssignableTo(ReflectedType? other) { + if (reflectedType == other?.reflectedType) { + return true; + } + if (reflectedType == ConsoleLogger && other?.reflectedType == Logger) { + return true; + } + return false; + } +} + +class MockReflectedInstance implements ReflectedInstance { + final dynamic value; + + MockReflectedInstance(this.value); + + @override + ReflectedClass get clazz => throw UnimplementedError(); + + @override + ReflectedInstance getField(String name) => throw UnimplementedError(); + + @override + dynamic get reflectee => value; + + @override + ReflectedType get type => throw UnimplementedError(); +} diff --git a/incubation/container/container/test/wrap_test.dart b/incubation/container/container/test/wrap_test.dart new file mode 100644 index 0000000..63201fa --- /dev/null +++ b/incubation/container/container/test/wrap_test.dart @@ -0,0 +1,306 @@ +import 'package:platformed_container/container.dart'; +import 'package:test/test.dart'; + +abstract class Logger { + void log(String message); +} + +class ConsoleLogger implements Logger { + @override + void log(String message) => print('Console: $message'); +} + +void main() { + late Container container; + + setUp(() { + container = Container(MockReflector()); + }); + + group('Wrap Tests', () { + test('wrap injects dependencies into closure', () { + container.bind(Logger).to(ConsoleLogger); + var messages = []; + + var wrapped = container.wrap((Logger logger, String message) { + messages.add(message); + }); + + wrapped('Hello world'); // Pass message directly + expect(messages, contains('Hello world')); + }); + + test('wrap preserves provided arguments', () { + container.bind(Logger).to(ConsoleLogger); + var messages = []; + + var wrapped = container.wrap((Logger logger, String message) { + messages.add(message); + }); + + wrapped('Custom message'); // Pass message directly + expect(messages, contains('Custom message')); + }); + + test('wrap throws when required dependency is missing', () { + var wrapped = container.wrap((Logger logger) {}); + + expect(() => wrapped(), throwsA(isA())); + }); + + test('wrap works with optional parameters', () { + container.bind(Logger).to(ConsoleLogger); + var messages = []; + + var wrapped = container.wrap((Logger logger, [String? message]) { + messages.add(message ?? 'default'); + }); + + wrapped(); // No arguments needed + expect(messages, contains('default')); + }); + + test('wrap works with named parameters', () { + container.bind(Logger).to(ConsoleLogger); + var messages = []; + + var wrapped = + container.wrap((Logger logger, {String message = 'default'}) { + messages.add(message); + }); + + wrapped(); // No arguments needed + expect(messages, contains('default')); + }); + }); +} + +class MockReflector extends Reflector { + @override + String? getName(Symbol symbol) => null; + + @override + ReflectedClass? reflectClass(Type clazz) { + if (clazz == ConsoleLogger) { + return MockReflectedClass( + 'ConsoleLogger', [], [], [MockConstructor('', [])], ConsoleLogger); + } + return null; + } + + @override + ReflectedType? reflectType(Type type) { + return reflectClass(type); + } + + @override + ReflectedInstance? reflectInstance(Object? instance) => null; + + @override + ReflectedFunction? reflectFunction(Function function) { + // Create mock parameters based on the function's runtime type + var parameters = []; + + // First parameter is always Logger + parameters.add(MockParameter('logger', Logger, true, false)); + + // Add message parameter based on function signature + if (function.toString().contains('String message')) { + parameters.add(MockParameter('message', String, true, false)); + } else if (function.toString().contains('String? message')) { + parameters.add(MockParameter('message', String, false, false)); + } else if (function.toString().contains('{String message')) { + parameters.add(MockParameter('message', String, false, true)); + } + + return MockFunction('', parameters); + } + + @override + ReflectedType reflectFutureOf(Type type) => throw UnimplementedError(); + + @override + Type? findTypeByName(String name) => null; + + @override + ReflectedFunction? findInstanceMethod(Object instance, String methodName) => + null; + + @override + List getAnnotations(Type type) => []; + + @override + List getParameterAnnotations( + Type type, String constructorName, String parameterName) => + []; +} + +class MockConstructor implements ReflectedFunction { + final String constructorName; + final List constructorParameters; + + MockConstructor(this.constructorName, this.constructorParameters); + + @override + List get annotations => []; + + @override + bool get isGetter => false; + + @override + bool get isSetter => false; + + @override + String get name => constructorName; + + @override + List get parameters => constructorParameters; + + @override + ReflectedType? get returnType => null; + + @override + List get typeParameters => []; + + @override + ReflectedInstance invoke(Invocation invocation) => throw UnimplementedError(); +} + +class MockReflectedClass extends ReflectedType implements ReflectedClass { + @override + final List annotations; + @override + final List constructors; + @override + final List declarations; + + MockReflectedClass( + String name, + List typeParameters, + this.annotations, + this.constructors, + Type reflectedType, + ) : declarations = [], + super(reflectedType.toString(), typeParameters, reflectedType); + + @override + ReflectedInstance newInstance( + String constructorName, List positionalArguments, + [Map namedArguments = const {}, + List typeArguments = const []]) { + if (reflectedType == ConsoleLogger) { + return MockReflectedInstance(ConsoleLogger()); + } + throw UnsupportedError('Unknown type: $reflectedType'); + } + + @override + bool isAssignableTo(ReflectedType? other) { + if (reflectedType == other?.reflectedType) { + return true; + } + if (reflectedType == ConsoleLogger && other?.reflectedType == Logger) { + return true; + } + return false; + } +} + +class MockFunction implements ReflectedFunction { + final String functionName; + final List functionParameters; + + MockFunction(this.functionName, this.functionParameters); + + @override + List get annotations => []; + + @override + bool get isGetter => false; + + @override + bool get isSetter => false; + + @override + String get name => functionName; + + @override + List get parameters => functionParameters; + + @override + ReflectedType? get returnType => null; + + @override + List get typeParameters => []; + + @override + ReflectedInstance invoke(Invocation invocation) => throw UnimplementedError(); +} + +class MockParameter implements ReflectedParameter { + @override + final String name; + @override + final bool isRequired; + @override + final bool isNamed; + final Type paramType; + final bool isVariadic; + + MockParameter(this.name, this.paramType, this.isRequired, this.isNamed, + {this.isVariadic = false}); + + @override + List get annotations => []; + + @override + ReflectedType get type => MockReflectedType(paramType); +} + +class MockReflectedType implements ReflectedType { + @override + final String name; + @override + final Type reflectedType; + + MockReflectedType(this.reflectedType) : name = reflectedType.toString(); + + @override + List get typeParameters => []; + + @override + bool isAssignableTo(ReflectedType? other) { + if (reflectedType == other?.reflectedType) { + return true; + } + if (reflectedType == ConsoleLogger && other?.reflectedType == Logger) { + return true; + } + return false; + } + + @override + ReflectedInstance newInstance( + String constructorName, List positionalArguments, + [Map namedArguments = const {}, + List typeArguments = const []]) => + throw UnimplementedError(); +} + +class MockReflectedInstance implements ReflectedInstance { + final dynamic value; + + MockReflectedInstance(this.value); + + @override + ReflectedClass get clazz => throw UnimplementedError(); + + @override + ReflectedInstance getField(String name) => throw UnimplementedError(); + + @override + dynamic get reflectee => value; + + @override + ReflectedType get type => throw UnimplementedError(); +}