Skip to content

Commit

Permalink
Merge pull request #3 from ReSwift/feature/1.1
Browse files Browse the repository at this point in the history
Add binding functions and change state lensing to use functions
  • Loading branch information
Qata authored Feb 16, 2021
2 parents a76d7a2 + 7f8ea98 commit b76faa0
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 37 deletions.
4 changes: 3 additions & 1 deletion Sources/RecombinePackage/Action.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
public enum ActionStrata<Raw, Refined> {
public enum ActionStrata<RawAction, RefinedAction> {
public typealias Raw = RawAction
public typealias Refined = RefinedAction
case raw(Raw)
case refined(Refined)
}
7 changes: 4 additions & 3 deletions Sources/RecombinePackage/Middleware.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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.
/// Middleware is a dependency injection structure that allows you to transform raw actions into refined ones,
/// Refined actions produced by Middleware are then forwarded to the main reducer.
///
public struct Middleware<State, Input, Output> {
public typealias StatePublisher = Publishers.First<Published<State>.Publisher>
public typealias Function = (StatePublisher, Input) -> AnyPublisher<Output, Never>
public typealias Transform<Result> = (StatePublisher, Output) -> Result
internal let transform: Function

/// Create a blank slate Middleware.
/// Create a passthrough Middleware.
public init() where Input == Output {
self.transform = { Just($1).eraseToAnyPublisher() }
}
Expand Down
11 changes: 6 additions & 5 deletions Sources/RecombinePackage/Store/AnyStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Combine

public class AnyStore<BaseState, SubState, RawAction, BaseRefinedAction, SubRefinedAction>: StoreProtocol {
public let underlying: BaseStore<BaseState, RawAction, BaseRefinedAction>
public let keyPath: KeyPath<BaseState, SubState>
public let stateLens: (BaseState) -> SubState
public let actionPromotion: (SubRefinedAction) -> BaseRefinedAction
private var cancellables = Set<AnyCancellable>()
@Published
Expand All @@ -17,7 +17,7 @@ public class AnyStore<BaseState, SubState, RawAction, BaseRefinedAction, SubRefi
Store.SubRefinedAction == SubRefinedAction
{
underlying = store.underlying
keyPath = store.keyPath
stateLens = store.stateLens
actionPromotion = store.actionPromotion
self.state = store.state
store.statePublisher.sink { [unowned self] state in
Expand All @@ -27,7 +27,7 @@ public class AnyStore<BaseState, SubState, RawAction, BaseRefinedAction, SubRefi
}

public func lensing<NewState, NewAction>(
state keyPath: KeyPath<SubState, NewState>,
state lens: @escaping (SubState) -> NewState,
actions transform: @escaping (NewAction) -> SubRefinedAction
) -> LensedStore<
BaseState,
Expand All @@ -36,9 +36,10 @@ public class AnyStore<BaseState, SubState, RawAction, BaseRefinedAction, SubRefi
BaseRefinedAction,
NewAction
> {
.init(
let stateLens = self.stateLens
return .init(
store: underlying,
lensing: self.keyPath.appending(path: keyPath),
lensing: { lens(stateLens($0)) },
actionPromotion: { self.actionPromotion(transform($0)) }
)
}
Expand Down
7 changes: 4 additions & 3 deletions Sources/RecombinePackage/Store/BaseStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class BaseStore<State, RawAction, RefinedAction>: StoreProtocol {
public private(set) var state: State
public var statePublisher: Published<State>.Publisher { $state }
public var underlying: BaseStore<State, RawAction, RefinedAction> { self }
public let keyPath: KeyPath<State, State> = \.self
public let stateLens: (State) -> State = { $0 }
public let rawActions = PassthroughSubject<RawAction, Never>()
public let refinedActions = PassthroughSubject<RefinedAction, Never>()
public let actionPromotion: (RefinedAction) -> RefinedAction = { $0 }
Expand Down Expand Up @@ -44,6 +44,7 @@ public class BaseStore<State, RawAction, RefinedAction>: StoreProtocol {
action: action
)
}
.removeDuplicates(by: stateEquality)
.receive(on: scheduler)
.sink { [unowned self] state in
self.state = state
Expand All @@ -67,7 +68,7 @@ public class BaseStore<State, RawAction, RefinedAction>: StoreProtocol {
}

public func lensing<NewState, NewAction>(
state keyPath: KeyPath<SubState, NewState>,
state lens: @escaping (SubState) -> NewState,
actions transform: @escaping (NewAction) -> SubRefinedAction
) -> LensedStore<
State,
Expand All @@ -76,7 +77,7 @@ public class BaseStore<State, RawAction, RefinedAction>: StoreProtocol {
RefinedAction,
NewAction
> {
.init(store: self, lensing: keyPath, actionPromotion: transform)
.init(store: self, lensing: lens, actionPromotion: transform)
}

open func dispatch<S: Sequence>(refined actions: S) where S.Element == RefinedAction {
Expand Down
23 changes: 12 additions & 11 deletions Sources/RecombinePackage/Store/LensedStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ public class LensedStore<BaseState, SubState: Equatable, RawAction, BaseRefinedA
public private(set) var state: SubState
public var statePublisher: Published<SubState>.Publisher { $state }
public let underlying: BaseStore<BaseState, RawAction, BaseRefinedAction>
public let keyPath: KeyPath<BaseState, SubState>
public let stateLens: (BaseState) -> SubState
public let actions = PassthroughSubject<SubRefinedAction, Never>()
public let actionPromotion: (SubRefinedAction) -> BaseRefinedAction
private var cancellables = Set<AnyCancellable>()

public required init(store: StoreType, lensing keyPath: KeyPath<BaseState, SubState>, actionPromotion: @escaping (SubRefinedAction) -> BaseRefinedAction) {
public required init(store: StoreType, lensing lens: @escaping (BaseState) -> SubState, actionPromotion: @escaping (SubRefinedAction) -> BaseRefinedAction) {
self.underlying = store
self.keyPath = keyPath
self.stateLens = lens
self.actionPromotion = actionPromotion
state = store.state[keyPath: keyPath]
state = lens(store.state)
store.$state
.map { $0[keyPath: keyPath] }
.map(lens)
.removeDuplicates()
.sink { [unowned self] state in
self.state = state
Expand All @@ -31,7 +31,7 @@ public class LensedStore<BaseState, SubState: Equatable, RawAction, BaseRefinedA
}

public func lensing<NewState, NewAction>(
state keyPath: KeyPath<SubState, NewState>,
state lens: @escaping (SubState) -> NewState,
actions transform: @escaping (NewAction) -> SubRefinedAction
) -> LensedStore<
BaseState,
Expand All @@ -40,9 +40,10 @@ public class LensedStore<BaseState, SubState: Equatable, RawAction, BaseRefinedA
BaseRefinedAction,
NewAction
> {
.init(
let stateLens = self.stateLens
return .init(
store: underlying,
lensing: self.keyPath.appending(path: keyPath),
lensing: { lens(stateLens($0)) },
actionPromotion: { self.actionPromotion(transform($0)) }
)
}
Expand All @@ -56,8 +57,8 @@ public class LensedStore<BaseState, SubState: Equatable, RawAction, BaseRefinedA
}
}

extension LensedStore where BaseRefinedAction == SubRefinedAction {
convenience init(store: StoreType, lensing keyPath: KeyPath<BaseState, SubState>) {
self.init(store: store, lensing: keyPath, actionPromotion: { $0 })
public extension LensedStore where BaseRefinedAction == SubRefinedAction {
convenience init(store: StoreType, lensing lens: @escaping (BaseState) -> SubState) {
self.init(store: store, lensing: lens, actionPromotion: { $0 })
}
}
106 changes: 93 additions & 13 deletions Sources/RecombinePackage/Store/StoreProtocol.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Combine
import SwiftUI

public protocol StoreProtocol: ObservableObject, Subscriber {
associatedtype BaseState
Expand All @@ -9,12 +10,12 @@ public protocol StoreProtocol: ObservableObject, Subscriber {
var state: SubState { get }
var statePublisher: Published<SubState>.Publisher { get }
var underlying: BaseStore<BaseState, RawAction, BaseRefinedAction> { get }
var keyPath: KeyPath<BaseState, SubState> { get }
var stateLens: (BaseState) -> SubState { get }
var actionPromotion: (SubRefinedAction) -> BaseRefinedAction { get }
func dispatch<S: Sequence>(raw: S) where S.Element == RawAction
func dispatch<S: Sequence>(refined: S) where S.Element == SubRefinedAction
func lensing<NewState, NewAction>(
state keyPath: KeyPath<SubState, NewState>,
state lens: @escaping (SubState) -> NewState,
actions transform: @escaping (NewAction) -> SubRefinedAction
) -> LensedStore<
BaseState,
Expand All @@ -27,30 +28,109 @@ public protocol StoreProtocol: ObservableObject, Subscriber {
}

public extension StoreProtocol {
func lensing<NewState, NewAction>(
actions transform: @escaping (NewAction) -> SubRefinedAction
func lensing<NewState>(
state lens: @escaping (SubState) -> NewState
) -> LensedStore<
BaseState,
NewState,
RawAction,
BaseRefinedAction,
SubRefinedAction
> {
lensing(state: lens, actions: { $0 })
}

func lensing<NewState>(
state keyPath: KeyPath<SubState, NewState>
) -> LensedStore<
BaseState,
NewState,
RawAction,
BaseRefinedAction,
SubRefinedAction
> {
lensing(state: { $0[keyPath: keyPath] })
}

func lensing<NewAction>(
actions transform: @escaping (NewAction) -> SubRefinedAction
) -> LensedStore<
BaseState,
SubState,
RawAction,
BaseRefinedAction,
NewAction
> where NewState == SubState {
lensing(state: \.self, actions: transform)
> {
lensing(state: { $0 }, actions: transform)
}

func lensing<NewState, NewAction>(
state keyPath: KeyPath<SubState, NewState>
state keyPath: KeyPath<SubState, NewState>,
actions transform: @escaping (NewAction) -> SubRefinedAction
) -> LensedStore<
BaseState,
NewState,
RawAction,
BaseRefinedAction,
NewAction
> where NewAction == SubRefinedAction {
lensing(state: keyPath, actions: { $0 })
> {
lensing(state: { $0[keyPath: keyPath] }, actions: transform)
}
}

public extension StoreProtocol {
/// Create a SwiftUI Binding from a lensing function and a `SubRefinedAction`.
/// - Parameters:
/// - lens: A lens to the state property.
/// - action: The refined action which will be called when the value is changed.
/// - Returns: A `Binding` whose getter is the property and whose setter dispatches the refined action.
func binding<Value>(
state lens: @escaping (SubState) -> Value,
actions transform: @escaping (Value) -> SubRefinedAction
) -> Binding<Value> {
.init(
get: { lens(self.state) },
set: { self.dispatch(refined: transform($0)) }
)
}

/// Create a SwiftUI Binding from the `SubState` of the store and a `SubRefinedAction`.
/// - Parameters:
/// - actions: The refined action which will be called when the value is changed.
/// - Returns: A `Binding` whose getter is the state and whose setter dispatches the refined action.
func binding(
actions transform: @escaping (SubState) -> SubRefinedAction
) -> Binding<SubState> {
.init(
get: { self.state },
set: { self.dispatch(refined: transform($0)) }
)
}

/// Create a SwiftUI Binding from a lensing function when the value of that function is equivalent to `SubRefinedAction`.
/// - Parameters:
/// - lens: A lens to the state property.
/// - Returns: A `Binding` whose getter is the property and whose setter dispatches the store's refined action.
func binding<Value>(
state lens: @escaping (SubState) -> Value
) -> Binding<Value> where SubRefinedAction == Value {
.init(
get: { lens(self.state) },
set: { self.dispatch(refined: $0) }
)
}

/// Create a SwiftUI Binding from the `SubState` when its value is equivalent to `SubRefinedAction`.
/// - Returns: A `Binding` whose getter is the state and whose setter dispatches the store's refined action.
func binding() -> Binding<SubState> where SubRefinedAction == SubState {
.init(
get: { self.state },
set: { self.dispatch(refined: $0) }
)
}
}

public extension StoreProtocol {
func dispatch(refined actions: SubRefinedAction...) {
dispatch(refined: actions)
}
Expand All @@ -64,12 +144,12 @@ public extension StoreProtocol {
}
}

extension StoreProtocol {
public func receive(subscription: Subscription) {
public extension StoreProtocol {
func receive(subscription: Subscription) {
subscription.request(.unlimited)
}

public func receive(_ input: ActionStrata<RawAction, SubRefinedAction>) -> Subscribers.Demand {
func receive(_ input: ActionStrata<RawAction, SubRefinedAction>) -> Subscribers.Demand {
switch input {
case let .raw(action):
dispatch(raw: action)
Expand All @@ -79,5 +159,5 @@ extension StoreProtocol {
return .unlimited
}

public func receive(completion: Subscribers.Completion<Never>) {}
func receive(completion: Subscribers.Completion<Never>) {}
}
42 changes: 41 additions & 1 deletion Tests/RecombineTests/StoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ class StoreTests: XCTestCase {
middleware: .init(),
publishOn: ImmediateScheduler.shared
)
let subStore = store.lensing(state: \.subState.value, actions: TestFakes.NestedTest.Action.sub)
let subStore = store.lensing(
state: \.subState.value,
actions: TestFakes.NestedTest.Action.sub
)
let stateRecorder = subStore.$state.dropFirst().record()
let actionsRecorder = subStore.actions.record()

Expand All @@ -43,6 +46,43 @@ class StoreTests: XCTestCase {
let actions = try wait(for: actionsRecorder.prefix(1), timeout: 1)
XCTAssertEqual(actions, [.set(string)])
}

func testBinding() throws {
let store = BaseStore(
state: TestFakes.NestedTest.State(),
reducer: TestFakes.NestedTest.reducer,
middleware: .init(),
publishOn: ImmediateScheduler.shared
)
let binding1 = store.binding(
state: \.subState.value,
actions: { .sub(.set("\($0)1")) }
)
let binding2 = store.lensing(
state: \.subState.value
).binding(
actions: { .sub(.set("\($0)2")) }
)
let binding3 = store.lensing(
state: \.subState,
actions: { .sub(.set("\($0)3")) }
).binding(
state: \.value
)
let stateRecorder = store.$state.dropFirst().record()

let string = "Oh Yeah!"

binding1.wrappedValue = string
binding2.wrappedValue = string
binding3.wrappedValue = string

let state = try wait(for: stateRecorder.prefix(3), timeout: 1)
XCTAssertEqual(
state.map(\.subState.value),
zip(repeatElement(string, count: 3), 1...).map { "\($0)\($1)" }
)
}
}

// Used for deinitialization test
Expand Down

0 comments on commit b76faa0

Please sign in to comment.