Skip to content

Commit

Permalink
Add support for value types
Browse files Browse the repository at this point in the history
  • Loading branch information
rafcabezas committed Jan 26, 2024
1 parent ebcc9af commit 7b169ba
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 15 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,36 @@ dependencies: [
## Usage
Grove simplifies dependency registration and resolving. Here's an example:

### Registration

```swift
let container = Grove.defaultContainer // You can use the default container or create your own

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

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

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

### Resolution

```swift
// Later in your code, you can resolve the dependency
let jsonEncoder: JSONEncoder = container.resolve()

// Alternatively, with the @Resolve property wrapper, usage becomes simpler:
@Resolve var jsonEncoder: JSONEncoder

// Value types are resolved the same way (here deploymentEnvironment would be .production)
@Resolve var deploymentEnvironment: DeploymentEnvironment
```

### Using a registrar

This shows how you can set up a registrar class both for production and for unit tests and SwiftUI previews:

```swift
Expand Down
47 changes: 33 additions & 14 deletions Sources/Grove/Grove.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import Foundation

/// Grove
/// Simple Dependency Injection Container Library
public final class Grove {
public final class Grove: @unchecked Sendable {

/// Scope, or lifetime of a dependency
/// 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.
Expand All @@ -20,13 +20,17 @@ public final class Grove {

private enum DependencyItem {
case initializer(() -> AnyObject, scope: Scope)
case instance(AnyObject)
case instance(Any)
}
private var dependencyItemsMap = [String: DependencyItem]()
private let dependencyItemsMapLock = NSLock()
public private(set) static var defaultContainer = Grove()
private static let defaultContainerLock = NSLock()
public init() {}

/// Default container
public private(set) static var defaultContainer = Grove()

/// Public initializer
public init() { /* No-Op */ }

/// Registers a dependency's initializer
/// - Parameters:
Expand All @@ -44,6 +48,21 @@ public final class Grove {
dependencyItemsMapLock.unlock()
}

/// Registers using a value
/// - Parameters:
/// - value: Value for the dependency to be registered
/// - type: Optional type of to use for registration
///
public func register<T>(value: T, type: T.Type = T.self) {
Self.defaultContainerLock.lock()
Self.defaultContainer = self
Self.defaultContainerLock.unlock()

dependencyItemsMapLock.lock()
dependencyItemsMap[key(for: T.self)] = DependencyItem.instance(value)
dependencyItemsMapLock.unlock()
}

/// Returns the resolved dependency
/// - Returns: The resolved dependency
/// Example: `let jsonEncoder: JSONEncodingProtocol = Grove.defaultContainer.resolve()`
Expand All @@ -54,10 +73,9 @@ public final class Grove {
let dependencyItem = dependencyItemsMap[key]
dependencyItemsMapLock.unlock()

let objectInstance: AnyObject
switch dependencyItem {
case .initializer(let initializer, let scope):
objectInstance = initializer()
let objectInstance = initializer()
switch scope {
case .singleton:
dependencyItemsMapLock.lock()
Expand All @@ -67,17 +85,18 @@ public final class Grove {
// 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):
objectInstance = instance
guard let instance = instance as? T else {
preconditionFailure("Grove: '\(key)' stored as '\(instance.self)' (requested: '\(T.self)').")
}
return instance
case .none:
preconditionFailure("Grove: '\(key)' Not registered.")
}

guard let objectInstance = objectInstance as? T else {
preconditionFailure("Grove: '\(key)' stored as '\(objectInstance.self)' (requested: '\(T.self)').")
}

return objectInstance
}

// MARK: Helpers
Expand Down
42 changes: 42 additions & 0 deletions Tests/GroveTests/GroveValueTypeDependencyTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// GroveValueTypeDependencyTests.swift
//
//
// Created by Raf Cabezas on 1/26/24.
//

import XCTest
@testable import Grove

private protocol TestEnumProtocol {
var value: String { get }
}

private enum TestEnum: String, TestEnumProtocol {
case g, r, o, v, e

var value: String { rawValue }
}

private enum NotProtocolConformingTestEnum {
case a, b, c
}

final class GroveValueTypeDependencyTests: XCTestCase {

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

let testEnum: NotProtocolConformingTestEnum = Grove.defaultContainer.resolve()
XCTAssertEqual(testEnum, .b)
}

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

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

0 comments on commit 7b169ba

Please sign in to comment.