diff --git a/incubation/container/container/lib/src/container.dart b/incubation/container/container/lib/src/container.dart index a147325..83b2bbc 100644 --- a/incubation/container/container/lib/src/container.dart +++ b/incubation/container/container/lib/src/container.dart @@ -1039,7 +1039,9 @@ class Container { /// ```dart /// container.call('Logger@log', ['Hello world']); /// ``` - dynamic call(String target, [List parameters = const []]) { + dynamic call(String target, + [List parameters = const [], + Map namedParameters = const {}]) { var parts = target.split('@'); if (parts.length != 2) { throw ArgumentError('Invalid Class@method syntax: $target'); @@ -1063,8 +1065,39 @@ class Container { throw ArgumentError('Method not found: $methodName on $className'); } + // Get method parameters + var methodParams = method.parameters; + var resolvedParams = []; + var paramIndex = 0; + + // Resolve each parameter + for (var param in methodParams) { + // If a value was provided for this parameter position, use it + if (paramIndex < parameters.length) { + var value = parameters[paramIndex++]; + // If null was provided and we can resolve from container, do so + if (value == null && has(param.type.reflectedType)) { + resolvedParams.add(make(param.type.reflectedType)); + } else { + resolvedParams.add(value); + } + continue; + } + + // Otherwise try to resolve from container + var paramType = param.type.reflectedType; + if (has(paramType)) { + resolvedParams.add(make(paramType)); + } else if (param.isRequired) { + throw BindingResolutionException( + 'No value provided for required parameter ${param.name} of type $paramType in $className@$methodName'); + } + } + + // Call the method with resolved parameters return method - .invoke(Invocation.method(Symbol(methodName), parameters)) + .invoke(Invocation.method( + Symbol(methodName), resolvedParams, namedParameters)) .reflectee; } diff --git a/incubation/container/container/test/class_method_syntax_test.dart b/incubation/container/container/test/class_method_syntax_test.dart index 769fc4a..b2d4719 100644 --- a/incubation/container/container/test/class_method_syntax_test.dart +++ b/incubation/container/container/test/class_method_syntax_test.dart @@ -39,20 +39,32 @@ class MockReflector extends Reflector { case 'log': return MockMethod('log', (invocation) { var args = invocation.positionalArguments; + if (args.isEmpty) { + throw ArgumentError('Method log requires a message parameter'); + } return MockReflectedInstance(instance.log(args[0] as String)); - }); + }, [MockParameter('message', String, true, false)]); case 'debug': return MockMethod('debug', (invocation) { var args = invocation.positionalArguments; + if (args.isEmpty) { + throw ArgumentError('Method debug requires a message parameter'); + } instance.debug(args[0] as String); return MockReflectedInstance(null); - }); + }, [ + MockParameter('message', String, true, false), + MockParameter('level', int, false, true) + ]); case 'count': return MockMethod('count', (invocation) { var args = invocation.positionalArguments; + if (args.isEmpty) { + throw ArgumentError('Method count requires a list parameter'); + } return MockReflectedInstance( instance.count(args[0] as List)); - }); + }, [MockParameter('items', List, true, false)]); } } return null; @@ -62,8 +74,11 @@ class MockReflector extends Reflector { class MockMethod implements ReflectedFunction { final String methodName; final ReflectedInstance Function(Invocation) handler; + final List _parameters; - MockMethod(this.methodName, this.handler); + MockMethod(this.methodName, this.handler, + [List? parameters]) + : _parameters = parameters ?? []; @override List get annotations => []; @@ -78,7 +93,7 @@ class MockMethod implements ReflectedFunction { String get name => methodName; @override - List get parameters => []; + List get parameters => _parameters; @override ReflectedType? get returnType => null; @@ -90,6 +105,46 @@ class MockMethod implements ReflectedFunction { ReflectedInstance invoke(Invocation invocation) => handler(invocation); } +class MockParameter implements ReflectedParameter { + @override + final String name; + @override + final bool isRequired; + @override + final bool isNamed; + final Type paramType; + + MockParameter(this.name, this.paramType, this.isRequired, this.isNamed); + + @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) => false; + + @override + ReflectedInstance newInstance( + String constructorName, List positionalArguments, + [Map namedArguments = const {}, + List typeArguments = const []]) => + throw UnimplementedError(); +} + class MockReflectedInstance implements ReflectedInstance { final dynamic value; diff --git a/incubation/container/container/test/parameter_injection_test.dart b/incubation/container/container/test/parameter_injection_test.dart new file mode 100644 index 0000000..9f90050 --- /dev/null +++ b/incubation/container/container/test/parameter_injection_test.dart @@ -0,0 +1,212 @@ +import 'package:platformed_container/container.dart'; +import 'package:test/test.dart'; + +class Config { + final String environment; + Config(this.environment); +} + +class Logger { + String log(String message) => message; + void configure(Config config) {} + String format(String message, {int? level}) => '$message (level: $level)'; + void setup(Config config, String name) {} +} + +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(); + + @override + Type? findTypeByName(String name) { + if (name == 'Logger') return Logger; + return null; + } + + @override + ReflectedFunction? findInstanceMethod(Object instance, String methodName) { + if (instance is Logger) { + switch (methodName) { + case 'log': + return MockMethod('log', (invocation) { + var args = invocation.positionalArguments; + return MockReflectedInstance(instance.log(args[0] as String)); + }, [MockParameter('message', String, true, false)]); + case 'configure': + return MockMethod('configure', (invocation) { + var args = invocation.positionalArguments; + instance.configure(args[0] as Config); + return MockReflectedInstance(null); + }, [MockParameter('config', Config, true, false)]); + case 'format': + return MockMethod('format', (invocation) { + var args = invocation.positionalArguments; + var namedArgs = invocation.namedArguments; + return MockReflectedInstance(instance.format(args[0] as String, + level: namedArgs[#level] as int?)); + }, [ + MockParameter('message', String, true, false), + MockParameter('level', int, false, true) + ]); + case 'setup': + return MockMethod('setup', (invocation) { + var args = invocation.positionalArguments; + instance.setup(args[0] as Config, args[1] as String); + return MockReflectedInstance(null); + }, [ + MockParameter('config', Config, true, false), + MockParameter('name', String, true, false) + ]); + } + } + return null; + } +} + +class MockMethod implements ReflectedFunction { + final String methodName; + final ReflectedInstance Function(Invocation) handler; + final List methodParameters; + + MockMethod(this.methodName, this.handler, this.methodParameters); + + @override + List get annotations => []; + + @override + bool get isGetter => false; + + @override + bool get isSetter => false; + + @override + String get name => methodName; + + @override + List get parameters => methodParameters; + + @override + ReflectedType? get returnType => null; + + @override + List get typeParameters => []; + + @override + ReflectedInstance invoke(Invocation invocation) => handler(invocation); +} + +class MockParameter implements ReflectedParameter { + @override + final String name; + @override + final bool isRequired; + @override + final bool isNamed; + final Type paramType; + + MockParameter(this.name, this.paramType, this.isRequired, this.isNamed); + + @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) => 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(); +} + +void main() { + late Container container; + + setUp(() { + container = Container(MockReflector()); + container.registerSingleton(Logger()); + container.registerSingleton(Config('test')); + }); + + group('Parameter Dependency Injection', () { + test('can inject dependencies into method parameters', () { + expect(() => container.call('Logger@configure'), returnsNormally); + }); + + test('uses provided parameters over container bindings', () { + var prodConfig = Config('production'); + container.call('Logger@configure', [prodConfig]); + }); + + test('throws when required parameter is missing', () { + expect(() => container.call('Logger@setup', [Config('test')]), + throwsA(isA())); + }); + + test('handles mix of injected and provided parameters', () { + // When null is provided for a parameter that can be resolved from container, + // the container should resolve it + container.call('Logger@setup', [null, 'test-logger']); + }); + + test('handles optional parameters', () { + var result = container.call('Logger@format', ['test message']); + expect(result, equals('test message (level: null)')); + }); + + test('handles optional parameters with provided values', () { + var result = + container.call('Logger@format', ['test message'], {#level: 1}); + expect(result, equals('test message (level: 1)')); + }); + }); +}