Skip to content

Commit

Permalink
Fix issue with dynamic keypath wrappers.
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Usbergo committed Mar 1, 2021
1 parent fb88de4 commit 34bf59e
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 64 deletions.
2 changes: 1 addition & 1 deletion Sources/Store/store/BindingProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import OpenCombineDispatch
}

public subscript<T>(dynamicMember keyPath: WritableKeyPath<M, T>) -> T {
get { store.modelStorage[dynamicMember: keyPath] }
get { store.modelStorage.model[keyPath: keyPath] }
set { store.run(action: Assign(keyPath, newValue), mode: .mainThread) }
}
}
Expand Down
28 changes: 3 additions & 25 deletions Sources/Store/store/ModelStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import OpenCombineDispatch
/// It provides observability and thread-safe access to the underlying model.
///
/// Abstract base class for `ModelStorage` and `ChildModelStorage`.
@dynamicMemberLookup
open class ModelStorageBase<M>: ObservableObject {

/// A publisher that publishes changes from observable objects.
Expand All @@ -19,11 +18,6 @@ open class ModelStorageBase<M>: ObservableObject {
/// Read-only wrapped immutable model.
public var model: M { fatalError() }

/// Managed acccess to the wrapped model.
open subscript<T>(dynamicMember keyPath: WritableKeyPath<M, T>) -> T {
fatalError()
}

/// Thread-safe access to the underlying wrapped immutable model.
public func reduce(_ closure: (inout M) -> Void) {
fatalError()
Expand All @@ -42,15 +36,9 @@ open class ModelStorageBase<M>: ObservableObject {
}

/// Concrete implementation for `ModelStorageBase`.
@dynamicMemberLookup
public final class ModelStorage<M>: ModelStorageBase<M> {

override public var model: M { _model }

override public final subscript<T>(dynamicMember keyPath: WritableKeyPath<M, T>) -> T {
get { _model[keyPath: keyPath] }
set { reduce { $0[keyPath: keyPath] = newValue } }
}

private var _model: M

Expand All @@ -70,10 +58,9 @@ public final class ModelStorage<M>: ModelStorageBase<M> {

/// Create a model storage from a `ModelStorage` model subtree defined by a key path.
/// The model for this store is shared with the parent.
@dynamicMemberLookup
public final class ChildModelStorage<P, M>: ModelStorageBase<M> {

override public var model: M { _parent[dynamicMember: _keyPath] }
override public var model: M { _parent.model[keyPath: _keyPath] }

private let _parent: ModelStorageBase<P>
private let _keyPath: WritableKeyPath<P, M>
Expand All @@ -86,11 +73,7 @@ public final class ChildModelStorage<P, M>: ModelStorageBase<M> {
self?.objectWillChange.send()
}
}

override public final subscript<T>(dynamicMember keyPath: WritableKeyPath<M, T>) -> T {
get { model[keyPath: keyPath] }
set { reduce { $0[keyPath: keyPath] = newValue } }
}


override public func reduce(_ closure: (inout M) -> Void) {
_parent.reduce { closure(&$0[keyPath: _keyPath]) }
Expand All @@ -100,7 +83,6 @@ public final class ChildModelStorage<P, M>: ModelStorageBase<M> {

/// Whenever this object change the parent model is reconciled with the change (and will
/// subsequently emit an `objectWillChange` notification).
@dynamicMemberLookup
public final class UnownedChildModelStorage<P, M>: ModelStorageBase<M> {
private let _parent: ModelStorageBase<P>
private var _model: M
Expand All @@ -117,11 +99,7 @@ public final class UnownedChildModelStorage<P, M>: ModelStorageBase<M> {
self?.objectWillChange.send()
}
}

override public final subscript<T>(dynamicMember keyPath: WritableKeyPath<M, T>) -> T {
get { _model[keyPath: keyPath] }
set { reduce { $0[keyPath: keyPath] = newValue } }
}


override public func reduce(_ closure: (inout M) -> Void) {
_modelLock.lock()
Expand Down
1 change: 1 addition & 0 deletions Sources/Store/store/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ open class Store<M>: ReducibleStore, ObservableObject, Identifiable {
// See `AnyStore`.
public var middleware: [Middleware] = []
public private(set) var modelStorage: ModelStorageBase<M>
public var model: M { modelStorage.model }

// Internal
public var parent: AnyStore?
Expand Down
54 changes: 27 additions & 27 deletions Tests/StoreTests/ChildStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ final class ChildStoreTests: XCTestCase {
rootStore.noteStore.register(middleware: LoggerMiddleware())
rootStore.listStore.register(middleware: LoggerMiddleware())

XCTAssertFalse(rootStore.modelStorage.todo.done)
XCTAssertFalse(rootStore.todoStore.modelStorage.done)
XCTAssertFalse(rootStore.model.todo.done)
XCTAssertFalse(rootStore.todoStore.model.done)
rootStore.todoStore.run(action: Root.Todo.Action_MarkAsDone(), mode: .sync)
XCTAssertTrue(rootStore.todoStore.modelStorage.done)
XCTAssertTrue(rootStore.modelStorage.todo.done)
XCTAssertTrue(rootStore.todoStore.model.done)
XCTAssertTrue(rootStore.model.todo.done)
}

func testChildStoreChangesTriggersRootObserver() {
Expand All @@ -109,13 +109,13 @@ final class ChildStoreTests: XCTestCase {
rootStore.listStore.register(middleware: LoggerMiddleware())

sink = rootStore.objectWillChange.sink {
XCTAssertTrue(rootStore.modelStorage.todo.done)
XCTAssertTrue(rootStore.todoStore.modelStorage.done)
XCTAssertTrue(rootStore.model.todo.done)
XCTAssertTrue(rootStore.todoStore.model.done)
observerExpectation.fulfill()
}
rootStore.todoStore.run(action: Root.Todo.Action_MarkAsDone(), mode: .sync)
XCTAssertTrue(rootStore.todoStore.modelStorage.done)
XCTAssertTrue(rootStore.modelStorage.todo.done)
XCTAssertTrue(rootStore.todoStore.model.done)
XCTAssertTrue(rootStore.model.todo.done)
waitForExpectations(timeout: 1)
}

Expand All @@ -127,32 +127,32 @@ final class ChildStoreTests: XCTestCase {
rootStore.register(middleware: LoggerMiddleware())
rootStore.listStore.register(middleware: LoggerMiddleware())

XCTAssertTrue(rootStore.listStore.modelStorage.model.count == 1)
XCTAssertTrue(rootStore.listStore.modelStorage[0].name == "New")
XCTAssertTrue(rootStore.listStore.modelStorage[0].description == "New")
XCTAssertTrue(rootStore.listStore.modelStorage[0].done == false)
XCTAssertTrue(rootStore.modelStorage.list.count == 1)
XCTAssertTrue(rootStore.modelStorage.list[0].name == "New")
XCTAssertTrue(rootStore.modelStorage.list[0].description == "New")
XCTAssertTrue(rootStore.modelStorage.list[0].done == false)
XCTAssertTrue(rootStore.listStore.model.count == 1)
XCTAssertTrue(rootStore.listStore.model[0].name == "New")
XCTAssertTrue(rootStore.listStore.model[0].description == "New")
XCTAssertTrue(rootStore.listStore.model[0].done == false)
XCTAssertTrue(rootStore.model.list.count == 1)
XCTAssertTrue(rootStore.model.list[0].name == "New")
XCTAssertTrue(rootStore.model.list[0].description == "New")
XCTAssertTrue(rootStore.model.list[0].done == false)

let listStore = rootStore.listStore

let todoStore = listStore.makeChildStore(keyPath: \.[0])
todoStore.register(middleware: LoggerMiddleware())

todoStore.run(action: Root.Todo.Action_MarkAsDone(), mode: .sync)
XCTAssertTrue(todoStore.modelStorage.name == "New")
XCTAssertTrue(todoStore.modelStorage.description == "New")
XCTAssertTrue(todoStore.modelStorage.done == true)
XCTAssertTrue(rootStore.listStore.modelStorage.model.count == 1)
XCTAssertTrue(rootStore.listStore.modelStorage[0].name == "New")
XCTAssertTrue(rootStore.listStore.modelStorage[0].description == "New")
XCTAssertTrue(rootStore.listStore.modelStorage[0].done == true)
XCTAssertTrue(rootStore.modelStorage.list.count == 1)
XCTAssertTrue(rootStore.modelStorage.list[0].name == "New")
XCTAssertTrue(rootStore.modelStorage.list[0].description == "New")
XCTAssertTrue(rootStore.modelStorage.list[0].done == true)
XCTAssertTrue(todoStore.model.name == "New")
XCTAssertTrue(todoStore.model.description == "New")
XCTAssertTrue(todoStore.model.done == true)
XCTAssertTrue(rootStore.listStore.model.count == 1)
XCTAssertTrue(rootStore.listStore.model[0].name == "New")
XCTAssertTrue(rootStore.listStore.model[0].description == "New")
XCTAssertTrue(rootStore.listStore.model[0].done == true)
XCTAssertTrue(rootStore.model.list.count == 1)
XCTAssertTrue(rootStore.model.list[0].name == "New")
XCTAssertTrue(rootStore.model.list[0].description == "New")
XCTAssertTrue(rootStore.model.list[0].done == true)
}
}

22 changes: 11 additions & 11 deletions Tests/StoreTests/StoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ final class StoreTests: XCTestCase {
store.register(middleware: LoggerMiddleware())
store.run(action: TestAction.increase(amount: 42)) { error in
XCTAssert(error == nil)
XCTAssert(store.modelStorage.count == 42)
XCTAssert(store.model.count == 42)
transactionExpectation.fulfill()
}
waitForExpectations(timeout: 1)
Expand All @@ -28,7 +28,7 @@ final class StoreTests: XCTestCase {
TestAction.increase(amount: 1),
TestAction.increase(amount: 1),
]) { context in
XCTAssert(store.modelStorage.count == 3)
XCTAssert(store.model.count == 3)
transactionExpectation.fulfill()
}
waitForExpectations(timeout: 10)
Expand All @@ -38,18 +38,18 @@ final class StoreTests: XCTestCase {
let store = CodableStore(model: TestModel(), diffing: .sync)
store.register(middleware: LoggerMiddleware())
store.run(action: TestAction.updateLabel(newLabel: "Bar"), mode: .sync)
XCTAssert(store.modelStorage.label == "Bar")
XCTAssert(store.modelStorage.nested.label == "Bar")
XCTAssert(store.model.label == "Bar")
XCTAssert(store.model.nested.label == "Bar")
store.run(action: TestAction.updateLabel(newLabel: "Foo"), mode: .sync)
XCTAssert(store.modelStorage.label == "Foo")
XCTAssert(store.modelStorage.nested.label == "Foo")
XCTAssert(store.model.label == "Foo")
XCTAssert(store.model.nested.label == "Foo")
}

func testAccessNestedKeyPathInArray() {
let store = CodableStore(model: TestModel(), diffing: .sync)
store.register(middleware: LoggerMiddleware())
store.run(action: TestAction.setArray(index: 1, value: "Foo"), mode: .sync)
XCTAssert(store.modelStorage.array[1].label == "Foo")
XCTAssert(store.model.array[1].label == "Foo")
}

func testDiffResult() {
Expand All @@ -74,7 +74,7 @@ final class StoreTests: XCTestCase {
let transaction = store.transaction(action: TestAction.increase(amount: 1))
sink = transaction.$state.sink { state in
XCTAssert(state != .completed)
XCTAssert(store.modelStorage.count == 0)
XCTAssert(store.model.count == 0)
if state == .canceled {
transactionExpectation.fulfill()
}
Expand All @@ -93,7 +93,7 @@ final class StoreTests: XCTestCase {
sink = store.futureOf(action: action1)
.replaceError(with: ())
.sink {
XCTAssert(store.modelStorage.count == 5)
XCTAssert(store.model.count == 5)
transactionExpectation.fulfill()
}
waitForExpectations(timeout: 1)
Expand All @@ -102,13 +102,13 @@ final class StoreTests: XCTestCase {
func testAccessToBindingProxy() {
let store = CodableStore(model: TestModel(), diffing: .sync)
store.binding.count = 3
XCTAssert(store.modelStorage.count == 3)
XCTAssert(store.model.count == 3)
}

func testreduceSynchronous() {
let store = CodableStore(model: TestModel(), diffing: .sync)
store.reduceSynchronous { $0.count = 3 }
XCTAssert(store.modelStorage.count == 3)
XCTAssert(store.model.count == 3)
}

static var allTests = [
Expand Down

0 comments on commit 34bf59e

Please sign in to comment.