LCOV - code coverage report
Current view: top level - lib - ioc_container.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 97 97 100.0 %
Date: 2023-03-28 15:06:06 Functions: 0 0 -

          Line data    Source code
       1             : ///❌ An exception that occurs when the service is not found
       2             : class ServiceNotFoundException<T> implements Exception {
       3             :   ///❌ Creates a new instance of [ServiceNotFoundException]
       4           1 :   const ServiceNotFoundException(this.message);
       5             : 
       6             :   ///The exception message
       7             :   final String message;
       8           1 :   @override
       9           2 :   String toString() => 'ServiceNotFoundException: $message';
      10             : }
      11             : 
      12             : ///📙 Defines a factory for the service and whether or not it is a singleton.
      13             : class ServiceDefinition<T> {
      14             :   ///📙 Defines a factory for the service and whether or not it is a singleton.
      15           2 :   const ServiceDefinition(
      16             :     this.factory, {
      17             :     this.isSingleton = false,
      18             :     this.dispose,
      19             :     this.disposeAsync,
      20             :   })  : assert(
      21           2 :           !isSingleton || dispose == null,
      22             :           'Singleton factories cannot have a dispose method',
      23             :         ),
      24             :         assert(
      25           2 :           dispose == null || disposeAsync == null,
      26             :           "Service definitions can't have both dispose and disposeAsync",
      27             :         );
      28             : 
      29             :   ///1️⃣ If true, only one instance of the service will be created and shared
      30             :   ///for the lifespan of the container.
      31             :   final bool isSingleton;
      32             : 
      33             :   ///🏭 The factory that creates the instance of the service and can access
      34             :   ///other services in this container
      35             :   final T Function(
      36             :     IocContainer container,
      37             :   ) factory;
      38             : 
      39             :   ///🗑️ The dispose method that is called when you dispose the scope
      40             :   final void Function(T service)? dispose;
      41             : 
      42             :   ///🗑️ The async dispose method that is called when you dispose the scope
      43             :   final Future<void> Function(T service)? disposeAsync;
      44             : 
      45           3 :   void _dispose(T instance) => dispose?.call(instance);
      46             : 
      47           3 :   Future<void> _disposeAsync(T instance) async => disposeAsync?.call(instance);
      48             : }
      49             : 
      50             : ///📦 A built Ioc Container. To create a new [IocContainer], use
      51             : ///[IocContainerBuilder]. To get a service from the container, call
      52             : ///[get], [getAsync], or [getAsyncSafe]
      53             : ///Call [scoped] to get a scoped container
      54             : class IocContainer {
      55             :   ///📦 Creates an IocContainer. You can build your own container by injecting
      56             :   ///service definitions and singletons here, but you should probably use
      57             :   ///[IocContainerBuilder] instead.
      58           2 :   const IocContainer(
      59             :     this.serviceDefinitionsByType,
      60             :     this.singletons, {
      61             :     this.isScoped = false,
      62             :   });
      63             : 
      64             :   ///📙 The service definitions by type
      65             :   final Map<Type, ServiceDefinition<dynamic>> serviceDefinitionsByType;
      66             : 
      67             :   ///1️⃣ Map of singletons or scoped services by type. This map is mutable
      68             :   ///so the container can store scope or singletons
      69             :   final Map<Type, Object> singletons;
      70             : 
      71             :   ///⌖ If true, this container is a scoped container. Scoped containers never
      72             :   ///create more than one instance of a service
      73             :   final bool isScoped;
      74             : 
      75             :   ///👐 Get an instance of the service by type
      76           2 :   T get<T extends Object>() {
      77           4 :     final serviceDefinition = serviceDefinitionsByType[T];
      78             : 
      79             :     if (serviceDefinition == null) {
      80           1 :       throw ServiceNotFoundException<T>(
      81           1 :         'Service $T not found',
      82             :       );
      83             :     }
      84             : 
      85           4 :     if (serviceDefinition.isSingleton || isScoped) {
      86           4 :       final singletonValue = singletons[T];
      87             : 
      88             :       if (singletonValue != null) {
      89             :         return singletonValue as T;
      90             :       }
      91             :     }
      92             : 
      93           4 :     final service = serviceDefinition.factory(this) as T;
      94             : 
      95           4 :     if (serviceDefinition.isSingleton || isScoped) {
      96           4 :       singletons[T] = service;
      97             :     }
      98             : 
      99             :     return service;
     100             :   }
     101             : 
     102             :   ///👐 This is a shortcut for [get]
     103           2 :   T call<T extends Object>() => get<T>();
     104             : }
     105             : 
     106             : ///👷 A builder for creating an [IocContainer].
     107             : class IocContainerBuilder {
     108             :   ///👷 Creates a container builder
     109           2 :   IocContainerBuilder({this.allowOverrides = false});
     110             :   final Map<Type, ServiceDefinition<dynamic>> _serviceDefinitionsByType = {};
     111             : 
     112             :   /// 🔃 Throw an error if a service is added more than once. Set this to true
     113             :   /// when you want to add mocks to set of services for a test.
     114             :   final bool allowOverrides;
     115             : 
     116             :   ///📙 Add a factory to the container.
     117           2 :   void addServiceDefinition<T>(
     118             :     ///Add a factory and whether or not this service is a singleton
     119             :     ServiceDefinition<T> serviceDefinition,
     120             :   ) {
     121           4 :     if (_serviceDefinitionsByType.containsKey(T)) {
     122           1 :       if (allowOverrides) {
     123           2 :         _serviceDefinitionsByType.remove(T);
     124             :       } else {
     125           1 :         throw Exception('Service already exists');
     126             :       }
     127             :     }
     128             : 
     129           6 :     _serviceDefinitionsByType.putIfAbsent(T, () => serviceDefinition);
     130             :   }
     131             : 
     132             :   ///📦 Create an [IocContainer] from the [IocContainerBuilder].
     133           4 :   IocContainer toContainer() => IocContainer(
     134           2 :         Map<Type, ServiceDefinition<dynamic>>.unmodifiable(
     135           2 :           _serviceDefinitionsByType,
     136             :         ),
     137           2 :         <Type, Object>{},
     138             :       );
     139             : 
     140             :   ///Add a singleton service to the container.
     141           2 :   void addSingletonService<T>(T service) => addServiceDefinition(
     142           1 :         ServiceDefinition<T>(
     143           1 :           (container) => service,
     144             :           isSingleton: true,
     145             :         ),
     146             :       );
     147             : 
     148             :   ///1️⃣ Add a singleton factory to the container. The container
     149             :   ///will only call this once throughout the lifespan of the container
     150           2 :   void addSingleton<T>(
     151             :     T Function(
     152             :       IocContainer container,
     153             :     )
     154             :         factory,
     155             :   ) =>
     156           2 :       addServiceDefinition<T>(
     157           2 :         ServiceDefinition<T>(
     158           4 :           (container) => factory(container),
     159             :           isSingleton: true,
     160             :         ),
     161             :       );
     162             : 
     163             :   ///🏭 Add a factory to the container.
     164           2 :   void add<T>(
     165             :     T Function(
     166             :       IocContainer container,
     167             :     )
     168             :         factory, {
     169             :     void Function(T service)? dispose,
     170             :   }) =>
     171           2 :       addServiceDefinition<T>(
     172           2 :         ServiceDefinition<T>(
     173           4 :           (container) => factory(container),
     174             :           dispose: dispose,
     175             :         ),
     176             :       );
     177             : 
     178             :   ///⌛ Adds an async [ServiceDefinition]
     179           1 :   void addAsync<T>(
     180             :     Future<T> Function(
     181             :       IocContainer container,
     182             :     )
     183             :         factory, {
     184             :     Future<void> Function(T service)? disposeAsync,
     185             :   }) =>
     186           1 :       addServiceDefinition<Future<T>>(
     187           1 :         ServiceDefinition<Future<T>>(
     188           2 :           (container) async => factory(container),
     189           2 :           disposeAsync: (service) async => disposeAsync?.call(await service),
     190             :         ),
     191             :       );
     192             : 
     193             :   ///1️⃣ ⌛ Add an async singleton factory to the container. The container
     194             :   ///will only call the factory once throughout the lifespan of the container
     195           1 :   void addSingletonAsync<T>(
     196             :     Future<T> Function(
     197             :       IocContainer container,
     198             :     )
     199             :         factory,
     200             :   ) =>
     201           1 :       addServiceDefinition<Future<T>>(
     202           1 :         ServiceDefinition<Future<T>>(
     203             :           isSingleton: true,
     204           2 :           (container) async => factory(container),
     205             :         ),
     206             :       );
     207             : }
     208             : 
     209             : ///Extensions for IocContainer
     210             : extension IocContainerExtensions on IocContainer {
     211             :   ///🗑️ Dispose all singletons or scope. Warning: don't use this on your root
     212             :   ///container. You should only use this on scoped containers.
     213           1 :   Future<void> dispose() async {
     214           1 :     assert(isScoped, 'Only dispose scoped containers');
     215           3 :     for (final type in singletons.keys) {
     216             :       //Note: we don't need to check if the service is a singleton because
     217             :       //singleton service definitions never have dispose
     218           2 :       final serviceDefinition = serviceDefinitionsByType[type]!;
     219             : 
     220             :       //We can't do a null check here because if a Dart issue
     221           4 :       serviceDefinition._dispose.call(singletons[type]);
     222             : 
     223           3 :       await serviceDefinition._disposeAsync(singletons[type]);
     224             :     }
     225           2 :     singletons.clear();
     226             :   }
     227             : 
     228             :   ///🏁 Initalizes and stores each singleton in case you want a zealous
     229             :   ///container instead of a lazy one
     230           1 :   void initializeSingletons() {
     231           3 :     serviceDefinitionsByType.forEach((type, serviceDefinition) {
     232           1 :       if (serviceDefinition.isSingleton) {
     233           2 :         singletons.putIfAbsent(
     234             :           type,
     235           3 :           () => serviceDefinition.factory(
     236             :             this,
     237             :           ) as Object,
     238             :         );
     239             :       }
     240             :     });
     241             :   }
     242             : 
     243             :   ///⌖ Gets a service, but each service in the object mesh will have only one
     244             :   ///instance. If you want to get multiple scoped objects, call [scoped] to
     245             :   ///get a reusable [IocContainer] and then call [get] or [getAsync] on that.
     246           3 :   T getScoped<T extends Object>() => scoped().get<T>();
     247             : 
     248             :   ///⌖ Creates a new Ioc Container for a particular scope. Does not use existing
     249             :   ///singletons/scope by default. Warning: if you use the existing singletons,
     250             :   ///calling [dispose] will dispose those singletons
     251           2 :   IocContainer scoped({
     252             :     bool useExistingSingletons = false,
     253             :   }) =>
     254           2 :       IocContainer(
     255           2 :         serviceDefinitionsByType,
     256           4 :         useExistingSingletons ? Map<Type, Object>.from(singletons) : {},
     257             :         isScoped: true,
     258             :       );
     259             : 
     260             :   ///⌛ Gets a service that requires async initialization. Add these services
     261             :   ///with [IocContainerBuilder.addAsync] or
     262             :   ///[IocContainerBuilder.addSingletonAsync] You can only use this on factories
     263             :   ///that return a Future<>.
     264             :   ///Warning: if the definition is singleton/scoped and the Future fails, the factory will never return a
     265             :   ///valid value, so use [getAsyncSafe] to ensure the container doesn't store
     266             :   ///failed singletons
     267           2 :   Future<T> getAsync<T>() async => get<Future<T>>();
     268             : 
     269             :   ///See [getAsync].
     270             :   ///Makes an async call by creating a temporary scoped container,
     271             :   ///attempting to make the async initialization and merging the result with the
     272             :   ///current container if there is success.
     273             :   ///
     274             :   ///⚠️ Warning: allows reentrancy and does not do error handling.
     275             :   ///If you call this more than once in parallel it will create multiple
     276             :   ///Futures - i.e. make multiple async calls. You need to guard against this
     277             :   ///and perform retries on failure. Be aware that this may happen even if
     278             :   ///you only call this method in a single location in your app.
     279             :   ///You may need a an async lock.
     280           1 :   Future<T> getAsyncSafe<T>() async {
     281           1 :     final scope = scoped();
     282             : 
     283           1 :     final service = await scope.getAsync<T>();
     284             : 
     285           1 :     merge(scope);
     286             : 
     287             :     return service;
     288             :   }
     289             : 
     290             :   ///⛙ Merge the singletons or scope from a container into this container. This
     291             :   ///only moves singleton definitions by default, but you can override this
     292             :   ///with [mergeTest]
     293           1 :   void merge(
     294             :     IocContainer container, {
     295             :     bool overwrite = false,
     296             :     bool Function(
     297             :       Type type,
     298             :       ServiceDefinition<dynamic>? serviceDefinition,
     299             :       Object? singleton,
     300             :     )?
     301             :         mergeTest,
     302             :   }) {
     303           3 :     for (final key in container.singletons.keys.where(
     304             :       mergeTest != null
     305           2 :           ? (type) => mergeTest(
     306             :                 type,
     307           2 :                 serviceDefinitionsByType[type],
     308           2 :                 container.singletons[type],
     309             :               )
     310           4 :           : (type) => serviceDefinitionsByType[type]?.isSingleton ?? false,
     311           1 :     )) {
     312             :       if (overwrite) {
     313           4 :         singletons[key] = container.singletons[key]!;
     314             :       } else {
     315           5 :         singletons.putIfAbsent(key, () => container.singletons[key]!);
     316             :       }
     317             :     }
     318             :   }
     319             : 
     320             :   ///✔️ Checks if an instance of the service already exists in the container.
     321             :   ///This will be true if the service is a singleton and has already been
     322             :   ///resolved once, or the container is scoped and the service has been
     323             :   ///resolved in the current scope.
     324           3 :   bool hasInstance<T extends Object>() => singletons.containsKey(T);
     325             : 
     326             :   ///➰ Recursively climbs through a hierarchy of containers until it finds an
     327             :   ///instance of the service, or calls [get] the last container returned by
     328             :   ///[nextParent] if the service does not already exist
     329           1 :   T fallback<T extends Object>(
     330             :     IocContainer? Function() nextParent,
     331             :   ) {
     332           1 :     if (hasInstance<T>()) {
     333           1 :       return get<T>();
     334             :     }
     335             : 
     336           1 :     final parent = nextParent();
     337           1 :     if (parent == null) return get<T>();
     338           1 :     return parent.hasInstance<T>()
     339           1 :         ? parent.get<T>()
     340           1 :         : parent.fallback(nextParent);
     341             :   }
     342             : }
     343             : 
     344             : extension IocContainersExtensions on List<IocContainer> {
     345             :   ///Iterates through the list of containers from the last element to the first
     346             :   /// and returns the first one that already has an instance of the service,
     347             :   /// or calls get<>() on the first container in the list
     348           1 :   T fallback<T extends Object>() {
     349           2 :     assert(length > 0, 'The list must have at least one element');
     350             : 
     351           2 :     if (length == 1) {
     352           2 :       return this[0].get<T>();
     353             :     }
     354             : 
     355           4 :     for (var i = length - 1; i >= 0; i--) {
     356           2 :       if (this[i].hasInstance<T>()) {
     357           2 :         return this[i].get<T>();
     358             :       }
     359             :     }
     360             : 
     361             :     // coverage:ignore-start
     362             :     //This shouldn't happen but the compiler doesn't know that
     363             :     return this[0].get<T>();
     364             :     // coverage:ignore-end
     365             :   }
     366             : }

Generated by: LCOV version 1.16