Skip to content
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

Improve the Middleware and Reducer pipeline #52

Merged
merged 5 commits into from
Nov 21, 2020
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ jobs:
mint install apple/swift-format@0.50300.0
- name: Check code formatting
run: |
swift-format -r -m lint Sources
./scripts/lint.sh
- name: Run tests
run: |
swift package generate-xcodeproj --enable-code-coverage
xcodebuild -project SwiftDux.xcodeproj -scheme SwiftDux-Package -destination "${destination}" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO test
xcodebuild -project SwiftDux.xcodeproj -scheme SwiftDux-Package -destination "${destination}" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO test
bash <(curl -s https://codecov.io/bash) -J 'SwiftDux'
env:
destination: ${{ matrix.destination }}
Expand Down
21 changes: 11 additions & 10 deletions Sources/SwiftDux/Action/Action.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Combine
import Foundation

/// A dispatchable action sent to a `Store<_>` to modify the state.
/// A dispatchable action to update the application state.
/// ```
/// enum TodoList : Action {
/// case setItems(items: [TodoItem])
Expand All @@ -19,7 +19,11 @@ extension Action {
/// - Parameter actions: An array of actions to chain together.
/// - Returns: A composite action.
@inlinable public func then(_ actions: [Action]) -> CompositeAction {
CompositeAction([self] + actions)
if var action = self as? CompositeAction {
action.actions += actions
return action
}
return CompositeAction([self] + actions)
}

/// Chains an array of actions to be dispatched next.
Expand All @@ -39,14 +43,6 @@ extension Action {
}
}

@inlinable public func + (lhs: Action, rhs: Action) -> CompositeAction {
if var lhs = lhs as? CompositeAction {
lhs.actions.append(rhs)
return lhs
}
return CompositeAction([lhs, rhs])
}

/// A noop action used by reducers that may not have their own actions.
public struct EmptyAction: Action {

Expand All @@ -57,3 +53,8 @@ public struct EmptyAction: Action {
///
/// - Parameter action: The action to dispatch.
public typealias SendAction = (Action) -> Void

/// A closure that dispatches a cancellable action.
///
/// - Parameter action: The action to dispatch.
public typealias SendCancellableAction = (Action) -> Cancellable
26 changes: 26 additions & 0 deletions Sources/SwiftDux/Action/ActionDispatcherProxy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Combine
import Foundation

/// A concrete `ActionDispatcher` that can acts as a proxy.
public struct ActionDispatcherProxy: ActionDispatcher {
@usableFromInline internal var sendBlock: SendAction
@usableFromInline internal var sendAsCancellableBlock: SendCancellableAction

/// Initiate a new BlockActionDispatcher.
///
/// - Parameters:
/// - send: A closure to dispatch an action.
/// - sendAsCancellable: A closure to dispatch a cancellable action.
public init(send: @escaping SendAction, sendAsCancellable: @escaping SendCancellableAction) {
self.sendBlock = send
self.sendAsCancellableBlock = sendAsCancellable
}

@inlinable public func send(_ action: Action) {
sendBlock(action)
}

@inlinable public func sendAsCancellable(_ action: Action) -> Cancellable {
sendAsCancellableBlock(action)
}
}
31 changes: 13 additions & 18 deletions Sources/SwiftDux/Action/ActionPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,43 +37,38 @@ public struct ActionPlan<State>: RunnableAction {
@usableFromInline
internal var body: Body

/// Create an action plan that returns a publisher.
/// Initiate an action plan that returns a publisher of actions.
///
/// - Parameter body: The body of the action plan.
@inlinable public init<P>(_ body: @escaping (StoreProxy<State>) -> P) where P: Publisher, P.Output == Action, P.Failure == Never {
self.body = { store in body(store).eraseToAnyPublisher() }
}

/// Create a synchronous action plan. The plan expects to be completed once the body has returned.
/// Initiate an asynchronous action plan that completes after the first emitted void value from its publisher.
///
/// Use this method to wrap asynchronous code in a publisher like `Future<Void, Never>`.
/// - Parameter body: The body of the action plan.
@inlinable public init(_ body: @escaping (StoreProxy<State>) -> Void) {
@inlinable public init<P>(_ body: @escaping (StoreProxy<State>) -> P) where P: Publisher, P.Output == Void, P.Failure == Never {
self.body = { store in
body(store)
return Empty().eraseToAnyPublisher()
.first()
.compactMap { _ -> Action? in nil }
.eraseToAnyPublisher()
}
}

/// Create an asynchronous action plan that returns a cancellable.
/// Initiate a synchronous action plan.
///
/// The plan expects to complete once the body has returned.
/// - Parameter body: The body of the action plan.
@inlinable public init(_ body: @escaping (StoreProxy<State>, @escaping () -> Void) -> Cancellable) {
@inlinable public init(_ body: @escaping (StoreProxy<State>) -> Void) {
self.body = { store in
var cancellable: Cancellable? = nil
return Deferred {
Future<Void, Never> { promise in
cancellable = body(store) {
promise(.success(()))
}
}
.compactMap { _ -> Action? in nil }
}
.handleEvents(receiveCancel: { cancellable?.cancel() })
.eraseToAnyPublisher()
body(store)
return Empty().eraseToAnyPublisher()
}
}

@inlinable public func run<T>(store: Store<T>) -> AnyPublisher<Action, Never> {
@inlinable public func run<T>(store: StoreProxy<T>) -> AnyPublisher<Action, Never> {
guard let storeProxy = store.proxy(for: State.self) else {
fatalError("Store does not support type `\(State.self)` from ActionPlan.")
}
Expand Down
20 changes: 5 additions & 15 deletions Sources/SwiftDux/Action/ActionSubscriber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,15 @@ final internal class ActionSubscriber: Subscriber {
typealias ReceivedCompletion = () -> Void

private let actionDispatcher: ActionDispatcher
private let receivedCompletion: ReceivedCompletion?
private var subscription: Subscription? = nil {
willSet {
guard let subscription = subscription else { return }
subscription.cancel()
}
}

internal init(actionDispatcher: ActionDispatcher, receivedCompletion: ReceivedCompletion?) {
internal init(actionDispatcher: ActionDispatcher) {
self.actionDispatcher = actionDispatcher
self.receivedCompletion = receivedCompletion
}

public func receive(subscription: Subscription) {
Expand All @@ -31,7 +29,6 @@ final internal class ActionSubscriber: Subscriber {
}

public func receive(completion: Subscribers.Completion<Never>) {
receivedCompletion?()
subscription = nil
}

Expand All @@ -45,18 +42,11 @@ extension Publisher where Output == Action, Failure == Never {

/// Subscribe to a publisher of actions, and send the results to an action dispatcher.
///
/// - Parameters:
/// - actionDispatcher: The ActionDispatcher
/// - receivedCompletion: An optional block called when the publisher completes.
/// - Parameter actionDispatcher: The ActionDispatcher
/// - Returns: A cancellable to unsubscribe.
public func send(to actionDispatcher: ActionDispatcher, receivedCompletion: (() -> Void)? = nil) -> AnyCancellable {
let subscriber = ActionSubscriber(
actionDispatcher: actionDispatcher,
receivedCompletion: receivedCompletion
)
public func send(to actionDispatcher: ActionDispatcher) -> AnyCancellable {
let subscriber = ActionSubscriber(actionDispatcher: actionDispatcher)
self.subscribe(subscriber)
return AnyCancellable { [subscriber] in
subscriber.cancel()
}
return AnyCancellable { subscriber.cancel() }
}
}
40 changes: 14 additions & 26 deletions Sources/SwiftDux/Action/CompositeAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public struct CompositeAction: RunnableAction {
self.actions = actions
}

public func run<T>(store: Store<T>) -> AnyPublisher<Action, Never> {
public func run<T>(store: StoreProxy<T>) -> AnyPublisher<Action, Never> {
actions
.publisher
.flatMap(maxPublishers: .max(1)) { action in
Expand All @@ -23,36 +23,24 @@ public struct CompositeAction: RunnableAction {
.eraseToAnyPublisher()
}

private func run<T>(action: Action, forStore store: Store<T>) -> AnyPublisher<Action, Never> {
private func run<T>(action: Action, forStore store: StoreProxy<T>) -> AnyPublisher<Action, Never> {
if let action = action as? RunnableAction {
return action.run(store: store)
}
return Just(action).eraseToAnyPublisher()
}
}

/// Chains an array of actions to be dispatched next.
///
/// - Parameter actions: An array of actions to chain together.
/// - Returns: A composite action.
@inlinable public func then(_ actions: [Action]) -> CompositeAction {
var nextAction = self
nextAction.actions += actions
return nextAction
}

/// Chains an array of actions to be dispatched next.
///
/// - Parameter actions: One or more actions to chain together.
/// - Returns: A composite action.
@inlinable public func then(_ actions: Action...) -> CompositeAction {
then(actions)
}

/// Call the provided block next.
///
/// - Parameter block: A block of code to execute once the previous action has completed.
/// - Returns: A composite action.
@inlinable public func then(_ block: @escaping () -> Void) -> CompositeAction {
then(ActionPlan<Any> { _ in block() })
/// Chain two actions together as a composite type.
///
/// - Parameters:
/// - lhs: The first action.
/// - rhs: The next action.
/// - Returns: A composite action.
@inlinable public func + (lhs: Action, rhs: Action) -> CompositeAction {
if var lhs = lhs as? CompositeAction {
lhs.actions.append(rhs)
return lhs
}
return CompositeAction([lhs, rhs])
}
2 changes: 1 addition & 1 deletion Sources/SwiftDux/Action/RunnableAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ public protocol RunnableAction: Action {
///
/// - Parameter store: The store that the action has been dispatched to.
/// - Returns: A cancellable object.
func run<T>(store: Store<T>) -> AnyPublisher<Action, Never>
func run<T>(store: StoreProxy<T>) -> AnyPublisher<Action, Never>
}
27 changes: 17 additions & 10 deletions Sources/SwiftDux/Middleware/CompositeMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,30 @@ import Foundation

/// Use the '+' operator to combine two or more middleware together.
public struct CompositeMiddleware<State, A, B>: Middleware where A: Middleware, B: Middleware, A.State == State, B.State == State {
private var previousMiddleware: A
private var nextMiddleware: B
@usableFromInline internal var previousMiddleware: A
@usableFromInline internal var nextMiddleware: B

@usableFromInline internal init(previousMiddleware: A, nextMiddleware: B) {
self.previousMiddleware = previousMiddleware
self.nextMiddleware = nextMiddleware
}

/// Unimplemented. It simply calls `store.next(_:)`.
@inlinable public func run(store: StoreProxy<State>, action: Action) {
store.next(action)
@inlinable public func run(store: StoreProxy<State>, action: Action) -> Action? {
guard let action = previousMiddleware.run(store: store, action: action) else {
return nil
}
return nextMiddleware.run(store: store, action: action)
}
}

/// Apply the middleware to a store proxy.
/// - Parameter store: The store proxy.
/// - Returns: A SendAction function that performs the middleware for the provided store proxy.
public func compile(store: StoreProxy<State>) -> SendAction {
previousMiddleware(store: StoreProxy(proxy: store, next: nextMiddleware(store: store)))
}
/// Compose two middleware together.
///
/// - Parameters:
/// - previousMiddleware: The middleware to be called first.
/// - nextMiddleware: The next middleware to call.
/// - Returns: The combined middleware.
@inlinable public func + <M1, M2>(previousMiddleware: M1, _ nextMiddleware: M2) -> CompositeMiddleware<M1.State, M1, M2>
where M1: Middleware, M2: Middleware, M1.State == M2.State {
CompositeMiddleware(previousMiddleware: previousMiddleware, nextMiddleware: nextMiddleware)
}
7 changes: 3 additions & 4 deletions Sources/SwiftDux/Middleware/HandleActionMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import Foundation

/// A simple middleware to perform any handling on a dispatched action.
public final class HandleActionMiddleware<State>: Middleware {
@usableFromInline
internal var perform: (StoreProxy<State>, Action) -> Void
@usableFromInline internal var perform: (StoreProxy<State>, Action) -> Action?

/// - Parameter body: The block to call when an action is dispatched.
@inlinable public init(perform: @escaping (StoreProxy<State>, Action) -> Void) {
@inlinable public init(perform: @escaping (StoreProxy<State>, Action) -> Action?) {
self.perform = perform
}

@inlinable public func run(store: StoreProxy<State>, action: Action) {
@inlinable public func run(store: StoreProxy<State>, action: Action) -> Action? {
perform(store, action)
}
}
28 changes: 9 additions & 19 deletions Sources/SwiftDux/Middleware/Middleware.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import Combine
import Foundation

/// Middleware perform actions on the the store when actions are dispatched to it.
/// Extends the store functionality by providing a middle layer between dispatched actions and the store's reducer.
///
/// Before an action is given to a reducer, middleware have an opportunity to handle it
/// themselves. They may dispatch their own actions, transform the current action, or
/// block an incoming ones from continuing.
/// block it entirely.
///
/// Middleware can also be used to set up external hooks from services.
///
/// For a reducer's own state and actions, implement the `reduce(state:action:)`.
/// For subreducers, implement the `reduceNext(state:action:)` method.
public protocol Middleware {
associatedtype State

Expand All @@ -19,9 +16,11 @@ public protocol Middleware {
/// - Parameters:
/// - store: The store object. Use `store.next` when the middleware is complete.
/// - action: The latest dispatched action to process.
func run(store: StoreProxy<State>, action: Action)
/// - Returns: An optional action to pass to the next middleware.
func run(store: StoreProxy<State>, action: Action) -> Action?

/// Compiles the middleware into a SendAction closure.
///
/// - Parameter store: A reference to the store used by the middleware.
/// - Returns: The SendAction that performs the middleware.
func compile(store: StoreProxy<State>) -> SendAction
Expand All @@ -30,30 +29,21 @@ public protocol Middleware {
extension Middleware {

/// Apply the middleware to a store proxy.
///
/// - Parameter store: The store proxy.
/// - Returns: A SendAction function that performs the middleware for the provided store proxy.
@inlinable public func callAsFunction(store: StoreProxy<State>) -> SendAction {
self.compile(store: store)
}

@inlinable public func compile(store: StoreProxy<State>) -> SendAction {
{ action in self.run(store: store, action: action) }
{ action in _ = self.run(store: store, action: action) }
}
}

/// Compose two middleware together.
/// - Parameters:
/// - previousMiddleware: The middleware to be called first.
/// - nextMiddleware: The next middleware to call.
/// - Returns: The combined middleware.
@inlinable public func + <M1, M2>(previousMiddleware: M1, _ nextMiddleware: M2) -> CompositeMiddleware<M1.State, M1, M2>
where M1: Middleware, M2: Middleware, M1.State == M2.State {
CompositeMiddleware(previousMiddleware: previousMiddleware, nextMiddleware: nextMiddleware)
}

internal final class NoopMiddleware<State>: Middleware {

@inlinable func run(store: StoreProxy<State>, action: Action) {
store.next(action)
@inlinable func run(store: StoreProxy<State>, action: Action) -> Action? {
action
}
}
8 changes: 8 additions & 0 deletions Sources/SwiftDux/Middleware/ReduceMiddleware.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//
// ReduceMiddleware.swift
// SwiftDuxTests
//
// Created by Steven Lambion on 11/19/20.
//

import Foundation
Loading