Skip to content

Commit

Permalink
Change Middleware to use Combine
Browse files Browse the repository at this point in the history
  • Loading branch information
Qata committed Nov 9, 2020
1 parent d3faf42 commit f3a0889
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 159 deletions.
11 changes: 11 additions & 0 deletions Sources/RecombinePackage/Action.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// File.swift
//
//
// Created by Lotte Tortorella on 30/10/20.
//

public enum ActionStrata<Raw, Refined> {
case raw(Raw)
case refined(Refined)
}
112 changes: 29 additions & 83 deletions Sources/RecombinePackage/Middleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,123 +6,69 @@
// Copyright © 2019 Charlotte Tortorella. All rights reserved.
//

import Combine

/**
Middleware is a structure that allows you to modify, filter out and create more
actions, before the action being handled reaches the store.
*/
public struct Middleware<State, Action> {
typealias Transform = (State, Action) -> [Action]
internal let transform: Transform
public struct Middleware<State, Input, Output> {
public typealias Function = (Publishers.Output<Published<State>.Publisher>, Input) -> AnyPublisher<Output, Never>
public typealias Transform<Result> = (Publishers.Output<Published<State>.Publisher>, Output) -> Result
internal let transform: Function

/// Create a blank slate Middleware.
private init() {
self.transform = { [$1] }
public init() where Input == Output {
self.transform = { Just($1).eraseToAnyPublisher() }
}

/**
Initialises the middleware with a transformative function.

- parameter transform: The function that will be able to modify passed actions.
*/
internal init(_ transform: @escaping Transform) {
private init(_ transform: @escaping Function) {
self.transform = transform
}

/**
Initialises the middleware by concatenating the transformative functions from
the middleware that was passed in.
*/
public init(_ middleware: Self...) {
self = .init(middleware)
}

/**
Initialises the middleware by concatenating the transformative functions from
the middleware that was passed in.
*/
public init<S: Sequence>(_ middleware: S) where S.Element == Middleware {
self = middleware.reduce(.init()) {
$0.concat($1)
}
}


/// Concatenates the transform function of the passed `Middleware` onto the callee's transform.
public func concat(_ other: Middleware) -> Middleware {
.init { state, action in
self.transform(state, action).flatMap {
other.transform(state, $0)
}
}
}

/// Safe encapsulation of side effects guaranteed not to affect the action being passed through the middleware.
public func sideEffect(_ effect: @escaping (State, Action) -> Void) -> Self {
.init { state, action in
self.transform(state, action).map {
effect(state, $0)
return $0
}
}
}

/// Safe encapsulation of side effects guaranteed not to affect the action being passed through the middleware.
public static func sideEffect(_ effect: @escaping (State, Action) -> Void) -> Self {
Middleware().sideEffect(effect)
public func concat<Result>(_ other: Middleware<State, Output, Result>) -> Middleware<State, Input, Result> {
return map(other.transform)
}

/// Transform the action into another action.
public func map(_ transform: @escaping (State, Action) -> Action) -> Self {
.init { state, action in
self.transform(state, action).map {
transform(state, $0)
}
}
}

/// Transform the action into another action.
public static func map(_ transform: @escaping (State, Action) -> Action) -> Self {
Middleware().map(transform)
}

/// One to many pattern allowing one action to be turned into multiple.
public func flatMap<S: Sequence>(_ transform: @escaping (State, Action) -> S) -> Self where S.Element == Action {
public func map<Result, P: Publisher>(
_ transform: @escaping Transform<P>
) -> Middleware<State, Input, Result> where P.Output == Result, P.Failure == Never {
.init { state, action in
self.transform(state, action).flatMap {
transform(state, $0)
}
.eraseToAnyPublisher()
}
}

/// One to many pattern allowing one action to be turned into multiple.
public static func flatMap<S: Sequence>(_ transform: @escaping (State, Action) -> S) -> Self where S.Element == Action {
Middleware().flatMap(transform)
}

/// Filters while mapping actions to new actions.
public func filterMap(_ transform: @escaping (State, Action) -> Action?) -> Self {
.init { state, action in
self.transform(state, action).compactMap {
transform(state, $0)
}
}
}

/// Filters while mapping actions to new actions.
public static func filterMap(_ transform: @escaping (State, Action) -> Action?) -> Self {
Middleware().filterMap(transform)
}

/// Drop the action iff `isIncluded(action) != true`.
public func filter(_ isIncluded: @escaping (State, Action) -> Bool) -> Self {
public func filter(_ isIncluded: @escaping Transform<Bool>) -> Self {
.init { state, action in
self.transform(state, action).filter {
isIncluded(state, $0)
}
.eraseToAnyPublisher()
}
}
}

extension Middleware where Input == Output {
/// Transform the action into another action.
public static func map<Result, P: Publisher>(
_ transform: @escaping Transform<P>
) -> Middleware<State, Input, Result> where P.Output == Result, P.Failure == Never {
Middleware<State, Input, Input>().map(transform)
}

/// Drop the action iff `isIncluded(action) != true`.
public static func filter(_ isIncluded: @escaping (State, Action) -> Bool) -> Self {
Middleware().filter(isIncluded)
public static func filter(_ isIncluded: @escaping Transform<Bool>) -> Self {
Middleware<State, Input, Input>().filter(isIncluded)
}
}
12 changes: 7 additions & 5 deletions Sources/RecombinePackage/Reducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public protocol Reducer {
var transform: Transform { get }
init()
init(_ transform: Transform)
func reduce(state: State, actions: [Action]) -> State
func reduce(state: State, action: Action) -> State
func concat<R: Reducer>(_ other: R) -> Self where R.Transform == Transform
}

Expand Down Expand Up @@ -52,8 +52,8 @@ public struct PureReducer<State, Action>: Reducer {
}
}

public func reduce(state: State, actions: [Action]) -> State {
actions.reduce(state, transform)
public func reduce(state: State, action: Action) -> State {
transform(state, action)
}
}

Expand All @@ -80,7 +80,9 @@ public struct MutatingReducer<State, Action>: Reducer {
}
}

public func reduce(state: State, actions: [Action]) -> State {
actions.reduce(into: state, transform)
public func reduce(state: State, action: Action) -> State {
var s = state
transform(&s, action)
return s
}
}
82 changes: 58 additions & 24 deletions Sources/RecombinePackage/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,58 @@

import Combine

public class Store<State, Action>: ObservableObject {
public class Store<State, RawAction, RefinedAction>: ObservableObject {
@Published
public private(set) var state: State
public let actions = PassthroughSubject<Action, Never>()
public let rawActions = PassthroughSubject<RawAction, Never>()
public let refinedActions = PassthroughSubject<RefinedAction, Never>()
private var cancellables = Set<AnyCancellable>()

public required init<S: Scheduler, R: Reducer>(
state: State,
reducer: R,
middleware: Middleware<State, Action> = .init(),
middleware: Middleware<State, RawAction, RefinedAction>,
publishOn scheduler: S
) where R.State == State, R.Action == Action {
) where R.State == State, R.Action == RefinedAction {
self.state = state

actions.scan(state) { state, action in
rawActions.flatMap { [unowned self] action in
middleware.transform(self.$state.prefix(1), action)
}
.subscribe(refinedActions)
.store(in: &cancellables)

refinedActions.scan(state) { state, action in
reducer.reduce(
state: state,
actions: middleware.transform(state, action)
action: action
)
}
.receive(on: scheduler).sink { [unowned self] state in
.receive(on: scheduler)
.sink { [unowned self] state in
self.state = state
}
.store(in: &cancellables)
}

public func lensing<SubState>(_ keyPath: KeyPath<State, SubState>) -> StoreTransform<State, SubState, Action> {
public func lensing<SubState>(_ keyPath: KeyPath<State, SubState>) -> StoreTransform<State, SubState, RawAction, RefinedAction> {
.init(store: self, lensing: keyPath)
}

open func dispatch(_ actions: Action...) {
dispatch(actions)
open func dispatch(refined actions: RefinedAction...) {
dispatch(refined: actions)
}

open func dispatch<S: Sequence>(refined actions: S) where S.Element == RefinedAction {
actions.forEach(self.refinedActions.send)
}

open func dispatch(raw actions: RawAction...) {
dispatch(raw: actions)
}

open func dispatch<S: Sequence>(_ actions: S) where S.Element == Action {
actions.forEach(self.actions.send)
open func dispatch<S: Sequence>(raw actions: S) where S.Element == RawAction {
actions.forEach(self.rawActions.send)
}
}

Expand All @@ -53,22 +69,27 @@ extension Store: Subscriber {
subscription.request(.unlimited)
}

public func receive(_ input: Action) -> Subscribers.Demand {
actions.send(input)
public func receive(_ input: ActionStrata<RawAction, RefinedAction>) -> Subscribers.Demand {
switch input {
case let .raw(action):
rawActions.send(action)
case let .refined(action):
refinedActions.send(action)
}
return .unlimited
}

public func receive(completion: Subscribers.Completion<Never>) {}
}

public class StoreTransform<Underlying, State, Action>: ObservableObject {
public class StoreTransform<Underlying, State, RawAction, RefinedAction>: ObservableObject {
@Published
public private(set) var state: State
private let store: Store<Underlying, Action>
private let store: Store<Underlying, RawAction, RefinedAction>
private let keyPath: KeyPath<Underlying, State>
private var cancellables = Set<AnyCancellable>()

public required init(store: Store<Underlying, Action>, lensing keyPath: KeyPath<Underlying, State>) {
public required init(store: Store<Underlying, RawAction, RefinedAction>, lensing keyPath: KeyPath<Underlying, State>) {
self.store = store
self.keyPath = keyPath
state = store.state[keyPath: keyPath]
Expand All @@ -80,16 +101,24 @@ public class StoreTransform<Underlying, State, Action>: ObservableObject {
.store(in: &cancellables)
}

public func lensing<SubState>(_ keyPath: KeyPath<State, SubState>) -> StoreTransform<Underlying, SubState, Action> {
public func lensing<SubState>(_ keyPath: KeyPath<State, SubState>) -> StoreTransform<Underlying, SubState, RawAction, RefinedAction> {
.init(store: store, lensing: self.keyPath.appending(path: keyPath))
}

open func dispatch(_ actions: Action...) {
store.dispatch(actions)
open func dispatch(refined actions: RefinedAction...) {
store.dispatch(refined: actions)
}

open func dispatch<S: Sequence>(_ actions: S) where S.Element == Action {
store.dispatch(actions)
open func dispatch<S: Sequence>(refined actions: S) where S.Element == RefinedAction {
store.dispatch(refined: actions)
}

open func dispatch(raw actions: RawAction...) {
store.dispatch(raw: actions)
}

open func dispatch<S: Sequence>(unrefined actions: S) where S.Element == RawAction {
store.dispatch(raw: actions)
}
}

Expand All @@ -99,8 +128,13 @@ extension StoreTransform: Subscriber {
subscription.request(.unlimited)
}

public func receive(_ input: Action) -> Subscribers.Demand {
store.dispatch(input)
public func receive(_ input: ActionStrata<RawAction, RefinedAction>) -> Subscribers.Demand {
switch input {
case let .raw(action):
store.dispatch(raw: action)
case let .refined(action):
store.dispatch(refined: action)
}
return .unlimited
}

Expand Down
24 changes: 14 additions & 10 deletions Tests/RecombineTests/MiddlewareFakes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,32 @@
//

import Recombine
import Combine

let firstMiddleware = Middleware<TestStringAppState, SetAction>().map { state, action in
let firstMiddleware = Middleware<TestStringAppState, SetAction, SetAction>.map { state, action -> Just<SetAction> in
switch action {
case let .string(value):
return .string(value + " First Middleware")
return Just(.string(value + " First Middleware"))
default:
return action
return Just(action)
}
}

let secondMiddleware = Middleware<TestStringAppState, SetAction>().map { state, action in
let secondMiddleware = Middleware<TestStringAppState, SetAction, SetAction>.map { state, action -> Just<SetAction> in
switch action {
case let .string(value):
return .string(value + " Second Middleware")
return Just(.string(value + " Second Middleware"))
default:
return action
return Just(action)
}
}

let stateAccessingMiddleware = Middleware<TestStringAppState, SetAction>().map { state, action in
if case let .string(value) = action {
return .string(state.testValue! + state.testValue!)
let stateAccessingMiddleware = Middleware<TestStringAppState, SetAction, SetAction>().map { state, action -> AnyPublisher<SetAction, Never> in
if case let .string(value) = action {
return state.map {
.string($0.testValue! + $0.testValue!)
}
.eraseToAnyPublisher()
}
return action
return Just(action).eraseToAnyPublisher()
}
Loading

0 comments on commit f3a0889

Please sign in to comment.