-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Async init #19
base: develop
Are you sure you want to change the base?
feat: Async init #19
Changes from all commits
015866a
250d052
f76d19d
f531742
481bda2
6690de5
edd8abb
8726479
6293bd4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -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 | ||||||||
public func clean() { | ||||||||
registrations.removeAll() | ||||||||
|
||||||||
releaseSharedInstances() | ||||||||
} | ||||||||
|
||||||||
/// Remove already instantiated shared instances from the container | ||||||||
public func releaseSharedInstances() { | ||||||||
sharedInstances.removeAll() | ||||||||
} | ||||||||
|
||||||||
// MARK: Register dependency, Autoregister dependency | ||||||||
|
||||||||
|
||||||||
/// Register a dependency | ||||||||
Comment on lines
+37
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
/// | ||||||||
/// - 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<Dependency>( | ||||||||
type: Dependency.Type, | ||||||||
in scope: DependencyScope, | ||||||||
factory: @escaping Factory<Dependency> | ||||||||
) 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<Dependency, Argument>(type: Dependency.Type, factory: @escaping FactoryWithArgument<Dependency, Argument>) 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<Dependency: Sendable, Argument: Sendable>(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<Dependency: Sendable>(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<Dependency: Sendable>(from registration: AsyncRegistration, with argument: (any Sendable)? = 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.asyncRegistrationFactory(self, argument) as! Dependency | ||||||||
|
||||||||
switch registration.scope { | ||||||||
case .shared: | ||||||||
sharedInstances[registration.identifier] = dependency | ||||||||
case .new: | ||||||||
break | ||||||||
} | ||||||||
|
||||||||
return dependency | ||||||||
} | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// | ||
// AsyncRegistration.swift | ||
// DependencyInjection | ||
// | ||
// Created by Róbert Oravec on 16.12.2024. | ||
// | ||
|
||
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: Sendable { | ||
let identifier: RegistrationIdentifier | ||
let scope: DependencyScope | ||
let asyncRegistrationFactory: AsyncRegistrationFactory | ||
|
||
/// Initializer for registrations that don't need any variable argument | ||
init<T: Sendable>(type: T.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving) async -> T) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. factory parameter I found a bit confusing because when initializing AsyncRegistration in AsyncContainer we pass argument of type Factory defined in AsyncDependencyRegistering, not sure if we can optimize those typealiases There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just copied the naming as it was in sync implementation, it did make sense to me. I will revisit it 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, as I look into it, it kinda makes sense, because |
||
self.identifier = RegistrationIdentifier(type: type) | ||
self.scope = scope | ||
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 | ||
init<T: Sendable, Argument: Sendable>(type: T.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument) async -> T) { | ||
let registrationIdentifier = RegistrationIdentifier(type: type, argument: Argument.self) | ||
|
||
self.identifier = registrationIdentifier | ||
self.scope = scope | ||
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)") | ||
} | ||
|
||
return await factory(resolver, argument) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// | ||
// AsyncModuleRegistration.swift | ||
// DependencyInjection | ||
// | ||
// Created by Róbert Oravec on 17.12.2024. | ||
// | ||
|
||
/// Protocol used to enforce common naming of registration in a module. | ||
public protocol AsyncModuleRegistration { | ||
static func registerDependencies(in container: AsyncContainer) async | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Dependency: Sendable> = @Sendable (any AsyncDependencyResolving) async -> Dependency | ||
|
||
/// Factory closure that instantiates the required dependency with the given variable argument | ||
typealias FactoryWithArgument<Dependency: Sendable, Argument: Sendable> = @Sendable (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<Dependency: Sendable>(type: Dependency.Type, in scope: DependencyScope, factory: @escaping Factory<Dependency>) 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<Dependency: Sendable, Argument: Sendable>(type: Dependency.Type, factory: @escaping FactoryWithArgument<Dependency, Argument>) 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<Dependency: Sendable>(type: Dependency.Type, factory: @escaping Factory<Dependency>) 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<Dependency: Sendable>(in scope: DependencyScope, factory: @escaping Factory<Dependency>) 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<Dependency: Sendable>(factory: @escaping Factory<Dependency>) 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<Dependency: Sendable, Argument: Sendable>(factory: @escaping FactoryWithArgument<Dependency, Argument>) async { | ||
await register(type: Dependency.self, factory: factory) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.