Skip to content

Commit d2fa8bd

Browse files
committed
Add StateMutationSink
1 parent 9a1ef1e commit d2fa8bd

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
import Workflow
19+
20+
extension RenderContext {
21+
/// Creates `StateMutationSink`.
22+
///
23+
/// To create a sink:
24+
/// ```
25+
/// let stateMutationSink = context.makeStateMutationSink()
26+
/// ```
27+
///
28+
/// To mutate `State` on an event:
29+
/// ```
30+
/// stateMutationSink.send(\State.value, value: 10)
31+
/// ```
32+
public func makeStateMutationSink() -> StateMutationSink<WorkflowType> {
33+
let sink = makeSink(of: AnyWorkflowAction<WorkflowType>.self)
34+
return StateMutationSink(sink)
35+
}
36+
}
37+
38+
/// StateMutationSink provides a `Sink` that helps mutate `State` using it's `KeyPath`.
39+
public struct StateMutationSink<WorkflowType: Workflow> {
40+
let sink: Sink<AnyWorkflowAction<WorkflowType>>
41+
42+
/// Sends message to `StateMutationSink` to update `State`'s value using the provided closure.
43+
///
44+
/// - Parameters:
45+
/// - update: The `State`` mutation to perform.
46+
public func send(_ update: @escaping (inout WorkflowType.State) -> Void) {
47+
sink.send(
48+
AnyWorkflowAction<WorkflowType> { state in
49+
update(&state)
50+
return nil
51+
}
52+
)
53+
}
54+
55+
/// Sends message to `StateMutationSink` to update `State`'s value at `KeyPath` with `Value`.
56+
///
57+
/// - Parameters:
58+
/// - keyPath: Key path of `State` whose value needs to be mutated.
59+
/// - value: Value to update `State` with.
60+
public func send<Value>(_ keyPath: WritableKeyPath<WorkflowType.State, Value>, value: Value) {
61+
sink.send { $0[keyPath: keyPath] = vaue }
62+
}
63+
64+
init(_ sink: Sink<AnyWorkflowAction<WorkflowType>>) {
65+
self.sink = sink
66+
}
67+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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 Workflow
18+
import XCTest
19+
20+
final class StateMutationSinkTests: XCTestCase {
21+
var output: Signal<Int, Never>!
22+
var input: Signal<Int, Never>.Observer!
23+
24+
override func setUp() {
25+
(output, input) = Signal<Int, Never>.pipe()
26+
}
27+
28+
func test_initialValue() {
29+
let host = WorkflowHost(workflow: TestWorkflow(value: 100, signal: output))
30+
XCTAssertEqual(0, host.rendering.value)
31+
}
32+
33+
func test_singleUpdate() {
34+
let host = WorkflowHost(workflow: TestWorkflow(value: 100, signal: output))
35+
36+
let gotValueExpectation = expectation(description: "Got expected value")
37+
host.rendering.producer.startWithValues { val in
38+
if val == 100 {
39+
gotValueExpectation.fulfill()
40+
}
41+
}
42+
43+
input.send(value: 100)
44+
waitForExpectations(timeout: 1, handler: nil)
45+
}
46+
47+
func test_multipleUpdates() {
48+
let host = WorkflowHost(workflow: TestWorkflow(value: 100, signal: output))
49+
50+
let gotValueExpectation = expectation(description: "Got expected value")
51+
52+
var values: [Int] = []
53+
host.rendering.producer.startWithValues { val in
54+
values.append(val)
55+
if val == 300 {
56+
gotValueExpectation.fulfill()
57+
}
58+
}
59+
60+
input.send(value: 100)
61+
input.send(value: 200)
62+
input.send(value: 300)
63+
XCTAssertEqual(values, [0, 100, 200, 300])
64+
waitForExpectations(timeout: 1, handler: nil)
65+
}
66+
67+
fileprivate struct TestWorkflow: Workflow {
68+
typealias State = Int
69+
typealias Rendering = Int
70+
71+
let value: Int
72+
let signal: Signal<Int, Never>
73+
74+
func makeInitialState() -> Int {
75+
0
76+
}
77+
78+
func render(state: State, context: RenderContext<TestWorkflow>) -> Rendering {
79+
let stateMutationSink = context.makeStateMutationSink()
80+
context.runSideEffect(key: "") { lifetime in
81+
let disposable = signal.observeValues { val in
82+
stateMutationSink.send(\State.self, value: val)
83+
}
84+
lifetime.onEnded {
85+
disposable?.dispose()
86+
}
87+
}
88+
return state
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)