Skip to content

Commit

Permalink
Merge pull request #3 from willowtreeapps/moyerr/lift-reference-type-…
Browse files Browse the repository at this point in the history
…restriction

Support lazy evaluation for all dependencies
  • Loading branch information
moyerr committed Feb 1, 2024
2 parents 381d573 + 9ba355b commit 9e04bbe
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 52 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ Grove simplifies dependency registration and resolving. Here's an example:
let container = Grove.defaultContainer // You can use the default container or create your own

// Register a reference type dependency as a singleton
container.register(JSONEncoder.init)
container.register(JSONEncoder())

// or with a transient lifetime
container.register(scope: .transient, JSONEncoder.init)
container.register(scope: .transient, JSONEncoder())

// Register a value type dependency (an enum for example)
container.register(value: DeploymentEnvironment.production)
container.register(DeploymentEnvironment.production)
```

### Resolution
Expand Down Expand Up @@ -75,7 +75,7 @@ final class DependenciesRegistrar {
return decoder
}

container.register(as: NASARepositoryProtocol.self, NASARepository.init)
container.register(as: NASARepositoryProtocol.self, NASARepository())
}

static func registerMocks() {
Expand All @@ -91,12 +91,12 @@ final class DependenciesRegistrar {
return decoder
}

container.register(as: NASARepositoryProtocol.self, MockNASARepository.init)
container.register(as: NASARepositoryProtocol.self, MockNASARepository())
}
}
```

You can then call DependenciesRegistrar.register() from your App's init() or AppDelegate. For unit tests or SwiftUI previews, you can call DependenciesRegistrar.registerMocks().
You can then call `DependenciesRegistrar.register()` from your App's `init()` or `AppDelegate`. For unit tests or SwiftUI previews, you can call `DependenciesRegistrar.registerMocks()`.

