Skip to content

Add mixin-like approach of connecting custom middleware to Store #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions Redux-ReactiveSwift/Classes/Dispatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// Dispatcher.swift
// Redux-ReactiveSwift
//
// Created by Petro Korienev on 1/1/18.
//

import Foundation
import ReactiveSwift
import Result

public class Dispatcher: StoreMiddleware {

private let scheduler: Scheduler

public init(queue: DispatchQueue,
qos: DispatchQoS,
name: String) {
scheduler = QueueScheduler(qos: qos, name: name, targeting: queue)
}

public init(scheduler: Scheduler) {
self.scheduler = scheduler
}

public func consume<Event>(event: Event) -> Signal<Event, NoError>? {
let pipe = Signal<Event, NoError>.pipe()
defer { pipe.input.send(value: event) }
return pipe.output.observe(on: scheduler)
}

// MARK: Protocol stubs unused for this middleware
public func stateDidChange<State>(state: State) {}
public func unsafeValue() -> Signal<Any, NoError>? { return nil }
}
43 changes: 43 additions & 0 deletions Redux-ReactiveSwift/Classes/JSONFilePersister.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// JSONFilePersister.swift
// Redux-ReactiveSwift
//
// Created by Petro Korienev on 1/1/18.
//

import Foundation
import ReactiveSwift
import Result

public class JSONFilePersister<State: Persistable>: Persister {

let url: URL
let writerQueue: DispatchQueue

init(url: URL, writerQueue: DispatchQueue) {
self.url = url;
self.writerQueue = writerQueue
}

public func persist(dictionary: [String : Any]) {
writerQueue.async { [url] in
guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { return }
try? data.write(to: url)
}
}

public func restore() -> [String : Any]? {
guard let data = try? Data(contentsOf: url) else { return nil }
guard let object = try? JSONSerialization.jsonObject(with: data) else { return nil }
return object as? [String : Any]
}

public func unsafeValue() -> Signal<Any, NoError>? {
guard let restored = restore() else { return nil }
guard let deserialized = State.deserialize(from: restored) else { return nil }
return Signal { observer, _ in
observer.send(value: deserialized)
observer.sendCompleted()
}
}
}
67 changes: 67 additions & 0 deletions Redux-ReactiveSwift/Classes/Logger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// Logger.swift
// Redux-ReactiveSwift
//
// Created by Petro Korienev on 1/1/18.
//

import Foundation
import ReactiveSwift
import Result

public struct LoggerFlags: OptionSet {
public let rawValue: Int

public static let logEvents = LoggerFlags(rawValue: 1 << 0)
public static let logStates = LoggerFlags(rawValue: 1 << 1)

public static let logAll: LoggerFlags = [.logEvents, .logStates]

public init(rawValue: Int) { self.rawValue = rawValue }
}

public class Logger: StoreMiddleware {

private let log: (String) -> ()
private let flags: LoggerFlags
private let name: String?

public init(log: @escaping (String) -> (), flags: LoggerFlags = .logAll, name: String? = nil) {
self.log = log
self.flags = flags
self.name = name
}

public func consume<Event>(event: Event) -> Signal<Event, NoError>? {
if flags.contains(.logEvents) { log(format(eventOrState: event, flags: [.logEvents])) }
return Signal { observer, _ in
observer.send(value: event)
observer.sendCompleted()
}
}

public func stateDidChange<State>(state: State) {
if flags.contains(.logStates) { log(format(eventOrState: state, flags: [.logStates])) }
}

// MARK: Protocol stubs unused for this middleware
public func unsafeValue() -> Signal<Any, NoError>? { return nil }

// MARK: Private formatting stuff
private func name(_ string: String) -> String {
guard let name = name else { return string }
return "\(name): \(string)"
}

private func format<T>(eventOrState: T, flags: LoggerFlags) -> String {
let valueString = toString(eventOrState: eventOrState)
if flags.contains(.logEvents) { return "Consumed event: \(valueString)" }
if flags.contains(.logStates) { return "Switched state: \(valueString)" }
fatalError("format is called with wrong flags set")
}

private func toString<T>(eventOrState: T) -> String {
guard let s = eventOrState as? CustomStringConvertible else { return "\(eventOrState)" }
return s.description
}
}
43 changes: 43 additions & 0 deletions Redux-ReactiveSwift/Classes/Persister.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// Persister.swift
// Redux-ReactiveSwift
//
// Created by Petro Korienev on 1/1/18.
//

import Foundation
import ReactiveSwift
import Result

public protocol Serializable {
func serialize() -> [String: Any]
static func deserialize(`from` dictionary: [String: Any]) -> Self?
}

public protocol Persistable: Serializable {
func shouldPersist() -> Bool
}

public protocol Persister: StoreMiddleware {
func persist(dictionary: [String: Any])
func restore() -> [String: Any]?
}

