Skip to content

Commit

Permalink
Merge pull request #4 from willowtreeapps/feature/enhance-property-wr…
Browse files Browse the repository at this point in the history
…apper-resolution

Enchance property wrapper resolution
  • Loading branch information
rafcabezas authored Mar 22, 2024
2 parents 9e04bbe + dea0d2c commit f64bc47
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 20 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ container.register(DeploymentEnvironment.production)
// 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
// Alternatively, you can use the @Resolve property wrapper:
@Resolve(JSONEncoder.self) var jsonEncoder

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

### Using a registrar
Expand Down
22 changes: 20 additions & 2 deletions Sources/Grove/Grove.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,34 @@ public final class Grove: @unchecked Sendable {
case .instance(let instance):
dependency = instance
case .none:
preconditionFailure("Grove: '\(key)' Not registered.")
preconditionFailure("Grove: '\(String(describing: Dependency.self))' Not registered.")
}

guard let dependency = dependency as? Dependency else {
preconditionFailure("Grove: '\(key)' stored as '\(dependency.self)' (requested: '\(Dependency.self)').")
preconditionFailure("Grove: '\(String(describing: Dependency.self))' stored as '\(dependency.self)' (requested: '\(Dependency.self)').")
}

return dependency
}

/// Returns the scope for a dependency
/// - Returns: The scope
///
public func scope<Dependency>(for type: Dependency.Type) -> Scope {
dependencyItemsMapLock.lock()
let scope = dependencyItemsMap[key(for: type)]
dependencyItemsMapLock.unlock()

switch scope {
case .initializer(_, let scope):
return scope
case .instance:
return .singleton
case .none:
preconditionFailure("Grove: '\(String(describing: Dependency.self))' Not registered.")
}
}

// MARK: Helpers

private func key<Dependency>(for type: Dependency.Type) -> ObjectIdentifier {
Expand Down
28 changes: 24 additions & 4 deletions Sources/Grove/PropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,30 @@ import Foundation
/// ```
///
@propertyWrapper
public struct Resolve<T> {
public var wrappedValue: T
public struct Resolve<Dependency> {
private var container: Grove
private var transientInstance: Dependency?

public init(container: Grove = .defaultContainer) {
self.wrappedValue = container.resolve()
public init(_ type: Dependency.Type, container: Grove = .defaultContainer) {
self.container = container

switch container.scope(for: type) {
case .singleton:
break
case .transient:
transientInstance = (container.resolve() as Dependency)
}
}

public var wrappedValue: Dependency {
switch container.scope(for: Dependency.self) {
case .singleton:
return container.resolve()
case .transient:
guard let transientInstance else {
preconditionFailure("Grove: Error resolving transient dependency: '\(String(describing: Dependency.self))'")
}
return transientInstance
}
}
}
52 changes: 52 additions & 0 deletions Tests/GroveTests/GrovePropertyWrapperTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// GrovePropertyWrapperTests.swift
//
//
// Created by Raf Cabezas on 3/7/24.
//

import XCTest
@testable import Grove

private final class TestClass: TestProtocol {
var value: Int

init(value: Int = 0) {
self.value = value
}

func increment() {
value += 1
}
}

final class GrovePropertyWrapperTests: XCTestCase {

func testUpdatedRegistration() {
// Given
@Resolve(TestProtocol.self) var testClass
Grove.defaultContainer.register(as: TestProtocol.self, scope: .singleton, TestClass(value: 10))

// When
Grove.defaultContainer.register(as: TestProtocol.self, scope: .singleton, TestClass(value: 20))

// Then
XCTAssertEqual(testClass.value, 20)
}

func testForTransientScopeDependencies() {
// Given
Grove.defaultContainer.register(as: TestProtocol.self, scope: .transient, TestClass(value: 100))
@Resolve(TestProtocol.self) var testClass
@Resolve(TestProtocol.self) var testClass2

// When
testClass.increment()
testClass.increment()
testClass.increment()

// Then
XCTAssertEqual(testClass.value, 103)
XCTAssertEqual(testClass2.value, 100)
}
}
31 changes: 22 additions & 9 deletions Tests/GroveTests/GroveTests.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import XCTest
@testable import Grove

private protocol TestProtocol {
protocol TestProtocol {
var value: Int { get }
func increment()
}
Expand All @@ -22,46 +22,59 @@ final class GroveTests: XCTestCase {

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

// When
let testClass: NotProtocolConformingTestClass = Grove.defaultContainer.resolve()
@Resolve var testClass2: TestProtocol

// Then
XCTAssertEqual(testClass.value, "grove")
}

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

@Resolve var testClass: TestProtocol
// When
let testClass: TestProtocol = Grove.defaultContainer.resolve()

// Then
XCTAssertEqual(testClass.value, 0)
}

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

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

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

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

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

@Resolve var testClass2: TestProtocol
// Then
XCTAssertEqual(testClass1.value, 3)
let testClass2: TestProtocol = Grove.defaultContainer.resolve()
XCTAssertEqual(testClass2.value, 3)
}
}
4 changes: 2 additions & 2 deletions Tests/GroveTests/GroveValueTypeDependencyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ final class GroveValueTypeDependencyTests: XCTestCase {
func testDirectTypeRegistration() {
Grove.defaultContainer.register(NotProtocolConformingTestEnum.b)

@Resolve var testEnum: NotProtocolConformingTestEnum
@Resolve(NotProtocolConformingTestEnum.self) var testEnum
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, TestEnum.v)

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

0 comments on commit f64bc47

Please sign in to comment.