Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
015866a
added async container
robha141 Dec 17, 2024
250d052
added module registration
robha141 Dec 17, 2024
f76d19d
module registration is now static
robha141 Dec 17, 2024
f531742
added tests
robha141 Dec 19, 2024
481bda2
updated registration protocols
robha141 Dec 19, 2024
6690de5
[chore] fix comment typo
cejanen Dec 19, 2024
edd8abb
increased swift tools version
robha141 Jan 7, 2025
8726479
changed factory in async registration to async registration factory
robha141 Jan 7, 2025
6293bd4
chore: Merge branch 'feat/rob-async-init' of github.com:strvcom/ios-d…
robha141 Jan 7, 2025
36a4e9e
Update Sources/Container/AsyncContainer.swift
DanielCech Feb 6, 2025
e531377
Update Sources/Container/AsyncContainer.swift
DanielCech Feb 6, 2025
e4550be
feat: minor fixes
DanielCech Feb 6, 2025
0000e4e
Merge branch 'feat/rob-async-init' of github.com:strvcom/ios-dependen…
DanielCech Feb 6, 2025
66d1149
feat: sync/async folders
DanielCech Feb 6, 2025
984d223
feat: async init test
DanielCech Feb 6, 2025
5362b87
feat: bundler version
DanielCech Feb 10, 2025
5327c73
feat: bundler fix
DanielCech Feb 10, 2025
3ec35e0
feat: bundler fix
DanielCech Feb 10, 2025
5c542cd
feat: updated changelog
DanielCech Feb 10, 2025
5da88e2
fix: line ending
DanielCech Feb 12, 2025
dc8dd20
feat: bundler update
DanielCech Feb 18, 2025
812cb70
fix: comment
DanielCech Feb 18, 2025
04e8c9e
feat: bump fastlane version
DanielCech Feb 18, 2025
ce2133e
Update Sources/Container/Async/AsyncContainer.swift
DanielCech Feb 19, 2025
ea58ced
Update Sources/Container/Async/AsyncContainer.swift
DanielCech Feb 19, 2025
480b459
fix: last line
DanielCech Feb 19, 2025
bc3aabf
Merge branch 'feat/rob-async-init' of github.com:strvcom/ios-dependen…
DanielCech Feb 19, 2025
2716660
fix: tests
DanielCech Feb 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -23,7 +23,8 @@ let package = Package(
dependencies: [],
path: "Sources",
swiftSettings: [
.define("APPLICATION_EXTENSION_API_ONLY")
.define("APPLICATION_EXTENSION_API_ONLY"),
.enableUpcomingFeature("StrictConcurrency")
]
),
.testTarget(
Expand Down
154 changes: 154 additions & 0 deletions Sources/Container/AsyncContainer.swift
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
///
/// - 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
}
}
2 changes: 1 addition & 1 deletion Sources/Container/Container.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
39 changes: 39 additions & 0 deletions Sources/Models/AsyncRegistration.swift
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) {
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)
}
}
}
2 changes: 1 addition & 1 deletion Sources/Models/DependencyScope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 11 additions & 0 deletions Sources/Protocols/AsyncModuleRegistration.swift
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
}
11 changes: 11 additions & 0 deletions Sources/Protocols/ModuleRegistration.swift
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 Sources/Protocols/Registration/AsyncDependencyRegistering.swift
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)
}
}
Loading
Loading