Skip to content

Commit

Permalink
Add selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
denizcoskun committed Feb 14, 2021
1 parent 763b576 commit 2a6b3a2
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 26 deletions.
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ appStore.dispatch(CounterState.Increment)
```swift

typealias TodosState = Dictionary<Int, Todo>
enum Action: RxStoreAction {
enum Action: RxStore.Action {
case LoadTodos, LoadTodosSuccess([Todo]), LoadTodosFailure
}

Expand Down Expand Up @@ -82,7 +82,36 @@ let cancellable = store.todosState.sink(receiveValue: {state in

store.dispatch(Action.LoadTodos) // This will fetch the todos from the server


```


## Selectors

Selectors allow you to work with combination of different states at the same time.

Below is an example of how a selector can be used:

```swift
let todoList = [mockTodo, mockTodo2]
let userTodoIds: Dictionary<Int, [Int]> = [userId:[mockTodo.id], userId2: [mockTodo2.id]]

class TestStore: RxStore {
var todos = RxStoreSubject(todoList)
var userTodos = RxStoreSubject(userTodoIds)
}

let store = TestStore().initialize()

let getTodosForSelectedUser = { (userId: Int) in
return TestStore.createSelector(path: \.todos, path2: \.userTodos, handler: { todos, userTodoIds -> [Todo] in
let todoIds = userTodoIds[userId] ?? []
let userTodos = todos.filter { todo in todoIds.contains(todo.id) }
return userTodos
})
}

let _ = store.select(getTodosForSelectedUser(userId2)).sink(receiveValue: {userTodos in
print(userTodos) // [mockTodo2]
})

```
51 changes: 34 additions & 17 deletions Sources/RxStore/RxStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,29 @@ final public class RxStoreSubject<T: Equatable & Codable>: Subject {

public protocol RxStoreAction {}

public enum RxStoreActions: RxStoreAction {
case Empty
public protocol RxStoreProtocol : AnyObject {
typealias Action = RxStoreAction
typealias Reducer<T> = (T, Action) -> T
typealias State = Equatable & Codable
typealias ActionSubject = PassthroughSubject<Action, Never>
var actions: PassthroughSubject<Action, Never>{ get }
var stream: AnyPublisher<Action, Never> {get set}
var _anyCancellable: AnyCancellable? {get set}
}

public protocol RxStoreState: Equatable, Codable {}

public protocol RxStoreEffects {
typealias ActionObservable = AnyPublisher<RxStoreAction, Never>
associatedtype Store
typealias Effect = (Store, ActionObservable) -> ActionObservable
public enum RxStoreActions: RxStoreAction {
case Empty
}

public protocol RxStoreProtocol : AnyObject {
typealias ActionSubject = PassthroughSubject<RxStoreAction, Never>
var actions: PassthroughSubject<RxStoreAction, Never>{ get }
var stream: AnyPublisher<RxStoreAction, Never> {get set}
var _anyCancellable: AnyCancellable? {get set}
}

extension RxStoreProtocol {
public typealias ActionObservable = AnyPublisher<RxStoreAction, Never>
public typealias Effect = (Self, ActionObservable) -> ActionObservable
}

extension RxStoreProtocol {
public typealias Reducer<T> = (T, RxStoreAction) -> T

public func registerReducer<T>(for property: KeyPath<Self, RxStoreSubject<T>> , reducer: @escaping (T, RxStoreAction) -> T) -> Self {
self.stream = stream.handleEvents(receiveOutput: { action in
let state = reducer(self[keyPath: property].value, action)
Expand All @@ -90,9 +91,6 @@ extension RxStoreProtocol {
}

extension RxStoreProtocol {
public typealias ActionObservable = AnyPublisher<RxStoreAction, Never>
public typealias Effect = (Self, ActionObservable) -> ActionObservable?

public func registerEffects(_ effects: [Effect] ) -> Self {
self.stream = self.stream
.flatMap({ action in
Expand Down Expand Up @@ -122,6 +120,25 @@ extension RxStoreProtocol {
}


extension RxStoreProtocol {

public typealias Selector<A: State,B: State,C> = (KeyPath<Self,RxStoreSubject<A>>,KeyPath<Self,RxStoreSubject<B>>, @escaping (A,B) -> C) -> (Self) -> AnyPublisher<C, Never>

public static func createSelector<A,B,C>(path: KeyPath<Self,RxStoreSubject<A>>, path2: KeyPath<Self,RxStoreSubject<B>>, handler: @escaping (A,B) -> C) -> (Self) -> AnyPublisher<C, Never> {
func result(store: Self) -> AnyPublisher<C, Never> {
return store.mergeStates(statePath: path, statePath2: path2).map {state, state2 in
handler(state, state2)
}.eraseToAnyPublisher()
}
return result
}

public func select<R>(_ selector: @escaping (Self) -> R) -> R {
return selector(self)
}
}


open class RxStore: RxStoreProtocol {
public var stream: AnyPublisher<RxStoreAction, Never>
public var actions = PassthroughSubject<RxStoreAction, Never>()
Expand Down
29 changes: 29 additions & 0 deletions Tests/RxStoreTests/Mocks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// File.swift
//
//
// Created by Coskun Deniz on 14/02/2021.
//

import Foundation
struct Todo: Codable, Equatable {
let userId: Int
let id: Int
let title: String
let completed: Bool
internal init(userId: Int, id: Int, title: String, completed: Bool) {
self.userId = userId
self.id = id
self.title = title
self.completed = completed
}

}



typealias TodosState = Dictionary<Int, Todo>
let userId = 1
let userId2 = 2
let mockTodo = Todo(userId: userId, id: 123, title: "Todo A", completed: false)
let mockTodo2 = Todo(userId: userId2, id: 444, title: "Todo B", completed: false)
34 changes: 27 additions & 7 deletions Tests/RxStoreTests/RxStoreTests.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import XCTest
import Combine

@testable import RxStore
import RxStore

final class RxStoreTests: XCTestCase {
func testExampleWithCounter() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
enum CounterAction: RxStoreAction {
enum CounterAction: RxStore.Action {
case Increment
case Decrement
}
Expand Down Expand Up @@ -51,7 +51,7 @@ final class RxStoreTests: XCTestCase {
return false
}).initialize()

enum Action: RxStoreAction {
enum Action: RxStore.Action {
case first
}
let _ = store.emptyState.sink(receiveValue: {state in
Expand All @@ -76,10 +76,8 @@ final class RxStoreTests: XCTestCase {

}

enum Action: RxStoreAction, Equatable{
case LoadTodos
case LoadTodosSuccess([Todo])
case LoadTodosFailure
enum Action: RxStore.Action {
case LoadTodos, LoadTodosSuccess([Todo]), LoadTodosFailure
}

typealias TodosState = Dictionary<Int, Todo>
Expand All @@ -99,6 +97,7 @@ final class RxStoreTests: XCTestCase {
return state
}
}

let loadTodos: RxStore.Effect = {state, action in
action.flatMap {action -> RxStore.ActionObservable in
if case Action.LoadTodos = action {
Expand All @@ -124,6 +123,27 @@ final class RxStoreTests: XCTestCase {
})

}

func testSelector() {
class TestStore: RxStore {
var todos = RxStoreSubject([mockTodo, mockTodo2])
var userTodoIds = RxStoreSubject<Dictionary<Int, [Int]>>([userId:[mockTodo.id], userId2: [mockTodo2.id]])
}

let store = TestStore().initialize()

let getTodosForSelectedUser = { (userId: Int) in
return TestStore.createSelector(path: \.todos, path2: \.userTodoIds, handler: { todos, userTodoIds -> [Todo] in
let todoIds = userTodoIds[userId] ?? []
let userTodos = todos.filter { todo in todoIds.contains(todo.id) }
return userTodos
})
}

let _ = store.select(getTodosForSelectedUser(userId2)).sink(receiveValue: {userTodos in
XCTAssertEqual(userTodos, [mockTodo2])
})
}

static var allTests = [
("testExample", testExampleWithCounter),
Expand Down

0 comments on commit 2a6b3a2

Please sign in to comment.