-
Couldn't load subscription status.
- Fork 0
feat: Async init #19
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
Merged
Merged
feat: Async init #19
Changes from 5 commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
015866a
added async container
robha141 250d052
added module registration
robha141 f76d19d
module registration is now static
robha141 f531742
added tests
robha141 481bda2
updated registration protocols
robha141 6690de5
[chore] fix comment typo
cejanen edd8abb
increased swift tools version
robha141 8726479
changed factory in async registration to async registration factory
robha141 6293bd4
chore: Merge branch 'feat/rob-async-init' of github.com:strvcom/ios-d…
robha141 36a4e9e
Update Sources/Container/AsyncContainer.swift
DanielCech e531377
Update Sources/Container/AsyncContainer.swift
DanielCech e4550be
feat: minor fixes
DanielCech 0000e4e
Merge branch 'feat/rob-async-init' of github.com:strvcom/ios-dependen…
DanielCech 66d1149
feat: sync/async folders
DanielCech 984d223
feat: async init test
DanielCech 5362b87
feat: bundler version
DanielCech 5327c73
feat: bundler fix
DanielCech 3ec35e0
feat: bundler fix
DanielCech 5c542cd
feat: updated changelog
DanielCech 5da88e2
fix: line ending
DanielCech dc8dd20
feat: bundler update
DanielCech 812cb70
fix: comment
DanielCech 04e8c9e
feat: bump fastlane version
DanielCech ce2133e
Update Sources/Container/Async/AsyncContainer.swift
DanielCech ea58ced
Update Sources/Container/Async/AsyncContainer.swift
DanielCech 480b459
fix: last line
DanielCech bc3aabf
Merge branch 'feat/rob-async-init' of github.com:strvcom/ios-dependen…
DanielCech 2716660
fix: tests
DanielCech File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
cernym46 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// Shared singleton | ||
| public static let shared: AsyncContainer = { | ||
| AsyncContainer() | ||
| }() | ||
|
|
||
| private var registrations = [RegistrationIdentifier: AsyncRegistration]() | ||
| private var sharedInstances = [RegistrationIdentifier: Any]() | ||
|
|
||
| /// Create new instance of ``Container`` | ||
DanielCech marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
|
|
||
|
|
||
DanielCech marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// Register a dependency | ||
DanielCech marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// | ||
| /// - 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.factory(self, argument) as! Dependency | ||
|
|
||
| switch registration.scope { | ||
| case .shared: | ||
| sharedInstances[registration.identifier] = dependency | ||
| case .new: | ||
| break | ||
| } | ||
|
|
||
| return dependency | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 factory: 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) { | ||
cejanen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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<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.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) | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // | ||
| // ModileRegistration.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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } |
94 changes: 94 additions & 0 deletions
94
Sources/Protocols/Registration/AsyncDependencyRegistering.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
cejanen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// | ||
| /// - 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) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.