extension Persister {
public func stateDidChange<State>(state: State) {
guard let persistable = state as? Persistable else {
fatalError("\(String(describing: State.self)) is asked to be persisted by \(self) middleware but it's not of Persistable type ") }
guard persistable.shouldPersist() else { return }
persist(dictionary: persistable.serialize())
}

public static func defaultPersisterURL() -> URL {
guard let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first else {
fatalError("Cannot find requested default caches folder path")
}
return URL(fileURLWithPath: cachePath.appending("Redux-ReactiveSwift.\(String(describing: Self.self)).json"))
}

// MARK: Protocol stubs unused for this middleware
public func consume<Event>(event: Event) -> Signal<Event, NoError>? { return nil }
}
33 changes: 33 additions & 0 deletions Redux-ReactiveSwift/Classes/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,48 @@ open class Store<State, Event> {

fileprivate var innerProperty: MutableProperty<State>
fileprivate var reducers: [Reducer]
fileprivate var middlewares: [StoreMiddleware] = []

public required init(state: State, reducers: [Reducer]) {
self.innerProperty = MutableProperty<State>(state)
self.reducers = reducers
}

public func applyMiddlewares(_ middlewares: [StoreMiddleware]) -> Self {
guard self.middlewares.count == 0 else { fatalError("Applying middlewares more than once is yet unsupported") }
self.middlewares = middlewares
self.middlewares.forEach(self.register(middleware:))
return self
}

public func consume(event: Event) {
consume(event: event, with: middlewares)
}

private func consume(event: Event, with middlewares: [StoreMiddleware]) {
guard middlewares.count > 0 else { return undecoratedConsume(event: event) }
let slicedMiddlewares = Array(middlewares.dropFirst())
if let signal = middlewares.first?.consume(event: event)?.take(first: 1) {
signal.observeValues { [weak self] value in self?.consume(event: event, with: slicedMiddlewares) }
}
else {
self.consume(event: event, with: slicedMiddlewares)
}
}

public func undecoratedConsume(event: Event) {
self.innerProperty.value = reducers.reduce(self.innerProperty.value) { $1($0, event) }
}

private func register(middleware: StoreMiddleware) {
self.innerProperty.signal.observeValues { middleware.stateDidChange(state: $0) }
middleware.unsafeValue()?.observeValues { [weak self] value in
guard let safeValue = value as? State else {
fatalError("Store got \(value) from unsafeValue() signal which is not of \(String(describing:State.self)) type")
}
self?.innerProperty.value = safeValue
}
}
}

extension Store: PropertyProtocol {
Expand Down
87 changes: 87 additions & 0 deletions Redux-ReactiveSwift/Classes/StoreBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// StoreBuilder.swift
// Redux-ReactiveSwift
//
// Created by Petro Korienev on 1/1/18.
//

import Foundation
import ReactiveSwift

open class StoreBuilder<StateType, EventType, StoreType: Store<StateType, EventType>> {

fileprivate var initialState: StateType
fileprivate var reducers: [StoreType.Reducer] = []
fileprivate var middlewares: [StoreMiddleware] = []

public init(state: StateType) {
initialState = state
}

public func build() -> StoreType {
return StoreType(state: initialState, reducers: reducers).applyMiddlewares(middlewares)
}

public func verboseBuild()
-> (store: StoreType, middlewares: [StoreMiddleware], reducers: [StoreType.Reducer]) {
return (store: build(), middlewares: middlewares, reducers: reducers)
}
}

public extension StoreBuilder where StateType: Defaultable {
convenience init() {
self.init(state: StateType.defaultValue)
}
}

// MARK: Builder DSL

typealias MiddlewareBuilder = StoreBuilder
public extension MiddlewareBuilder {
public func middleware(_ middleware: StoreMiddleware) -> Self {
middlewares.append(middleware); return self
}
}

typealias ReducerBuilder = StoreBuilder
public extension ReducerBuilder {
public func reducer(_ reducer: @escaping (StateType, EventType) -> (StateType)) -> Self {
reducers.append(reducer); return self
}
}

typealias LoggerBuilder = StoreBuilder
public extension LoggerBuilder {
public func logger(log: @escaping (String) -> (), flags: LoggerFlags = .logAll, name: String? = nil) -> Self {
middlewares.append(Logger(log: log, flags: flags, name: name)); return self
}
public func nslogger(flags: LoggerFlags = .logAll, name: String? = "Redux-ReactiveSwift-NSLogger") -> Self {
middlewares.append(Logger(log: { NSLog($0) }, flags: flags, name: name)); return self
}
public func nsloggerDebug(flags: LoggerFlags = .logAll, name: String? = "Redux-ReactiveSwift-NSLogger-Debug") -> Self {
#if DEBUG
middlewares.append(Logger(log: { NSLog($0) }, flags: flags, name: name))
#endif
return self
}
}

typealias DispatcherBuilder = StoreBuilder
public extension DispatcherBuilder {
public func dispatcher(queue: DispatchQueue = DispatchQueue(label:"Redux-ReactiveSwift.Dispatcher"),
qos: DispatchQoS = .default,
name: String = "Redux-ReactiveSwift.Dispatcher") -> Self {
middlewares.append(Dispatcher(queue: queue, qos: qos, name: name)); return self
}
public func dispatcher(scheduler: Scheduler) -> Self {
middlewares.append(Dispatcher(scheduler: scheduler)); return self
}
}

typealias PersisterBuilder = StoreBuilder
public extension PersisterBuilder where StateType: Persistable {
public func jsonFilePersister(url: URL = JSONFilePersister<StateType>.defaultPersisterURL(),
writerQueue: DispatchQueue = DispatchQueue(label: "Redux-ReactiveSwift.JSONFilePersister")) -> Self {
middlewares.append(JSONFilePersister<StateType>(url: url, writerQueue: writerQueue)); return self
}
}
16 changes: 16 additions & 0 deletions Redux-ReactiveSwift/Classes/StoreMiddleware.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// StoreMiddleware.swift
// Redux-ReactiveSwift
//
// Created by Petro Korienev on 1/1/18.
//

import Foundation
import ReactiveSwift
import Result

public protocol StoreMiddleware {
func consume<Event>(event: Event) -> Signal<Event, NoError>?
func stateDidChange<State>(state: State)
func unsafeValue() -> Signal<Any, NoError>?
}
Loading