diff --git a/packages/ioc_container/.gitignore b/packages/ioc_container/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/packages/ioc_container/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/ioc_container/CHANGELOG.md b/packages/ioc_container/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/packages/ioc_container/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/ioc_container/LICENSE.md b/packages/ioc_container/LICENSE.md new file mode 100644 index 0000000..0fd0d03 --- /dev/null +++ b/packages/ioc_container/LICENSE.md @@ -0,0 +1,10 @@ +The MIT License (MIT) + +The Laravel Framework is Copyright (c) Taylor Otwell +The Fabric Framework is Copyright (c) Vieo, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/ioc_container/README.md b/packages/ioc_container/README.md new file mode 100644 index 0000000..8b55e73 --- /dev/null +++ b/packages/ioc_container/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/ioc_container/analysis_options.yaml b/packages/ioc_container/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/packages/ioc_container/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/ioc_container/example/.gitkeep b/packages/ioc_container/example/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/ioc_container/lib/container.dart b/packages/ioc_container/lib/container.dart new file mode 100644 index 0000000..8ff3124 --- /dev/null +++ b/packages/ioc_container/lib/container.dart @@ -0,0 +1,20 @@ +// This is the barrel file for the ioc_container package + +// Export the main Container class +export 'src/container.dart'; + +// Export other important classes and utilities +export 'src/bound_method.dart'; +export 'src/contextual_binding_builder.dart'; +export 'src/entry_not_found_exception.dart'; +export 'src/util.dart'; + +// Export any interfaces or contracts if they exist +// export 'src/contracts/container_contract.dart'; + +// Export any additional exceptions + +// Export any additional utilities or helpers +// export 'src/helpers/parameter_resolver.dart'; + +// You can add more exports as needed for your package diff --git a/packages/ioc_container/lib/src/bound_method.dart b/packages/ioc_container/lib/src/bound_method.dart new file mode 100644 index 0000000..f3a3d8a --- /dev/null +++ b/packages/ioc_container/lib/src/bound_method.dart @@ -0,0 +1,143 @@ +import 'dart:mirrors'; +import 'package:ioc_container/src/container.dart'; +import 'package:ioc_container/src/util.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) { + return _callClass(container, callback, parameters, defaultMethod); + } + + return _callBoundMethod(container, callback, () { + var dependencies = + _getMethodDependencies(container, callback, parameters); + return Function.apply(callback, dependencies); + }); + } + + static bool _hasInvokeMethod(String className) { + ClassMirror? classMirror = _getClassMirror(className); + return classMirror?.declarations[Symbol('__invoke')] != null; + } + + static dynamic _callClass(Container container, String target, + List parameters, String? defaultMethod) { + var segments = target.split('@'); + + var method = segments.length == 2 ? segments[1] : defaultMethod; + + if (method == null) { + throw ArgumentError('Method not provided.'); + } + + var instance = container.make(segments[0]); + return call(container, [instance, method], parameters); + } + + static dynamic _callBoundMethod( + Container container, dynamic callback, Function defaultCallback) { + if (callback is! List) { + return Util.unwrapIfClosure(defaultCallback); + } + + 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 String _normalizeMethod(List callback) { + var className = callback[0] is String + ? callback[0] + : MirrorSystem.getName( + reflectClass(callback[0].runtimeType).simpleName); + 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); + } + + 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 (callback is List) { + return (reflectClass(callback[0].runtimeType) + .declarations[Symbol(callback[1])] as MethodMirror); + } else { + return (reflect(callback) as ClosureMirror).function; + } + } + + static void _addDependencyForCallParameter(Container container, + ParameterMirror parameter, List parameters, List dependencies) { + var pendingDependencies = []; + var paramName = MirrorSystem.getName(parameter.simpleName); + + if (parameters.any((p) => p is Map && p.containsKey(paramName))) { + var param = + parameters.firstWhere((p) => p is Map && p.containsKey(paramName)); + pendingDependencies.add(param[paramName]); + parameters.remove(param); + } else if (parameter.type.reflectedType != dynamic) { + var className = parameter.type.reflectedType.toString(); + if (parameters.any((p) => p is Map && p.containsKey(className))) { + var param = + parameters.firstWhere((p) => p is Map && p.containsKey(className)); + pendingDependencies.add(param[className]); + parameters.remove(param); + } else if (parameter.isNamed) { + var variadicDependencies = container.make(className); + pendingDependencies.addAll(variadicDependencies is List + ? variadicDependencies + : [variadicDependencies]); + } else { + pendingDependencies.add(container.make(className)); + } + } else if (parameter.hasDefaultValue) { + pendingDependencies.add(parameter.defaultValue?.reflectee); + } else if (!parameter.isOptional && + !parameters.any((p) => p is Map && p.containsKey(paramName))) { + throw Exception( + "Unable to resolve dependency [$parameter] in class ${parameter.owner?.qualifiedName ?? 'Unknown'}"); + } + + dependencies.addAll(pendingDependencies); + } + + static bool _isCallableWithAtSign(dynamic callback) { + return callback is String && callback.contains('@'); + } + + static ClassMirror? _getClassMirror(String className) { + try { + return reflectClass(className as Type); + } catch (_) { + return null; + } + } +} diff --git a/packages/ioc_container/lib/src/container.dart b/packages/ioc_container/lib/src/container.dart new file mode 100644 index 0000000..f2903fd --- /dev/null +++ b/packages/ioc_container/lib/src/container.dart @@ -0,0 +1,830 @@ +import 'dart:mirrors'; +import 'package:platform_contracts/contracts.dart'; +import 'package:ioc_container/src/bound_method.dart'; +import 'package:ioc_container/src/contextual_binding_builder.dart'; +import 'package:ioc_container/src/entry_not_found_exception.dart'; +import 'package:ioc_container/src/util.dart'; + +class Container implements ContainerContract { + static Container? _instance; + + final Map _resolved = {}; + final Map> _bindings = {}; + + final Map _methodBindings = {}; + + final Map _instances = {}; + final List _scopedInstances = []; + final Map _aliases = {}; + final Map> _abstractAliases = {}; + final Map> _extenders = {}; + final Map> _tags = {}; + final List _buildStack = []; + final List> _with = []; + final Map> _contextual = {}; + final Map _contextualAttributes = {}; + final Map> _reboundCallbacks = {}; + final List _globalBeforeResolvingCallbacks = []; + final List _globalResolvingCallbacks = []; + final List _globalAfterResolvingCallbacks = []; + final Map> _beforeResolvingCallbacks = {}; + final Map> _resolvingCallbacks = {}; + final Map> _afterResolvingCallbacks = {}; + final Map> _afterResolvingAttributeCallbacks = {}; + + Container(); + + static Container getInstance() { + return _instance ??= Container(); + } + + static void setInstance(Container? container) { + _instance = container; + } + + Function wrap(Function callback, [List parameters = const []]) { + return () => call(callback, parameters); + } + + dynamic refresh(String abstract, dynamic target, String method) { + return rebinding(abstract, (Container container, dynamic instance) { + Function.apply(target[method], [instance]); + }); + } + + dynamic rebinding(String abstract, Function callback) { + abstract = getAlias(abstract); + + _reboundCallbacks[abstract] ??= []; + _reboundCallbacks[abstract]!.add(callback); + + if (bound(abstract)) { + return make(abstract); + } + + return null; + } + + @override + void bindMethod(dynamic method, Function callback) { + _methodBindings[_parseBindMethod(method)] = callback; + } + + String _parseBindMethod(dynamic method) { + if (method is List && method.length == 2) { + return '${method[0]}@${method[1]}'; + } + return method.toString(); + } + + bool hasMethodBinding(String method) { + return _methodBindings.containsKey(method); + } + + dynamic callMethodBinding(String method, dynamic instance) { + if (!hasMethodBinding(method)) { + throw Exception("Method binding not found for $method"); + } + return _methodBindings[method]!(instance); + } + + @override + bool bound(String abstract) { + return _bindings.containsKey(abstract) || + _instances.containsKey(abstract) || + isAlias(abstract); + } + + @override + void alias(String abstract, String alias) { + if (alias == abstract) { + throw ArgumentError("[$abstract] is aliased to itself."); + } + + _aliases[alias] = abstract; + + _abstractAliases[abstract] ??= []; + _abstractAliases[abstract]!.add(alias); + } + + @override + void tag(dynamic abstracts, String tag, + [List additionalTags = const []]) { + var tags = [tag, ...additionalTags]; + var abstractList = abstracts is List ? abstracts : [abstracts]; + + for (var tag in tags) { + if (!_tags.containsKey(tag)) { + _tags[tag] = []; + } + + _tags[tag]!.addAll(abstractList.cast()); + } + } + + @override + Iterable tagged(String tag) { + if (!_tags.containsKey(tag)) { + return []; + } + + return _tags[tag]!.map((abstract) => make(abstract)); + } + + @override + void bind(String abstract, dynamic concrete, {bool shared = false}) { + _dropStaleInstances(abstract); + + if (concrete == null) { + concrete = abstract; + } + + // If concrete is not a function and not null, we store it directly + if (concrete is! Function && concrete != null) { + _bindings[abstract] = {'concrete': concrete, 'shared': shared}; + } else { + // For functions or null, we wrap it in a closure + _bindings[abstract] = { + 'concrete': (Container container) => + concrete is Function ? concrete(container) : concrete, + 'shared': shared + }; + } + + if (resolved(abstract)) { + _rebound(abstract); + } + } + + @override + void bindIf(String abstract, dynamic concrete, {bool shared = false}) { + if (!bound(abstract)) { + bind(abstract, concrete, shared: shared); + } + } + + @override + void singleton(String abstract, [dynamic concrete]) { + bind(abstract, concrete ?? abstract, shared: true); + } + + @override + void singletonIf(String abstract, [dynamic concrete]) { + if (!bound(abstract)) { + singleton(abstract, concrete); + } + } + + @override + void scoped(String abstract, [dynamic concrete]) { + _scopedInstances.add(abstract); + singleton(abstract, concrete); + } + + @override + void scopedIf(String abstract, [dynamic concrete]) { + if (!bound(abstract)) { + scoped(abstract, concrete); + } + } + + @override + void extend(String abstract, Function(dynamic service) closure) { + abstract = getAlias(abstract); + + if (_instances.containsKey(abstract)) { + _instances[abstract] = closure(_instances[abstract]!); + _rebound(abstract); + } else { + _extenders[abstract] ??= []; + _extenders[abstract]!.add(closure); + + if (resolved(abstract)) { + _rebound(abstract); + } + } + } + + @override + T instance(String abstract, T instance) { + _removeAbstractAlias(abstract); + + bool isBound = bound(abstract); + + _aliases.remove(abstract); + + _instances[abstract] = instance as Object; + + if (isBound) { + _rebound(abstract); + } + + return instance; + } + + @override + void addContextualBinding( + String concrete, String abstract, dynamic implementation) { + _contextual[concrete] ??= {}; + _contextual[concrete]![getAlias(abstract)] = implementation; + } + + @override + ContextualBindingBuilderContract when(dynamic concrete) { + return ContextualBindingBuilder( + this, Util.arrayWrap(concrete).map((c) => getAlias(c)).toList()); + } + + @override + void whenHasAttribute(String attribute, Function handler) { + _contextualAttributes[attribute] = handler; + } + + @override + Function factory(String abstract) { + return () => make(abstract); + } + + @override + void flush() { + _aliases.clear(); + _resolved.clear(); + _bindings.clear(); + _instances.clear(); + _abstractAliases.clear(); + _scopedInstances.clear(); + } + + @override + T make(String abstract, [List parameters = const []]) { + return resolve(abstract, parameters) as T; + } + + @override + dynamic call(dynamic callback, + [List parameters = const [], String? defaultMethod]) { + return BoundMethod.call(this, callback, parameters, defaultMethod); + } + + @override + bool resolved(String abstract) { + if (isAlias(abstract)) { + abstract = getAlias(abstract); + } + + return _resolved.containsKey(abstract) || _instances.containsKey(abstract); + } + + @override + void beforeResolving(dynamic abstract, [Function? callback]) { + if (abstract is String) { + abstract = getAlias(abstract); + } + + if (abstract is Function && callback == null) { + _globalBeforeResolvingCallbacks.add(abstract); + } else { + _beforeResolvingCallbacks[abstract.toString()] ??= []; + if (callback != null) { + _beforeResolvingCallbacks[abstract.toString()]!.add(callback); + } + } + } + + @override + void resolving(dynamic abstract, [Function? callback]) { + if (abstract is String) { + abstract = getAlias(abstract); + } + + if (callback == null && abstract is Function) { + _globalResolvingCallbacks.add(abstract); + } else { + _resolvingCallbacks[abstract.toString()] ??= []; + if (callback != null) { + _resolvingCallbacks[abstract.toString()]!.add(callback); + } + } + } + + @override + void afterResolving(dynamic abstract, [Function? callback]) { + if (abstract is String) { + abstract = getAlias(abstract); + } + + if (abstract is Function && callback == null) { + _globalAfterResolvingCallbacks.add(abstract); + } else { + _afterResolvingCallbacks[abstract.toString()] ??= []; + if (callback != null) { + _afterResolvingCallbacks[abstract.toString()]!.add(callback); + } + } + } + + void afterResolvingAttribute(String attribute, Function callback) { + _afterResolvingAttributeCallbacks[attribute] ??= []; + _afterResolvingAttributeCallbacks[attribute]!.add(callback); + } + + @override + dynamic get(String id) { + try { + return resolve(id); + } catch (e) { + if (has(id) || e is CircularDependencyException) { + rethrow; + } + throw EntryNotFoundException(id); + } + } + + @override + bool has(String id) { + return bound(id); + } + + void _dropStaleInstances(String abstract) { + _instances.remove(abstract); + _aliases.remove(abstract); + } + + void _removeAbstractAlias(String abstract) { + if (!_aliases.containsKey(abstract)) return; + + for (var entry in _abstractAliases.entries) { + entry.value.remove(abstract); + } + } + + void _rebound(String abstract) { + var instance = make(abstract); + + for (var callback in _getReboundCallbacks(abstract)) { + callback(this, instance); + } + } + + List _getReboundCallbacks(String abstract) { + return _reboundCallbacks[abstract] ?? []; + } + + dynamic resolve(String abstract, + [List parameters = const [], bool raiseEvents = true]) { + abstract = getAlias(abstract); + + if (_buildStack.contains(abstract)) { + throw CircularDependencyException([..._buildStack, abstract]); + } + + _buildStack.add(abstract); + + try { + if (raiseEvents) { + _fireBeforeResolvingCallbacks(abstract, parameters); + } + + var concrete = _getContextualConcrete(abstract); + + var needsContextualBuild = parameters.isNotEmpty || concrete != null; + + if (_instances.containsKey(abstract) && !needsContextualBuild) { + return _instances[abstract]; + } + + _with.add(Map.fromEntries(parameters + .asMap() + .entries + .map((e) => MapEntry(e.key.toString(), e.value)))); + + if (concrete == null) { + concrete = _getConcrete(abstract); + } + + var object; + if (_isBuildable(concrete, abstract)) { + object = build(concrete); + // If the result is still a function, execute it + if (object is Function) { + object = object(this); + } + } else { + object = make(concrete); + } + + for (var extender in _getExtenders(abstract)) { + object = extender(object); + } + + if (isShared(abstract) && !needsContextualBuild) { + _instances[abstract] = object; + } + + if (raiseEvents) { + _fireResolvingCallbacks(abstract, object); + } + + if (!needsContextualBuild) { + _resolved[abstract] = true; + } + + _with.removeLast(); + + return object; + } finally { + _buildStack.removeLast(); + } + } + + dynamic _getConcrete(String abstract) { + if (_bindings.containsKey(abstract)) { + return _bindings[abstract]!['concrete']; + } + + return abstract; + } + + bool _isBuildable(dynamic concrete, String abstract) { + return concrete == abstract || concrete is Function; + } + + dynamic _resolveContextualAttribute(ContextualAttribute attribute) { + var attributeType = attribute.runtimeType.toString(); + if (_contextualAttributes.containsKey(attributeType)) { + return _contextualAttributes[attributeType]!(attribute, this); + } + // Try to find a handler based on superclasses + for (var handler in _contextualAttributes.entries) { + if (reflectClass(attribute.runtimeType) + .isSubclassOf(reflectClass(handler.key as Type))) { + return handler.value(attribute, this); + } + } + throw BindingResolutionException( + "No handler registered for ContextualAttribute: $attributeType"); + } + + void fireBeforeResolvingAttributeCallbacks( + 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 (_beforeResolvingCallbacks.containsKey(attributeType)) { + for (var callback in _beforeResolvingCallbacks[attributeType]!) { + callback(instance, object, this); + } + } + } + } + } + + 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); + } + } + } + } + } + + void forgetInstance(String abstract) { + _instances.remove(abstract); + } + + void forgetInstances() { + _instances.clear(); + } + + void forgetScopedInstances() { + for (var scoped in _scopedInstances) { + _instances.remove(scoped); + } + } + + T makeScoped(String abstract) { + // This is similar to make, but ensures the instance is scoped + var instance = make(abstract); + if (!_scopedInstances.contains(abstract)) { + _scopedInstances.add(abstract); + } + return instance; + } + + Map> getBindings() { + return Map.from(_bindings); + } + + dynamic build(dynamic concrete) { + if (concrete is Function) { + _buildStack.add(concrete.toString()); + try { + return concrete(this); + } finally { + _buildStack.removeLast(); + } + } + + if (concrete is String) { + // First, check if it's a simple value binding + if (_bindings.containsKey(concrete) && + _bindings[concrete]!['concrete'] is! Function) { + return _bindings[concrete]!['concrete']; + } + + // If it's not a simple value, proceed with class resolution + try { + Symbol classSymbol = MirrorSystem.getSymbol(concrete)!; + ClassMirror? classMirror; + + // Search for the class in all libraries + for (var lib in currentMirrorSystem().libraries.values) { + if (lib.declarations.containsKey(classSymbol)) { + var declaration = lib.declarations[classSymbol]!; + if (declaration is ClassMirror) { + classMirror = declaration; + break; + } + } + } + + if (classMirror == null) { + // If we can't find a class, return the string as is + return concrete; + } + + var classAttributes = classMirror.metadata; + fireBeforeResolvingAttributeCallbacks(classAttributes, null); + + MethodMirror? constructor = classMirror.declarations.values + .whereType() + .firstWhere((d) => d.isConstructor, + orElse: () => null as MethodMirror); + + if (constructor == null) { + throw BindingResolutionException( + "No constructor found for [$concrete]"); + } + + List parameters = _resolveDependencies(constructor.parameters); + var instance = + classMirror.newInstance(Symbol.empty, parameters).reflectee; + + fireAfterResolvingAttributeCallbacks(classAttributes, instance); + _fireAfterResolvingCallbacks(concrete, instance); + + return instance; + } catch (e) { + // If any error occurs during class instantiation, return the string as is + return concrete; + } + } + + // If concrete is neither a Function nor a String, return it as is + return concrete; + } + + dynamic _getContextualConcrete(String abstract) { + if (_buildStack.isNotEmpty) { + var building = _buildStack.last; + if (_contextual.containsKey(building) && + _contextual[building]!.containsKey(abstract)) { + return _contextual[building]![abstract]; + } + + // Check for attribute-based contextual bindings + try { + var buildingType = MirrorSystem.getSymbol(building); + var buildingMirror = currentMirrorSystem() + .findLibrary(buildingType) + ?.declarations[buildingType]; + if (buildingMirror is ClassMirror) { + for (var attribute in buildingMirror.metadata) { + if (attribute.reflectee is ContextualAttribute) { + var contextualAttribute = + attribute.reflectee as ContextualAttribute; + if (_contextualAttributes + .containsKey(contextualAttribute.runtimeType.toString())) { + var handler = _contextualAttributes[ + contextualAttribute.runtimeType.toString()]!; + return handler(contextualAttribute, this); + } + } + } + } + } catch (e) { + // If we can't find the class, just continue + } + } + + if (_buildStack.isNotEmpty) { + if (_contextual.containsKey(_buildStack.last) && + _contextual[_buildStack.last]!.containsKey(abstract)) { + return _contextual[_buildStack.last]![abstract]; + } + } + + if (_abstractAliases.containsKey(abstract)) { + for (var alias in _abstractAliases[abstract]!) { + if (_buildStack.isNotEmpty && + _contextual.containsKey(_buildStack.last) && + _contextual[_buildStack.last]!.containsKey(alias)) { + return _contextual[_buildStack.last]![alias]; + } + } + } + + return null; + } + + dynamic resolveFromAnnotation(InstanceMirror annotation) { + var instance = annotation.reflectee; + + if (instance is ContextualAttribute) { + // Handle ContextualAttribute + return _resolveContextualAttribute(instance); + } + + // Add more annotation handling as needed + + throw BindingResolutionException( + "Unsupported annotation type: ${annotation.type}"); + } + + void fireAfterResolvingAnnotationCallbacks( + List annotations, dynamic object) { + for (var annotation in annotations) { + if (annotation.reflectee is ContextualAttribute) { + var instance = annotation.reflectee as ContextualAttribute; + if (_afterResolvingAttributeCallbacks + .containsKey(instance.runtimeType.toString())) { + for (var callback in _afterResolvingAttributeCallbacks[ + instance.runtimeType.toString()]!) { + callback(instance, object, this); + } + } + } + } + } + + List _resolveDependencies(List parameters) { + var results = []; + for (var parameter in parameters) { + var parameterName = MirrorSystem.getName(parameter.simpleName); + if (_hasParameterOverride(parameterName)) { + results.add(_getParameterOverride(parameterName)); + } else { + var annotations = parameter.metadata; + if (annotations.isNotEmpty) { + results.add(resolveFromAnnotation(annotations.first)); + } else if (parameter.type.reflectedType != dynamic) { + results.add(make(parameter.type.reflectedType.toString())); + } else if (parameter.isOptional && parameter.defaultValue != null) { + results.add(parameter.defaultValue!.reflectee); + } else { + throw BindingResolutionException( + "Unable to resolve parameter $parameterName"); + } + } + } + return results; + } + + bool _hasParameterOverride(String parameterName) { + return _getLastParameterOverride().containsKey(parameterName); + } + + dynamic _getParameterOverride(String parameterName) { + return _getLastParameterOverride()[parameterName]; + } + + List _getExtenders(String abstract) { + return _extenders[getAlias(abstract)] ?? []; + } + + Map _getLastParameterOverride() { + return _with.isNotEmpty ? _with.last : {}; + } + + void _fireBeforeResolvingCallbacks( + String abstract, List parameters) { + _fireCallbackArray(abstract, parameters, _globalBeforeResolvingCallbacks); + + for (var entry in _beforeResolvingCallbacks.entries) { + if (entry.key == abstract || isSubclassOf(abstract, entry.key)) { + _fireCallbackArray(abstract, parameters, entry.value); + } + } + } + + void _fireResolvingCallbacks(String abstract, dynamic object) { + _fireCallbackArray(object, null, _globalResolvingCallbacks); + + var callbacks = _getCallbacksForType(abstract, object, _resolvingCallbacks); + _fireCallbackArray(object, null, callbacks); + + _fireAfterResolvingCallbacks(abstract, object); + } + + void _fireAfterResolvingCallbacks(String abstract, dynamic object) { + _fireCallbackArray(object, null, _globalAfterResolvingCallbacks); + + var callbacks = + _getCallbacksForType(abstract, object, _afterResolvingCallbacks); + _fireCallbackArray(object, null, callbacks); + } + + void _fireCallbackArray( + dynamic argument, List? parameters, List callbacks) { + for (var callback in callbacks) { + if (parameters != null) { + callback(argument, parameters, this); + } else { + callback(argument, this); + } + } + } + + List _getCallbacksForType(String abstract, dynamic object, + Map> callbacksPerType) { + var results = []; + + for (var entry in callbacksPerType.entries) { + if (entry.key == abstract || object.runtimeType.toString() == entry.key) { + results.addAll(entry.value); + } + } + + return results; + } + + bool isSubclassOf(String child, String parent) { + ClassMirror? childClass = _getClassMirror(child); + ClassMirror? parentClass = _getClassMirror(parent); + + if (childClass == null || parentClass == null) { + return false; + } + + if (childClass == parentClass) { + return true; + } + + ClassMirror? currentClass = childClass.superclass; + while (currentClass != null) { + if (currentClass == parentClass) { + return true; + } + currentClass = currentClass.superclass; + } + + return false; + } + + ClassMirror? _getClassMirror(String className) { + Symbol classSymbol = MirrorSystem.getSymbol(className)!; + for (var lib in currentMirrorSystem().libraries.values) { + if (lib.declarations.containsKey(classSymbol)) { + var declaration = lib.declarations[classSymbol]!; + if (declaration is ClassMirror) { + return declaration; + } + } + } + return null; + } + + String getAlias(String abstract) { + if (!_aliases.containsKey(abstract)) { + return abstract; + } + + if (_aliases[abstract] == abstract) { + throw Exception("[$abstract] is aliased to itself."); + } + + 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/lib/src/contextual_binding_builder.dart b/packages/ioc_container/lib/src/contextual_binding_builder.dart new file mode 100644 index 0000000..efef745 --- /dev/null +++ b/packages/ioc_container/lib/src/contextual_binding_builder.dart @@ -0,0 +1,47 @@ +import 'package:platform_contracts/contracts.dart'; +import 'package:ioc_container/src/util.dart'; + +class ContextualBindingBuilder implements ContextualBindingBuilderContract { + /// The underlying container instance. + final ContainerContract _container; + + /// The concrete instance. + final dynamic _concrete; + + /// The abstract target. + dynamic _needs; + + /// Create a new contextual binding builder. + ContextualBindingBuilder(this._container, this._concrete); + + /// Define the abstract target that depends on the context. + @override + ContextualBindingBuilderContract needs(dynamic abstract) { + _needs = abstract; + return this; + } + + /// Define the implementation for the contextual binding. + @override + void give(dynamic implementation) { + for (var concrete in Util.arrayWrap(_concrete)) { + _container.addContextualBinding(concrete, _needs, implementation); + } + } + + /// Define tagged services to be used as the implementation for the contextual binding. + @override + void giveTagged(String tag) { + give((ContainerContract container) { + var taggedServices = container.tagged(tag); + return taggedServices is List ? taggedServices : taggedServices.toList(); + }); + } + + /// Specify the configuration item to bind as a primitive. + @override + void giveConfig(String key, [dynamic defaultValue]) { + give((ContainerContract container) => + container.get('config').get(key, defaultValue)); + } +} diff --git a/packages/ioc_container/lib/src/entry_not_found_exception.dart b/packages/ioc_container/lib/src/entry_not_found_exception.dart new file mode 100644 index 0000000..9ef37a9 --- /dev/null +++ b/packages/ioc_container/lib/src/entry_not_found_exception.dart @@ -0,0 +1,14 @@ +import 'package:dsr_container/container.dart'; + +class EntryNotFoundException implements Exception, NotFoundExceptionInterface { + @override + final String message; + + EntryNotFoundException([this.message = '']); + + @override + String get id => message; + + @override + String toString() => 'EntryNotFoundException: $message'; +} diff --git a/packages/ioc_container/lib/src/rewindable_generator.dart b/packages/ioc_container/lib/src/rewindable_generator.dart new file mode 100644 index 0000000..da5f5e5 --- /dev/null +++ b/packages/ioc_container/lib/src/rewindable_generator.dart @@ -0,0 +1,24 @@ +import 'dart:collection'; + +class RewindableGenerator extends IterableBase { + /// The generator callback. + final Function _generator; + + /// The number of tagged services. + dynamic _count; + + /// Create a new generator instance. + RewindableGenerator(this._generator, this._count); + + @override + Iterator get iterator => _generator() as Iterator; + + /// Get the total number of tagged services. + @override + int get length { + if (_count is Function) { + _count = _count(); + } + return _count as int; + } +} diff --git a/packages/ioc_container/lib/src/util.dart b/packages/ioc_container/lib/src/util.dart new file mode 100644 index 0000000..4c12d20 --- /dev/null +++ b/packages/ioc_container/lib/src/util.dart @@ -0,0 +1,56 @@ +import 'dart:mirrors'; +import 'package:platform_contracts/contracts.dart'; + +/// @internal +class Util { + /// If the given value is not an array and not null, wrap it in one. + static List arrayWrap(dynamic value) { + if (value == null) { + return []; + } + + return value is List ? value : [value]; + } + + /// Return the default value of the given value. + static dynamic unwrapIfClosure(dynamic value, + [List args = const []]) { + return value is Function ? Function.apply(value, args) : value; + } + + /// Get the class name of the given parameter's type, if possible. + static String? getParameterClassName(ParameterMirror parameter) { + var type = parameter.type; + + if (type.reflectedType == dynamic || + type.isSubtypeOf(reflectType(num)) || + type.isSubtypeOf(reflectType(String)) || + type.isSubtypeOf(reflectType(bool))) { + return null; + } + + var name = MirrorSystem.getName(type.simpleName); + + var declaringClass = parameter.owner as ClassMirror?; + if (declaringClass != null) { + if (name == 'self') { + return MirrorSystem.getName(declaringClass.simpleName); + } + + if (name == 'parent' && declaringClass.superclass != null) { + return MirrorSystem.getName(declaringClass.superclass!.simpleName); + } + } + + return name; + } + + /// Get a contextual attribute from a dependency. + static InstanceMirror? getContextualAttributeFromDependency( + ParameterMirror dependency) { + var contextualAttributes = dependency.metadata.where( + (attr) => attr.type.isSubtypeOf(reflectType(ContextualAttribute))); + + return contextualAttributes.isNotEmpty ? contextualAttributes.first : null; + } +} diff --git a/packages/ioc_container/pubspec.yaml b/packages/ioc_container/pubspec.yaml new file mode 100644 index 0000000..f84237b --- /dev/null +++ b/packages/ioc_container/pubspec.yaml @@ -0,0 +1,19 @@ +name: ioc_container +description: The Container Package for the Protevus Platform +version: 0.0.1 +homepage: https://protevus.com +documentation: https://docs.protevus.com +repository: https://github.com/protevus/platformo + +environment: + sdk: ^3.4.2 + +# Add regular dependencies here. +dependencies: + dsr_container: ^0.1.0 + platform_contracts: ^0.1.0 + # path: ^1.8.0 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.24.0 diff --git a/packages/ioc_container/test/container_test.dart b/packages/ioc_container/test/container_test.dart new file mode 100644 index 0000000..a7209b4 --- /dev/null +++ b/packages/ioc_container/test/container_test.dart @@ -0,0 +1,279 @@ +import 'package:test/test.dart'; +import 'package:ioc_container/container.dart'; +import 'package:platform_contracts/contracts.dart'; + +class ContainerConcreteStub {} + +class IContainerContractStub {} + +class ContainerImplementationStub implements IContainerContractStub {} + +class ContainerDependentStub { + final IContainerContractStub impl; + + ContainerDependentStub(this.impl); +} + +void main() { + group('ContainerTest', () { + late Container container; + + setUp(() { + container = Container(); + setUpBindings(container); + }); + + test('testContainerSingleton', () { + var container1 = Container.getInstance(); + var container2 = Container.getInstance(); + expect(container1, same(container2)); + }); + + test('testClosureResolution', () { + container.bind('name', (Container c) => 'Taylor'); + expect(container.make('name'), equals('Taylor')); + }); + + test('testBindIfDoesntRegisterIfServiceAlreadyRegistered', () { + container.bind('name', (Container c) => 'Taylor'); + container.bindIf('name', (Container c) => 'Dayle'); + + expect(container.make('name'), equals('Taylor')); + }); + + test('testBindIfDoesRegisterIfServiceNotRegisteredYet', () { + container.bind('surname', (Container c) => 'Taylor'); + container.bindIf('name', (Container c) => 'Dayle'); + + expect(container.make('name'), equals('Dayle')); + }); + + test('testSingletonIfDoesntRegisterIfBindingAlreadyRegistered', () { + container.singleton('class', (Container c) => ContainerConcreteStub()); + var firstInstantiation = container.make('class'); + container.singletonIf('class', (Container c) => ContainerConcreteStub()); + var secondInstantiation = container.make('class'); + expect(firstInstantiation, same(secondInstantiation)); + }); + + test('testSingletonIfDoesRegisterIfBindingNotRegisteredYet', () { + container.singleton('class', (Container c) => ContainerConcreteStub()); + container.singletonIf( + 'otherClass', (Container c) => ContainerConcreteStub()); + var firstInstantiation = container.make('otherClass'); + var secondInstantiation = container.make('otherClass'); + expect(firstInstantiation, same(secondInstantiation)); + }); + + test('testSharedClosureResolution', () { + container.singleton('class', (Container c) => ContainerConcreteStub()); + var firstInstantiation = container.make('class'); + var secondInstantiation = container.make('class'); + expect(firstInstantiation, same(secondInstantiation)); + }); + + test('testAutoConcreteResolution', () { + var instance = container.make('ContainerConcreteStub'); + expect(instance, isA()); + }); + + test('testSharedConcreteResolution', () { + container.singleton( + 'ContainerConcreteStub', (Container c) => ContainerConcreteStub()); + + var var1 = container.make('ContainerConcreteStub'); + var var2 = container.make('ContainerConcreteStub'); + expect(var1, same(var2)); + }); + + test('testAbstractToConcreteResolution', () { + container.bind('IContainerContractStub', + (Container c) => ContainerImplementationStub()); + var instance = container.make('ContainerDependentStub'); + expect(instance.impl, isA()); + }); + + test('testNestedDependencyResolution', () { + container.bind('IContainerContractStub', + (Container c) => ContainerImplementationStub()); + var instance = container.make('ContainerNestedDependentStub'); + expect(instance.inner, isA()); + expect(instance.inner.impl, isA()); + }); + + test('testContainerIsPassedToResolvers', () { + container.bind('something', (Container c) => c); + var c = container.make('something'); + expect(c, same(container)); + }); + + test('testArrayAccess', () { + container['something'] = (Container c) => 'foo'; + expect(container['something'], equals('foo')); + }); + + test('testAliases', () { + container['foo'] = 'bar'; + container.alias('foo', 'baz'); + container.alias('baz', 'bat'); + expect(container.make('foo'), equals('bar')); + expect(container.make('baz'), equals('bar')); + expect(container.make('bat'), equals('bar')); + }); + + test('testBindingsCanBeOverridden', () { + container['foo'] = 'bar'; + container['foo'] = 'baz'; + expect(container['foo'], equals('baz')); + }); + + test('testResolutionOfDefaultParameters', () { + container.bind('foo', (Container c) => 'bar'); + container.bind( + 'ContainerDefaultValueStub', + (Container c) => ContainerDefaultValueStub( + c.make('ContainerConcreteStub'), c.make('foo'))); + var result = container.make('ContainerDefaultValueStub'); + expect(result.stub, isA()); + expect(result.defaultValue, equals('bar')); + }); + + test('testUnsetRemoveBoundInstances', () { + container.instance('obj', Object()); + expect(container.bound('obj'), isTrue); + container.forgetInstance('obj'); + expect(container.bound('obj'), isFalse); + }); + + test('testExtendMethod', () { + container.singleton('foo', (Container c) => 'foo'); + container.extend('foo', (dynamic original) { + return '$original bar'; + }); + expect(container.make('foo'), equals('foo bar')); + }); + + test('testFactoryMethod', () { + container.bind('foo', (Container c) => 'foo'); + var factory = container.factory('foo'); + expect(factory(), equals('foo')); + }); + + test('testTaggedBindings', () { + container.tag(['foo', 'bar'], 'foobar'); + container.bind('foo', (Container c) => 'foo'); + container.bind('bar', (Container c) => 'bar'); + var tagged = container.tagged('foobar'); + expect(tagged, containsAll(['foo', 'bar'])); + }); + + test('testCircularDependencies', () { + container.bind('circular1', (Container c) => c.make('circular2')); + container.bind('circular2', (Container c) => c.make('circular1')); + expect(() => container.make('circular1'), + throwsA(isA())); + }); + + test('testScopedClosureResolution', () { + container.scoped('class', (Container c) => Object()); + var firstInstantiation = container.make('class'); + var secondInstantiation = container.make('class'); + expect(firstInstantiation, same(secondInstantiation)); + }); + + test('testScopedClosureResets', () { + container.scoped('class', (Container c) => Object()); + var firstInstantiation = container.makeScoped('class'); + container.forgetScopedInstances(); + var secondInstantiation = container.makeScoped('class'); + expect(firstInstantiation, isNot(same(secondInstantiation))); + }); + + test('testScopedClosureResolution', () { + container.scoped('class', (Container c) => Object()); + var firstInstantiation = container.makeScoped('class'); + var secondInstantiation = container.makeScoped('class'); + expect(firstInstantiation, same(secondInstantiation)); + }); + test('testForgetInstanceForgetsInstance', () { + var containerConcreteStub = ContainerConcreteStub(); + container.instance('ContainerConcreteStub', containerConcreteStub); + expect(container.isShared('ContainerConcreteStub'), isTrue); + container.forgetInstance('ContainerConcreteStub'); + expect(container.isShared('ContainerConcreteStub'), isFalse); + }); + + test('testForgetInstancesForgetsAllInstances', () { + var stub1 = ContainerConcreteStub(); + var stub2 = ContainerConcreteStub(); + var stub3 = ContainerConcreteStub(); + container.instance('Instance1', stub1); + container.instance('Instance2', stub2); + container.instance('Instance3', stub3); + expect(container.isShared('Instance1'), isTrue); + expect(container.isShared('Instance2'), isTrue); + expect(container.isShared('Instance3'), isTrue); + container.forgetInstances(); + expect(container.isShared('Instance1'), isFalse); + expect(container.isShared('Instance2'), isFalse); + expect(container.isShared('Instance3'), isFalse); + }); + + test('testContainerFlushFlushesAllBindingsAliasesAndResolvedInstances', () { + container.bind('ConcreteStub', (Container c) => ContainerConcreteStub(), + shared: true); + container.alias('ConcreteStub', 'ContainerConcreteStub'); + container.make('ConcreteStub'); + expect(container.resolved('ConcreteStub'), isTrue); + expect(container.isAlias('ContainerConcreteStub'), isTrue); + expect(container.getBindings().containsKey('ConcreteStub'), isTrue); + expect(container.isShared('ConcreteStub'), isTrue); + container.flush(); + expect(container.resolved('ConcreteStub'), isFalse); + expect(container.isAlias('ContainerConcreteStub'), isFalse); + expect(container.getBindings().isEmpty, isTrue); + expect(container.isShared('ConcreteStub'), isFalse); + }); + + test('testResolvedResolvesAliasToBindingNameBeforeChecking', () { + container.bind('ConcreteStub', (Container c) => ContainerConcreteStub(), + shared: true); + container.alias('ConcreteStub', 'foo'); + + expect(container.resolved('ConcreteStub'), isFalse); + expect(container.resolved('foo'), isFalse); + + container.make('ConcreteStub'); + + expect(container.resolved('ConcreteStub'), isTrue); + expect(container.resolved('foo'), isTrue); + }); + }); +} + +class ContainerDefaultValueStub { + final ContainerConcreteStub stub; + final String defaultValue; + + ContainerDefaultValueStub(this.stub, [this.defaultValue = 'taylor']); +} + +class ContainerNestedDependentStub { + final ContainerDependentStub inner; + + ContainerNestedDependentStub(this.inner); +} + +// Helper function to set up bindings +void setUpBindings(Container container) { + container.bind( + 'ContainerConcreteStub', (Container c) => ContainerConcreteStub()); + container.bind( + 'ContainerDependentStub', + (Container c) => + ContainerDependentStub(c.make('IContainerContractStub'))); + container.bind( + 'ContainerNestedDependentStub', + (Container c) => + ContainerNestedDependentStub(c.make('ContainerDependentStub'))); +} diff --git a/packages/service_container/lib/service_container.dart b/packages/service_container/lib/service_container.dart index b6ab475..bad1462 100644 --- a/packages/service_container/lib/service_container.dart +++ b/packages/service_container/lib/service_container.dart @@ -1 +1,6 @@ +export 'src/bound_method.dart'; export 'src/container.dart'; +export 'src/contextual_binding_builder.dart'; +export 'src/entry_not_found_exception.dart'; +export 'src/rewindable_generator.dart'; +export 'src/util.dart'; diff --git a/packages/service_container/pubspec.yaml b/packages/service_container/pubspec.yaml index 074dbb9..b462c26 100644 --- a/packages/service_container/pubspec.yaml +++ b/packages/service_container/pubspec.yaml @@ -18,3 +18,4 @@ dependencies: dev_dependencies: lints: ^3.0.0 test: ^1.24.0 + platform_config: ^0.1.0 diff --git a/packages/service_container/test/container_resolve_non_instantiable_test.dart b/packages/service_container/test/container_resolve_non_instantiable_test.dart new file mode 100644 index 0000000..6f6384d --- /dev/null +++ b/packages/service_container/test/container_resolve_non_instantiable_test.dart @@ -0,0 +1,61 @@ +import 'package:test/test.dart'; +import 'package:platform_service_container/service_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/service_container/test/container_tagging_test.dart b/packages/service_container/test/container_tagging_test.dart new file mode 100644 index 0000000..f0116a0 --- /dev/null +++ b/packages/service_container/test/container_tagging_test.dart @@ -0,0 +1,99 @@ +import 'package:test/test.dart'; +import 'package:platform_service_container/service_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/service_container/test/contextual_attribute_binding_test.dart b/packages/service_container/test/contextual_attribute_binding_test.dart new file mode 100644 index 0000000..36eb233 --- /dev/null +++ b/packages/service_container/test/contextual_attribute_binding_test.dart @@ -0,0 +1,171 @@ +import 'package:test/test.dart'; +import 'package:platform_service_container/service_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/service_container/test/contextual_binding_test.dart b/packages/service_container/test/contextual_binding_test.dart new file mode 100644 index 0000000..bf04458 --- /dev/null +++ b/packages/service_container/test/contextual_binding_test.dart @@ -0,0 +1,164 @@ +import 'package:test/test.dart'; +import 'package:platform_service_container/service_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/service_container/test/resolving_callback_test.dart b/packages/service_container/test/resolving_callback_test.dart new file mode 100644 index 0000000..0ba555e --- /dev/null +++ b/packages/service_container/test/resolving_callback_test.dart @@ -0,0 +1,165 @@ +import 'package:test/test.dart'; +import 'package:platform_service_container/service_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/service_container/test/rewindable_generator_test.dart b/packages/service_container/test/rewindable_generator_test.dart new file mode 100644 index 0000000..3d525e7 --- /dev/null +++ b/packages/service_container/test/rewindable_generator_test.dart @@ -0,0 +1,37 @@ +import 'package:test/test.dart'; +import 'package:platform_service_container/service_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/service_container/test/util_test.dart b/packages/service_container/test/util_test.dart new file mode 100644 index 0000000..777715f --- /dev/null +++ b/packages/service_container/test/util_test.dart @@ -0,0 +1,36 @@ +import 'package:test/test.dart'; +import 'package:platform_service_container/service_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); + }); + }); +}