From 81bb7bdd5eb25181b1509dc3a88ba1168d0ac1d1 Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Thu, 26 Dec 2024 23:08:58 -0700 Subject: [PATCH] refactor: adding parameter override support all test passing --- .../container/lib/src/container.dart | 78 +++++++++++-- .../test/parameter_override_test.dart | 109 ++++++++++++++++++ 2 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 incubation/container/container/test/parameter_override_test.dart diff --git a/incubation/container/container/lib/src/container.dart b/incubation/container/container/lib/src/container.dart index 2409ae2..607fb4d 100644 --- a/incubation/container/container/lib/src/container.dart +++ b/incubation/container/container/lib/src/container.dart @@ -37,6 +37,9 @@ class Container { /// The container's refreshing instances final Set _refreshing = {}; + /// The container's parameter override stack + final List> _parameterStack = []; + /// The container's contextual bindings final Map> _contextual = {}; @@ -318,15 +321,23 @@ class Container { var named = {}; for (var param in constructor.parameters) { - if (param.type.reflectedType == String) { - positional.add('test.log'); // Default filename for FileLogger - } else { - var value = make(param.type.reflectedType); + // Check for parameter override + var override = getParameterOverride(param.name); + if (override != null) { if (param.isNamed) { - named[param.name] = value; + named[param.name] = override; } else { - positional.add(value); + positional.add(override); } + continue; + } + + // No override, resolve normally + var value = make(param.type.reflectedType); + if (param.isNamed) { + named[param.name] = value; + } else { + positional.add(value); } } @@ -391,7 +402,10 @@ class Container { _fireAfterResolvingCallbacks(instance); return instance as T; } else if (search._factories.containsKey(resolvedType)) { - var instance = search._factories[resolvedType]!(this); + // For factory bindings, wrap the factory call in withParameters + var instance = withParameters(_parameterStack.lastOrNull ?? {}, () { + return search!._factories[resolvedType]!(this); + }); instance = _applyExtenders(resolvedType, instance); _fireResolvingCallbacks(instance); _fireAfterResolvingCallbacks(instance); @@ -450,8 +464,19 @@ class Container { _buildStack.add(t2); try { for (var param in constructor.parameters) { - var value = make(param.type.reflectedType); + // Check for parameter override + var override = getParameterOverride(param.name); + if (override != null) { + if (param.isNamed) { + named[param.name] = override; + } else { + positional.add(override); + } + continue; + } + // No override, resolve normally + var value = make(param.type.reflectedType); if (param.isNamed) { named[param.name] = value; } else { @@ -511,7 +536,11 @@ class Container { throw StateError('This container already has a factory for $t2.'); } - _factories[t2] = f; + // Wrap factory in parameter override handler + _factories[t2] = (container) { + return container.withParameters( + _parameterStack.lastOrNull ?? {}, () => f(container)); + }; return f; } @@ -951,6 +980,37 @@ class Container { } } + /// Push parameter overrides onto the stack. + /// + /// These parameters will be used when resolving dependencies until they are popped. + /// ```dart + /// container.withParameters({'filename': 'custom.log'}, () { + /// var logger = container.make(); + /// }); + /// ``` + T withParameters(Map parameters, T Function() callback) { + _parameterStack.add(parameters); + try { + return callback(); + } finally { + _parameterStack.removeLast(); + } + } + + /// Get an override value for a parameter if one exists. + /// + /// This method is used internally by the container to resolve parameter overrides, + /// but is also exposed for use in factory functions. + dynamic getParameterOverride(String name) { + for (var i = _parameterStack.length - 1; i >= 0; i--) { + var parameters = _parameterStack[i]; + if (parameters.containsKey(name)) { + return parameters[name]; + } + } + return null; + } + /// Check if we're in danger of a circular dependency. void _checkCircularDependency(Type type) { if (_buildStack.contains(type)) { diff --git a/incubation/container/container/test/parameter_override_test.dart b/incubation/container/container/test/parameter_override_test.dart new file mode 100644 index 0000000..16b6252 --- /dev/null +++ b/incubation/container/container/test/parameter_override_test.dart @@ -0,0 +1,109 @@ +import 'package:platformed_container/container.dart'; +import 'package:test/test.dart'; + +class FileLogger { + final String filename; + FileLogger(this.filename); +} + +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(); +} + +void main() { + late Container container; + + setUp(() { + container = Container(MockReflector()); + }); + + group('Parameter Override Tests', () { + test('can override constructor parameters', () { + container.registerFactory((c) { + var filename = + c.getParameterOverride('filename') as String? ?? 'default.log'; + return FileLogger(filename); + }); + + var logger = container.withParameters( + {'filename': 'custom.log'}, () => container.make()); + + expect(logger.filename, equals('custom.log')); + }); + + test('parameter overrides are scoped', () { + container.registerFactory((c) { + var filename = + c.getParameterOverride('filename') as String? ?? 'default.log'; + return FileLogger(filename); + }); + + var customLogger = container.withParameters( + {'filename': 'custom.log'}, () => container.make()); + + var defaultLogger = container.make(); + + expect(customLogger.filename, equals('custom.log')); + expect(defaultLogger.filename, equals('default.log')); + }); + + test('nested parameter overrides', () { + container.registerFactory((c) { + var filename = + c.getParameterOverride('filename') as String? ?? 'default.log'; + return FileLogger(filename); + }); + + var logger = container.withParameters( + {'filename': 'outer.log'}, + () => container.withParameters( + {'filename': 'inner.log'}, () => container.make())); + + expect(logger.filename, equals('inner.log')); + }); + + test('parameter overrides in child container', () { + container.registerFactory((c) { + var filename = + c.getParameterOverride('filename') as String? ?? 'default.log'; + return FileLogger(filename); + }); + var child = container.createChild(); + + var logger = child.withParameters( + {'filename': 'custom.log'}, () => child.make()); + + expect(logger.filename, equals('custom.log')); + }); + + test('parameter overrides with multiple parameters', () { + container.registerFactory((c) { + var filename = + c.getParameterOverride('filename') as String? ?? 'default.log'; + return FileLogger(filename); + }); + + var logger = container.withParameters( + {'filename': 'custom.log', 'level': 'debug', 'maxSize': 1024}, + () => container.make()); + + expect(logger.filename, equals('custom.log')); + }); + }); +}