## Contributing
Contributions are immensely appreciated. Feel free to submit pull requests or to create issues to discuss any potential bugs or improvements.
Expand Down
74 changes: 34 additions & 40 deletions Sources/Grove/Grove.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ public final class Grove: @unchecked Sendable {

/// Scope, or lifetime of a reference-type dependency
public enum Scope {
// singleton: dependency is initialized once and then reused. Its lifetime is the lifetime of the container (the app in most cases).
// transient: dependency is initialized every time it is resolved. Its lifetime is the lifetime of the object that owns the dependency.
case singleton, transient
/// Dependency is initialized once and then reused. Its lifetime is the lifetime of the container (the app in most cases).
case singleton
/// Dependency is initialized every time it is resolved. Its lifetime is the lifetime of the object that owns the dependency.
case transient
}

private enum DependencyItem {
case initializer(() -> Any, scope: Scope)
case instance(Any)
}
private var dependencyItemsMap = [String: DependencyItem]()

private var dependencyItemsMap = [ObjectIdentifier: DependencyItem]()
private let dependencyItemsMapLock = NSLock()

/// Default container
Expand All @@ -37,73 +39,65 @@ public final class Grove: @unchecked Sendable {
/// - scope: Optional scope to use for registration: singleton or transient. Transient dependencies are initialized every time they are resolved
/// - initializer: Initializer for the dependency to be registered (ex. JSONEncoder.init, or { JSONEncoder() })
///
public func register<T>(as type: T.Type = T.self, scope: Scope = .singleton, _ initializer: @escaping () -> T) {
public func register<Dependency>(
as type: Dependency.Type = Dependency.self,
scope: Scope = .singleton,
with initializer: @escaping () -> Dependency
) {
dependencyItemsMapLock.lock()
dependencyItemsMap[key(for: T.self)] = DependencyItem.initializer(initializer, scope: scope)
dependencyItemsMap[key(for: Dependency.self)] = .initializer(initializer, scope: scope)
dependencyItemsMapLock.unlock()
}

/// Registers using a value
/// - Parameters:
/// - type: Optional type of to use for registration
/// - value: Value for the dependency to be registered
///
public func register<T>(as type: T.Type = T.self, value: T) {
dependencyItemsMapLock.lock()
dependencyItemsMap[key(for: T.self)] = DependencyItem.instance(value)
dependencyItemsMapLock.unlock()
public func register<Dependency>(
as type: Dependency.Type = Dependency.self,
scope: Scope = .singleton,
_ initializer: @autoclosure @escaping () -> Dependency
) {
register(as: type, scope: scope, with: initializer)
}

/// Returns the resolved dependency
/// - Returns: The resolved dependency
/// Example: `let jsonEncoder: JSONEncodingProtocol = Grove.defaultContainer.resolve()`
///
public func resolve<T>() -> T {
let key = key(for: T.self)
public func resolve<Dependency>() -> Dependency {
let key = key(for: Dependency.self)

dependencyItemsMapLock.lock()
let dependencyItem = dependencyItemsMap[key]
dependencyItemsMapLock.unlock()

let dependency: Any

switch dependencyItem {
case .initializer(let initializer, let scope):
let objectInstance = initializer()
dependency = initializer()
switch scope {
case .singleton:
dependencyItemsMapLock.lock()
dependencyItemsMap[key] = DependencyItem.instance(objectInstance)
dependencyItemsMap[key] = .instance(dependency)
dependencyItemsMapLock.unlock()
case .transient:
// No-Op
break
}
guard let objectInstance = objectInstance as? T else {
preconditionFailure("Grove: '\(key)' stored as '\(objectInstance.self)' (requested: '\(T.self)').")
}
return objectInstance
case .instance(let instance):
guard let instance = instance as? T else {
preconditionFailure("Grove: '\(key)' stored as '\(instance.self)' (requested: '\(T.self)').")
}
return instance
dependency = instance
case .none:
preconditionFailure("Grove: '\(key)' Not registered.")
}
}

// MARK: Helpers

private func key<T>(for type: T.Type) -> String {
let rawKey = String(describing: T.self)
if !rawKey.hasPrefix("Optional<") {
return rawKey
.replacingOccurrences(of: "Optional<", with: "")
.replacingOccurrences(of: ">", with: "")
} else {
return rawKey
guard let dependency = dependency as? Dependency else {
preconditionFailure("Grove: '\(key)' stored as '\(dependency.self)' (requested: '\(Dependency.self)').")
}

return dependency
}

public func register<T>(_ initializer: @escaping () -> T, scope: Scope = .singleton) {
register(as: T.self, scope: scope, initializer)
// MARK: Helpers

private func key<Dependency>(for type: Dependency.Type) -> ObjectIdentifier {
ObjectIdentifier(type)
}
}
8 changes: 4 additions & 4 deletions Tests/GroveTests/GroveTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class GroveTests: XCTestCase {

/// Tests registering a class and resolving it.
func testDirectClassRegistration() {
Grove.defaultContainer.register(NotProtocolConformingTestClass.init)
Grove.defaultContainer.register(NotProtocolConformingTestClass())

let testClass: NotProtocolConformingTestClass = Grove.defaultContainer.resolve()
@Resolve var testClass2: TestProtocol
Expand All @@ -31,15 +31,15 @@ final class GroveTests: XCTestCase {

/// Tests registering a class as a protocol and resolving it as a protocol.
func testClassAsProtocolRegistration() {
Grove.defaultContainer.register(as: TestProtocol.self, TestClass.init)
Grove.defaultContainer.register(as: TestProtocol.self, TestClass())

@Resolve var testClass: TestProtocol
XCTAssertEqual(testClass.value, 0)
}

/// Tests registering a class using the transient lifetime scope
func testTransientScope() {
Grove.defaultContainer.register(as: TestProtocol.self, scope: .transient, TestClass.init)
Grove.defaultContainer.register(as: TestProtocol.self, scope: .transient, TestClass())

@Resolve var testClass1: TestProtocol
testClass1.increment()
Expand All @@ -53,7 +53,7 @@ final class GroveTests: XCTestCase {

/// Tests registering a class using the singleton lifetime scope
func testSingletonScope() {
Grove.defaultContainer.register(as: TestProtocol.self, scope: .singleton, TestClass.init)
Grove.defaultContainer.register(as: TestProtocol.self, scope: .singleton, TestClass())

@Resolve var testClass1: TestProtocol
testClass1.increment()
Expand Down
4 changes: 2 additions & 2 deletions Tests/GroveTests/GroveValueTypeDependencyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ final class GroveValueTypeDependencyTests: XCTestCase {

/// Tests registering a value type and resolving it.
func testDirectTypeRegistration() {
Grove.defaultContainer.register(value: NotProtocolConformingTestEnum.b)
Grove.defaultContainer.register(NotProtocolConformingTestEnum.b)

@Resolve var testEnum: NotProtocolConformingTestEnum
XCTAssertEqual(testEnum, .b)
}

/// Tests registering a value type as a protocol and resolving it as a protocol.
func testValueTypeAsProtocolRegistration() {
Grove.defaultContainer.register(as: TestEnumProtocol.self, value: TestEnum.v)
Grove.defaultContainer.register(as: TestEnumProtocol.self, TestEnum.v)

@Resolve var testEnum: TestEnumProtocol
XCTAssertEqual(testEnum.value, "v")
Expand Down

0 comments on commit 9e04bbe

Please sign in to comment.