From 015866aa649d35b75c149c53492bf5d1b1eb13ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Oravec?= Date: Tue, 17 Dec 2024 15:09:19 +0100 Subject: [PATCH 1/8] added async container --- Sources/Container/AsyncContainer.swift | 154 ++++++++++++++++++ Sources/Models/AsyncRegistration.swift | 37 +++++ .../AsyncDependencyRegistering.swift | 94 +++++++++++ .../Resolution/AsyncDependencyResolving.swift | 72 ++++++++ 4 files changed, 357 insertions(+) create mode 100644 Sources/Container/AsyncContainer.swift create mode 100644 Sources/Models/AsyncRegistration.swift create mode 100644 Sources/Protocols/Registration/AsyncDependencyRegistering.swift create mode 100644 Sources/Protocols/Resolution/AsyncDependencyResolving.swift diff --git a/Sources/Container/AsyncContainer.swift b/Sources/Container/AsyncContainer.swift new file mode 100644 index 0000000..6dfaeea --- /dev/null +++ b/Sources/Container/AsyncContainer.swift @@ -0,0 +1,154 @@ +// +// AsyncContainer.swift +// DependencyInjection +// +// Created by Róbert Oravec on 17.12.2024. +// + +import Foundation + +/// Dependency Injection Container where dependencies are registered and from where they are consequently retrieved (i.e. resolved) +public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegistering { + /// Shared singleton + public static let shared: AsyncContainer = { + AsyncContainer() + }() + + private var registrations = [RegistrationIdentifier: AsyncRegistration]() + private var sharedInstances = [RegistrationIdentifier: Any]() + + /// Create new instance of ``Container`` + public init() {} + + /// Remove all registrations and already instantiated shared instances from the container + func clean() { + registrations.removeAll() + + releaseSharedInstances() + } + + /// Remove already instantiated shared instances from the container + func releaseSharedInstances() { + sharedInstances.removeAll() + } + + // MARK: Register dependency, Autoregister dependency + + + /// Register a dependency + /// + /// - Parameters: + /// - type: Type of the dependency to register + /// - scope: Scope of the dependency. If `.new` is used, the `factory` closure is called on each `resolve` call. If `.shared` is used, the `factory` closure is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton + /// - factory: Closure that is called when the dependency is being resolved + public func register( + type: Dependency.Type, + in scope: DependencyScope, + factory: @escaping Factory + ) async { + let registration = AsyncRegistration(type: type, scope: scope, factory: factory) + + registrations[registration.identifier] = registration + + // With a new registration we should clean all shared instances + // because the new registered factory most likely returns different objects and we have no way to tell + sharedInstances[registration.identifier] = nil + } + + // MARK: Register dependency with argument, Autoregister dependency with argument + + /// Register a dependency with an argument + /// + /// The argument is typically a parameter in an initiliazer of the dependency that is not registered in the same container, + /// therefore, it needs to be passed in `resolve` call + /// + /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. + /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. + /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? + /// Shared instances are typically not dependent on variable input parameters by definition. + /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. + /// + /// - Parameters: + /// - type: Type of the dependency to register + /// - factory: Closure that is called when the dependency is being resolved + public func register(type: Dependency.Type, factory: @escaping FactoryWithArgument) async { + let registration = AsyncRegistration(type: type, scope: .new, factory: factory) + + registrations[registration.identifier] = registration + } + + // MARK: Resolve dependency + + /// Resolve a dependency that was previously registered with `register` method + /// + /// If a dependency of the given type with the given argument wasn't registered before this method call + /// the method throws ``ResolutionError.dependencyNotRegistered`` + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument: Argument that will passed as an input parameter to the factory method that was defined with `register` method + public func tryResolve(type: Dependency.Type, argument: Argument) async throws -> Dependency { + let identifier = RegistrationIdentifier(type: type, argument: Argument.self) + + let registration = try getRegistration(with: identifier) + + let dependency: Dependency = try await getDependency(from: registration, with: argument) + + return dependency + } + + /// Resolve a dependency that was previously registered with `register` method + /// + /// If a dependency of the given type wasn't registered before this method call + /// the method throws ``ResolutionError.dependencyNotRegistered`` + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + public func tryResolve(type: Dependency.Type) async throws -> Dependency { + let identifier = RegistrationIdentifier(type: type) + + let registration = try getRegistration(with: identifier) + + let dependency: Dependency = try await getDependency(from: registration) + + return dependency + } +} + +// MARK: Private methods +private extension AsyncContainer { + func getRegistration(with identifier: RegistrationIdentifier) throws -> AsyncRegistration { + guard let registration = registrations[identifier] else { + throw ResolutionError.dependencyNotRegistered( + message: "Dependency of type \(identifier.description) wasn't registered in container \(self)" + ) + } + + return registration + } + + func getDependency(from registration: AsyncRegistration, with argument: Any? = nil) async throws -> Dependency { + switch registration.scope { + case .shared: + if let dependency = sharedInstances[registration.identifier] as? Dependency { + return dependency + } + case .new: + break + } + + // We use force cast here because we are sure that the type-casting always succeed + // The reason why the `factory` closure returns ``Any`` is that we have to erase the generic type in order to store the registration + // When the registration is created it can be initialized just with a `factory` that returns the matching type + let dependency = try await registration.factory(self, argument) as! Dependency + + switch registration.scope { + case .shared: + sharedInstances[registration.identifier] = dependency + case .new: + break + } + + return dependency + } +} diff --git a/Sources/Models/AsyncRegistration.swift b/Sources/Models/AsyncRegistration.swift new file mode 100644 index 0000000..c0966b5 --- /dev/null +++ b/Sources/Models/AsyncRegistration.swift @@ -0,0 +1,37 @@ +// +// AsyncRegistration.swift +// DependencyInjection +// +// Created by Róbert Oravec on 16.12.2024. +// + +import Foundation + +/// Object that represents a registered dependency and stores a closure, i.e. a factory that returns the desired dependency +struct AsyncRegistration { + let identifier: RegistrationIdentifier + let scope: DependencyScope + let factory: (any AsyncDependencyResolving, Any?) async throws -> Any + + /// Initializer for registrations that don't need any variable argument + init(type: T.Type, scope: DependencyScope, factory: @escaping (any AsyncDependencyResolving) async -> T) { + self.identifier = RegistrationIdentifier(type: type) + self.scope = scope + self.factory = { resolver, _ in await factory(resolver) } + } + + /// Initializer for registrations that expect a variable argument passed to the factory closure when the dependency is being resolved + init(type: T.Type, scope: DependencyScope, factory: @escaping (any AsyncDependencyResolving, Argument) async -> T) { + let registrationIdentifier = RegistrationIdentifier(type: type, argument: Argument.self) + + self.identifier = registrationIdentifier + self.scope = scope + self.factory = { resolver, arg in + guard let argument = arg as? Argument else { + throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept an argument of type \(Argument.self)") + } + + return await factory(resolver, argument) + } + } +} diff --git a/Sources/Protocols/Registration/AsyncDependencyRegistering.swift b/Sources/Protocols/Registration/AsyncDependencyRegistering.swift new file mode 100644 index 0000000..38ac66a --- /dev/null +++ b/Sources/Protocols/Registration/AsyncDependencyRegistering.swift @@ -0,0 +1,94 @@ +// +// AsyncDependencyRegistering.swift +// DependencyInjection +// +// Created by Róbert Oravec on 17.12.2024. +// + +import Foundation + +/// A type that is able to register a dependency +public protocol AsyncDependencyRegistering { + /// Factory closure that instantiates the required dependency + typealias Factory = (any AsyncDependencyResolving) async -> Dependency + + /// Factory closure that instantiates the required dependency with the given variable argument + typealias FactoryWithArgument = (any AsyncDependencyResolving, Argument) async -> Dependency + + /// Register a dependency + /// + /// - Parameters: + /// - type: Type of the dependency to register + /// - scope: Scope of the dependency. If `.new` is used, the `factory` closure is called on each `resolve` call. If `.shared` is used, the `factory` closure is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton + /// - factory: Closure that is called when the dependency is being resolved + func register(type: Dependency.Type, in scope: DependencyScope, factory: @escaping Factory) async + + /// Register a dependency with a variable argument + /// + /// The argument is typically a parameter in an initiliazer of the dependency that is not registered in the same resolver (i.e. container), + /// therefore, it needs to be passed in `resolve` call + /// + /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. + /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. + /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? + /// Shared instances are typically not dependent on variable input parameters by definition. + /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. + /// + /// - Parameters: + /// - type: Type of the dependency to register + /// - factory: Closure that is called when the dependency is being resolved + func register(type: Dependency.Type, factory: @escaping FactoryWithArgument) async +} + +// MARK: Overloaded factory methods +public extension AsyncDependencyRegistering { + /// Default ``DependencyScope`` value + /// + /// The default value is `shared` + static var defaultScope: DependencyScope { + DependencyScope.shared + } + + /// Register a dependency in the default ``DependencyScope``, i.e. in the `shared` scope + /// + /// - Parameters: + /// - type: Type of the dependency to register + /// - factory: Closure that is called when the dependency is being resolved + func register(type: Dependency.Type, factory: @escaping Factory) async { + await register(type: type, in: Self.defaultScope, factory: factory) + } + + /// Register a dependency with an implicit type determined by the factory closure return type + /// + /// - Parameters: + /// - scope: Scope of the dependency. If `.new` is used, the `factory` closure is called on each `resolve` call. If `.shared` is used, the `factory` closure is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton + /// - factory: Closure that is called when the dependency is being resolved + func register(in scope: DependencyScope, factory: @escaping Factory) async { + await register(type: Dependency.self, in: scope, factory: factory) + } + + /// Register a dependency with an implicit type determined by the factory closure return type and in the default ``DependencyScope``, i.e. in the `shared` scope + /// + /// - Parameters: + /// - factory: Closure that is called when the dependency is being resolved + func register(factory: @escaping Factory) async { + await register(type: Dependency.self, in: Self.defaultScope, factory: factory) + } + + /// Register a dependency with a variable argument. The type of the dependency is determined implicitly based on the factory closure return type + /// + /// The argument is typically a parameter in an initializer of the dependency that is not registered in the same resolver (i.e. container), + /// therefore, it needs to be passed in `resolve` call + /// + /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. + /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. + /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? + /// Shared instances are typically not dependent on variable input parameters by definition. + /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. + /// + /// - Parameters: + /// - factory: Closure that is called when the dependency is being resolved + func register(factory: @escaping FactoryWithArgument) async { + await register(type: Dependency.self, factory: factory) + } +} diff --git a/Sources/Protocols/Resolution/AsyncDependencyResolving.swift b/Sources/Protocols/Resolution/AsyncDependencyResolving.swift new file mode 100644 index 0000000..b4a43fe --- /dev/null +++ b/Sources/Protocols/Resolution/AsyncDependencyResolving.swift @@ -0,0 +1,72 @@ +// +// AsyncDependencyResolving.swift +// DependencyInjection +// +// Created by Róbert Oravec on 17.12.2024. +// + +import Foundation + +/// A type that is able to resolve a dependency +public protocol AsyncDependencyResolving { + /// Resolve a dependency that was previously registered within the container + /// + /// If the container doesn't contain any registration for a dependency with the given type, ``ResolutionError`` is thrown + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + func tryResolve(type: T.Type) async throws -> T + + /// Resolve a dependency with a variable argument that was previously registered within the container + /// + /// If the container doesn't contain any registration for a dependency with the given type or if an argument of a different type than expected is passed, ``ResolutionError`` is thrown + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument: Argument that will be passed as an input parameter to the factory method + func tryResolve(type: T.Type, argument: Argument) async throws -> T +} + +public extension AsyncDependencyResolving { + /// Resolve a dependency that was previously registered within the container + /// + /// If the container doesn't contain any registration for a dependency with the given type, a runtime error occurs + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + func resolve(type: T.Type) async -> T { + try! await tryResolve(type: type) + } + + /// Resolve a dependency that was previously registered within the container. A type of the required dependency is inferred from the return type + /// + /// If the container doesn't contain any registration for a dependency with the given type, a runtime error occurs + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + func resolve() async -> T { + await resolve(type: T.self) + } + + /// Resolve a dependency with a variable argument that was previously registered within the container + /// + /// If the container doesn't contain any registration for a dependency with the given type or if an argument of a different type than expected is passed, a runtime error occurs + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument: Argument that will be passed as an input parameter to the factory method + func resolve(type: T.Type, argument: Argument) async -> T { + try! await tryResolve(type: type, argument: argument) + } + + /// Resolve a dependency with a variable argument that was previously registered within the container. The type of the required dependency is inferred from the return type + /// + /// If the container doesn't contain any registration for a dependency with the given type or if an argument of a different type than expected is passed, a runtime error occurs + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument: Argument that will be passed as an input parameter to the factory method + func resolve(argument: Argument) async -> T { + await resolve(type: T.self, argument: argument) + } +} From 250d052700ae373086a812241885bf2f966ace92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Oravec?= Date: Tue, 17 Dec 2024 15:12:10 +0100 Subject: [PATCH 2/8] added module registration --- Sources/Protocols/ModileRegistration.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Sources/Protocols/ModileRegistration.swift diff --git a/Sources/Protocols/ModileRegistration.swift b/Sources/Protocols/ModileRegistration.swift new file mode 100644 index 0000000..3ed4b16 --- /dev/null +++ b/Sources/Protocols/ModileRegistration.swift @@ -0,0 +1,10 @@ +// +// ModileRegistration.swift +// DependencyInjection +// +// Created by Róbert Oravec on 17.12.2024. +// + +public protocol ModuleRegistration { + func registerDependencies(in container: AsyncContainer) async +} From f76d19dfc75a877c5d647513a9f990498e6bfac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Oravec?= Date: Tue, 17 Dec 2024 15:36:05 +0100 Subject: [PATCH 3/8] module registration is now static --- Sources/Protocols/ModileRegistration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Protocols/ModileRegistration.swift b/Sources/Protocols/ModileRegistration.swift index 3ed4b16..45b2ef5 100644 --- a/Sources/Protocols/ModileRegistration.swift +++ b/Sources/Protocols/ModileRegistration.swift @@ -6,5 +6,5 @@ // public protocol ModuleRegistration { - func registerDependencies(in container: AsyncContainer) async + static func registerDependencies(in container: AsyncContainer) async } From f53174232b71977bd1516908eb9d5c092b9f3305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Oravec?= Date: Thu, 19 Dec 2024 11:38:12 +0100 Subject: [PATCH 4/8] added tests --- Package.swift | 5 +- Sources/Container/AsyncContainer.swift | 10 +- Sources/Container/Container.swift | 2 +- Sources/Models/AsyncRegistration.swift | 10 +- Sources/Models/DependencyScope.swift | 2 +- .../AsyncDependencyRegistering.swift | 16 +- .../Resolution/AsyncDependencyResolving.swift | 12 +- Tests/Common/AsyncDITestCase.swift | 25 +++ Tests/Common/Dependencies.swift | 16 +- Tests/Container/AsyncArgumentTests.swift | 59 ++++++ Tests/Container/AsyncBaseTests.swift | 86 +++++++++ Tests/Container/AsyncComplexTests.swift | 175 ++++++++++++++++++ 12 files changed, 383 insertions(+), 35 deletions(-) create mode 100644 Tests/Common/AsyncDITestCase.swift create mode 100644 Tests/Container/AsyncArgumentTests.swift create mode 100644 Tests/Container/AsyncBaseTests.swift create mode 100644 Tests/Container/AsyncComplexTests.swift diff --git a/Package.swift b/Package.swift index fb47bfb..ba86923 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.8 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -23,7 +23,8 @@ let package = Package( dependencies: [], path: "Sources", swiftSettings: [ - .define("APPLICATION_EXTENSION_API_ONLY") + .define("APPLICATION_EXTENSION_API_ONLY"), + .enableUpcomingFeature("StrictConcurrency") ] ), .testTarget( diff --git a/Sources/Container/AsyncContainer.swift b/Sources/Container/AsyncContainer.swift index 6dfaeea..5a2e384 100644 --- a/Sources/Container/AsyncContainer.swift +++ b/Sources/Container/AsyncContainer.swift @@ -21,14 +21,14 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin public init() {} /// Remove all registrations and already instantiated shared instances from the container - func clean() { + public func clean() { registrations.removeAll() releaseSharedInstances() } /// Remove already instantiated shared instances from the container - func releaseSharedInstances() { + public func releaseSharedInstances() { sharedInstances.removeAll() } @@ -87,7 +87,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// - Parameters: /// - type: Type of the dependency that should be resolved /// - argument: Argument that will passed as an input parameter to the factory method that was defined with `register` method - public func tryResolve(type: Dependency.Type, argument: Argument) async throws -> Dependency { + public func tryResolve(type: Dependency.Type, argument: Argument) async throws -> Dependency { let identifier = RegistrationIdentifier(type: type, argument: Argument.self) let registration = try getRegistration(with: identifier) @@ -104,7 +104,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// /// - Parameters: /// - type: Type of the dependency that should be resolved - public func tryResolve(type: Dependency.Type) async throws -> Dependency { + public func tryResolve(type: Dependency.Type) async throws -> Dependency { let identifier = RegistrationIdentifier(type: type) let registration = try getRegistration(with: identifier) @@ -127,7 +127,7 @@ private extension AsyncContainer { return registration } - func getDependency(from registration: AsyncRegistration, with argument: Any? = nil) async throws -> Dependency { + func getDependency(from registration: AsyncRegistration, with argument: (any Sendable)? = nil) async throws -> Dependency { switch registration.scope { case .shared: if let dependency = sharedInstances[registration.identifier] as? Dependency { diff --git a/Sources/Container/Container.swift b/Sources/Container/Container.swift index 836a8a1..3903dff 100644 --- a/Sources/Container/Container.swift +++ b/Sources/Container/Container.swift @@ -8,7 +8,7 @@ import Foundation /// Dependency Injection Container where dependencies are registered and from where they are consequently retrieved (i.e. resolved) -open class Container: DependencyWithArgumentAutoregistering, DependencyAutoregistering, DependencyWithArgumentResolving { +open class Container: DependencyWithArgumentAutoregistering, DependencyAutoregistering, DependencyWithArgumentResolving, @unchecked Sendable { /// Shared singleton public static let shared: Container = { Container() diff --git a/Sources/Models/AsyncRegistration.swift b/Sources/Models/AsyncRegistration.swift index c0966b5..266ead7 100644 --- a/Sources/Models/AsyncRegistration.swift +++ b/Sources/Models/AsyncRegistration.swift @@ -7,21 +7,23 @@ import Foundation +typealias AsyncRegistrationFactory = @Sendable (any AsyncDependencyResolving, (any Sendable)?) async throws -> any Sendable + /// Object that represents a registered dependency and stores a closure, i.e. a factory that returns the desired dependency -struct AsyncRegistration { +struct AsyncRegistration: Sendable { let identifier: RegistrationIdentifier let scope: DependencyScope - let factory: (any AsyncDependencyResolving, Any?) async throws -> Any + let factory: AsyncRegistrationFactory /// Initializer for registrations that don't need any variable argument - init(type: T.Type, scope: DependencyScope, factory: @escaping (any AsyncDependencyResolving) async -> T) { + init(type: T.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving) async -> T) { self.identifier = RegistrationIdentifier(type: type) self.scope = scope self.factory = { resolver, _ in await factory(resolver) } } /// Initializer for registrations that expect a variable argument passed to the factory closure when the dependency is being resolved - init(type: T.Type, scope: DependencyScope, factory: @escaping (any AsyncDependencyResolving, Argument) async -> T) { + init(type: T.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument) async -> T) { let registrationIdentifier = RegistrationIdentifier(type: type, argument: Argument.self) self.identifier = registrationIdentifier diff --git a/Sources/Models/DependencyScope.swift b/Sources/Models/DependencyScope.swift index 81a0146..98238eb 100644 --- a/Sources/Models/DependencyScope.swift +++ b/Sources/Models/DependencyScope.swift @@ -8,7 +8,7 @@ import Foundation /// Scope of a dependency -public enum DependencyScope { +public enum DependencyScope: Sendable { /// A new instance of the dependency is created each time the dependency is resolved from the container. case new diff --git a/Sources/Protocols/Registration/AsyncDependencyRegistering.swift b/Sources/Protocols/Registration/AsyncDependencyRegistering.swift index 38ac66a..fe68a8d 100644 --- a/Sources/Protocols/Registration/AsyncDependencyRegistering.swift +++ b/Sources/Protocols/Registration/AsyncDependencyRegistering.swift @@ -10,10 +10,10 @@ import Foundation /// A type that is able to register a dependency public protocol AsyncDependencyRegistering { /// Factory closure that instantiates the required dependency - typealias Factory = (any AsyncDependencyResolving) async -> Dependency + typealias Factory = @Sendable (any AsyncDependencyResolving) async -> Dependency /// Factory closure that instantiates the required dependency with the given variable argument - typealias FactoryWithArgument = (any AsyncDependencyResolving, Argument) async -> Dependency + typealias FactoryWithArgument = @Sendable (any AsyncDependencyResolving, Argument) async -> Dependency /// Register a dependency /// @@ -21,7 +21,7 @@ public protocol AsyncDependencyRegistering { /// - type: Type of the dependency to register /// - scope: Scope of the dependency. If `.new` is used, the `factory` closure is called on each `resolve` call. If `.shared` is used, the `factory` closure is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton /// - factory: Closure that is called when the dependency is being resolved - func register(type: Dependency.Type, in scope: DependencyScope, factory: @escaping Factory) async + func register(type: Dependency.Type, in scope: DependencyScope, factory: @escaping Factory) async /// Register a dependency with a variable argument /// @@ -37,7 +37,7 @@ public protocol AsyncDependencyRegistering { /// - Parameters: /// - type: Type of the dependency to register /// - factory: Closure that is called when the dependency is being resolved - func register(type: Dependency.Type, factory: @escaping FactoryWithArgument) async + func register(type: Dependency.Type, factory: @escaping FactoryWithArgument) async } // MARK: Overloaded factory methods @@ -54,7 +54,7 @@ public extension AsyncDependencyRegistering { /// - Parameters: /// - type: Type of the dependency to register /// - factory: Closure that is called when the dependency is being resolved - func register(type: Dependency.Type, factory: @escaping Factory) async { + func register(type: Dependency.Type, factory: @escaping Factory) async { await register(type: type, in: Self.defaultScope, factory: factory) } @@ -63,7 +63,7 @@ public extension AsyncDependencyRegistering { /// - Parameters: /// - scope: Scope of the dependency. If `.new` is used, the `factory` closure is called on each `resolve` call. If `.shared` is used, the `factory` closure is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton /// - factory: Closure that is called when the dependency is being resolved - func register(in scope: DependencyScope, factory: @escaping Factory) async { + func register(in scope: DependencyScope, factory: @escaping Factory) async { await register(type: Dependency.self, in: scope, factory: factory) } @@ -71,7 +71,7 @@ public extension AsyncDependencyRegistering { /// /// - Parameters: /// - factory: Closure that is called when the dependency is being resolved - func register(factory: @escaping Factory) async { + func register(factory: @escaping Factory) async { await register(type: Dependency.self, in: Self.defaultScope, factory: factory) } @@ -88,7 +88,7 @@ public extension AsyncDependencyRegistering { /// /// - Parameters: /// - factory: Closure that is called when the dependency is being resolved - func register(factory: @escaping FactoryWithArgument) async { + func register(factory: @escaping FactoryWithArgument) async { await register(type: Dependency.self, factory: factory) } } diff --git a/Sources/Protocols/Resolution/AsyncDependencyResolving.swift b/Sources/Protocols/Resolution/AsyncDependencyResolving.swift index b4a43fe..67fd4f5 100644 --- a/Sources/Protocols/Resolution/AsyncDependencyResolving.swift +++ b/Sources/Protocols/Resolution/AsyncDependencyResolving.swift @@ -15,7 +15,7 @@ public protocol AsyncDependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - func tryResolve(type: T.Type) async throws -> T + func tryResolve(type: T.Type) async throws -> T /// Resolve a dependency with a variable argument that was previously registered within the container /// @@ -24,7 +24,7 @@ public protocol AsyncDependencyResolving { /// - Parameters: /// - type: Type of the dependency that should be resolved /// - argument: Argument that will be passed as an input parameter to the factory method - func tryResolve(type: T.Type, argument: Argument) async throws -> T + func tryResolve(type: T.Type, argument: Argument) async throws -> T } public extension AsyncDependencyResolving { @@ -34,7 +34,7 @@ public extension AsyncDependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - func resolve(type: T.Type) async -> T { + func resolve(type: T.Type) async -> T { try! await tryResolve(type: type) } @@ -44,7 +44,7 @@ public extension AsyncDependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - func resolve() async -> T { + func resolve() async -> T { await resolve(type: T.self) } @@ -55,7 +55,7 @@ public extension AsyncDependencyResolving { /// - Parameters: /// - type: Type of the dependency that should be resolved /// - argument: Argument that will be passed as an input parameter to the factory method - func resolve(type: T.Type, argument: Argument) async -> T { + func resolve(type: T.Type, argument: Argument) async -> T { try! await tryResolve(type: type, argument: argument) } @@ -66,7 +66,7 @@ public extension AsyncDependencyResolving { /// - Parameters: /// - type: Type of the dependency that should be resolved /// - argument: Argument that will be passed as an input parameter to the factory method - func resolve(argument: Argument) async -> T { + func resolve(argument: Argument) async -> T { await resolve(type: T.self, argument: argument) } } diff --git a/Tests/Common/AsyncDITestCase.swift b/Tests/Common/AsyncDITestCase.swift new file mode 100644 index 0000000..9f0b615 --- /dev/null +++ b/Tests/Common/AsyncDITestCase.swift @@ -0,0 +1,25 @@ +// +// AsyncDITestCase.swift +// DependencyInjection +// +// Created by Róbert Oravec on 19.12.2024. +// + +import XCTest +import DependencyInjection + +class AsyncDITestCase: XCTestCase { + var container: AsyncContainer! + + override func setUp() { + super.setUp() + + container = AsyncContainer() + } + + override func tearDown() { + container = nil + + super.tearDown() + } +} diff --git a/Tests/Common/Dependencies.swift b/Tests/Common/Dependencies.swift index 0a6de2b..76127b5 100644 --- a/Tests/Common/Dependencies.swift +++ b/Tests/Common/Dependencies.swift @@ -7,7 +7,7 @@ import Foundation -protocol DIProtocol {} +protocol DIProtocol: Sendable {} struct StructureDependency: Equatable, DIProtocol { static let `default` = StructureDependency(property1: "test") @@ -15,9 +15,9 @@ struct StructureDependency: Equatable, DIProtocol { let property1: String } -class SimpleDependency: DIProtocol {} +final class SimpleDependency: DIProtocol {} -class DependencyWithValueTypeParameter { +final class DependencyWithValueTypeParameter: Sendable { let subDependency: StructureDependency init(subDependency: StructureDependency = .default) { @@ -25,7 +25,7 @@ class DependencyWithValueTypeParameter { } } -class DependencyWithParameter { +final class DependencyWithParameter: Sendable { let subDependency: SimpleDependency init(subDependency: SimpleDependency) { @@ -33,7 +33,7 @@ class DependencyWithParameter { } } -class DependencyWithParameter2 { +final class DependencyWithParameter2: Sendable { let subDependency1: SimpleDependency let subDependency2: DependencyWithValueTypeParameter @@ -43,7 +43,7 @@ class DependencyWithParameter2 { } } -class DependencyWithParameter3 { +final class DependencyWithParameter3: Sendable { let subDependency1: SimpleDependency let subDependency2: DependencyWithValueTypeParameter let subDependency3: DependencyWithParameter @@ -59,7 +59,7 @@ class DependencyWithParameter3 { } } -class DependencyWithParameter4 { +final class DependencyWithParameter4: Sendable { let subDependency1: SimpleDependency let subDependency2: DependencyWithValueTypeParameter let subDependency3: DependencyWithParameter @@ -78,7 +78,7 @@ class DependencyWithParameter4 { } } -class DependencyWithParameter5 { +final class DependencyWithParameter5: Sendable { let subDependency1: SimpleDependency let subDependency2: DependencyWithValueTypeParameter let subDependency3: DependencyWithParameter diff --git a/Tests/Container/AsyncArgumentTests.swift b/Tests/Container/AsyncArgumentTests.swift new file mode 100644 index 0000000..49ac08e --- /dev/null +++ b/Tests/Container/AsyncArgumentTests.swift @@ -0,0 +1,59 @@ +// +// AsyncArgumentTests.swift +// DependencyInjection +// +// Created by Róbert Oravec on 19.12.2024. +// + +import XCTest +import DependencyInjection + +final class AsyncContainerArgumentTests: AsyncDITestCase { + func testRegistration() async { + await container.register { (resolver, argument) -> DependencyWithValueTypeParameter in + DependencyWithValueTypeParameter(subDependency: argument) + } + + let argument = StructureDependency(property1: "48") + let resolvedDependency: DependencyWithValueTypeParameter = await container.resolve(argument: argument) + + XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") + } + + func testRegistrationWithExplicitType() async { + await container.register(type: DependencyWithValueTypeParameter.self) { (resolver, argument) in + DependencyWithValueTypeParameter(subDependency: argument) + } + + let argument = StructureDependency(property1: "48") + let resolvedDependency: DependencyWithValueTypeParameter = await container.resolve(argument: argument) + + XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") + } + + func testUnmatchingArgumentType() async { + await container.register { (resolver, argument) -> DependencyWithValueTypeParameter in + DependencyWithValueTypeParameter(subDependency: argument) + } + + let argument = 48 + + do { + _ = try await container.tryResolve(type: DependencyWithValueTypeParameter.self, argument: argument) + + XCTFail("Expected to throw error") + } catch { + guard let resolutionError = error as? ResolutionError else { + XCTFail("Incorrect error type") + return + } + + switch resolutionError { + case .unmatchingArgumentType: + XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") + default: + XCTFail("Incorrect resolution error") + } + } + } +} diff --git a/Tests/Container/AsyncBaseTests.swift b/Tests/Container/AsyncBaseTests.swift new file mode 100644 index 0000000..0507f84 --- /dev/null +++ b/Tests/Container/AsyncBaseTests.swift @@ -0,0 +1,86 @@ +// +// AsyncBaseTests.swift +// DependencyInjection +// +// Created by Róbert Oravec on 19.12.2024. +// + +import XCTest +import DependencyInjection + +final class AsyncBaseTests: AsyncDITestCase { + func testDependencyRegisteredInDefaultScope() async { + await container.register { _ -> SimpleDependency in + SimpleDependency() + } + + let resolvedDependency1: SimpleDependency = await container.resolve() + let resolvedDependency2: SimpleDependency = await container.resolve() + + XCTAssertTrue(resolvedDependency1 === resolvedDependency2, "Container returned different instance") + } + + func testDependencyRegisteredInDefaultScopeWithExplicitType() async { + await container.register(type: SimpleDependency.self) { _ -> SimpleDependency in + SimpleDependency() + } + + let resolvedDependency1: SimpleDependency = await container.resolve() + let resolvedDependency2: SimpleDependency = await container.resolve() + + XCTAssertTrue(resolvedDependency1 === resolvedDependency2, "Container returned different instance") + } + + func testSharedDependency() async { + await container.register(in: .shared) { _ -> SimpleDependency in + SimpleDependency() + } + + let resolvedDependency1: SimpleDependency = await container.resolve() + let resolvedDependency2: SimpleDependency = await container.resolve() + + XCTAssertTrue(resolvedDependency1 === resolvedDependency2, "Container returned different instance") + } + + func testNonSharedDependency() async { + await container.register(in: .new) { _ -> SimpleDependency in + SimpleDependency() + } + + let resolvedDependency1: SimpleDependency = await container.resolve() + let resolvedDependency2: SimpleDependency = await container.resolve() + + XCTAssertTrue(resolvedDependency1 !== resolvedDependency2, "Container returned the same instance") + } + + func testNonSharedDependencyWithExplicitType() async { + await container.register(type: SimpleDependency.self, in: .new) { _ in + SimpleDependency() + } + + let resolvedDependency1: SimpleDependency = await container.resolve() + let resolvedDependency2: SimpleDependency = await container.resolve() + + XCTAssertTrue(resolvedDependency1 !== resolvedDependency2, "Container returned the same instance") + } + + func testUnregisteredDependency() async { + do { + _ = try await container.tryResolve(type: SimpleDependency.self) + + XCTFail("Expected to fail tryResolve") + } catch { + guard let resolutionError = error as? ResolutionError else { + XCTFail("Incorrect error type") + return + } + + switch resolutionError { + case .dependencyNotRegistered: + XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") + default: + XCTFail("Incorrect resolution error") + } + } + } +} diff --git a/Tests/Container/AsyncComplexTests.swift b/Tests/Container/AsyncComplexTests.swift new file mode 100644 index 0000000..b2528b2 --- /dev/null +++ b/Tests/Container/AsyncComplexTests.swift @@ -0,0 +1,175 @@ +// +// AsyncComplexTests.swift +// DependencyInjection +// +// Created by Róbert Oravec on 19.12.2024. +// + +import XCTest +import DependencyInjection + +final class AsyncComplexTests: AsyncDITestCase { + func testCleanContainer() async { + await container.register { _ in + SimpleDependency() + } + + let resolvedDependency = try? await container.tryResolve(type: SimpleDependency.self) + + XCTAssertNotNil(resolvedDependency, "Couldn't resolve dependency") + + await container.clean() + + let unresolvedDependency = try? await container.tryResolve(type: SimpleDependency.self) + + XCTAssertNil(unresolvedDependency, "Dependency wasn't cleaned") + } + + func testReleaseSharedInstances() async { + await container.register(in: .shared) { _ in + SimpleDependency() + } + + var resolvedDependency1: SimpleDependency? = await container.resolve(type: SimpleDependency.self) + weak var resolvedDependency2 = await container.resolve(type: SimpleDependency.self) + + XCTAssertNotNil(resolvedDependency1, "Shared instance wasn't resolved") + XCTAssertTrue(resolvedDependency1 === resolvedDependency2, "Different instancies of a shared dependency") + + await container.releaseSharedInstances() + + let resolvedDependency3 = await container.resolve(type: SimpleDependency.self) + + XCTAssertFalse(resolvedDependency1 === resolvedDependency3, "Shared instance wasn't released") + + resolvedDependency1 = nil + + XCTAssertNil(resolvedDependency1, "Shared instance wasn't released") + } + + func testReregistration() async { + await container.register(type: DIProtocol.self, in: .shared) { _ in + SimpleDependency() + } + + let resolvedSimpleDependency = await container.resolve(type: DIProtocol.self) + + XCTAssertTrue(resolvedSimpleDependency is SimpleDependency, "Resolved dependency of wrong type") + + await container.register(type: DIProtocol.self, in: .shared) { _ in + StructureDependency.default + } + + let resolvedStructureDependency = await container.resolve(type: DIProtocol.self) + + XCTAssertTrue(resolvedStructureDependency is StructureDependency, "Resolved dependency of wrong type") + } + + func testSameDependencyTypeRegisteredWithDifferentTypes() async { + await container.register(type: DIProtocol.self, in: .shared) { _ in + StructureDependency(property1: "first") + } + + await container.register(type: StructureDependency.self, in: .shared) { _ in + StructureDependency(property1: "second") + } + + let resolvedProtocolDependency: DIProtocol = await container.resolve() + let resolvedTypeDependency: StructureDependency = await container.resolve() + + XCTAssertTrue(resolvedProtocolDependency is StructureDependency, "Resolved dependency of wrong type") + XCTAssertEqual(resolvedTypeDependency.property1, "second", "Resolved dependency from a wrong factory") + + XCTAssertNotEqual( + (resolvedProtocolDependency as? StructureDependency)?.property1, + resolvedTypeDependency.property1, + "Resolved same instances" + ) + } + + func testCombiningSharedAndNonsharedDependencies() async { + await container.register(in: .new) { _ in + SimpleDependency() + } + await container.register(in: .shared) { + DependencyWithParameter(subDependency: await $0.resolve()) + } + await container.register { + DependencyWithParameter3( + subDependency1: await $0.resolve(), + subDependency2: $1, + subDependency3: await $0.resolve() + ) + } + + let argumentDependency1 = DependencyWithValueTypeParameter( + subDependency: StructureDependency(property1: "first") + ) + let argumentDependency2 = DependencyWithValueTypeParameter( + subDependency: StructureDependency(property1: "second") + ) + + let resolvedDependency1: DependencyWithParameter = await container.resolve() + let resolvedDependency2: DependencyWithParameter = await container.resolve() + let resolvedDependency3 = await container.resolve(type: DependencyWithParameter3.self, argument: argumentDependency1) + let resolvedDependency4 = await container.resolve(type: DependencyWithParameter3.self, argument: argumentDependency2) + + XCTAssertTrue(resolvedDependency1 === resolvedDependency2, "Resolved different instances") + XCTAssertTrue(resolvedDependency1.subDependency === resolvedDependency2.subDependency, "Resolved different instances") + + XCTAssertFalse(resolvedDependency1.subDependency === resolvedDependency3.subDependency1, "Resolved the same instance for a subdependency") + XCTAssertFalse(resolvedDependency3.subDependency1 === resolvedDependency4.subDependency1, "Resolved the same instance for a subdependency") + + XCTAssertFalse(resolvedDependency3 === resolvedDependency4, "Resolved same instances") + + XCTAssertNotEqual( + resolvedDependency3.subDependency2.subDependency.property1, + resolvedDependency4.subDependency2.subDependency.property1, + "Resolved instances with the same argument" + ) + } + + func testCombiningSharedAndNonsharedDependenciesWithExplicitFactories() async { + await container.register(in: .new) { _ in + SimpleDependency() + } + await container.register(in: .shared) { + DependencyWithParameter( + subDependency: await $0.resolve() + ) + } + await container.register { resolver, argument in + DependencyWithParameter3( + subDependency1: await resolver.resolve(), + subDependency2: argument, + subDependency3: await resolver.resolve() + ) + } + + let argumentDependency1 = DependencyWithValueTypeParameter( + subDependency: StructureDependency(property1: "first") + ) + let argumentDependency2 = DependencyWithValueTypeParameter( + subDependency: StructureDependency(property1: "second") + ) + + let resolvedDependency1: DependencyWithParameter = await container.resolve() + let resolvedDependency2: DependencyWithParameter = await container.resolve() + let resolvedDependency3 = await container.resolve(type: DependencyWithParameter3.self, argument: argumentDependency1) + let resolvedDependency4 = await container.resolve(type: DependencyWithParameter3.self, argument: argumentDependency2) + + XCTAssertTrue(resolvedDependency1 === resolvedDependency2, "Resolved different instances") + XCTAssertTrue(resolvedDependency1.subDependency === resolvedDependency2.subDependency, "Resolved different instances") + + XCTAssertFalse(resolvedDependency1.subDependency === resolvedDependency3.subDependency1, "Resolved the same instance for a subdependency") + XCTAssertFalse(resolvedDependency3.subDependency1 === resolvedDependency4.subDependency1, "Resolved the same instance for a subdependency") + + XCTAssertFalse(resolvedDependency3 === resolvedDependency4, "Resolved same instances") + + XCTAssertNotEqual( + resolvedDependency3.subDependency2.subDependency.property1, + resolvedDependency4.subDependency2.subDependency.property1, + "Resolved instances with the same argument" + ) + } +} From 481bda2e0615a229beb72068cb56fb7980cde2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Oravec?= Date: Thu, 19 Dec 2024 12:47:47 +0100 Subject: [PATCH 5/8] updated registration protocols --- ...gistration.swift => AsyncModuleRegistration.swift} | 3 ++- Sources/Protocols/ModuleRegistration.swift | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) rename Sources/Protocols/{ModileRegistration.swift => AsyncModuleRegistration.swift} (61%) create mode 100644 Sources/Protocols/ModuleRegistration.swift diff --git a/Sources/Protocols/ModileRegistration.swift b/Sources/Protocols/AsyncModuleRegistration.swift similarity index 61% rename from Sources/Protocols/ModileRegistration.swift rename to Sources/Protocols/AsyncModuleRegistration.swift index 45b2ef5..4b2212f 100644 --- a/Sources/Protocols/ModileRegistration.swift +++ b/Sources/Protocols/AsyncModuleRegistration.swift @@ -5,6 +5,7 @@ // Created by Róbert Oravec on 17.12.2024. // -public protocol ModuleRegistration { +/// Protocol used to enforce common naming of registration in a module. +public protocol AsyncModuleRegistration { static func registerDependencies(in container: AsyncContainer) async } diff --git a/Sources/Protocols/ModuleRegistration.swift b/Sources/Protocols/ModuleRegistration.swift new file mode 100644 index 0000000..e7a182b --- /dev/null +++ b/Sources/Protocols/ModuleRegistration.swift @@ -0,0 +1,11 @@ +// +// ModuleRegistration.swift +// DependencyInjection +// +// Created by Róbert Oravec on 19.12.2024. +// + +/// Protocol used to enforce common naming of registration in a module. +public protocol ModuleRegistration { + static func registerDependencies(in container: Container) +} From 6690de5064960795d0b948ab8c7aca328a65d1b6 Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Thu, 19 Dec 2024 17:58:00 +0100 Subject: [PATCH 6/8] [chore] fix comment typo --- Sources/Protocols/AsyncModuleRegistration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Protocols/AsyncModuleRegistration.swift b/Sources/Protocols/AsyncModuleRegistration.swift index 4b2212f..0abe881 100644 --- a/Sources/Protocols/AsyncModuleRegistration.swift +++ b/Sources/Protocols/AsyncModuleRegistration.swift @@ -1,5 +1,5 @@ // -// ModileRegistration.swift +// AsyncModuleRegistration.swift // DependencyInjection // // Created by Róbert Oravec on 17.12.2024. From edd8abb7350dd025004c345eb8054fd60f61abaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Oravec?= Date: Tue, 7 Jan 2025 08:41:52 +0100 Subject: [PATCH 7/8] increased swift tools version --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index ba86923..c3beb2c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.8 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription From 8726479204ecfd42865d973bb14633a39ccd79c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Oravec?= Date: Tue, 7 Jan 2025 08:46:20 +0100 Subject: [PATCH 8/8] changed factory in async registration to async registration factory --- Sources/Container/AsyncContainer.swift | 2 +- Sources/Models/AsyncRegistration.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Container/AsyncContainer.swift b/Sources/Container/AsyncContainer.swift index 5a2e384..c212208 100644 --- a/Sources/Container/AsyncContainer.swift +++ b/Sources/Container/AsyncContainer.swift @@ -140,7 +140,7 @@ private extension AsyncContainer { // We use force cast here because we are sure that the type-casting always succeed // The reason why the `factory` closure returns ``Any`` is that we have to erase the generic type in order to store the registration // When the registration is created it can be initialized just with a `factory` that returns the matching type - let dependency = try await registration.factory(self, argument) as! Dependency + let dependency = try await registration.asyncRegistrationFactory(self, argument) as! Dependency switch registration.scope { case .shared: diff --git a/Sources/Models/AsyncRegistration.swift b/Sources/Models/AsyncRegistration.swift index 266ead7..bbfd543 100644 --- a/Sources/Models/AsyncRegistration.swift +++ b/Sources/Models/AsyncRegistration.swift @@ -13,13 +13,13 @@ typealias AsyncRegistrationFactory = @Sendable (any AsyncDependencyResolving, (a struct AsyncRegistration: Sendable { let identifier: RegistrationIdentifier let scope: DependencyScope - let factory: AsyncRegistrationFactory + let asyncRegistrationFactory: AsyncRegistrationFactory /// Initializer for registrations that don't need any variable argument init(type: T.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving) async -> T) { self.identifier = RegistrationIdentifier(type: type) self.scope = scope - self.factory = { resolver, _ in await factory(resolver) } + self.asyncRegistrationFactory = { resolver, _ in await factory(resolver) } } /// Initializer for registrations that expect a variable argument passed to the factory closure when the dependency is being resolved @@ -28,7 +28,7 @@ struct AsyncRegistration: Sendable { self.identifier = registrationIdentifier self.scope = scope - self.factory = { resolver, arg in + self.asyncRegistrationFactory = { resolver, arg in guard let argument = arg as? Argument else { throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept an argument of type \(Argument.self)") }