Skip to content

Commit 14984a3

Browse files
committed
Fix issue with shared scope when type was optional
1 parent b55f2ca commit 14984a3

File tree

6 files changed

+129
-25
lines changed

6 files changed

+129
-25
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Factory Changelog
22

3+
### 1.2.1
4+
5+
* Fix issue with shared scope when type was optional
6+
37
### 1.2.0 - Stable release
48

59
* Revised structure to better support ParameterFactory

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Failure to find a matching type can lead to an application crash if we attempt t
2828
* **Safe:** Factory is compile-time safe; a factory for a given type *must* exist or the code simply will not compile.
2929
* **Flexible:** It's easy to override dependencies at runtime and for use in SwiftUI Previews.
3030
* **Powerful:** Like Resolver, Factory supports application, cached, shared, and custom scopes, custom containers, arguments, decorators, and more.
31-
* **Lightweight:** With all of that Factory is slim and trim, well under 400 lines of code and half the size of Resolver.
31+
* **Lightweight:** With all of that Factory is slim and trim, just 400 lines of code and half the size of Resolver.
3232
* **Performant:** Little to no setup time is needed for the vast majority of your services, resolutions are extremely fast, and no compile-time scripts or build phases are needed.
3333
* **Concise:** Defining a registration usually takes just a single line of code. Same for resolution.
3434
* **Tested:** Unit tests ensure correct operation of registrations, resolutions, and scopes.

Sources/Factory/Factory.swift

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,22 +196,33 @@ open class SharedContainer {
196196
defer { lock.unlock() }
197197
lock.lock()
198198
if let box = cache[id], let instance = box.instance as? T {
199-
return instance
199+
if let optional = instance as? OptionalProtocol {
200+
if optional.hasWrappedValue {
201+
return instance
202+
}
203+
} else {
204+
return instance
205+
}
200206
}
201207
let instance: T = factory()
202-
if let box = box(instance) {
208+
if let optional = instance as? OptionalProtocol {
209+
if optional.hasWrappedValue, let box = box(instance) {
210+
cache[id] = box
211+
}
212+
} else if let box = box(instance) {
203213
cache[id] = box
204214
}
205215
return instance
206216
}
207217

208-
/// Internal reset function used by Factory
218+
/// Internal reset function used by Factory
209219
fileprivate func reset(_ id: UUID) {
210220
defer { lock.unlock() }
211221
lock.lock()
212222
cache.removeValue(forKey: id)
213223
}
214224

225+
/// Internal function correctly boxes cache value depending upon scope type
215226
fileprivate func box<T>(_ instance: T) -> AnyBox? {
216227
StrongBox<T>(boxed: instance)
217228
}
@@ -247,7 +258,15 @@ extension SharedContainer.Scope {
247258
super.init()
248259
}
249260
fileprivate override func box<T>(_ instance: T) -> AnyBox? {
250-
type(of: instance) is AnyClass ? WeakBox(boxed: instance as AnyObject) : nil
261+
if let optional = instance as? OptionalProtocol {
262+
// Actual wrapped type could be value, protocol, or something else so we need to check if wrapped instance is class type
263+
if optional.hasWrappedValue, type(of: optional.unwrap()) is AnyObject.Type {
264+
return WeakBox(boxed: instance as AnyObject)
265+
}
266+
} else if type(of: instance) is AnyClass {
267+
return WeakBox(boxed: instance as AnyObject)
268+
}
269+
return nil
251270
}
252271
}
253272

@@ -343,6 +362,27 @@ private struct Registration<P,T> {
343362

344363
}
345364

