diff --git a/incubation/container/container/lib/src/contextual_binding_builder.dart b/incubation/container/container/lib/src/contextual_binding_builder.dart index 07f69e2..ae699bc 100644 --- a/incubation/container/container/lib/src/contextual_binding_builder.dart +++ b/incubation/container/container/lib/src/contextual_binding_builder.dart @@ -8,6 +8,8 @@ */ import 'container.dart'; +import 'exception.dart'; +import 'reflector.dart'; /// A builder class for defining contextual bindings in the container. /// @@ -66,4 +68,43 @@ class ContextualImplementationBuilder { container.addContextualBinding(concreteType, abstract, factory); } } + + /// Specify that the implementation should be resolved from a tagged binding + void giveTagged(String tag) { + giveFactory((container) { + var tagged = container.tagged(tag); + if (tagged.isEmpty) { + throw BindingResolutionException( + 'No implementations found for tag: $tag'); + } + return tagged.first; + }); + } + + /// Specify the implementation type and its configuration + void giveConfig(Type implementation, Map config) { + giveFactory((container) { + // Get reflected type to validate required parameters + var reflectedType = container.reflector.reflectType(implementation); + if (reflectedType is ReflectedClass) { + var constructor = reflectedType.constructors.firstWhere( + (c) => c.name.isEmpty || c.name == reflectedType.name, + orElse: () => reflectedType.constructors.first); + + // Check required parameters + for (var param in constructor.parameters) { + if (param.isRequired && + param.isNamed && + !config.containsKey(param.name)) { + throw BindingResolutionException( + 'Required parameter ${param.name} is missing for ${reflectedType.name}'); + } + } + } + + return container.withParameters(config, () { + return container.make(implementation); + }); + }); + } } diff --git a/incubation/container/container/test/tagged_config_test.dart b/incubation/container/container/test/tagged_config_test.dart new file mode 100644 index 0000000..48bb377 --- /dev/null +++ b/incubation/container/container/test/tagged_config_test.dart @@ -0,0 +1,353 @@ +import 'package:platformed_container/container.dart'; +import 'package:test/test.dart'; + +abstract class Logger { + void log(String message); +} + +class ConsoleLogger implements Logger { + final String level; + ConsoleLogger({this.level = 'info'}); + @override + void log(String message) => print('Console($level): $message'); +} + +class FileLogger implements Logger { + final String filename; + FileLogger({required this.filename}); + @override + void log(String message) => print('File($filename): $message'); +} + +class Service { + final Logger logger; + Service(this.logger); +} + +class MockReflector extends Reflector { + @override + String? getName(Symbol symbol) => null; + + @override + ReflectedClass? reflectClass(Type clazz) { + if (clazz == Service) { + return MockReflectedClass( + 'Service', + [], + [], + [ + MockConstructor('', [ + MockParameter('logger', Logger, true, false), + ]) + ], + Service); + } + return null; + } + + @override + ReflectedType? reflectType(Type type) { + if (type == Service) { + return MockReflectedClass( + 'Service', + [], + [], + [ + MockConstructor('', [ + MockParameter('logger', Logger, true, false), + ]) + ], + Service); + } + if (type == ConsoleLogger) { + return MockReflectedClass( + 'ConsoleLogger', + [], + [], + [ + MockConstructor('', [ + MockParameter('level', String, false, true), + ]) + ], + ConsoleLogger); + } + if (type == FileLogger) { + return MockReflectedClass( + 'FileLogger', + [], + [], + [ + MockConstructor('', [ + MockParameter('filename', String, true, true), + ]) + ], + FileLogger); + } + if (type == Logger) { + return MockReflectedClass( + 'Logger', [], [], [MockConstructor('', [])], Logger); + } + if (type == String) { + return MockReflectedClass( + 'String', [], [], [MockConstructor('', [])], String); + } + return 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) => null; + + @override + ReflectedFunction? findInstanceMethod(Object instance, String methodName) => + null; +} + +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); + + void _validateParameters(List parameters, + List positionalArguments, Map namedArguments) { + var paramIndex = 0; + for (var param in parameters) { + if (param.isNamed) { + if (param.isRequired && !namedArguments.containsKey(param.name)) { + throw BindingResolutionException( + 'Required parameter ${param.name} is missing'); + } + } else { + if (param.isRequired && paramIndex >= positionalArguments.length) { + throw BindingResolutionException( + 'Required parameter ${param.name} is missing'); + } + paramIndex++; + } + } + } + + @override + ReflectedInstance newInstance( + String constructorName, List positionalArguments, + [Map namedArguments = const {}, + List typeArguments = const []]) { + // Find constructor + var constructor = constructors.firstWhere((c) => c.name == constructorName, + orElse: () => constructors.first); + + // Validate parameters + _validateParameters( + constructor.parameters, positionalArguments, namedArguments); + + if (reflectedType == Service) { + return MockReflectedInstance(Service(positionalArguments[0] as Logger)); + } + if (reflectedType == ConsoleLogger) { + return MockReflectedInstance( + ConsoleLogger(level: namedArguments['level'] as String? ?? 'info')); + } + if (reflectedType == FileLogger) { + return MockReflectedInstance( + FileLogger(filename: namedArguments['filename'] as String)); + } + if (reflectedType == Logger) { + throw BindingResolutionException( + 'No implementation was provided for Logger'); + } + throw UnsupportedError('Unknown type: $reflectedType'); + } + + @override + bool isAssignableTo(ReflectedType? other) { + if (reflectedType == ConsoleLogger && other?.reflectedType == Logger) { + return true; + } + if (reflectedType == FileLogger && other?.reflectedType == Logger) { + return true; + } + return false; + } +} + +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 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) { + // Handle primitive types + if (reflectedType == other?.reflectedType) { + 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(); +} + +void main() { + late Container container; + + setUp(() { + container = Container(MockReflector()); + }); + + group('Tagged and Config Tests', () { + test('can bind implementation from tagged type', () { + container.tag([ConsoleLogger], 'loggers'); + container.when([Service]).needs().giveTagged('loggers'); + + var service = container.make(); + expect(service.logger, isA()); + }); + + test('throws when tag has no implementations', () { + container.when([Service]).needs().giveTagged('loggers'); + + expect( + () => container.make(), + throwsA(predicate((e) => + e is BindingResolutionException && + e.toString().contains('No implementations found for tag')))); + }); + + test('can bind implementation with config', () { + container + .when([Service]) + .needs() + .giveConfig(ConsoleLogger, {'level': 'debug'}); + + var service = container.make(); + expect(service.logger, isA()); + expect((service.logger as ConsoleLogger).level, equals('debug')); + }); + + test('throws when required config is missing', () { + container.when([Service]).needs().giveConfig(FileLogger, {}); + + expect( + () => container.make(), + throwsA(predicate((e) => + e is BindingResolutionException && + e.toString().contains( + 'Required parameter filename is missing for FileLogger')))); + }); + + test('can mix tagged and config bindings', () { + container.tag([ConsoleLogger], 'console'); + container.tag([FileLogger], 'file'); + + container.when([Service]).needs().giveTagged('console'); + container + .when([Service]) + .needs() + .giveConfig(FileLogger, {'filename': 'app.log'}); + + var service = container.make(); + expect(service.logger, isA()); + expect((service.logger as FileLogger).filename, equals('app.log')); + }); + }); +}