Skip to content

Commit 015866a

Browse files
committed
added async container
1 parent 0220c30 commit 015866a

File tree

4 files changed

+357
-0
lines changed

4 files changed

+357
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
//
2+
// AsyncContainer.swift
3+
// DependencyInjection
4+
//
5+
// Created by Róbert Oravec on 17.12.2024.
6+
//
7+
8+
import Foundation
9+
10+
/// Dependency Injection Container where dependencies are registered and from where they are consequently retrieved (i.e. resolved)
11+
public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegistering {
12+
/// Shared singleton
13+
public static let shared: AsyncContainer = {
14+
AsyncContainer()
15+
}()
16+
17+
private var registrations = [RegistrationIdentifier: AsyncRegistration]()
18+
private var sharedInstances = [RegistrationIdentifier: Any]()
19+
20+
/// Create new instance of ``Container``
21+
public init() {}
22+
23+
/// Remove all registrations and already instantiated shared instances from the container
24+
func clean() {
25+
registrations.removeAll()
26+
27+
releaseSharedInstances()
28+
}
29+
30+
/// Remove already instantiated shared instances from the container
31+
func releaseSharedInstances() {
32+
sharedInstances.removeAll()
33+
}
34+
35+
// MARK: Register dependency, Autoregister dependency
36+
37+
38+
/// Register a dependency
39+
///
40+
/// - Parameters:
41+
/// - type: Type of the dependency to register
42+
/// - 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
43+
/// - factory: Closure that is called when the dependency is being resolved
44+
public func register<Dependency>(
45+
type: Dependency.Type,
46+
in scope: DependencyScope,
47+
factory: @escaping Factory<Dependency>
48+
) async {
49+
let registration = AsyncRegistration(type: type, scope: scope, factory: factory)
50+
51+
registrations[registration.identifier] = registration
52+
53+
// With a new registration we should clean all shared instances
54+
// because the new registered factory most likely returns different objects and we have no way to tell
55+
sharedInstances[registration.identifier] = nil
56+
}
57+
58+
// MARK: Register dependency with argument, Autoregister dependency with argument
59+
60+
/// Register a dependency with an argument
61+
///
62+
/// The argument is typically a parameter in an initiliazer of the dependency that is not registered in the same container,
63+
/// therefore, it needs to be passed in `resolve` call
64+
///
65+
/// DISCUSSION: This registration method doesn't have any scope parameter for a reason.
66+
/// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined.
67+
/// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved?
68+
/// Shared instances are typically not dependent on variable input parameters by definition.
69+
/// If you need to support this usecase, please, keep references to the variable singletons outside of the container.
70+
///
71+
/// - Parameters:
72+
/// - type: Type of the dependency to register
73+
/// - factory: Closure that is called when the dependency is being resolved
74+
public func register<Dependency, Argument>(type: Dependency.Type, factory: @escaping FactoryWithArgument<Dependency, Argument>) async {
75+
let registration = AsyncRegistration(type: type, scope: .new, factory: factory)
76+
77+
registrations[registration.identifier] = registration
78+
}
79+
80+
// MARK: Resolve dependency
81+
82+
/// Resolve a dependency that was previously registered with `register` method
83+
///
84+
/// If a dependency of the given type with the given argument wasn't registered before this method call
85+
/// the method throws ``ResolutionError.dependencyNotRegistered``
86+
///
87+
/// - Parameters:
88+
/// - type: Type of the dependency that should be resolved
89+
/// - argument: Argument that will passed as an input parameter to the factory method that was defined with `register` method
90+
public func tryResolve<Dependency, Argument>(type: Dependency.Type, argument: Argument) async throws -> Dependency {
91+
let identifier = RegistrationIdentifier(type: type, argument: Argument.self)
92+
93+
let registration = try getRegistration(with: identifier)
94+
95+
let dependency: Dependency = try await getDependency(from: registration, with: argument)
96+
97+
return dependency
98+
}
99+
100+
/// Resolve a dependency that was previously registered with `register` method
101+
///
102+
/// If a dependency of the given type wasn't registered before this method call
103+
/// the method throws ``ResolutionError.dependencyNotRegistered``
104+
///
105+
/// - Parameters:
106+
/// - type: Type of the dependency that should be resolved
107+
public func tryResolve<Dependency>(type: Dependency.Type) async throws -> Dependency {
108+
let identifier = RegistrationIdentifier(type: type)
109+
110+
let registration = try getRegistration(with: identifier)
111+
112+
let dependency: Dependency = try await getDependency(from: registration)
113+
114+
return dependency
115+
}
116+
}
117+
118+
// MARK: Private methods
119+
private extension AsyncContainer {
120+
func getRegistration(with identifier: RegistrationIdentifier) throws -> AsyncRegistration {
121+
guard let registration = registrations[identifier] else {
122+
throw ResolutionError.dependencyNotRegistered(
123+
message: "Dependency of type \(identifier.description) wasn't registered in container \(self)"
124+
)
125+
}
126+
127+
return registration
128+
}
129+
130+
func getDependency<Dependency>(from registration: AsyncRegistration, with argument: Any? = nil) async throws -> Dependency {
131+
switch registration.scope {
132+
case .shared:
133+
if let dependency = sharedInstances[registration.identifier] as? Dependency {
134+
return dependency
135+
}
136+
case .new:
137+
break
138+
}
139+
140+
// We use force cast here because we are sure that the type-casting always succeed
141+
// The reason why the `factory` closure returns ``Any`` is that we have to erase the generic type in order to store the registration
142+
// When the registration is created it can be initialized just with a `factory` that returns the matching type
143+
let dependency = try await registration.factory(self, argument) as! Dependency
144+
145+
switch registration.scope {
146+
case .shared:
147+
sharedInstances[registration.identifier] = dependency
148+
case .new:
149+
break
150+
}
151+
152+
return dependency
153+
}
154+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// AsyncRegistration.swift
3+
// DependencyInjection
4+
//
5+
// Created by Róbert Oravec on 16.12.2024.
6+
//
7+
8+
import Foundation
9+
10+
/// Object that represents a registered dependency and stores a closure, i.e. a factory that returns the desired dependency
11+
struct AsyncRegistration {
12+
let identifier: RegistrationIdentifier
13+
let scope: DependencyScope
14+
let factory: (any AsyncDependencyResolving, Any?) async throws -> Any
15+
16+
/// Initializer for registrations that don't need any variable argument
17+
init<T>(type: T.Type, scope: DependencyScope, factory: @escaping (any AsyncDependencyResolving) async -> T) {
18+
self.identifier = RegistrationIdentifier(type: type)
19+
self.scope = scope
20+
self.factory = { resolver, _ in await factory(resolver) }
21+
}
22+
23+
/// Initializer for registrations that expect a variable argument passed to the factory closure when the dependency is being resolved
24+
init<T, Argument>(type: T.Type, scope: DependencyScope, factory: @escaping (any AsyncDependencyResolving, Argument) async -> T) {
25+
let registrationIdentifier = RegistrationIdentifier(type: type, argument: Argument.self)
26+
27+
self.identifier = registrationIdentifier
28+
self.scope = scope
29+
self.factory = { resolver, arg in
30+
guard let argument = arg as? Argument else {
31+
throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept an argument of type \(Argument.self)")
32+
}
33+
34+
return await factory(resolver, argument)
35+
}
36+
}
37+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//
2+
// AsyncDependencyRegistering.swift
3+
// DependencyInjection
4+
//
5+
// Created by Róbert Oravec on 17.12.2024.
6+
//
7+
8+
import Foundation
9+
10+
/// A type that is able to register a dependency
11+
public protocol AsyncDependencyRegistering {
12+
/// Factory closure that instantiates the required dependency
13+
typealias Factory<Dependency> = (any AsyncDependencyResolving) async -> Dependency
14+
15+
/// Factory closure that instantiates the required dependency with the given variable argument
16+
typealias FactoryWithArgument<Dependency, Argument> = (any AsyncDependencyResolving, Argument) async -> Dependency
17+
18+
/// Register a dependency
19+
///
20+
/// - Parameters:
21+
/// - type: Type of the dependency to register
22+
/// - 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
23+
/// - factory: Closure that is called when the dependency is being resolved
24+
func register<Dependency>(type: Dependency.Type, in scope: DependencyScope, factory: @escaping Factory<Dependency>) async
25+
26+
/// Register a dependency with a variable argument
27+
///
28+
/// The argument is typically a parameter in an initiliazer of the dependency that is not registered in the same resolver (i.e. container),
29+
/// therefore, it needs to be passed in `resolve` call
30+
///
31+
/// DISCUSSION: This registration method doesn't have any scope parameter for a reason.
32+
/// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined.
33+
/// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved?
34+
/// Shared instances are typically not dependent on variable input parameters by definition.
35+
/// If you need to support this usecase, please, keep references to the variable singletons outside of the container.
36+
///
37+
/// - Parameters:
38+
/// - type: Type of the dependency to register
39+
/// - factory: Closure that is called when the dependency is being resolved
40+
func register<Dependency, Argument>(type: Dependency.Type, factory: @escaping FactoryWithArgument<Dependency, Argument>) async
41+
}
42+
43+
// MARK: Overloaded factory methods
44+
public extension AsyncDependencyRegistering {
45+
/// Default ``DependencyScope`` value
46+
///
47+
/// The default value is `shared`
48+
static var defaultScope: DependencyScope {
49+
DependencyScope.shared
50+
}
51+
52+
/// Register a dependency in the default ``DependencyScope``, i.e. in the `shared` scope
53+
///
54+
/// - Parameters:
55+
/// - type: Type of the dependency to register
56+
/// - factory: Closure that is called when the dependency is being resolved
57+
func register<Dependency>(type: Dependency.Type, factory: @escaping Factory<Dependency>) async {
58+
await register(type: type, in: Self.defaultScope, factory: factory)
59+
}
60+
61+
/// Register a dependency with an implicit type determined by the factory closure return type
62+
///
63+
/// - Parameters:
64+
/// - 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
65+
/// - factory: Closure that is called when the dependency is being resolved
66+
func register<Dependency>(in scope: DependencyScope, factory: @escaping Factory<Dependency>) async {
67+
await register(type: Dependency.self, in: scope, factory: factory)
68+
}
69+
70+
/// 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
71+
///
72+
/// - Parameters:
73+
/// - factory: Closure that is called when the dependency is being resolved
74+
func register<Dependency>(factory: @escaping Factory<Dependency>) async {
75+
await register(type: Dependency.self, in: Self.defaultScope, factory: factory)
76+
}
77+
78+
/// Register a dependency with a variable argument. The type of the dependency is determined implicitly based on the factory closure return type
79+
///
80+
/// The argument is typically a parameter in an initializer of the dependency that is not registered in the same resolver (i.e. container),
81+
/// therefore, it needs to be passed in `resolve` call
82+
///
83+
/// DISCUSSION: This registration method doesn't have any scope parameter for a reason.
84+
/// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined.
85+
/// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved?
86+
/// Shared instances are typically not dependent on variable input parameters by definition.
87+
/// If you need to support this usecase, please, keep references to the variable singletons outside of the container.
88+
///
89+
/// - Parameters:
90+
/// - factory: Closure that is called when the dependency is being resolved
91+
func register<Dependency, Argument>(factory: @escaping FactoryWithArgument<Dependency, Argument>) async {
92+
await register(type: Dependency.self, factory: factory)
93+
}
94+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// AsyncDependencyResolving.swift
3+
// DependencyInjection
4+
//
5+
// Created by Róbert Oravec on 17.12.2024.
6+
//
7+
8+
import Foundation
9+
10+
/// A type that is able to resolve a dependency
11+
public protocol AsyncDependencyResolving {
12+
/// Resolve a dependency that was previously registered within the container
13+
///
14+
/// If the container doesn't contain any registration for a dependency with the given type, ``ResolutionError`` is thrown
15+
///
16+
/// - Parameters:
17+
/// - type: Type of the dependency that should be resolved
18+
func tryResolve<T>(type: T.Type) async throws -> T
19+
20+
/// Resolve a dependency with a variable argument that was previously registered within the container
21+
///
22+
/// 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
23+
///
24+
/// - Parameters:
25+
/// - type: Type of the dependency that should be resolved
26+
/// - argument: Argument that will be passed as an input parameter to the factory method
27+
func tryResolve<T, Argument>(type: T.Type, argument: Argument) async throws -> T
28+
}
29+
30+
public extension AsyncDependencyResolving {
31+
/// Resolve a dependency that was previously registered within the container
32+
///
33+
/// If the container doesn't contain any registration for a dependency with the given type, a runtime error occurs
34+
///
35+
/// - Parameters:
36+
/// - type: Type of the dependency that should be resolved
37+
func resolve<T>(type: T.Type) async -> T {
38+
try! await tryResolve(type: type)
39+
}
40+
41+
/// Resolve a dependency that was previously registered within the container. A type of the required dependency is inferred from the return type
42+
///
43+
/// If the container doesn't contain any registration for a dependency with the given type, a runtime error occurs
44+
///
45+
/// - Parameters:
46+
/// - type: Type of the dependency that should be resolved
47+
func resolve<T>() async -> T {
48+
await resolve(type: T.self)
49+
}
50+
51+
/// Resolve a dependency with a variable argument that was previously registered within the container
52+
///
53+
/// 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
54+
///
55+
/// - Parameters:
56+
/// - type: Type of the dependency that should be resolved
57+
/// - argument: Argument that will be passed as an input parameter to the factory method
58+
func resolve<T, Argument>(type: T.Type, argument: Argument) async -> T {
59+
try! await tryResolve(type: type, argument: argument)
60+
}
61+
62+
/// 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
63+
///
64+
/// 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
65+
///
66+
/// - Parameters:
67+
/// - type: Type of the dependency that should be resolved
68+
/// - argument: Argument that will be passed as an input parameter to the factory method
69+
func resolve<T, Argument>(argument: Argument) async -> T {
70+
await resolve(type: T.self, argument: argument)
71+
}
72+
}

0 commit comments

Comments
 (0)