diff --git a/incubation/container/container/lib/container.dart b/incubation/container/container/lib/container.dart index db227c5..0978765 100644 --- a/incubation/container/container/lib/container.dart +++ b/incubation/container/container/lib/container.dart @@ -7,9 +7,9 @@ * file that was distributed with this source code. */ -library platform_container; - +export 'src/attributes.dart'; export 'src/container.dart'; +export 'src/contextual_binding_builder.dart'; export 'src/empty/empty.dart'; export 'src/static/static.dart'; export 'src/exception.dart'; diff --git a/incubation/container/container/lib/src/attribute_binding.dart b/incubation/container/container/lib/src/attribute_binding.dart new file mode 100644 index 0000000..3365ec8 --- /dev/null +++ b/incubation/container/container/lib/src/attribute_binding.dart @@ -0,0 +1,106 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'attributes.dart'; +import 'container.dart'; +import 'reflector.dart'; + +/// Extension methods for attribute-based binding support +extension AttributeBindingExtension on Container { + /// Register all attribute-based bindings for a type + void registerAttributeBindings(Type type) { + var annotations = reflector.getAnnotations(type); + for (var annotation in annotations) { + var value = annotation.reflectee; + if (value is Injectable) { + // Register the binding + if (value.bindTo != null) { + bind(value.bindTo!).to(type); + } + + // Apply tags + if (value.tags.isNotEmpty) { + tag([type], value.tags.join(',')); + } + + // Make it a singleton if requested + if (value.singleton) { + singleton(type); + } + } + } + } + + /// Resolve constructor parameters using attribute-based injection + List resolveConstructorParameters( + Type type, String constructorName, List parameters) { + var result = []; + + for (var param in parameters) { + var annotations = + reflector.getParameterAnnotations(type, constructorName, param.name); + + // Find injection annotation + ReflectedInstance? injectAnnotation; + try { + injectAnnotation = annotations.firstWhere( + (a) => a.reflectee is Inject || a.reflectee is InjectTagged); + } catch (_) { + try { + injectAnnotation = + annotations.firstWhere((a) => a.reflectee is InjectAll); + } catch (_) { + // No injection annotation found + } + } + + if (injectAnnotation != null) { + var value = injectAnnotation.reflectee; + if (value is Inject) { + // Inject specific implementation with config + result.add( + withParameters(value.config, () => make(value.implementation))); + } else if (value is InjectTagged) { + // Inject tagged implementation + var tagged = this.tagged(value.tag); + if (tagged.isEmpty) { + throw Exception('No implementations found for tag: ${value.tag}'); + } + result.add(tagged.first); + } else if (value is InjectAll) { + // Inject all implementations + if (value.tag != null) { + result.add(tagged(value.tag!).toList()); + } else { + result.add(makeAll(param.type.reflectedType)); + } + } + } else { + // No injection annotation, use default resolution + result.add(make(param.type.reflectedType)); + } + } + + return result; + } + + /// Make all instances of a type + List makeAll(Type type) { + var reflectedType = reflector.reflectType(type); + if (reflectedType == null) { + throw Exception('Type not found: $type'); + } + + return reflector + .getAnnotations(type) + .where((a) => a.reflectee is Injectable) + .map((a) => make(type)) + .toList(); + } +} diff --git a/incubation/container/container/lib/src/attributes.dart b/incubation/container/container/lib/src/attributes.dart new file mode 100644 index 0000000..d89a5f4 --- /dev/null +++ b/incubation/container/container/lib/src/attributes.dart @@ -0,0 +1,58 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/// Base class for all container binding attributes +abstract class BindingAttribute { + const BindingAttribute(); +} + +/// Marks a class as injectable and optionally specifies how it should be bound +class Injectable extends BindingAttribute { + /// The type to bind this implementation to (usually an interface) + final Type? bindTo; + + /// Whether this should be bound as a singleton + final bool singleton; + + /// Tags that can be used to identify this implementation + final List tags; + + const Injectable({ + this.bindTo, + this.singleton = false, + this.tags = const [], + }); +} + +/// Marks a parameter as requiring a specific implementation +class Inject extends BindingAttribute { + /// The implementation type to inject + final Type implementation; + + /// Configuration parameters for the implementation + final Map config; + + const Inject(this.implementation, {this.config = const {}}); +} + +/// Marks a parameter as requiring a tagged implementation +class InjectTagged extends BindingAttribute { + /// The tag to use when resolving the implementation + final String tag; + + const InjectTagged(this.tag); +} + +/// Marks a parameter as requiring all implementations of a type +class InjectAll extends BindingAttribute { + /// Optional tag to filter implementations + final String? tag; + + const InjectAll({this.tag}); +} diff --git a/incubation/container/container/lib/src/container.dart b/incubation/container/container/lib/src/container.dart index 8863874..f6870f5 100644 --- a/incubation/container/container/lib/src/container.dart +++ b/incubation/container/container/lib/src/container.dart @@ -8,6 +8,7 @@ */ import 'dart:async'; +import 'attributes.dart'; import 'exception.dart'; import 'reflector.dart'; import 'contextual_binding_builder.dart'; @@ -420,6 +421,11 @@ class Container { return '' as T; } + // Handle List specially + if (t2.toString().startsWith('List<')) { + return [] as T; + } + // Use reflection to create instance var reflectedType = reflector.reflectType(t2); if (reflectedType == null) { @@ -1132,4 +1138,127 @@ class Container { ); } } + + /// Bind an abstract type to a concrete implementation + ContextualBindingBuilder bind(Type abstract) { + return ContextualBindingBuilder(this, [abstract]); + } + + /// Register a singleton type and initialize it + void singleton(Type type) { + if (!_singletons.containsKey(type)) { + var instance = make(type); + _singletons[type] = instance; + } + } + + /// Helper method to bind a concrete type to an abstract type + void bindTo(Type abstract, Type concrete) { + bind(abstract).to(concrete); + } + + /// Register all attribute-based bindings for a type + void registerAttributeBindings(Type type) { + var annotations = reflector.getAnnotations(type); + for (var annotation in annotations) { + var value = annotation.reflectee; + if (value is Injectable) { + // Register the binding + if (value.bindTo != null) { + bind(value.bindTo!).to(type); + + // Apply tags to both the concrete type and the abstract type + if (value.tags.isNotEmpty) { + for (var tag in value.tags) { + _tags[tag] ??= []; + _tags[tag]!.add(type); + _tags[tag]!.add(value.bindTo!); + } + } + } else { + // Apply tags to just the concrete type + if (value.tags.isNotEmpty) { + for (var tag in value.tags) { + _tags[tag] ??= []; + _tags[tag]!.add(type); + } + } + } + + // Make it a singleton if requested + if (value.singleton) { + singleton(type); + } + } + } + } + + /// Resolve constructor parameters using attribute-based injection + List resolveConstructorParameters( + Type type, String constructorName, List parameters) { + var result = []; + + for (var param in parameters) { + var annotations = + reflector.getParameterAnnotations(type, constructorName, param.name); + + // Find injection annotation + ReflectedInstance? injectAnnotation; + try { + injectAnnotation = annotations.firstWhere( + (a) => a.reflectee is Inject || a.reflectee is InjectTagged); + } catch (_) { + try { + injectAnnotation = + annotations.firstWhere((a) => a.reflectee is InjectAll); + } catch (_) {} + } + + if (injectAnnotation != null) { + var value = injectAnnotation.reflectee; + if (value is Inject) { + // Inject specific implementation with config + result.add( + withParameters(value.config, () => make(value.implementation))); + } else if (value is InjectTagged) { + // Inject tagged implementation + var tagged = this.tagged(value.tag); + if (tagged.isEmpty) { + throw BindingResolutionException( + 'No implementations found for tag: ${value.tag}'); + } + result.add(tagged.first); + } else if (value is InjectAll) { + // Inject all implementations + if (value.tag != null) { + result.add(tagged(value.tag!).toList()); + } else { + result.add(makeAll(param.type.reflectedType)); + } + } + } else { + // No injection annotation, use default resolution + result.add(make(param.type.reflectedType)); + } + } + + return result; + } + + /// Make all instances of a type + List makeAll(Type type) { + var result = []; + + // Get all tagged implementations + var allTags = _tags.entries + .where((entry) => entry.value.any((t) => t == type)) + .map((entry) => entry.key) + .toList(); + + for (var tag in allTags) { + result.addAll(tagged(tag)); + } + + return result; + } } diff --git a/incubation/container/container/lib/src/contextual_binding_builder.dart b/incubation/container/container/lib/src/contextual_binding_builder.dart index ae699bc..8aeae90 100644 --- a/incubation/container/container/lib/src/contextual_binding_builder.dart +++ b/incubation/container/container/lib/src/contextual_binding_builder.dart @@ -30,6 +30,14 @@ class ContextualBindingBuilder { ContextualImplementationBuilder needs() { return ContextualImplementationBuilder(container, concrete, T); } + + /// Bind directly to a concrete implementation + void to(Type implementation) { + for (var concreteType in concrete) { + container.addContextualBinding( + concreteType, concreteType, implementation); + } + } } /// A builder class for defining the implementation for a contextual binding. @@ -62,6 +70,13 @@ class ContextualImplementationBuilder { } } + /// Bind to a concrete implementation type + void to(Type implementation) { + for (var concreteType in concrete) { + container.addContextualBinding(concreteType, abstract, implementation); + } + } + /// Specify a factory function that should be used to create the implementation void giveFactory(dynamic Function(Container container) factory) { for (var concreteType in concrete) { diff --git a/incubation/container/container/lib/src/reflector.dart b/incubation/container/container/lib/src/reflector.dart index 7dec960..0c08bd9 100644 --- a/incubation/container/container/lib/src/reflector.dart +++ b/incubation/container/container/lib/src/reflector.dart @@ -59,6 +59,25 @@ abstract class Reflector { ReflectedFunction? findInstanceMethod(Object instance, String methodName) { throw UnsupportedError('`findInstanceMethod` requires `dart:mirrors`.'); } + + /// Get annotations for a type. + /// + /// This method returns a list of reflected instances representing the annotations + /// applied to the given type. + List getAnnotations(Type type) { + throw UnsupportedError('`getAnnotations` requires `dart:mirrors`.'); + } + + /// Get annotations for a parameter. + /// + /// This method returns a list of reflected instances representing the annotations + /// applied to the parameter with the given name in the specified constructor of + /// the given type. + List getParameterAnnotations( + Type type, String constructorName, String parameterName) { + throw UnsupportedError( + '`getParameterAnnotations` requires `dart:mirrors`.'); + } } /// Represents a reflected instance of an object. diff --git a/incubation/container/container/test/attribute_binding_test.dart b/incubation/container/container/test/attribute_binding_test.dart new file mode 100644 index 0000000..eeb899c --- /dev/null +++ b/incubation/container/container/test/attribute_binding_test.dart @@ -0,0 +1,456 @@ +import 'package:platformed_container/container.dart'; +import 'package:test/test.dart'; + +abstract class Logger { + void log(String message); +} + +@Injectable(bindTo: Logger, tags: ['console']) +class ConsoleLogger implements Logger { + final String level; + + ConsoleLogger({this.level = 'info'}); + + @override + void log(String message) => print('Console($level): $message'); +} + +@Injectable(bindTo: Logger, tags: ['file']) +class FileLogger implements Logger { + final String filename; + + FileLogger({required this.filename}); + + @override + void log(String message) => print('File($filename): $message'); +} + +class Service { + final Logger consoleLogger; + final Logger fileLogger; + final List allLoggers; + + Service( + @InjectTagged('console') this.consoleLogger, + @Inject(FileLogger, config: {'filename': 'app.log'}) this.fileLogger, + @InjectAll() this.allLoggers, + ); + + void logMessage(String message) { + for (var logger in allLoggers) { + logger.log(message); + } + } +} + +@Injectable(singleton: true) +class SingletonService { + static int instanceCount = 0; + final int instanceNumber; + + SingletonService() : instanceNumber = ++instanceCount; +} + +class MockReflector extends Reflector { + @override + String? getName(Symbol symbol) => null; + + @override + ReflectedClass? reflectClass(Type clazz) { + if (clazz == Service) { + return MockReflectedClass( + 'Service', + [], + [], + [ + MockConstructor('', [ + MockParameter('consoleLogger', Logger, true, false), + MockParameter('fileLogger', Logger, true, false), + MockParameter('allLoggers', List, true, false), + ]) + ], + Service); + } + if (clazz == SingletonService) { + return MockReflectedClass('SingletonService', [], [], + [MockConstructor('', [])], SingletonService); + } + return null; + } + + @override + ReflectedType? reflectType(Type type) { + if (type == List) { + return MockReflectedClass('List', [], [], [], List); + } + if (type == Service) { + return MockReflectedClass( + 'Service', + [], + [], + [ + MockConstructor('', [ + MockParameter('consoleLogger', Logger, true, false), + MockParameter('fileLogger', Logger, true, false), + MockParameter('allLoggers', List, 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 == SingletonService) { + return MockReflectedClass('SingletonService', [], [], + [MockConstructor('', [])], SingletonService); + } + 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; + + @override + List getAnnotations(Type type) { + if (type == ConsoleLogger) { + return [ + MockReflectedInstance(Injectable(bindTo: Logger, tags: ['console'])) + ]; + } + if (type == FileLogger) { + return [ + MockReflectedInstance(Injectable(bindTo: Logger, tags: ['file'])) + ]; + } + if (type == SingletonService) { + return [MockReflectedInstance(Injectable(singleton: true))]; + } + return []; + } + + @override + List getParameterAnnotations( + Type type, String constructorName, String parameterName) { + if (type == Service) { + if (parameterName == 'consoleLogger') { + return [MockReflectedInstance(InjectTagged('console'))]; + } + if (parameterName == 'fileLogger') { + return [ + MockReflectedInstance( + Inject(FileLogger, config: {'filename': 'app.log'})) + ]; + } + if (parameterName == 'allLoggers') { + return [MockReflectedInstance(InjectAll())]; + } + } + return []; + } +} + +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 []]) { + // Handle List specially + if (reflectedType == List) { + var loggers = []; + for (var arg in positionalArguments) { + if (arg is Logger) { + loggers.add(arg); + } + } + return MockReflectedInstance(loggers); + } + + // Find constructor + var constructor = constructors.firstWhere((c) => c.name == constructorName, + orElse: () => constructors.first); + + // Validate parameters + _validateParameters( + constructor.parameters, positionalArguments, namedArguments); + + if (reflectedType == Service) { + var loggers = []; + if (positionalArguments[2] is List) { + for (var item in positionalArguments[2] as List) { + if (item is Logger) { + loggers.add(item); + } + } + } + return MockReflectedInstance(Service( + positionalArguments[0] as Logger, + positionalArguments[1] as Logger, + loggers, + )); + } + 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 == SingletonService) { + return MockReflectedInstance(SingletonService()); + } + 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('Attribute Binding Tests', () { + setUp(() { + // Reset instance count + SingletonService.instanceCount = 0; + + // Register implementations + container.registerAttributeBindings(ConsoleLogger); + container.registerAttributeBindings(FileLogger); + container.registerAttributeBindings(SingletonService); + + // Set ConsoleLogger as default implementation for Logger + container.bind(Logger).to(ConsoleLogger); + }); + + test('can bind implementation using @Injectable', () { + var logger = container.make(); + expect(logger, isA()); + }); + + test('can bind implementation using @Injectable with tags', () { + var consoleLogger = container.tagged('console').first; + expect(consoleLogger, isA()); + + var fileLogger = container.tagged('file').first; + expect(fileLogger, isA()); + }); + + test('can inject tagged implementation using @InjectTagged', () { + var service = container.make(); + expect(service.consoleLogger, isA()); + }); + + test('can inject configured implementation using @Inject', () { + var service = container.make(); + expect(service.fileLogger, isA()); + expect((service.fileLogger as FileLogger).filename, equals('app.log')); + }); + + test('can inject all implementations using @InjectAll', () { + var service = container.make(); + expect(service.allLoggers, hasLength(2)); + expect(service.allLoggers[0], isA()); + expect(service.allLoggers[1], isA()); + }); + + test('can bind singleton using @Injectable', () { + var first = container.make(); + var second = container.make(); + expect(first.instanceNumber, equals(1)); + expect(second.instanceNumber, equals(1)); + expect(identical(first, second), isTrue); + }); + }); +}