diff --git a/incubation/container/container/lib/src/container.dart b/incubation/container/container/lib/src/container.dart index 83b2bbc..059a85f 100644 --- a/incubation/container/container/lib/src/container.dart +++ b/incubation/container/container/lib/src/container.dart @@ -1072,16 +1072,24 @@ class Container { // 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); + // Handle variadic parameters + if (param.isVariadic) { + // Collect all remaining parameters into a list + var variadicArgs = parameters.skip(paramIndex).toList(); + resolvedParams.add(variadicArgs); + break; // Variadic parameter must be last + } else { + // 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; } - continue; } // Otherwise try to resolve from container diff --git a/incubation/container/container/lib/src/reflector.dart b/incubation/container/container/lib/src/reflector.dart index a9bd47c..7dec960 100644 --- a/incubation/container/container/lib/src/reflector.dart +++ b/incubation/container/container/lib/src/reflector.dart @@ -284,13 +284,15 @@ class ReflectedParameter { final ReflectedType type; final bool isRequired; final bool isNamed; + final bool isVariadic; const ReflectedParameter( - this.name, this.annotations, this.type, this.isRequired, this.isNamed); + this.name, this.annotations, this.type, this.isRequired, this.isNamed, + {this.isVariadic = false}); @override int get hashCode => - hashObjects([name, annotations, type, isRequired, isNamed]); + hashObjects([name, annotations, type, isRequired, isNamed, isVariadic]); @override bool operator ==(other) => @@ -300,7 +302,8 @@ class ReflectedParameter { .equals(other.annotations, annotations) && other.type == type && other.isRequired == isRequired && - other.isNamed == isNamed; + other.isNamed == isNamed && + other.isVariadic == isVariadic; } class ReflectedTypeParameter { diff --git a/incubation/container/container/test/class_method_syntax_test.dart b/incubation/container/container/test/class_method_syntax_test.dart index b2d4719..faa236d 100644 --- a/incubation/container/container/test/class_method_syntax_test.dart +++ b/incubation/container/container/test/class_method_syntax_test.dart @@ -112,9 +112,11 @@ class MockParameter implements ReflectedParameter { final bool isRequired; @override final bool isNamed; + final bool isVariadic; final Type paramType; - MockParameter(this.name, this.paramType, this.isRequired, this.isNamed); + MockParameter(this.name, this.paramType, this.isRequired, this.isNamed, + {this.isVariadic = false}); @override List get annotations => []; diff --git a/incubation/container/container/test/parameter_injection_test.dart b/incubation/container/container/test/parameter_injection_test.dart index 9f90050..d1973fb 100644 --- a/incubation/container/container/test/parameter_injection_test.dart +++ b/incubation/container/container/test/parameter_injection_test.dart @@ -117,9 +117,11 @@ class MockParameter implements ReflectedParameter { final bool isRequired; @override final bool isNamed; + final bool isVariadic; final Type paramType; - MockParameter(this.name, this.paramType, this.isRequired, this.isNamed); + MockParameter(this.name, this.paramType, this.isRequired, this.isNamed, + {this.isVariadic = false}); @override List get annotations => []; diff --git a/incubation/container/container/test/variadic_parameter_test.dart b/incubation/container/container/test/variadic_parameter_test.dart new file mode 100644 index 0000000..190dec6 --- /dev/null +++ b/incubation/container/container/test/variadic_parameter_test.dart @@ -0,0 +1,223 @@ +import 'package:platformed_container/container.dart'; +import 'package:test/test.dart'; + +class Logger { + String log(String prefix, String message) => '$prefix: $message'; + String logMany(String prefix, List messages) => + '$prefix: ${messages.join(", ")}'; + String format(String message, {List tags = const []}) => + '$message [${tags.join(", ")}]'; +} + +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; + if (args.length < 2) { + throw ArgumentError( + 'Method log requires prefix and message parameters'); + } + return MockReflectedInstance( + instance.log(args[0] as String, args[1] as String)); + }, [ + MockParameter('prefix', String, true, false), + MockParameter('message', String, true, false) + ]); + case 'logMany': + return MockMethod('logMany', (invocation) { + var args = invocation.positionalArguments; + if (args.isEmpty) { + throw ArgumentError('Method logMany requires a prefix parameter'); + } + var prefix = args[0] as String; + var messages = args.length > 1 + ? args.skip(1).map((e) => e.toString()).toList() + : []; + return MockReflectedInstance(instance.logMany(prefix, messages)); + }, [ + MockParameter('prefix', String, true, false), + MockParameter('messages', List, true, false, + isVariadic: true) + ]); + case 'format': + return MockMethod('format', (invocation) { + var args = invocation.positionalArguments; + var namedArgs = invocation.namedArguments; + if (args.isEmpty) { + throw ArgumentError('Method format requires a message parameter'); + } + var tags = (namedArgs[#tags] as List?) + ?.map((e) => e.toString()) + .toList() ?? + const []; + return MockReflectedInstance( + instance.format(args[0] as String, tags: tags)); + }, [ + MockParameter('message', String, true, false), + MockParameter('tags', List, false, true, isVariadic: true) + ]); + } + } + return null; + } +} + +class MockMethod implements ReflectedFunction { + final String methodName; + final ReflectedInstance Function(Invocation) handler; + final List _parameters; + + MockMethod(this.methodName, this.handler, + [List? parameters]) + : _parameters = parameters ?? []; + + @override + List get annotations => []; + + @override + bool get isGetter => false; + + @override + bool get isSetter => false; + + @override + String get name => methodName; + + @override + List get parameters => _parameters; + + @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; + 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) => 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()); + }); + + group('Variadic Parameter Tests', () { + test('can call method with variadic positional parameters', () { + var result = container.call('Logger@logMany', + ['INFO', 'first message', 'second message', 'third message']); + expect(result, + equals('INFO: [first message, second message, third message]')); + }); + + test('can call method with variadic named parameters', () { + var result = container.call('Logger@format', [ + 'Hello world' + ], { + #tags: ['info', 'debug', 'test'] + }); + expect(result, equals('Hello world [info, debug, test]')); + }); + + test('variadic parameters are optional', () { + var result = container.call('Logger@format', ['Hello world']); + expect(result, equals('Hello world []')); + }); + + test('can mix regular and variadic parameters', () { + var result = + container.call('Logger@logMany', ['DEBUG', 'single message']); + expect(result, equals('DEBUG: [single message]')); + }); + }); +}