diff --git a/Redux-ReactiveSwift/Classes/Dispatcher.swift b/Redux-ReactiveSwift/Classes/Dispatcher.swift new file mode 100644 index 0000000..fce2692 --- /dev/null +++ b/Redux-ReactiveSwift/Classes/Dispatcher.swift @@ -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) -> Signal? { + let pipe = Signal.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) {} + public func unsafeValue() -> Signal? { return nil } +} diff --git a/Redux-ReactiveSwift/Classes/JSONFilePersister.swift b/Redux-ReactiveSwift/Classes/JSONFilePersister.swift new file mode 100644 index 0000000..58899fa --- /dev/null +++ b/Redux-ReactiveSwift/Classes/JSONFilePersister.swift @@ -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: 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? { + 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() + } + } +} diff --git a/Redux-ReactiveSwift/Classes/Logger.swift b/Redux-ReactiveSwift/Classes/Logger.swift new file mode 100644 index 0000000..e5cf5c8 --- /dev/null +++ b/Redux-ReactiveSwift/Classes/Logger.swift @@ -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) -> Signal? { + 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) { + if flags.contains(.logStates) { log(format(eventOrState: state, flags: [.logStates])) } + } + + // MARK: Protocol stubs unused for this middleware + public func unsafeValue() -> Signal? { 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(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(eventOrState: T) -> String { + guard let s = eventOrState as? CustomStringConvertible else { return "\(eventOrState)" } + return s.description + } +} diff --git a/Redux-ReactiveSwift/Classes/Persister.swift b/Redux-ReactiveSwift/Classes/Persister.swift new file mode 100644 index 0000000..9170f5f --- /dev/null +++ b/Redux-ReactiveSwift/Classes/Persister.swift @@ -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) { + 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) -> Signal? { return nil } +} diff --git a/Redux-ReactiveSwift/Classes/Store.swift b/Redux-ReactiveSwift/Classes/Store.swift index ab52357..ff52fca 100644 --- a/Redux-ReactiveSwift/Classes/Store.swift +++ b/Redux-ReactiveSwift/Classes/Store.swift @@ -22,15 +22,48 @@ open class Store { fileprivate var innerProperty: MutableProperty fileprivate var reducers: [Reducer] + fileprivate var middlewares: [StoreMiddleware] = [] public required init(state: State, reducers: [Reducer]) { self.innerProperty = MutableProperty(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 { diff --git a/Redux-ReactiveSwift/Classes/StoreBuilder.swift b/Redux-ReactiveSwift/Classes/StoreBuilder.swift new file mode 100644 index 0000000..72dc974 --- /dev/null +++ b/Redux-ReactiveSwift/Classes/StoreBuilder.swift @@ -0,0 +1,87 @@ +// +// StoreBuilder.swift +// Redux-ReactiveSwift +// +// Created by Petro Korienev on 1/1/18. +// + +import Foundation +import ReactiveSwift + +open class StoreBuilder> { + + 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.defaultPersisterURL(), + writerQueue: DispatchQueue = DispatchQueue(label: "Redux-ReactiveSwift.JSONFilePersister")) -> Self { + middlewares.append(JSONFilePersister(url: url, writerQueue: writerQueue)); return self + } +} diff --git a/Redux-ReactiveSwift/Classes/StoreMiddleware.swift b/Redux-ReactiveSwift/Classes/StoreMiddleware.swift new file mode 100644 index 0000000..7197a38 --- /dev/null +++ b/Redux-ReactiveSwift/Classes/StoreMiddleware.swift @@ -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) -> Signal? + func stateDidChange(state: State) + func unsafeValue() -> Signal? +} diff --git a/Redux-ReactiveSwift/Redux-ReactiveSwift.xcodeproj/project.pbxproj b/Redux-ReactiveSwift/Redux-ReactiveSwift.xcodeproj/project.pbxproj index ed9638d..afe4ddd 100644 --- a/Redux-ReactiveSwift/Redux-ReactiveSwift.xcodeproj/project.pbxproj +++ b/Redux-ReactiveSwift/Redux-ReactiveSwift.xcodeproj/project.pbxproj @@ -9,6 +9,24 @@ /* Begin PBXBuildFile section */ 023BC7D2C86D2C51DA5B6A3F /* Pods_CocoaPods_Redux_ReactiveSwift_macOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29196EB9593C0E834768F018 /* Pods_CocoaPods_Redux_ReactiveSwift_macOS.framework */; }; 12647EBE419B7D423332663F /* Pods_CocoaPods_Redux_ReactiveSwift_iOSTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F2E76FEE2C25B4A7C5774A40 /* Pods_CocoaPods_Redux_ReactiveSwift_iOSTests.framework */; }; + 4BBF000A1FFAE2D800FA8377 /* StoreBuilderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF00091FFAE2D800FA8377 /* StoreBuilderSpec.swift */; }; + 4BBF000B1FFAE2D800FA8377 /* StoreBuilderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF00091FFAE2D800FA8377 /* StoreBuilderSpec.swift */; }; + 4BC6AFDA1FFA9F050057EA7F /* JSONFilePersister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFD91FFA9F050057EA7F /* JSONFilePersister.swift */; }; + 4BC6AFDB1FFA9F050057EA7F /* JSONFilePersister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFD91FFA9F050057EA7F /* JSONFilePersister.swift */; }; + 4BC6AFDC1FFA9F050057EA7F /* JSONFilePersister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFD91FFA9F050057EA7F /* JSONFilePersister.swift */; }; + 4BC6AFDD1FFA9F050057EA7F /* JSONFilePersister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFD91FFA9F050057EA7F /* JSONFilePersister.swift */; }; + 4BC6AFDE1FFA9F050057EA7F /* JSONFilePersister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFD91FFA9F050057EA7F /* JSONFilePersister.swift */; }; + 4BC6AFDF1FFA9F050057EA7F /* JSONFilePersister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFD91FFA9F050057EA7F /* JSONFilePersister.swift */; }; + 4BC6AFE01FFA9F050057EA7F /* JSONFilePersister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFD91FFA9F050057EA7F /* JSONFilePersister.swift */; }; + 4BC6AFE11FFA9F050057EA7F /* JSONFilePersister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFD91FFA9F050057EA7F /* JSONFilePersister.swift */; }; + 4BC6AFE31FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFE21FFAB8AA0057EA7F /* StoreBuilder.swift */; }; + 4BC6AFE41FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFE21FFAB8AA0057EA7F /* StoreBuilder.swift */; }; + 4BC6AFE51FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFE21FFAB8AA0057EA7F /* StoreBuilder.swift */; }; + 4BC6AFE61FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFE21FFAB8AA0057EA7F /* StoreBuilder.swift */; }; + 4BC6AFE71FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFE21FFAB8AA0057EA7F /* StoreBuilder.swift */; }; + 4BC6AFE81FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFE21FFAB8AA0057EA7F /* StoreBuilder.swift */; }; + 4BC6AFE91FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFE21FFAB8AA0057EA7F /* StoreBuilder.swift */; }; + 4BC6AFEA1FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6AFE21FFAB8AA0057EA7F /* StoreBuilder.swift */; }; 4BD3B3591F9691420077E8A5 /* CocoaPods_Redux_ReactiveSwift_iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 4BD3B3571F9691420077E8A5 /* CocoaPods_Redux_ReactiveSwift_iOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4BD3B3661F9692090077E8A5 /* CocoaPods_Redux_ReactiveSwift_watchOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 4BD3B3641F9692090077E8A5 /* CocoaPods_Redux_ReactiveSwift_watchOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4BD3B3731F9692630077E8A5 /* CocoaPods_Redux_ReactiveSwift_tvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 4BD3B3711F9692630077E8A5 /* CocoaPods_Redux_ReactiveSwift_tvOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -44,6 +62,38 @@ 4BD3B4101F96A6A30077E8A5 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BD3B40D1F96A6A30077E8A5 /* Nimble.framework */; }; 4BD3B4111F96A6A30077E8A5 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BD3B40E1F96A6A30077E8A5 /* Quick.framework */; }; 4BD3B4121F96A6AC0077E8A5 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BD3B3F11F96A2DA0077E8A5 /* Result.framework */; }; + 4BD82C831FFA4291007055E5 /* StoreMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C821FFA4291007055E5 /* StoreMiddleware.swift */; }; + 4BD82C841FFA4291007055E5 /* StoreMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C821FFA4291007055E5 /* StoreMiddleware.swift */; }; + 4BD82C851FFA4291007055E5 /* StoreMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C821FFA4291007055E5 /* StoreMiddleware.swift */; }; + 4BD82C861FFA4291007055E5 /* StoreMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C821FFA4291007055E5 /* StoreMiddleware.swift */; }; + 4BD82C871FFA4291007055E5 /* StoreMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C821FFA4291007055E5 /* StoreMiddleware.swift */; }; + 4BD82C881FFA4291007055E5 /* StoreMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C821FFA4291007055E5 /* StoreMiddleware.swift */; }; + 4BD82C891FFA4291007055E5 /* StoreMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C821FFA4291007055E5 /* StoreMiddleware.swift */; }; + 4BD82C8A1FFA4291007055E5 /* StoreMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C821FFA4291007055E5 /* StoreMiddleware.swift */; }; + 4BD82C8C1FFA779B007055E5 /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C8B1FFA779B007055E5 /* Dispatcher.swift */; }; + 4BD82C8D1FFA779B007055E5 /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C8B1FFA779B007055E5 /* Dispatcher.swift */; }; + 4BD82C8E1FFA779B007055E5 /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C8B1FFA779B007055E5 /* Dispatcher.swift */; }; + 4BD82C8F1FFA779B007055E5 /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C8B1FFA779B007055E5 /* Dispatcher.swift */; }; + 4BD82C901FFA779B007055E5 /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C8B1FFA779B007055E5 /* Dispatcher.swift */; }; + 4BD82C911FFA779B007055E5 /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C8B1FFA779B007055E5 /* Dispatcher.swift */; }; + 4BD82C921FFA779B007055E5 /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C8B1FFA779B007055E5 /* Dispatcher.swift */; }; + 4BD82C931FFA779B007055E5 /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C8B1FFA779B007055E5 /* Dispatcher.swift */; }; + 4BD82C951FFA7AED007055E5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C941FFA7AED007055E5 /* Logger.swift */; }; + 4BD82C961FFA7AED007055E5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C941FFA7AED007055E5 /* Logger.swift */; }; + 4BD82C971FFA7AED007055E5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C941FFA7AED007055E5 /* Logger.swift */; }; + 4BD82C981FFA7AED007055E5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C941FFA7AED007055E5 /* Logger.swift */; }; + 4BD82C991FFA7AED007055E5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C941FFA7AED007055E5 /* Logger.swift */; }; + 4BD82C9A1FFA7AED007055E5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C941FFA7AED007055E5 /* Logger.swift */; }; + 4BD82C9B1FFA7AED007055E5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C941FFA7AED007055E5 /* Logger.swift */; }; + 4BD82C9C1FFA7AED007055E5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C941FFA7AED007055E5 /* Logger.swift */; }; + 4BD82C9E1FFA8A1B007055E5 /* Persister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C9D1FFA8A1B007055E5 /* Persister.swift */; }; + 4BD82C9F1FFA8A1B007055E5 /* Persister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C9D1FFA8A1B007055E5 /* Persister.swift */; }; + 4BD82CA01FFA8A1B007055E5 /* Persister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C9D1FFA8A1B007055E5 /* Persister.swift */; }; + 4BD82CA11FFA8A1B007055E5 /* Persister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C9D1FFA8A1B007055E5 /* Persister.swift */; }; + 4BD82CA21FFA8A1B007055E5 /* Persister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C9D1FFA8A1B007055E5 /* Persister.swift */; }; + 4BD82CA31FFA8A1B007055E5 /* Persister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C9D1FFA8A1B007055E5 /* Persister.swift */; }; + 4BD82CA41FFA8A1B007055E5 /* Persister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C9D1FFA8A1B007055E5 /* Persister.swift */; }; + 4BD82CA51FFA8A1B007055E5 /* Persister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD82C9D1FFA8A1B007055E5 /* Persister.swift */; }; 4FCB578E67F9D1EBE3B3C6EA /* Pods_CocoaPods_Redux_ReactiveSwift_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCFC9CB6AD3428B8821D1EDD /* Pods_CocoaPods_Redux_ReactiveSwift_tvOS.framework */; }; AEB2827B2F7E4517E511F7DC /* Pods_CocoaPods_Redux_ReactiveSwift_watchOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CA0E20721D6E20F17D04FB3D /* Pods_CocoaPods_Redux_ReactiveSwift_watchOS.framework */; }; EB63CF763B3AF97417923595 /* Pods_CocoaPods_Redux_ReactiveSwift_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AE25E5AB988651ADB2B640D /* Pods_CocoaPods_Redux_ReactiveSwift_iOS.framework */; }; @@ -72,6 +122,9 @@ 385806C8F60FEB4ED4845D9C /* Pods-CocoaPods-Redux-ReactiveSwift-iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CocoaPods-Redux-ReactiveSwift-iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CocoaPods-Redux-ReactiveSwift-iOS/Pods-CocoaPods-Redux-ReactiveSwift-iOS.debug.xcconfig"; sourceTree = ""; }; 38FA98D88138A0868A6E0408 /* Pods-CocoaPods-Redux-ReactiveSwift-iOSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CocoaPods-Redux-ReactiveSwift-iOSTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CocoaPods-Redux-ReactiveSwift-iOSTests/Pods-CocoaPods-Redux-ReactiveSwift-iOSTests.debug.xcconfig"; sourceTree = ""; }; 4400C9971548681461EE22FD /* Pods-CocoaPods-Redux-ReactiveSwift-macOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CocoaPods-Redux-ReactiveSwift-macOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CocoaPods-Redux-ReactiveSwift-macOS/Pods-CocoaPods-Redux-ReactiveSwift-macOS.debug.xcconfig"; sourceTree = ""; }; + 4BBF00091FFAE2D800FA8377 /* StoreBuilderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreBuilderSpec.swift; sourceTree = ""; }; + 4BC6AFD91FFA9F050057EA7F /* JSONFilePersister.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONFilePersister.swift; sourceTree = ""; }; + 4BC6AFE21FFAB8AA0057EA7F /* StoreBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreBuilder.swift; sourceTree = ""; }; 4BD3B3541F9691420077E8A5 /* Redux_ReactiveSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Redux_ReactiveSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4BD3B3571F9691420077E8A5 /* CocoaPods_Redux_ReactiveSwift_iOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CocoaPods_Redux_ReactiveSwift_iOS.h; sourceTree = ""; }; 4BD3B3581F9691420077E8A5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -114,6 +167,10 @@ 4BD3B40C1F96A6A30077E8A5 /* ReactiveCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveCocoa.framework; path = Carthage/Build/iOS/ReactiveCocoa.framework; sourceTree = ""; }; 4BD3B40D1F96A6A30077E8A5 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = ""; }; 4BD3B40E1F96A6A30077E8A5 /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quick.framework; path = Carthage/Build/iOS/Quick.framework; sourceTree = ""; }; + 4BD82C821FFA4291007055E5 /* StoreMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreMiddleware.swift; sourceTree = ""; }; + 4BD82C8B1FFA779B007055E5 /* Dispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dispatcher.swift; sourceTree = ""; }; + 4BD82C941FFA7AED007055E5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 4BD82C9D1FFA8A1B007055E5 /* Persister.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persister.swift; sourceTree = ""; }; 5A8CA91B58E04ACE518C5552 /* Pods-CocoaPods-Redux-ReactiveSwift-macOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CocoaPods-Redux-ReactiveSwift-macOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-CocoaPods-Redux-ReactiveSwift-macOS/Pods-CocoaPods-Redux-ReactiveSwift-macOS.release.xcconfig"; sourceTree = ""; }; 711321921BB197A13E05FE25 /* Pods-CocoaPods-Redux-ReactiveSwift-watchOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CocoaPods-Redux-ReactiveSwift-watchOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-CocoaPods-Redux-ReactiveSwift-watchOS/Pods-CocoaPods-Redux-ReactiveSwift-watchOS.release.xcconfig"; sourceTree = ""; }; 7AE25E5AB988651ADB2B640D /* Pods_CocoaPods_Redux_ReactiveSwift_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CocoaPods_Redux_ReactiveSwift_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -306,6 +363,12 @@ isa = PBXGroup; children = ( 4BD3B3951F96958D0077E8A5 /* Store.swift */, + 4BD82C821FFA4291007055E5 /* StoreMiddleware.swift */, + 4BD82C8B1FFA779B007055E5 /* Dispatcher.swift */, + 4BD82C941FFA7AED007055E5 /* Logger.swift */, + 4BD82C9D1FFA8A1B007055E5 /* Persister.swift */, + 4BC6AFD91FFA9F050057EA7F /* JSONFilePersister.swift */, + 4BC6AFE21FFAB8AA0057EA7F /* StoreBuilder.swift */, ); path = Classes; sourceTree = ""; @@ -315,6 +378,7 @@ children = ( 4BD3B39C1F9695B80077E8A5 /* CallSpy.swift */, 4BD3B39E1F9695B80077E8A5 /* StoreSpec.swift */, + 4BBF00091FFAE2D800FA8377 /* StoreBuilderSpec.swift */, ); path = Tests; sourceTree = ""; @@ -1038,7 +1102,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4BD82C9E1FFA8A1B007055E5 /* Persister.swift in Sources */, + 4BC6AFE31FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */, 4BD3B3961F96959A0077E8A5 /* Store.swift in Sources */, + 4BD82C831FFA4291007055E5 /* StoreMiddleware.swift in Sources */, + 4BD82C8C1FFA779B007055E5 /* Dispatcher.swift in Sources */, + 4BD82C951FFA7AED007055E5 /* Logger.swift in Sources */, + 4BC6AFDA1FFA9F050057EA7F /* JSONFilePersister.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1046,7 +1116,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4BD82C9F1FFA8A1B007055E5 /* Persister.swift in Sources */, + 4BC6AFE41FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */, 4BD3B3971F96959B0077E8A5 /* Store.swift in Sources */, + 4BD82C841FFA4291007055E5 /* StoreMiddleware.swift in Sources */, + 4BD82C8D1FFA779B007055E5 /* Dispatcher.swift in Sources */, + 4BD82C961FFA7AED007055E5 /* Logger.swift in Sources */, + 4BC6AFDB1FFA9F050057EA7F /* JSONFilePersister.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1054,7 +1130,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4BD82CA01FFA8A1B007055E5 /* Persister.swift in Sources */, + 4BC6AFE51FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */, 4BD3B3981F96959C0077E8A5 /* Store.swift in Sources */, + 4BD82C851FFA4291007055E5 /* StoreMiddleware.swift in Sources */, + 4BD82C8E1FFA779B007055E5 /* Dispatcher.swift in Sources */, + 4BD82C971FFA7AED007055E5 /* Logger.swift in Sources */, + 4BC6AFDC1FFA9F050057EA7F /* JSONFilePersister.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1062,7 +1144,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4BD82CA11FFA8A1B007055E5 /* Persister.swift in Sources */, + 4BC6AFE61FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */, 4BD3B3991F96959C0077E8A5 /* Store.swift in Sources */, + 4BD82C861FFA4291007055E5 /* StoreMiddleware.swift in Sources */, + 4BD82C8F1FFA779B007055E5 /* Dispatcher.swift in Sources */, + 4BD82C981FFA7AED007055E5 /* Logger.swift in Sources */, + 4BC6AFDD1FFA9F050057EA7F /* JSONFilePersister.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1072,6 +1160,7 @@ files = ( 4BD3B39F1F9695BF0077E8A5 /* CallSpy.swift in Sources */, 4BD3B3A01F9695C20077E8A5 /* StoreSpec.swift in Sources */, + 4BBF000A1FFAE2D800FA8377 /* StoreBuilderSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1079,7 +1168,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4BD82CA21FFA8A1B007055E5 /* Persister.swift in Sources */, + 4BC6AFE71FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */, 4BD3B4021F96A3690077E8A5 /* Store.swift in Sources */, + 4BD82C871FFA4291007055E5 /* StoreMiddleware.swift in Sources */, + 4BD82C901FFA779B007055E5 /* Dispatcher.swift in Sources */, + 4BD82C991FFA7AED007055E5 /* Logger.swift in Sources */, + 4BC6AFDE1FFA9F050057EA7F /* JSONFilePersister.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1087,7 +1182,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4BD82CA31FFA8A1B007055E5 /* Persister.swift in Sources */, + 4BC6AFE81FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */, 4BD3B4031F96A3690077E8A5 /* Store.swift in Sources */, + 4BD82C881FFA4291007055E5 /* StoreMiddleware.swift in Sources */, + 4BD82C911FFA779B007055E5 /* Dispatcher.swift in Sources */, + 4BD82C9A1FFA7AED007055E5 /* Logger.swift in Sources */, + 4BC6AFDF1FFA9F050057EA7F /* JSONFilePersister.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1095,7 +1196,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4BD82CA41FFA8A1B007055E5 /* Persister.swift in Sources */, + 4BC6AFE91FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */, 4BD3B4041F96A36A0077E8A5 /* Store.swift in Sources */, + 4BD82C891FFA4291007055E5 /* StoreMiddleware.swift in Sources */, + 4BD82C921FFA779B007055E5 /* Dispatcher.swift in Sources */, + 4BD82C9B1FFA7AED007055E5 /* Logger.swift in Sources */, + 4BC6AFE01FFA9F050057EA7F /* JSONFilePersister.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1103,7 +1210,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4BD82CA51FFA8A1B007055E5 /* Persister.swift in Sources */, + 4BC6AFEA1FFAB8AA0057EA7F /* StoreBuilder.swift in Sources */, 4BD3B4051F96A36B0077E8A5 /* Store.swift in Sources */, + 4BD82C8A1FFA4291007055E5 /* StoreMiddleware.swift in Sources */, + 4BD82C931FFA779B007055E5 /* Dispatcher.swift in Sources */, + 4BD82C9C1FFA7AED007055E5 /* Logger.swift in Sources */, + 4BC6AFE11FFA9F050057EA7F /* JSONFilePersister.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1113,6 +1226,7 @@ files = ( 4BD3B3E61F969DD30077E8A5 /* CallSpy.swift in Sources */, 4BD3B3E71F969DD30077E8A5 /* StoreSpec.swift in Sources */, + 4BBF000B1FFAE2D800FA8377 /* StoreBuilderSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Redux-ReactiveSwift/Tests/StoreBuilderSpec.swift b/Redux-ReactiveSwift/Tests/StoreBuilderSpec.swift new file mode 100644 index 0000000..8e84254 --- /dev/null +++ b/Redux-ReactiveSwift/Tests/StoreBuilderSpec.swift @@ -0,0 +1,55 @@ +// +// StoreBuilderSpec.swift +// Redux-ReactiveSwift +// +// Created by Petro Korienev on 1/1/18. +// + +import Quick +import Nimble +import ReactiveSwift +import Redux_ReactiveSwift + +class StoreBuilderSpec: QuickSpec { + override func spec() { + struct AppState: Defaultable { let some: String; static var defaultValue = AppState(some: "default")} + enum AppEvent { case some } + func appReducer(state: AppState, event: AppEvent) -> AppState { return state } + + describe("Core concepts") { + it("should pass initialization with state") { + let s = StoreBuilder> + .init(state: AppState(some: "s")) + .build() + expect(s.value.some) == "s" + } + it("should pass defaultable initialization") { + let s = StoreBuilder>() + .build() + expect(s.value.some) == "default" + } + it("should correctly perform verbose build") { + let s = StoreBuilder>() + .verboseBuild() + expect(s.store.value.some) == "default" + expect(s.middlewares.count) == 0 + expect(s.reducers.count) == 0 + } + it("should build store with reducers") { + let spy = CallSpy.makeCallSpy(f2: appReducer) + let s = StoreBuilder>() + .reducer(spy.1) + .reducer(spy.1) + .reducer(spy.1) + .verboseBuild() + s.store.consume(event: .some) + expect(s.reducers.count) == 3 + expect(spy.0.callCount) == 3 + } + } + + describe("DSL tests") { + + } + } +}