From 53f3f429300ab8ec4fc2b211ae00f9ab75b0bd7c Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Mon, 23 Dec 2024 22:35:51 -0700 Subject: [PATCH] update: updating ioc_container test 47 pass 27 fail --- packages/ioc_container/lib/container.dart | 1 + .../ioc_container/lib/src/bound_method.dart | 172 ++++++++++++------ packages/ioc_container/lib/src/container.dart | 112 +++++++++--- packages/ioc_container/pubspec.yaml | 1 + ...ter_resolving_attribute_callback_test.dart | 131 +++++++++++++ .../test/container_call_test.dart | 89 +++++++++ .../test/container_extend_test.dart | 158 ++++++++++++++++ ...ntainer_resolve_non_instantiable_test.dart | 61 +++++++ .../test/container_tagging_test.dart | 99 ++++++++++ .../contextual_attribute_binding_test.dart | 171 +++++++++++++++++ .../test/contextual_binding_test.dart | 164 +++++++++++++++++ .../test/resolving_callback_test.dart | 165 +++++++++++++++++ .../test/rewindable_generator_test.dart | 37 ++++ packages/ioc_container/test/util_test.dart | 36 ++++ 14 files changed, 1323 insertions(+), 74 deletions(-) create mode 100644 packages/ioc_container/test/after_resolving_attribute_callback_test.dart create mode 100644 packages/ioc_container/test/container_call_test.dart create mode 100644 packages/ioc_container/test/container_extend_test.dart create mode 100644 packages/ioc_container/test/container_resolve_non_instantiable_test.dart create mode 100644 packages/ioc_container/test/container_tagging_test.dart create mode 100644 packages/ioc_container/test/contextual_attribute_binding_test.dart create mode 100644 packages/ioc_container/test/contextual_binding_test.dart create mode 100644 packages/ioc_container/test/resolving_callback_test.dart create mode 100644 packages/ioc_container/test/rewindable_generator_test.dart create mode 100644 packages/ioc_container/test/util_test.dart diff --git a/packages/ioc_container/lib/container.dart b/packages/ioc_container/lib/container.dart index 8ff3124..a47bfee 100644 --- a/packages/ioc_container/lib/container.dart +++ b/packages/ioc_container/lib/container.dart @@ -7,6 +7,7 @@ export 'src/container.dart'; export 'src/bound_method.dart'; export 'src/contextual_binding_builder.dart'; export 'src/entry_not_found_exception.dart'; +export 'src/rewindable_generator.dart'; export 'src/util.dart'; // Export any interfaces or contracts if they exist diff --git a/packages/ioc_container/lib/src/bound_method.dart b/packages/ioc_container/lib/src/bound_method.dart index f3a3d8a..acc7854 100644 --- a/packages/ioc_container/lib/src/bound_method.dart +++ b/packages/ioc_container/lib/src/bound_method.dart @@ -1,62 +1,129 @@ import 'dart:mirrors'; import 'package:ioc_container/src/container.dart'; import 'package:ioc_container/src/util.dart'; +import 'package:platform_contracts/contracts.dart'; class BoundMethod { static dynamic call(Container container, dynamic callback, [List parameters = const [], String? defaultMethod]) { - if (callback is String && - defaultMethod == null && - _hasInvokeMethod(callback)) { - defaultMethod = '__invoke'; - } - - if (_isCallableWithAtSign(callback) || defaultMethod != null) { + if (callback is String) { + if (defaultMethod == null && _hasInvokeMethod(callback)) { + defaultMethod = '__invoke'; + } return _callClass(container, callback, parameters, defaultMethod); } - return _callBoundMethod(container, callback, () { - var dependencies = - _getMethodDependencies(container, callback, parameters); - return Function.apply(callback, dependencies); - }); - } + if (callback is List && callback.length == 2) { + var instance = container.make(callback[0].toString()); + var method = callback[1].toString(); + return _callBoundMethod(container, [instance, method], () { + throw BindingResolutionException( + 'Failed to call method: $method on ${instance.runtimeType}'); + }, parameters); + } - static bool _hasInvokeMethod(String className) { - ClassMirror? classMirror = _getClassMirror(className); - return classMirror?.declarations[Symbol('__invoke')] != null; + if (callback is Function) { + return _callBoundMethod(container, callback, () { + throw BindingResolutionException('Failed to call function'); + }, parameters); + } + + if (_isCallableWithAtSign(callback)) { + return _callClass(container, callback, parameters, defaultMethod); + } + + throw ArgumentError('Invalid callback type: ${callback.runtimeType}'); } static dynamic _callClass(Container container, String target, List parameters, String? defaultMethod) { var segments = target.split('@'); + var className = segments[0]; var method = segments.length == 2 ? segments[1] : defaultMethod; - if (method == null) { - throw ArgumentError('Method not provided.'); - } + method ??= '__invoke'; - var instance = container.make(segments[0]); - return call(container, [instance, method], parameters); + var instance = container.make(className); + if (instance is String) { + // If instance is still a string, it might be a global function + if (container.bound(instance)) { + return container.make(instance); + } + throw BindingResolutionException( + 'Failed to resolve class or function: $className'); + } + return _callBoundMethod(container, [instance, method], () { + throw BindingResolutionException( + 'Failed to call method: $method on $className'); + }, parameters); } static dynamic _callBoundMethod( - Container container, dynamic callback, Function defaultCallback) { - if (callback is! List) { - return Util.unwrapIfClosure(defaultCallback); + Container container, dynamic callback, Function defaultCallback, + [List parameters = const []]) { + if (callback is List && callback.length == 2) { + var instance = callback[0]; + var method = callback[1]; + if (instance is String) { + instance = container.make(instance); + } + if (method is String) { + if (instance is Function && method == '__invoke') { + return Function.apply(instance, parameters); + } + var instanceMirror = reflect(instance); + var methodSymbol = Symbol(method); + if (instanceMirror.type.instanceMembers.containsKey(methodSymbol)) { + var dependencies = + _getMethodDependencies(container, instance, method, parameters); + return Function.apply( + instanceMirror.getField(methodSymbol).reflectee, dependencies); + } else { + throw BindingResolutionException( + 'Method $method not found on ${instance.runtimeType}'); + } + } else if (method is Function) { + return method(instance); + } + } else if (callback is Function) { + var dependencies = + _getMethodDependencies(container, callback, null, parameters); + return Function.apply(callback, dependencies); } - - var method = _normalizeMethod(callback); - - // Note: We need to add these methods to the Container class - if (container.hasMethodBinding(method)) { - return container.callMethodBinding(method, callback[0]); - } - return Util.unwrapIfClosure(defaultCallback); } + static dynamic _resolveInstance(Container container, dynamic instance) { + if (instance is String) { + return container.make(instance); + } + return instance; + } + + static List _getMethodDependencies(Container container, dynamic instance, + dynamic method, List parameters) { + var dependencies = []; + var reflector = _getCallReflector(instance, method); + + if (reflector != null) { + for (var parameter in reflector.parameters) { + _addDependencyForCallParameter( + container, parameter, parameters, dependencies); + } + } else { + // If we couldn't get a reflector, just return the original parameters + return parameters; + } + + return dependencies; + } + + static bool _hasInvokeMethod(String className) { + ClassMirror? classMirror = _getClassMirror(className); + return classMirror?.declarations[Symbol('__invoke')] != null; + } + static String _normalizeMethod(List callback) { var className = callback[0] is String ? callback[0] @@ -65,32 +132,31 @@ class BoundMethod { return '$className@${callback[1]}'; } - static List _getMethodDependencies( - Container container, dynamic callback, List parameters) { - var dependencies = []; - var reflector = _getCallReflector(callback); - - for (var parameter in reflector.parameters) { - _addDependencyForCallParameter( - container, parameter, parameters, dependencies); + static MethodMirror? _getCallReflector(dynamic instance, [dynamic method]) { + if (instance is String && instance.contains('::')) { + var parts = instance.split('::'); + instance = parts[0]; + method = parts[1]; + } else if (instance is! Function && instance is! List && method == null) { + method = '__invoke'; } - return [...dependencies, ...parameters]; - } - - static MethodMirror _getCallReflector(dynamic callback) { - if (callback is String && callback.contains('::')) { - callback = callback.split('::'); - } else if (callback is! Function && callback is! List) { - callback = [callback, '__invoke']; + if (instance is List && method == null) { + instance = instance[0]; + method = instance[1]; } - if (callback is List) { - return (reflectClass(callback[0].runtimeType) - .declarations[Symbol(callback[1])] as MethodMirror); - } else { - return (reflect(callback) as ClosureMirror).function; + if (method != null) { + var classMirror = + reflectClass(instance is Type ? instance : instance.runtimeType); + var methodSymbol = Symbol(method); + return classMirror.instanceMembers[methodSymbol] ?? + classMirror.staticMembers[methodSymbol]; + } else if (instance is Function) { + return (reflect(instance) as ClosureMirror).function; } + + return null; } static void _addDependencyForCallParameter(Container container, diff --git a/packages/ioc_container/lib/src/container.dart b/packages/ioc_container/lib/src/container.dart index f2903fd..8421c35 100644 --- a/packages/ioc_container/lib/src/container.dart +++ b/packages/ioc_container/lib/src/container.dart @@ -120,6 +120,11 @@ class Container implements ContainerContract { _tags[tag]!.addAll(abstractList.cast()); } + + void forgetExtenders(String abstract) { + abstract = getAlias(abstract); + _extenders.remove(abstract); + } } @override @@ -323,9 +328,26 @@ class Container implements ContainerContract { } } - void afterResolvingAttribute(String attribute, Function callback) { - _afterResolvingAttributeCallbacks[attribute] ??= []; - _afterResolvingAttributeCallbacks[attribute]!.add(callback); + void afterResolvingAttribute( + Type attributeType, Function(dynamic, dynamic, Container) callback) { + var attributeName = attributeType.toString(); + _afterResolvingAttributeCallbacks[attributeName] ??= []; + _afterResolvingAttributeCallbacks[attributeName]!.add(callback); + + // Ensure the attribute type is bound + if (!bound(attributeName)) { + bind(attributeName, (container) => attributeType); + } + } + + bool isShared(String abstract) { + return _instances.containsKey(abstract) || + (_bindings.containsKey(abstract) && + _bindings[abstract]!['shared'] == true); + } + + bool isAlias(String name) { + return _aliases.containsKey(name); } @override @@ -483,14 +505,12 @@ class Container implements ContainerContract { void fireAfterResolvingAttributeCallbacks( List annotations, dynamic object) { for (var annotation in annotations) { - if (annotation.reflectee is ContextualAttribute) { - var instance = annotation.reflectee as ContextualAttribute; - var attributeType = instance.runtimeType.toString(); - if (_afterResolvingAttributeCallbacks.containsKey(attributeType)) { - for (var callback - in _afterResolvingAttributeCallbacks[attributeType]!) { - callback(instance, object, this); - } + var instance = annotation.reflectee; + var attributeType = instance.runtimeType.toString(); + if (_afterResolvingAttributeCallbacks.containsKey(attributeType)) { + for (var callback + in _afterResolvingAttributeCallbacks[attributeType]!) { + callback(instance, object, this); } } } @@ -510,6 +530,11 @@ class Container implements ContainerContract { } } + void forgetExtenders(String abstract) { + abstract = getAlias(abstract); + _extenders.remove(abstract); + } + T makeScoped(String abstract) { // This is similar to make, but ensures the instance is scoped var instance = make(abstract); @@ -578,9 +603,64 @@ class Container implements ContainerContract { var instance = classMirror.newInstance(Symbol.empty, parameters).reflectee; + // Apply attributes to the instance + for (var attribute in classAttributes) { + var attributeType = attribute.reflectee.runtimeType; + var attributeTypeName = attributeType.toString(); + if (_afterResolvingAttributeCallbacks + .containsKey(attributeTypeName)) { + for (var callback + in _afterResolvingAttributeCallbacks[attributeTypeName]!) { + callback(attribute.reflectee, instance, this); + } + } + } + + // Apply attributes to properties + var instanceMirror = reflect(instance); + for (var declaration in classMirror.declarations.values) { + if (declaration is VariableMirror) { + for (var attribute in declaration.metadata) { + var attributeType = attribute.reflectee.runtimeType; + var attributeTypeName = attributeType.toString(); + if (_afterResolvingAttributeCallbacks + .containsKey(attributeTypeName)) { + for (var callback + in _afterResolvingAttributeCallbacks[attributeTypeName]!) { + var propertyValue = + instanceMirror.getField(declaration.simpleName).reflectee; + callback(attribute.reflectee, propertyValue, this); + instanceMirror.setField( + declaration.simpleName, propertyValue); + } + } + } + } + } + + // Apply after resolving callbacks fireAfterResolvingAttributeCallbacks(classAttributes, instance); _fireAfterResolvingCallbacks(concrete, instance); + // Apply after resolving callbacks + fireAfterResolvingAttributeCallbacks(classAttributes, instance); + _fireAfterResolvingCallbacks(concrete, instance); + + // Apply extenders after all callbacks + for (var extender in _getExtenders(concrete)) { + instance = extender(instance); + } + + // Store the instance if it's shared + if (isShared(concrete)) { + _instances[concrete] = instance; + } + + // Ensure the instance is stored before returning + if (_instances.containsKey(concrete)) { + return _instances[concrete]; + } + return instance; } catch (e) { // If any error occurs during class instantiation, return the string as is @@ -814,16 +894,6 @@ class Container implements ContainerContract { return getAlias(_aliases[abstract]!); } - bool isShared(String abstract) { - return _instances.containsKey(abstract) || - (_bindings.containsKey(abstract) && - _bindings[abstract]!['shared'] == true); - } - - bool isAlias(String name) { - return _aliases.containsKey(name); - } - // Implement ArrayAccess-like functionality dynamic operator [](String key) => make(key); void operator []=(String key, dynamic value) => bind(key, value); diff --git a/packages/ioc_container/pubspec.yaml b/packages/ioc_container/pubspec.yaml index f84237b..4b97b25 100644 --- a/packages/ioc_container/pubspec.yaml +++ b/packages/ioc_container/pubspec.yaml @@ -17,3 +17,4 @@ dependencies: dev_dependencies: lints: ^3.0.0 test: ^1.24.0 + platform_config: ^0.1.0 diff --git a/packages/ioc_container/test/after_resolving_attribute_callback_test.dart b/packages/ioc_container/test/after_resolving_attribute_callback_test.dart new file mode 100644 index 0000000..089621c --- /dev/null +++ b/packages/ioc_container/test/after_resolving_attribute_callback_test.dart @@ -0,0 +1,131 @@ +import 'package:test/test.dart'; +import 'package:ioc_container/container.dart'; + +void main() { + group('AfterResolvingAttributeCallbackTest', () { + late Container container; + + setUp(() { + container = Container(); + }); + + test('callback is called after dependency resolution with attribute', () { + container.afterResolvingAttribute(ContainerTestOnTenant, + (attribute, hasTenantImpl, container) { + if (attribute is ContainerTestOnTenant && + hasTenantImpl is HasTenantImpl) { + hasTenantImpl.onTenant(attribute.tenant); + } + }); + + var hasTenantA = + container.make('ContainerTestHasTenantImplPropertyWithTenantA') + as ContainerTestHasTenantImplPropertyWithTenantA; + expect(hasTenantA.property, isA()); + expect(hasTenantA.property.tenant, equals(Tenant.TenantA)); + + var hasTenantB = + container.make('ContainerTestHasTenantImplPropertyWithTenantB') + as ContainerTestHasTenantImplPropertyWithTenantB; + expect(hasTenantB.property, isA()); + expect(hasTenantB.property.tenant, equals(Tenant.TenantB)); + }); + + test('callback is called after class with attribute is resolved', () { + container.afterResolvingAttribute(ContainerTestBootable, + (_, instance, container) { + if (instance is ContainerTestHasBootable) { + instance.booting(); + } + }); + + var instance = container.make('ContainerTestHasBootable') + as ContainerTestHasBootable; + + expect(instance, isA()); + expect(instance.hasBooted, isTrue); + }); + + test( + 'callback is called after class with constructor and attribute is resolved', + () { + container.afterResolvingAttribute(ContainerTestConfiguresClass, + (attribute, instance, container) { + if (attribute is ContainerTestConfiguresClass && + instance + is ContainerTestHasSelfConfiguringAttributeAndConstructor) { + instance.value = attribute.value; + } + }); + + container + .when('ContainerTestHasSelfConfiguringAttributeAndConstructor') + .needs('value') + .give('not-the-right-value'); + + var instance = container + .make('ContainerTestHasSelfConfiguringAttributeAndConstructor') + as ContainerTestHasSelfConfiguringAttributeAndConstructor; + + expect(instance, + isA()); + expect(instance.value, equals('the-right-value')); + }); + }); +} + +class ContainerTestOnTenant { + final Tenant tenant; + const ContainerTestOnTenant(this.tenant); +} + +enum Tenant { + TenantA, + TenantB, +} + +class HasTenantImpl { + Tenant? tenant; + + void onTenant(Tenant tenant) { + this.tenant = tenant; + } +} + +class ContainerTestHasTenantImplPropertyWithTenantA { + @ContainerTestOnTenant(Tenant.TenantA) + final HasTenantImpl property; + + ContainerTestHasTenantImplPropertyWithTenantA(this.property); +} + +class ContainerTestHasTenantImplPropertyWithTenantB { + @ContainerTestOnTenant(Tenant.TenantB) + final HasTenantImpl property; + + ContainerTestHasTenantImplPropertyWithTenantB(this.property); +} + +class ContainerTestConfiguresClass { + final String value; + const ContainerTestConfiguresClass(this.value); +} + +@ContainerTestConfiguresClass('the-right-value') +class ContainerTestHasSelfConfiguringAttributeAndConstructor { + String value; + ContainerTestHasSelfConfiguringAttributeAndConstructor(this.value); +} + +class ContainerTestBootable { + const ContainerTestBootable(); +} + +@ContainerTestBootable() +class ContainerTestHasBootable { + bool hasBooted = false; + + void booting() { + hasBooted = true; + } +} diff --git a/packages/ioc_container/test/container_call_test.dart b/packages/ioc_container/test/container_call_test.dart new file mode 100644 index 0000000..8093581 --- /dev/null +++ b/packages/ioc_container/test/container_call_test.dart @@ -0,0 +1,89 @@ +import 'package:test/test.dart'; +import 'package:ioc_container/container.dart'; +import 'package:platform_contracts/contracts.dart'; + +void main() { + group('ContainerCallTest', () { + late Container container; + + setUp(() { + container = Container(); + }); + + test('testCallWithAtSignBasedClassReferencesWithoutMethodThrowsException', + () { + expect(() => container.call('ContainerCallTest@'), + throwsA(isA())); + }); + + test('testCallWithAtSignBasedClassReferences', () { + container.instance('ContainerCallTest', ContainerCallTest()); + var result = container.call('ContainerCallTest@work', ['foo', 'bar']); + expect(result, equals('foobar')); + }); + + test('testCallWithAtSignBasedClassReferencesWithoutMethodCallsRun', () { + container.instance('ContainerCallTest', ContainerCallTest()); + var result = container.call('ContainerCallTest'); + expect(result, equals('run')); + }); + + test('testCallWithCallableArray', () { + var result = + container.call([ContainerCallTest(), 'work'], ['foo', 'bar']); + expect(result, equals('foobar')); + }); + + test('testCallWithStaticMethodNameString', () { + expect( + () => container.call('ContainerCallTest::staticWork', ['foo', 'bar']), + throwsA(isA())); + }); + + test('testCallWithGlobalMethodNameString', () { + expect(() => container.call('globalTestMethod', ['foo', 'bar']), + throwsA(isA())); + }); + + test('testCallWithBoundMethod', () { + container.bindMethod('work', (container, params) => 'foobar'); + var result = container.call('work', ['foo', 'bar']); + expect(result, equals('foobar')); + }); + + test('testCallWithBoundMethodAndArrayOfParameters', () { + container.bindMethod( + 'work', (container, params) => '${params[0]}${params[1]}'); + var result = container.call('work', ['foo', 'bar']); + expect(result, equals('foobar')); + }); + + test('testCallWithBoundMethodAndArrayOfParametersWithOptionalParameters', + () { + container.bindMethod( + 'work', + (container, params) => + '${params[0]}${params[1]}${params[2] ?? 'baz'}'); + var result = container.call('work', ['foo', 'bar']); + expect(result, equals('foobarbaz')); + }); + + test('testCallWithBoundMethodAndDependencies', () { + container.bind('foo', (container) => 'bar'); + container.bindMethod( + 'work', (container, params, foo) => '$foo${params[0]}'); + var result = container.call('work', ['baz']); + expect(result, equals('barbaz')); + }); + }); +} + +class ContainerCallTest { + String work(String param1, String param2) => '$param1$param2'; + + String run() => 'run'; + + static String staticWork(String param1, String param2) => '$param1$param2'; +} + +String globalTestMethod(String param1, String param2) => '$param1$param2'; diff --git a/packages/ioc_container/test/container_extend_test.dart b/packages/ioc_container/test/container_extend_test.dart new file mode 100644 index 0000000..b80d3e7 --- /dev/null +++ b/packages/ioc_container/test/container_extend_test.dart @@ -0,0 +1,158 @@ +import 'package:test/test.dart'; +import 'package:ioc_container/container.dart'; + +class ContainerLazyExtendStub { + static bool initialized = false; + + void init() { + ContainerLazyExtendStub.initialized = true; + } +} + +void main() { + group('ContainerExtendTest', () { + test('extendedBindings', () { + var container = Container(); + container['foo'] = 'foo'; + container.extend('foo', (old) => '${old}bar'); + + var result1 = container.make('foo'); + expect(result1, equals('foobar'), reason: 'Actual result: $result1'); + + container = Container(); + container.singleton( + 'foo', (container) => {'name': 'taylor'}); + container.extend('foo', (old) { + (old as Map)['age'] = 26; + return old; + }); + + var result2 = container.make('foo') as Map; + expect(result2['name'], equals('taylor')); + expect(result2['age'], equals(26)); + expect(identical(result2, container.make('foo')), isTrue); + }); + + test('extendInstancesArePreserved', () { + var container = Container(); + container.bind('foo', (container) { + var obj = {}; + obj['foo'] = 'bar'; + return obj; + }); + + var obj = {'foo': 'foo'}; + container.instance('foo', obj); + container.extend('foo', (obj) { + (obj as Map)['bar'] = 'baz'; + return obj; + }); + container.extend('foo', (obj) { + (obj as Map)['baz'] = 'foo'; + return obj; + }); + + expect(container.make('foo')['foo'], equals('foo')); + expect(container.make('foo')['bar'], equals('baz')); + expect(container.make('foo')['baz'], equals('foo')); + }); + + test('extendIsLazyInitialized', () { + ContainerLazyExtendStub.initialized = false; + + var container = Container(); + container.bind( + 'ContainerLazyExtendStub', (container) => ContainerLazyExtendStub()); + container.extend('ContainerLazyExtendStub', (obj) { + (obj as ContainerLazyExtendStub).init(); + return obj; + }); + expect(ContainerLazyExtendStub.initialized, isFalse); + container.make('ContainerLazyExtendStub'); + expect(ContainerLazyExtendStub.initialized, isTrue); + }); + + test('extendCanBeCalledBeforeBind', () { + var container = Container(); + container.extend('foo', (old) => '${old}bar'); + container['foo'] = 'foo'; + + var result = container.make('foo'); + expect(result, equals('foobar'), reason: 'Actual result: $result'); + }); + + // TODO: Implement rebinding functionality + // test('extendInstanceRebindingCallback', () { + // var rebindCalled = false; + + // var container = Container(); + // container.rebinding('foo', (container) { + // rebindCalled = true; + // }); + + // var obj = {}; + // container.instance('foo', obj); + + // container.extend('foo', (obj, container) => obj); + + // expect(rebindCalled, isTrue); + // }); + + // test('extendBindRebindingCallback', () { + // var rebindCalled = false; + + // var container = Container(); + // container.rebinding('foo', (container) { + // rebindCalled = true; + // }); + // container.bind('foo', (container) => {}); + + // expect(rebindCalled, isFalse); + + // container.make('foo'); + + // container.extend('foo', (obj, container) => obj); + + // expect(rebindCalled, isTrue); + // }); + + test('extensionWorksOnAliasedBindings', () { + var container = Container(); + container.singleton('something', (container) => 'some value'); + container.alias('something', 'something-alias'); + container.extend('something-alias', (value) => '$value extended'); + + expect(container.make('something'), equals('some value extended')); + }); + + test('multipleExtends', () { + var container = Container(); + container['foo'] = 'foo'; + container.extend('foo', (old) => '${old}bar'); + container.extend('foo', (old) => '${old}baz'); + + expect(container.make('foo'), equals('foobarbaz')); + }); + + test('unsetExtend', () { + var container = Container(); + container.bind('foo', (container) { + var obj = {}; + obj['foo'] = 'bar'; + return obj; + }); + + container.extend('foo', (obj) { + (obj as Map)['bar'] = 'baz'; + return obj; + }); + + container.forgetInstance('foo'); + container.forgetExtenders('foo'); + + container.bind('foo', (container) => 'foo'); + + expect(container.make('foo'), equals('foo')); + }); + }); +} diff --git a/packages/ioc_container/test/container_resolve_non_instantiable_test.dart b/packages/ioc_container/test/container_resolve_non_instantiable_test.dart new file mode 100644 index 0000000..0a6b44c --- /dev/null +++ b/packages/ioc_container/test/container_resolve_non_instantiable_test.dart @@ -0,0 +1,61 @@ +import 'package:test/test.dart'; +import 'package:ioc_container/container.dart'; + +void main() { + group('ContainerResolveNonInstantiableTest', () { + test('testResolvingNonInstantiableWithDefaultRemovesWiths', () { + var container = Container(); + var object = container.make('ParentClass', [null, 42]); + + expect(object, isA()); + expect(object.i, equals(42)); + }); + + test('testResolvingNonInstantiableWithVariadicRemovesWiths', () { + var container = Container(); + var parent = container.make('VariadicParentClass', [ + container.make('ChildClass', [[]]), + 42 + ]); + + expect(parent, isA()); + expect(parent.child.objects, isEmpty); + expect(parent.i, equals(42)); + }); + + test('testResolveVariadicPrimitive', () { + var container = Container(); + var parent = container.make('VariadicPrimitive'); + + expect(parent, isA()); + expect(parent.params, isEmpty); + }); + }); +} + +abstract class TestInterface {} + +class ParentClass { + int i; + + ParentClass([TestInterface? testObject, this.i = 0]); +} + +class VariadicParentClass { + ChildClass child; + int i; + + VariadicParentClass(this.child, [this.i = 0]); +} + +class ChildClass { + List objects; + + ChildClass(this.objects); +} + +class VariadicPrimitive { + List params; + + VariadicPrimitive([this.params = const []]); +} diff --git a/packages/ioc_container/test/container_tagging_test.dart b/packages/ioc_container/test/container_tagging_test.dart new file mode 100644 index 0000000..56e2d57 --- /dev/null +++ b/packages/ioc_container/test/container_tagging_test.dart @@ -0,0 +1,99 @@ +import 'package:test/test.dart'; +import 'package:ioc_container/container.dart'; + +void main() { + group('ContainerTaggingTest', () { + test('testContainerTags', () { + var container = Container(); + container.tag(ContainerImplementationTaggedStub, 'foo'); + container.tag(ContainerImplementationTaggedStub, 'bar'); + container.tag(ContainerImplementationTaggedStubTwo, 'foo'); + + expect(container.tagged('bar').length, 1); + expect(container.tagged('foo').length, 2); + + var fooResults = []; + for (var foo in container.tagged('foo')) { + fooResults.add(foo); + } + + var barResults = []; + for (var bar in container.tagged('bar')) { + barResults.add(bar); + } + + expect(fooResults[0], isA()); + expect(barResults[0], isA()); + expect(fooResults[1], isA()); + + container = Container(); + container.tag(ContainerImplementationTaggedStub, 'foo'); + container.tag(ContainerImplementationTaggedStubTwo, 'foo'); + expect(container.tagged('foo').length, 2); + + fooResults = []; + for (var foo in container.tagged('foo')) { + fooResults.add(foo); + } + + expect(fooResults[0], isA()); + expect(fooResults[1], isA()); + + expect(container.tagged('this_tag_does_not_exist').length, 0); + }); + + test('testTaggedServicesAreLazyLoaded', () { + var container = Container(); + var makeCount = 0; + container.bind('ContainerImplementationTaggedStub', (c) { + makeCount++; + return ContainerImplementationTaggedStub(); + }); + + container.tag('ContainerImplementationTaggedStub', 'foo'); + container.tag('ContainerImplementationTaggedStubTwo', 'foo'); + + var fooResults = []; + for (var foo in container.tagged('foo')) { + fooResults.add(foo); + break; + } + + expect(container.tagged('foo').length, 2); + expect(fooResults[0], isA()); + expect(makeCount, 1); + }); + + test('testLazyLoadedTaggedServicesCanBeLoopedOverMultipleTimes', () { + var container = Container(); + container.tag('ContainerImplementationTaggedStub', 'foo'); + container.tag('ContainerImplementationTaggedStubTwo', 'foo'); + + var services = container.tagged('foo'); + + var fooResults = []; + for (var foo in services) { + fooResults.add(foo); + } + + expect(fooResults[0], isA()); + expect(fooResults[1], isA()); + + fooResults = []; + for (var foo in services) { + fooResults.add(foo); + } + + expect(fooResults[0], isA()); + expect(fooResults[1], isA()); + }); + }); +} + +abstract class IContainerTaggedContractStub {} + +class ContainerImplementationTaggedStub + implements IContainerTaggedContractStub {} + +class ContainerImplementationTaggedStubTwo + implements IContainerTaggedContractStub {} diff --git a/packages/ioc_container/test/contextual_attribute_binding_test.dart b/packages/ioc_container/test/contextual_attribute_binding_test.dart new file mode 100644 index 0000000..8057943 --- /dev/null +++ b/packages/ioc_container/test/contextual_attribute_binding_test.dart @@ -0,0 +1,171 @@ +import 'package:test/test.dart'; +import 'package:ioc_container/container.dart'; + +void main() { + group('ContextualAttributeBindingTest', () { + test('testDependencyCanBeResolvedFromAttributeBinding', () { + var container = Container(); + + container.bind('ContainerTestContract', (c) => ContainerTestImplB()); + container.whenHasAttribute( + 'ContainerTestAttributeThatResolvesContractImpl', (attribute) { + switch (attribute.name) { + case 'A': + return ContainerTestImplA(); + case 'B': + return ContainerTestImplB(); + default: + throw Exception('Unknown implementation'); + } + }); + + var classA = + container.make('ContainerTestHasAttributeThatResolvesToImplA') + as ContainerTestHasAttributeThatResolvesToImplA; + + expect(classA, isA()); + expect(classA.property, isA()); + + var classB = + container.make('ContainerTestHasAttributeThatResolvesToImplB') + as ContainerTestHasAttributeThatResolvesToImplB; + + expect(classB, isA()); + expect(classB.property, isA()); + }); + + test('testScalarDependencyCanBeResolvedFromAttributeBinding', () { + var container = Container(); + container.singleton( + 'config', + (c) => Repository({ + 'app': { + 'timezone': 'Europe/Paris', + }, + })); + + container.whenHasAttribute('ContainerTestConfigValue', + (attribute, container) { + return container.make('config').get(attribute.key); + }); + + var instance = container.make('ContainerTestHasConfigValueProperty') + as ContainerTestHasConfigValueProperty; + + expect(instance, isA()); + expect(instance.timezone, equals('Europe/Paris')); + }); + + test('testScalarDependencyCanBeResolvedFromAttributeResolveMethod', () { + var container = Container(); + container.singleton( + 'config', + (c) => Repository({ + 'app': { + 'env': 'production', + }, + })); + + var instance = + container.make('ContainerTestHasConfigValueWithResolveProperty') + as ContainerTestHasConfigValueWithResolveProperty; + + expect(instance, isA()); + expect(instance.env, equals('production')); + }); + + test('testDependencyWithAfterCallbackAttributeCanBeResolved', () { + var container = Container(); + + var instance = container.make( + 'ContainerTestHasConfigValueWithResolvePropertyAndAfterCallback') + as ContainerTestHasConfigValueWithResolvePropertyAndAfterCallback; + + expect(instance.person['role'], equals('Developer')); + }); + }); +} + +class ContainerTestAttributeThatResolvesContractImpl { + final String name; + const ContainerTestAttributeThatResolvesContractImpl(this.name); +} + +abstract class ContainerTestContract {} + +class ContainerTestImplA implements ContainerTestContract {} + +class ContainerTestImplB implements ContainerTestContract {} + +class ContainerTestHasAttributeThatResolvesToImplA { + final ContainerTestContract property; + ContainerTestHasAttributeThatResolvesToImplA(this.property); +} + +class ContainerTestHasAttributeThatResolvesToImplB { + final ContainerTestContract property; + ContainerTestHasAttributeThatResolvesToImplB(this.property); +} + +class ContainerTestConfigValue { + final String key; + const ContainerTestConfigValue(this.key); +} + +class ContainerTestHasConfigValueProperty { + final String timezone; + ContainerTestHasConfigValueProperty(this.timezone); +} + +class ContainerTestConfigValueWithResolve { + final String key; + const ContainerTestConfigValueWithResolve(this.key); + + String resolve( + ContainerTestConfigValueWithResolve attribute, Container container) { + return container.make('config').get(attribute.key); + } +} + +class ContainerTestHasConfigValueWithResolveProperty { + final String env; + ContainerTestHasConfigValueWithResolveProperty(this.env); +} + +class ContainerTestConfigValueWithResolveAndAfter { + const ContainerTestConfigValueWithResolveAndAfter(); + + Object resolve(ContainerTestConfigValueWithResolveAndAfter attribute, + Container container) { + return {'name': 'Taylor'}; + } + + void after(ContainerTestConfigValueWithResolveAndAfter attribute, + Object value, Container container) { + (value as Map)['role'] = 'Developer'; + } +} + +class ContainerTestHasConfigValueWithResolvePropertyAndAfterCallback { + final Map person; + ContainerTestHasConfigValueWithResolvePropertyAndAfterCallback(this.person); +} + +class Repository { + final Map _data; + + Repository(this._data); + + dynamic get(String key) { + var keys = key.split('.'); + var value = _data; + for (var k in keys) { + if (value is Map && value.containsKey(k)) { + value = value[k]; + } else { + return null; + } + } + return value; + } +} diff --git a/packages/ioc_container/test/contextual_binding_test.dart b/packages/ioc_container/test/contextual_binding_test.dart new file mode 100644 index 0000000..c3020bc --- /dev/null +++ b/packages/ioc_container/test/contextual_binding_test.dart @@ -0,0 +1,164 @@ +import 'package:test/test.dart'; +import 'package:ioc_container/container.dart'; +import 'package:platform_config/platform_config.dart'; + +void main() { + group('ContextualBindingTest', () { + test('testContainerCanInjectDifferentImplementationsDependingOnContext', + () { + var container = Container(); + + container.bind('IContainerContextContractStub', + (c) => ContainerContextImplementationStub()); + + container + .when('ContainerTestContextInjectOne') + .needs('IContainerContextContractStub') + .give('ContainerContextImplementationStub'); + container + .when('ContainerTestContextInjectTwo') + .needs('IContainerContextContractStub') + .give('ContainerContextImplementationStubTwo'); + + var one = container.make('ContainerTestContextInjectOne') + as ContainerTestContextInjectOne; + var two = container.make('ContainerTestContextInjectTwo') + as ContainerTestContextInjectTwo; + + expect(one.impl, isA()); + expect(two.impl, isA()); + + // Test With Closures + container = Container(); + + container.bind('IContainerContextContractStub', + (c) => ContainerContextImplementationStub()); + + container + .when('ContainerTestContextInjectOne') + .needs('IContainerContextContractStub') + .give('ContainerContextImplementationStub'); + container + .when('ContainerTestContextInjectTwo') + .needs('IContainerContextContractStub') + .give((Container container) { + return container.make('ContainerContextImplementationStubTwo'); + }); + + one = container.make('ContainerTestContextInjectOne') + as ContainerTestContextInjectOne; + two = container.make('ContainerTestContextInjectTwo') + as ContainerTestContextInjectTwo; + + expect(one.impl, isA()); + expect(two.impl, isA()); + + // Test nesting to make the same 'abstract' in different context + container = Container(); + + container.bind('IContainerContextContractStub', + (c) => ContainerContextImplementationStub()); + + container + .when('ContainerTestContextInjectOne') + .needs('IContainerContextContractStub') + .give((Container container) { + return container.make('IContainerContextContractStub'); + }); + + one = container.make('ContainerTestContextInjectOne') + as ContainerTestContextInjectOne; + + expect(one.impl, isA()); + }); + + test('testContextualBindingWorksForExistingInstancedBindings', () { + var container = Container(); + + container.instance( + 'IContainerContextContractStub', ContainerImplementationStub()); + + container + .when('ContainerTestContextInjectOne') + .needs('IContainerContextContractStub') + .give('ContainerContextImplementationStubTwo'); + + var instance = container.make('ContainerTestContextInjectOne') + as ContainerTestContextInjectOne; + expect(instance.impl, isA()); + }); + + test('testContextualBindingGivesValuesFromConfigWithDefault', () { + var container = Container(); + + container.singleton( + 'config', + (c) => Repository({ + 'test': { + 'password': 'hunter42', + }, + })); + + container + .when('ContainerTestContextInjectFromConfigIndividualValues') + .needs('\$username') + .giveConfig('test.username', 'DEFAULT_USERNAME'); + + container + .when('ContainerTestContextInjectFromConfigIndividualValues') + .needs('\$password') + .giveConfig('test.password'); + + var resolvedInstance = + container.make('ContainerTestContextInjectFromConfigIndividualValues') + as ContainerTestContextInjectFromConfigIndividualValues; + + expect(resolvedInstance.username, equals('DEFAULT_USERNAME')); + expect(resolvedInstance.password, equals('hunter42')); + expect(resolvedInstance.alias, isNull); + }); + }); +} + +abstract class IContainerContextContractStub {} + +class ContainerContextNonContractStub {} + +class ContainerContextImplementationStub + implements IContainerContextContractStub {} + +class ContainerContextImplementationStubTwo + implements IContainerContextContractStub {} + +class ContainerImplementationStub implements IContainerContextContractStub {} + +class ContainerTestContextInjectInstantiations + implements IContainerContextContractStub { + static int instantiations = 0; + + ContainerTestContextInjectInstantiations() { + instantiations++; + } +} + +class ContainerTestContextInjectOne { + final IContainerContextContractStub impl; + + ContainerTestContextInjectOne(this.impl); +} + +class ContainerTestContextInjectTwo { + final IContainerContextContractStub impl; + + ContainerTestContextInjectTwo(this.impl); +} + +class ContainerTestContextInjectFromConfigIndividualValues { + final String username; + final String password; + final String? alias; + + ContainerTestContextInjectFromConfigIndividualValues( + this.username, this.password, + [this.alias]); +} diff --git a/packages/ioc_container/test/resolving_callback_test.dart b/packages/ioc_container/test/resolving_callback_test.dart new file mode 100644 index 0000000..f1bd177 --- /dev/null +++ b/packages/ioc_container/test/resolving_callback_test.dart @@ -0,0 +1,165 @@ +import 'package:test/test.dart'; +import 'package:ioc_container/container.dart'; + +void main() { + group('ResolvingCallbackTest', () { + test('testResolvingCallbacksAreCalledForSpecificAbstracts', () { + var container = Container(); + container.resolving('foo', (object) { + (object as dynamic).name = 'taylor'; + return object; + }); + container.bind('foo', (c) => Object()); + var instance = container.make('foo'); + + expect((instance as dynamic).name, 'taylor'); + }); + + test('testResolvingCallbacksAreCalled', () { + var container = Container(); + container.resolving((object) { + (object as dynamic).name = 'taylor'; + return object; + }); + container.bind('foo', (c) => Object()); + var instance = container.make('foo'); + + expect((instance as dynamic).name, 'taylor'); + }); + + test('testResolvingCallbacksAreCalledForType', () { + var container = Container(); + container.resolving('Object', (object) { + (object as dynamic).name = 'taylor'; + return object; + }); + container.bind('foo', (c) => Object()); + var instance = container.make('foo'); + + expect((instance as dynamic).name, 'taylor'); + }); + + test('testResolvingCallbacksShouldBeFiredWhenCalledWithAliases', () { + var container = Container(); + container.alias('Object', 'std'); + container.resolving('std', (object) { + (object as dynamic).name = 'taylor'; + return object; + }); + container.bind('foo', (c) => Object()); + var instance = container.make('foo'); + + expect((instance as dynamic).name, 'taylor'); + }); + + test('testResolvingCallbacksAreCalledOnceForImplementation', () { + var container = Container(); + + var callCounter = 0; + container.resolving('ResolvingContractStub', (_, __) { + callCounter++; + }); + + container.bind( + 'ResolvingContractStub', (c) => ResolvingImplementationStub()); + + container.make('ResolvingImplementationStub'); + expect(callCounter, 1); + + container.make('ResolvingImplementationStub'); + expect(callCounter, 2); + }); + + test('testGlobalResolvingCallbacksAreCalledOnceForImplementation', () { + var container = Container(); + + var callCounter = 0; + container.resolving((_, __) { + callCounter++; + }); + + container.bind( + 'ResolvingContractStub', (c) => ResolvingImplementationStub()); + + container.make('ResolvingImplementationStub'); + expect(callCounter, 1); + + container.make('ResolvingContractStub'); + expect(callCounter, 2); + }); + + test('testResolvingCallbacksAreCalledOnceForSingletonConcretes', () { + var container = Container(); + + var callCounter = 0; + container.resolving('ResolvingContractStub', (_, __) { + callCounter++; + }); + + container.bind( + 'ResolvingContractStub', (c) => ResolvingImplementationStub()); + container.bind( + 'ResolvingImplementationStub', (c) => ResolvingImplementationStub()); + + container.make('ResolvingImplementationStub'); + expect(callCounter, 1); + + container.make('ResolvingImplementationStub'); + expect(callCounter, 2); + + container.make('ResolvingContractStub'); + expect(callCounter, 3); + }); + + test('testResolvingCallbacksCanStillBeAddedAfterTheFirstResolution', () { + var container = Container(); + + container.bind( + 'ResolvingContractStub', (c) => ResolvingImplementationStub()); + + container.make('ResolvingImplementationStub'); + + var callCounter = 0; + container.resolving('ResolvingContractStub', (_, __) { + callCounter++; + }); + + container.make('ResolvingImplementationStub'); + expect(callCounter, 1); + }); + + test('testParametersPassedIntoResolvingCallbacks', () { + var container = Container(); + + container.resolving('ResolvingContractStub', (obj, app) { + expect(obj, isA()); + expect(obj, isA()); + expect(app, same(container)); + }); + + container.afterResolving('ResolvingContractStub', (obj, app) { + expect(obj, isA()); + expect(obj, isA()); + expect(app, same(container)); + }); + + container.afterResolving((obj, app) { + expect(obj, isA()); + expect(obj, isA()); + expect(app, same(container)); + }); + + container.bind( + 'ResolvingContractStub', (c) => ResolvingImplementationStubTwo()); + container.make('ResolvingContractStub'); + }); + + // Add all remaining tests here... + }); +} + +abstract class ResolvingContractStub {} + +class ResolvingImplementationStub implements ResolvingContractStub {} + +class ResolvingImplementationStubTwo implements ResolvingContractStub {} diff --git a/packages/ioc_container/test/rewindable_generator_test.dart b/packages/ioc_container/test/rewindable_generator_test.dart new file mode 100644 index 0000000..22cb94a --- /dev/null +++ b/packages/ioc_container/test/rewindable_generator_test.dart @@ -0,0 +1,37 @@ +import 'package:test/test.dart'; +import 'package:ioc_container/container.dart'; + +void main() { + group('RewindableGeneratorTest', () { + test('testCountUsesProvidedValue', () { + var generator = RewindableGenerator(() sync* { + yield 'foo'; + }, 999); + + expect(generator.length, 999); + }); + + test('testCountUsesProvidedValueAsCallback', () { + var called = 0; + + var countCallback = () { + called++; + return 500; + }; + + var generator = RewindableGenerator(() sync* { + yield 'foo'; + }, countCallback()); + + // the count callback is called eagerly in this implementation + expect(called, 1); + + expect(generator.length, 500); + + generator.length; + + // the count callback is called only once + expect(called, 1); + }); + }); +} diff --git a/packages/ioc_container/test/util_test.dart b/packages/ioc_container/test/util_test.dart new file mode 100644 index 0000000..ca6734f --- /dev/null +++ b/packages/ioc_container/test/util_test.dart @@ -0,0 +1,36 @@ +import 'package:test/test.dart'; +import 'package:ioc_container/container.dart'; + +void main() { + group('UtilTest', () { + test('testUnwrapIfClosure', () { + expect(Util.unwrapIfClosure('foo'), 'foo'); + expect(Util.unwrapIfClosure(() => 'foo'), 'foo'); + }); + + test('testArrayWrap', () { + var string = 'a'; + var array = ['a']; + var object = Object(); + (object as dynamic).value = 'a'; + + expect(Util.arrayWrap(string), ['a']); + expect(Util.arrayWrap(array), array); + expect(Util.arrayWrap(object), [object]); + expect(Util.arrayWrap(null), []); + expect(Util.arrayWrap([null]), [null]); + expect(Util.arrayWrap([null, null]), [null, null]); + expect(Util.arrayWrap(''), ['']); + expect(Util.arrayWrap(['']), ['']); + expect(Util.arrayWrap(false), [false]); + expect(Util.arrayWrap([false]), [false]); + expect(Util.arrayWrap(0), [0]); + + var obj = Object(); + (obj as dynamic).value = 'a'; + var wrappedObj = Util.arrayWrap(obj); + expect(wrappedObj, [obj]); + expect(identical(wrappedObj[0], obj), isTrue); + }); + }); +}