365+
/// Internal protocol used to evaluate optional types for caching
366+
private protocol OptionalProtocol {
367+
var hasWrappedValue: Bool { get }
368+
func unwrap() -> Any
369+
}
370+
371+
extension Optional : OptionalProtocol {
372+
fileprivate var hasWrappedValue: Bool {
373+
if case .some = self {
374+
return true
375+
}
376+
return false
377+
}
378+
fileprivate func unwrap() -> Any {
379+
if case .some(let unwrapped) = self {
380+
return unwrapped
381+
}
382+
preconditionFailure("trying to unwrap nil")
383+
}
384+
}
385+
346386
/// Internal box protocol for scope functionality
347387
private protocol AnyBox {
348388
var instance: Any { get }

Tests/FactoryTests/FactoryDefectTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ final class FactoryDefectTests: XCTestCase {
1111

1212
// scope would not correctly resolve a factory with an optional type. e.g. Factory<MyType?>(scope: .cached) { nil }
1313
func testNilScopedService() throws {
14-
Container.nilScopedService.reset()
15-
let service1 = Container.nilScopedService()
14+
Container.nilCachedService.reset()
15+
let service1 = Container.nilCachedService()
1616
XCTAssertNil(service1)
17-
Container.nilScopedService.register {
17+
Container.nilCachedService.register {
1818
MyService()
1919
}
20-
let service2 = Container.nilScopedService()
20+
let service2 = Container.nilCachedService()
2121
XCTAssertNotNil(service2)
2222
}
2323

Tests/FactoryTests/FactoryScopeTests.swift

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,44 @@ final class FactoryScopeTests: XCTestCase {
4242
}
4343

4444
func testSharedScope() throws {
45-
var service1: MyService? = Container.sharedService()
46-
var service2: MyService? = Container.sharedService()
45+
var service1: MyServiceType? = Container.sharedService()
46+
var service2: MyServiceType? = Container.sharedService()
47+
XCTAssertNotNil(service1)
48+
XCTAssertNotNil(service2)
49+
// Shared cached item ids should match
4750
XCTAssertTrue(service1?.id == service2?.id)
4851
service1 = nil
4952
service2 = nil
50-
let service3: MyService? = Container.sharedService()
53+
let service3: MyServiceType? = Container.sharedService()
54+
XCTAssertNotNil(service3)
55+
XCTAssertTrue(service2?.id != service3?.id)
56+
}
57+
58+
func testOptionalSharedScope() throws {
59+
var service1: MyServiceType? = Container.optionalSharedService()
60+
var service2: MyServiceType? = Container.optionalSharedService()
61+
XCTAssertNotNil(service1)
62+
XCTAssertNotNil(service2)
63+
// Shared cached item ids should match
64+
XCTAssertTrue(service1?.id == service2?.id)
65+
service1 = nil
66+
service2 = nil
67+
let service3: MyServiceType? = Container.optionalSharedService()
68+
XCTAssertNotNil(service3)
69+
XCTAssertTrue(service2?.id != service3?.id)
70+
}
71+
72+
func testOptionalValueSharedScope() throws {
73+
var service1: MyServiceType? = Container.optionalValueService()
74+
var service2: MyServiceType? = Container.optionalValueService()
75+
XCTAssertNotNil(service1)
76+
XCTAssertNotNil(service2)
77+
// Value types aren't shared so cached item ids should NOT match
78+
XCTAssertTrue(service1?.id != service2?.id)
79+
service1 = nil
80+
service2 = nil
81+
let service3: MyServiceType? = Container.optionalValueService()
82+
XCTAssertNotNil(service3)
5183
XCTAssertTrue(service2?.id != service3?.id)
5284
}
5385

@@ -160,26 +192,51 @@ final class FactoryScopeTests: XCTestCase {
160192
}
161193

162194
func testNilScopedServiceCaching() throws {
163-
Container.nilScopedService.reset()
195+
Container.nilCachedService.reset()
164196
XCTAssertTrue(Container.Scope.cached.isEmpty)
165-
let service1 = Container.nilScopedService()
197+
let service1 = Container.nilCachedService()
166198
XCTAssertNil(service1)
167-
XCTAssertFalse(Container.Scope.cached.isEmpty) // nil was cached
168-
let service2 = Container.nilScopedService()
169-
XCTAssertNil(service2) // cached nil was returned
170-
XCTAssertFalse(Container.Scope.cached.isEmpty)
171-
Container.nilScopedService.register {
199+
XCTAssertTrue(Container.Scope.cached.isEmpty) // nothing caches
200+
let service2 = Container.nilCachedService()
201+
XCTAssertNil(service2)
202+
XCTAssertTrue(Container.Scope.cached.isEmpty) // nothing caches
203+
Container.nilCachedService.register {
172204
MyService()
173205
}
174-
let service3 = Container.nilScopedService()
206+
let service3 = Container.nilCachedService()
175207
XCTAssertNotNil(service3)
176-
XCTAssertFalse(Container.Scope.cached.isEmpty)
177-
Container.nilScopedService.register {
208+
XCTAssertFalse(Container.Scope.cached.isEmpty) // cached value
209+
Container.nilCachedService.register {
178210
nil
179211
}
180-
let service4 = Container.nilScopedService()
212+
let service4 = Container.nilCachedService()
181213
XCTAssertNil(service4) // cache was reset by registration
182-
XCTAssertFalse(Container.Scope.cached.isEmpty)
214+
XCTAssertTrue(Container.Scope.cached.isEmpty) // nothing cached
183215
}
184216

217+
func testNilSharedServiceCaching() throws {
218+
Container.nilSharedService.reset()
219+
XCTAssertTrue(Container.Scope.shared.isEmpty)
220+
let service1 = Container.nilSharedService()
221+
XCTAssertNil(service1)
222+
XCTAssertTrue(Container.Scope.shared.isEmpty) // nothing caches
223+
let service2 = Container.nilSharedService()
224+
XCTAssertNil(service2)
225+
XCTAssertTrue(Container.Scope.shared.isEmpty) // nothing caches
226+
Container.nilSharedService.register {
227+
MyService()
228+
}
229+
let service3 = Container.nilSharedService()
230+
XCTAssertNotNil(service3)
231+
let service4 = Container.nilSharedService()
232+
XCTAssertNotNil(service4)
233+
XCTAssertTrue(service3?.id == service4?.id)
234+
XCTAssertFalse(Container.Scope.shared.isEmpty) // cached value
235+
Container.nilSharedService.register {
236+
nil
237+
}
238+
let service5 = Container.nilSharedService()
239+
XCTAssertNil(service5) // cache was reset by registration
240+
XCTAssertTrue(Container.Scope.shared.isEmpty) // nothing cached
241+
}
185242
}

Tests/FactoryTests/MockServices.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,11 @@ extension Container {
8585
static let sharedService = Factory(scope: .shared) { MyService() }
8686
static let singletonService = Factory(scope: .singleton) { MyService() }
8787
static let optionalService = Factory<MyServiceType?> { MyService() }
88+
static let optionalSharedService = Factory<MyServiceType?>(scope: .shared) { MyService() }
89+
static let optionalValueService = Factory<MyServiceType?> { ValueService() }
8890
static let nilSService = Factory<MyServiceType?> { nil }
89-
static let nilScopedService = Factory<MyServiceType?>(scope: .cached) { nil }
91+
static let nilCachedService = Factory<MyServiceType?>(scope: .cached) { nil }
92+
static let nilSharedService = Factory<MyServiceType?>(scope: .shared) { nil }
9093
static let sessionService = Factory(scope: .session) { MyService() }
9194
static let valueService = Factory(scope: .cached) { ValueService() }
9295
static let sharedValueService = Factory(scope: .shared) { ValueService() }

0 commit comments

Comments
 (0)