Skip to content

Commit

Permalink
Merge pull request #1 from willowtreeapps/feature/value-types-support
Browse files Browse the repository at this point in the history
Add support for value types
  • Loading branch information
rafcabezas committed Jan 26, 2024
2 parents ebcc9af + bd46933 commit d12431c
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 28 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
57 changes: 38 additions & 19 deletions Sources/Grove/Grove.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,36 @@ 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.
case singleton, transient
}

private enum DependencyItem {
case initializer(() -> AnyObject, scope: Scope)
case instance(AnyObject)
case initializer(() -> Any, scope: Scope)
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:
/// - initializer: Initializer for the dependency to be registered (ex. JSONEncoder.init, or { JSONEncoder() })
/// - type: Optional type of to use for registration (ex. JSONEncodingProtocol for the above initializer)
/// - 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>(_ initializer: @escaping () -> AnyObject, type: T.Type = T.self, scope: Scope = .singleton) {
public func register<T>(as type: T.Type = T.self, scope: Scope = .singleton, _ initializer: @escaping () -> T) {
Self.defaultContainerLock.lock()
Self.defaultContainer = self
Self.defaultContainerLock.unlock()
Expand All @@ -44,6 +48,21 @@ public final class Grove {
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) {
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 All @@ -93,7 +112,7 @@ public final class Grove {
}
}

public func register<T>(_ initializer: @escaping () -> T, scope: Scope = .singleton) where T: AnyObject {
register(initializer, type: T.self, scope: scope)
public func register<T>(_ initializer: @escaping () -> T, scope: Scope = .singleton) {
register(as: T.self, scope: scope, initializer)
}
}
8 changes: 4 additions & 4 deletions Sources/Grove/PropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import Foundation

/// Grove resolution property wrapper
///
/// In most cases, where only a single container is used, this property wrapper can be used to simplify resolution.
/// This property wrapper can be used to simplify resolution. Optionally a container other than the default can be specified.
///
/// Allows to resolve dependencies in this fashion:
/// It allows to resolve dependencies in this fashion:
/// ```
/// @Resolve var jsonEncoder: JSONEncodingProtocol
/// ```
Expand All @@ -24,7 +24,7 @@ import Foundation
public struct Resolve<T> {
public var wrappedValue: T

public init() {
self.wrappedValue = Grove.defaultContainer.resolve()
public init(container: Grove = .defaultContainer) {
self.wrappedValue = container.resolve()
}
}
10 changes: 6 additions & 4 deletions Tests/GroveTests/GroveTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ private protocol TestProtocol {

private final class TestClass: TestProtocol {
var value: Int = 0

func increment() {
value += 1
}
Expand All @@ -24,34 +25,35 @@ final class GroveTests: XCTestCase {
Grove.defaultContainer.register(NotProtocolConformingTestClass.init)

let testClass: NotProtocolConformingTestClass = Grove.defaultContainer.resolve()
@Resolve var testClass2: TestProtocol
XCTAssertEqual(testClass.value, "grove")
}

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

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

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

@Resolve var testClass1: TestProtocol
testClass1.increment()
testClass1.increment()
testClass1.increment()
XCTAssertEqual(testClass1.value, 3)

let testClass2: TestProtocol = Grove.defaultContainer.resolve()
@Resolve var testClass2: TestProtocol
XCTAssertEqual(testClass2.value, 0)
}

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

@Resolve var testClass1: TestProtocol
testClass1.increment()
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)

@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)

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

0 comments on commit d12431c

Please sign in to comment.