refactor: adding parameter dependency injection
This commit is contained in:
parent
a07bd666c2
commit
433da78695
3 changed files with 307 additions and 7 deletions
|
@ -1039,7 +1039,9 @@ class Container {
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// container.call('Logger@log', ['Hello world']);
|
/// container.call('Logger@log', ['Hello world']);
|
||||||
/// ```
|
/// ```
|
||||||
dynamic call(String target, [List<dynamic> parameters = const []]) {
|
dynamic call(String target,
|
||||||
|
[List<dynamic> parameters = const [],
|
||||||
|
Map<Symbol, dynamic> namedParameters = const {}]) {
|
||||||
var parts = target.split('@');
|
var parts = target.split('@');
|
||||||
if (parts.length != 2) {
|
if (parts.length != 2) {
|
||||||
throw ArgumentError('Invalid Class@method syntax: $target');
|
throw ArgumentError('Invalid Class@method syntax: $target');
|
||||||
|
@ -1063,8 +1065,39 @@ class Container {
|
||||||
throw ArgumentError('Method not found: $methodName on $className');
|
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
|
return method
|
||||||
.invoke(Invocation.method(Symbol(methodName), parameters))
|
.invoke(Invocation.method(
|
||||||
|
Symbol(methodName), resolvedParams, namedParameters))
|
||||||
.reflectee;
|
.reflectee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,20 +39,32 @@ class MockReflector extends Reflector {
|
||||||
case 'log':
|
case 'log':
|
||||||
return MockMethod('log', (invocation) {
|
return MockMethod('log', (invocation) {
|
||||||
var args = invocation.positionalArguments;
|
var args = invocation.positionalArguments;
|
||||||
|
if (args.isEmpty) {
|
||||||
|
throw ArgumentError('Method log requires a message parameter');
|
||||||
|
}
|
||||||
return MockReflectedInstance(instance.log(args[0] as String));
|
return MockReflectedInstance(instance.log(args[0] as String));
|
||||||
});
|
}, [MockParameter('message', String, true, false)]);
|
||||||
case 'debug':
|
case 'debug':
|
||||||
return MockMethod('debug', (invocation) {
|
return MockMethod('debug', (invocation) {
|
||||||
var args = invocation.positionalArguments;
|
var args = invocation.positionalArguments;
|
||||||
|
if (args.isEmpty) {
|
||||||
|
throw ArgumentError('Method debug requires a message parameter');
|
||||||
|
}
|
||||||
instance.debug(args[0] as String);
|
instance.debug(args[0] as String);
|
||||||
return MockReflectedInstance(null);
|
return MockReflectedInstance(null);
|
||||||
});
|
}, [
|
||||||
|
MockParameter('message', String, true, false),
|
||||||
|
MockParameter('level', int, false, true)
|
||||||
|
]);
|
||||||
case 'count':
|
case 'count':
|
||||||
return MockMethod('count', (invocation) {
|
return MockMethod('count', (invocation) {
|
||||||
var args = invocation.positionalArguments;
|
var args = invocation.positionalArguments;
|
||||||
|
if (args.isEmpty) {
|
||||||
|
throw ArgumentError('Method count requires a list parameter');
|
||||||
|
}
|
||||||
return MockReflectedInstance(
|
return MockReflectedInstance(
|
||||||
instance.count(args[0] as List<String>));
|
instance.count(args[0] as List<String>));
|
||||||
});
|
}, [MockParameter('items', List<String>, true, false)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -62,8 +74,11 @@ class MockReflector extends Reflector {
|
||||||
class MockMethod implements ReflectedFunction {
|
class MockMethod implements ReflectedFunction {
|
||||||
final String methodName;
|
final String methodName;
|
||||||
final ReflectedInstance Function(Invocation) handler;
|
final ReflectedInstance Function(Invocation) handler;
|
||||||
|
final List<ReflectedParameter> _parameters;
|
||||||
|
|
||||||
MockMethod(this.methodName, this.handler);
|
MockMethod(this.methodName, this.handler,
|
||||||
|
[List<ReflectedParameter>? parameters])
|
||||||
|
: _parameters = parameters ?? [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<ReflectedInstance> get annotations => [];
|
List<ReflectedInstance> get annotations => [];
|
||||||
|
@ -78,7 +93,7 @@ class MockMethod implements ReflectedFunction {
|
||||||
String get name => methodName;
|
String get name => methodName;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<ReflectedParameter> get parameters => [];
|
List<ReflectedParameter> get parameters => _parameters;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ReflectedType? get returnType => null;
|
ReflectedType? get returnType => null;
|
||||||
|
@ -90,6 +105,46 @@ class MockMethod implements ReflectedFunction {
|
||||||
ReflectedInstance invoke(Invocation invocation) => handler(invocation);
|
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<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) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ReflectedInstance newInstance(
|
||||||
|
String constructorName, List positionalArguments,
|
||||||
|
[Map<String, dynamic> namedArguments = const {},
|
||||||
|
List<Type> typeArguments = const []]) =>
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
class MockReflectedInstance implements ReflectedInstance {
|
class MockReflectedInstance implements ReflectedInstance {
|
||||||
final dynamic value;
|
final dynamic value;
|
||||||
|
|
||||||
|
|
|
@ -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<ReflectedParameter> methodParameters;
|
||||||
|
|
||||||
|
MockMethod(this.methodName, this.handler, this.methodParameters);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ReflectedInstance> get annotations => [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isGetter => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isSetter => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => methodName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ReflectedParameter> get parameters => methodParameters;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ReflectedType? get returnType => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ReflectedTypeParameter> 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<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) => 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());
|
||||||
|
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<BindingResolutionException>()));
|
||||||
|
});
|
||||||
|
|
||||||
|
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)'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue