refactor: adding support for attribute binding pass 144 fail 2

This commit is contained in:
Patrick Stewart 2024-12-27 01:10:02 -07:00
parent 103b6e2553
commit 40eadbe408
7 changed files with 785 additions and 2 deletions

View file

@ -7,9 +7,9 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
library platform_container; export 'src/attributes.dart';
export 'src/container.dart'; export 'src/container.dart';
export 'src/contextual_binding_builder.dart';
export 'src/empty/empty.dart'; export 'src/empty/empty.dart';
export 'src/static/static.dart'; export 'src/static/static.dart';
export 'src/exception.dart'; export 'src/exception.dart';

View file

@ -0,0 +1,106 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<dynamic> resolveConstructorParameters(
Type type, String constructorName, List<ReflectedParameter> parameters) {
var result = <dynamic>[];
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<dynamic> 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();
}
}

View file

@ -0,0 +1,58 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String> 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<String, dynamic> 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});
}

View file

@ -8,6 +8,7 @@
*/ */
import 'dart:async'; import 'dart:async';
import 'attributes.dart';
import 'exception.dart'; import 'exception.dart';
import 'reflector.dart'; import 'reflector.dart';
import 'contextual_binding_builder.dart'; import 'contextual_binding_builder.dart';
@ -420,6 +421,11 @@ class Container {
return '' as T; return '' as T;
} }
// Handle List<T> specially
if (t2.toString().startsWith('List<')) {
return [] as T;
}
// Use reflection to create instance // Use reflection to create instance
var reflectedType = reflector.reflectType(t2); var reflectedType = reflector.reflectType(t2);
if (reflectedType == null) { 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<dynamic> resolveConstructorParameters(
Type type, String constructorName, List<ReflectedParameter> parameters) {
var result = <dynamic>[];
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<dynamic> makeAll(Type type) {
var result = <dynamic>[];
// 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;
}
} }

View file

@ -30,6 +30,14 @@ class ContextualBindingBuilder {
ContextualImplementationBuilder needs<T>() { ContextualImplementationBuilder needs<T>() {
return ContextualImplementationBuilder(container, concrete, T); 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. /// 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 /// Specify a factory function that should be used to create the implementation
void giveFactory(dynamic Function(Container container) factory) { void giveFactory(dynamic Function(Container container) factory) {
for (var concreteType in concrete) { for (var concreteType in concrete) {

View file

@ -59,6 +59,25 @@ abstract class Reflector {
ReflectedFunction? findInstanceMethod(Object instance, String methodName) { ReflectedFunction? findInstanceMethod(Object instance, String methodName) {
throw UnsupportedError('`findInstanceMethod` requires `dart:mirrors`.'); 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<ReflectedInstance> 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<ReflectedInstance> getParameterAnnotations(
Type type, String constructorName, String parameterName) {
throw UnsupportedError(
'`getParameterAnnotations` requires `dart:mirrors`.');
}
} }
/// Represents a reflected instance of an object. /// Represents a reflected instance of an object.

View file

@ -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<Logger> 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<Logger>, true, false),
])
],
Service);
}
if (clazz == SingletonService) {
return MockReflectedClass('SingletonService', [], [],
[MockConstructor('', [])], SingletonService);
}
return null;
}
@override
ReflectedType? reflectType(Type type) {
if (type == List<Logger>) {
return MockReflectedClass('List<Logger>', [], [], [], List<Logger>);
}
if (type == Service) {
return MockReflectedClass(
'Service',
[],
[],
[
MockConstructor('', [
MockParameter('consoleLogger', Logger, true, false),
MockParameter('fileLogger', Logger, true, false),
MockParameter('allLoggers', List<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 == 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<ReflectedInstance> 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<ReflectedInstance> 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<ReflectedInstance> annotations;
@override
final List<ReflectedFunction> constructors;
@override
final List<ReflectedDeclaration> declarations;
MockReflectedClass(
String name,
List<ReflectedTypeParameter> typeParameters,
this.annotations,
this.constructors,
Type reflectedType,
) : declarations = [],
super(reflectedType.toString(), typeParameters, reflectedType);
void _validateParameters(List<ReflectedParameter> parameters,
List positionalArguments, Map<String, dynamic> 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<String, dynamic> namedArguments = const {},
List<Type> typeArguments = const []]) {
// Handle List<Logger> specially
if (reflectedType == List<Logger>) {
var loggers = <Logger>[];
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 = <Logger>[];
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<ReflectedParameter> constructorParameters;
MockConstructor(this.constructorName, this.constructorParameters);
@override
List<ReflectedInstance> get annotations => [];
@override
bool get isGetter => false;
@override
bool get isSetter => false;
@override
String get name => constructorName;
@override
List<ReflectedParameter> get parameters => constructorParameters;
@override
ReflectedType? get returnType => null;
@override
List<ReflectedTypeParameter> 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<ReflectedInstance> 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<ReflectedTypeParameter> 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<String, dynamic> namedArguments = const {},
List<Type> 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<Logger>();
expect(logger, isA<ConsoleLogger>());
});
test('can bind implementation using @Injectable with tags', () {
var consoleLogger = container.tagged('console').first;
expect(consoleLogger, isA<ConsoleLogger>());
var fileLogger = container.tagged('file').first;
expect(fileLogger, isA<FileLogger>());
});
test('can inject tagged implementation using @InjectTagged', () {
var service = container.make<Service>();
expect(service.consoleLogger, isA<ConsoleLogger>());
});
test('can inject configured implementation using @Inject', () {
var service = container.make<Service>();
expect(service.fileLogger, isA<FileLogger>());
expect((service.fileLogger as FileLogger).filename, equals('app.log'));
});
test('can inject all implementations using @InjectAll', () {
var service = container.make<Service>();
expect(service.allLoggers, hasLength(2));
expect(service.allLoggers[0], isA<ConsoleLogger>());
expect(service.allLoggers[1], isA<FileLogger>());
});
test('can bind singleton using @Injectable', () {
var first = container.make<SingletonService>();
var second = container.make<SingletonService>();
expect(first.instanceNumber, equals(1));
expect(second.instanceNumber, equals(1));
expect(identical(first, second), isTrue);
});
});
}