Skip to content

Commit 778a642

Browse files
committed
Add StateMutationSink
1 parent 9a1ef1e commit 778a642

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2022 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Foundation
18+
19+
extension RenderContext {
20+
/// Creates `StateMutationSink`.
21+
///
22+
/// To create a sink:
23+
/// ```
24+
/// let stateMutationSink = context.makeStateMutationSink()
25+
/// ```
26+
///
27+
/// To mutate `State` on an event:
28+
/// ```
29+
/// stateMutationSink.send(\State.value, value: 10)
30+
/// ```
31+
public func makeStateMutationSink() -> StateMutationSink<WorkflowType> {
32+
let sink = makeSink(of: AnyWorkflowAction<WorkflowType>.self)
33+
return StateMutationSink(sink)
34+
}
35+
}
36+
37+
/// StateMutationSink provides a `Sink` that helps mutate `State` using it's `KeyPath`.
38+
public struct StateMutationSink<WorkflowType: Workflow> {
39+
let sink: Sink<AnyWorkflowAction<WorkflowType>>
40+
41+
/// Sends message to `StateMutationSink` to update `State`'s value using the provided closure.
42+
///
43+
/// - Parameters:
44+
/// - update: The `State`` mutation to perform.
45+
public func send(_ update: @escaping (inout WorkflowType.State) -> Void) {
46+
sink.send(
47+
AnyWorkflowAction<WorkflowType> { state in
48+
update(&state)
49+
return nil
50+
}
51+
)
52+
}
53+
54+
/// Sends message to `StateMutationSink` to update `State`'s value at `KeyPath` with `Value`.
55+
///
56+
/// - Parameters:
57+
/// - keyPath: Key path of `State` whose value needs to be mutated.
58+
/// - value: Value to update `State` with.
59+
public func send<Value>(_ keyPath: WritableKeyPath<WorkflowType.State, Value>, value: Value) {
60+
send { $0[keyPath: keyPath] = value }
61+
}
62+
63+
init(_ sink: Sink<AnyWorkflowAction<WorkflowType>>) {
64+
self.sink = sink
65+
}
66+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2022 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import ReactiveSwift
18+
import Workflow
19+
import XCTest
20+
21+
final class StateMutationSinkTests: XCTestCase {
22+
var output: Signal<Int, Never>!
23+
var input: Signal<Int, Never>.Observer!
24+
25+
override func setUp() {
26+
(output, input) = Signal<Int, Never>.pipe()
27+
}
28+
29+
func test_initialValue() {
30+
let host = WorkflowHost(workflow: TestWorkflow(value: 100, signal: output))
31+
XCTAssertEqual(0, host.rendering.value)
32+
}
33+
34+
func test_singleUpdate() {
35+
let host = WorkflowHost(workflow: TestWorkflow(value: 100, signal: output))
36+
37+
let gotValueExpectation = expectation(description: "Got expected value")
38+
host.rendering.producer.startWithValues { val in
39+
if val == 100 {
40+
gotValueExpectation.fulfill()
41+
}
42+
}
43+
44+
input.send(value: 100)
45+
waitForExpectations(timeout: 1, handler: nil)
46+
}
47+
48+
func test_multipleUpdates() {
49+
let host = WorkflowHost(workflow: TestWorkflow(value: 100, signal: output))
50+
51+
let gotValueExpectation = expectation(description: "Got expected value")
52+
53+
var values: [Int] = []
54+
host.rendering.producer.startWithValues { val in
55+
values.append(val)
56+
if val == 300 {
57+
gotValueExpectation.fulfill()
58+
}
59+
}
60+
61+
input.send(value: 100)
62+
input.send(value: 200)
63+
input.send(value: 300)
64+
XCTAssertEqual(values, [0, 100, 200, 300])
65+
waitForExpectations(timeout: 1, handler: nil)
66+
}
67+
68+
fileprivate struct TestWorkflow: Workflow {
69+
typealias State = Int
70+
typealias Rendering = Int
71+
72+
let value: Int
73+
let signal: Signal<Int, Never>
74+
75+
func makeInitialState() -> Int {
76+
0
77+
}
78+
79+
func render(state: State, context: RenderContext<TestWorkflow>) -> Rendering {
80+
let stateMutationSink = context.makeStateMutationSink()
81+
context.runSideEffect(key: "") { lifetime in
82+
let disposable = signal.observeValues { val in
83+
stateMutationSink.send(\State.self, value: val)
84+
}
85+
lifetime.onEnded {
86+
disposable?.dispose()
87+
}
88+
}
89+
return state
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)