Skip to content

Commit 3c8c7c0

Browse files
authored
Merge pull request #149 from JensAyton/tutorial-support
Add beginnerLoop and test wiki tutorial
2 parents 6c7aa12 + e06fa28 commit 3c8c7c0

File tree

3 files changed

+201
-0
lines changed

3 files changed

+201
-0
lines changed

Mobius.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
2D405B7824337E9F00A39BD4 /* MobiusThrowableAssertion.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D25B9AA24337C580077FB07 /* MobiusThrowableAssertion.h */; settings = {ATTRIBUTES = (Public, ); }; };
4040
2D405B7924337EAB00A39BD4 /* module.modulemap in Headers */ = {isa = PBXBuildFile; fileRef = 2D25B9A724337C580077FB07 /* module.modulemap */; settings = {ATTRIBUTES = (Public, ); }; };
4141
2D587360238EC60F001F21ED /* EventRouterDisposalLogicalRaceRegressionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D58735F238EC60F001F21ED /* EventRouterDisposalLogicalRaceRegressionTest.swift */; };
42+
2DA1E89F2449FBA800D240B7 /* BeginnerLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA1E89A2449EF6E00D240B7 /* BeginnerLoop.swift */; };
43+
2DA1E8A02449FBA800D240B7 /* BeginnerLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA1E89A2449EF6E00D240B7 /* BeginnerLoop.swift */; };
44+
2DA1E8A12449FBC500D240B7 /* WikiTutorialTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA1E89D2449F1ED00D240B7 /* WikiTutorialTest.swift */; };
4245
2DA9A41E23FAEA4800BF5534 /* AnyEffectHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA9A41D23FAEA4800BF5534 /* AnyEffectHandlerTests.swift */; };
4346
2DB61AFD23A8F485009E55DB /* NonReentrancyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB61AFC23A8F485009E55DB /* NonReentrancyTests.swift */; };
4447
2DDF54C0229BDB4800D05861 /* CompositeEventSourceBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF54BF229BDB4700D05861 /* CompositeEventSourceBuilder.swift */; };
@@ -314,6 +317,8 @@
314317
2D3EEB9523FADA9E006E478A /* AsyncStartStopStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncStartStopStateMachine.swift; sourceTree = "<group>"; };
315318
2D3F26EC237B02B8004C2B75 /* AsyncDispatchQueueConnectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AsyncDispatchQueueConnectable.swift; path = ConnectableConvenienceClasses/AsyncDispatchQueueConnectable.swift; sourceTree = "<group>"; };
316319
2D58735F238EC60F001F21ED /* EventRouterDisposalLogicalRaceRegressionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventRouterDisposalLogicalRaceRegressionTest.swift; sourceTree = "<group>"; };
320+
2DA1E89A2449EF6E00D240B7 /* BeginnerLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeginnerLoop.swift; sourceTree = "<group>"; };
321+
2DA1E89D2449F1ED00D240B7 /* WikiTutorialTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikiTutorialTest.swift; sourceTree = "<group>"; };
317322
2DA9A41D23FAEA4800BF5534 /* AnyEffectHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEffectHandlerTests.swift; sourceTree = "<group>"; };
318323
2DB61AFC23A8F485009E55DB /* NonReentrancyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonReentrancyTests.swift; sourceTree = "<group>"; };
319324
2DDF54BF229BDB4700D05861 /* CompositeEventSourceBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompositeEventSourceBuilder.swift; sourceTree = "<group>"; };
@@ -743,6 +748,7 @@
743748
5BB287E7209995420043B530 /* Source */ = {
744749
isa = PBXGroup;
745750
children = (
751+
2DA1E89A2449EF6E00D240B7 /* BeginnerLoop.swift */,
746752
5BCF5F0020F3620700721C0D /* ConnectableClass.swift */,
747753
5B9CE80421197FE000DB79A7 /* ConnectableContramap.swift */,
748754
5BB287E8209995420043B530 /* SimpleLogger.swift */,
@@ -841,6 +847,7 @@
841847
5B9CE80621199D4400DB79A7 /* ConnectableContramapTests.swift */,
842848
DD748324212DEEC1008EEECD /* CopyableTests.swift */,
843849
5B1F104F21105CC00067193C /* EventSource+ExtensionsTests.swift */,
850+
2DA1E89D2449F1ED00D240B7 /* WikiTutorialTest.swift */,
844851
);
845852
path = Test;
846853
sourceTree = "<group>";
@@ -1240,6 +1247,7 @@
12401247
isa = PBXSourcesBuildPhase;
12411248
buildActionMask = 2147483647;
12421249
files = (
1250+
2DA1E8A02449FBA800D240B7 /* BeginnerLoop.swift in Sources */,
12431251
DD748326212DEEC6008EEECD /* Copyable.swift in Sources */,
12441252
3EE5AF052110BF2E00CF8CA8 /* ConnectableClass.swift in Sources */,
12451253
2DF4C42420DBDF1B00A4B6DE /* SimpleLogger.swift in Sources */,
@@ -1344,6 +1352,7 @@
13441352
isa = PBXSourcesBuildPhase;
13451353
buildActionMask = 2147483647;
13461354
files = (
1355+
2DA1E89F2449FBA800D240B7 /* BeginnerLoop.swift in Sources */,
13471356
DD748323212DB525008EEECD /* Copyable.swift in Sources */,
13481357
5B9CE80521197FE000DB79A7 /* ConnectableContramap.swift in Sources */,
13491358
5B1F104D21105CAD0067193C /* EventSource+Extensions.swift in Sources */,
@@ -1412,6 +1421,7 @@
14121421
DD748325212DEEC1008EEECD /* CopyableTests.swift in Sources */,
14131422
5B1F105121105CC50067193C /* EventSource+ExtensionsTests.swift in Sources */,
14141423
5BCF5F0420F3636800721C0D /* ConnectableClassTests.swift in Sources */,
1424+
2DA1E8A12449FBC500D240B7 /* WikiTutorialTest.swift in Sources */,
14151425
5B9CE80721199D4400DB79A7 /* ConnectableContramapTests.swift in Sources */,
14161426
);
14171427
runOnlyForDeploymentPostprocessing = 0;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) 2020 Spotify AB.
2+
//
3+
// Licensed to the Apache Software Foundation (ASF) under one
4+
// or more contributor license agreements. See the NOTICE file
5+
// distributed with this work for additional information
6+
// regarding copyright ownership. The ASF licenses this file
7+
// to you under the Apache License, Version 2.0 (the
8+
// "License"); you may not use this file except in compliance
9+
// with the License. You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing,
14+
// software distributed under the License is distributed on an
15+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
// KIND, either express or implied. See the License for the
17+
// specific language governing permissions and limitations
18+
// under the License.
19+
20+
import MobiusCore
21+
22+
public extension Mobius {
23+
24+
/// A simplified version of `Mobius.loop` for use in tutorials.
25+
///
26+
/// This helper simplifies setting up a loop with no effects.
27+
///
28+
/// - Parameter update: A function taking a model and event and returning a new model.
29+
@inlinable
30+
static func beginnerLoop<Model, Event>(
31+
update: @escaping (Model, Event) -> Model
32+
) -> Builder<Model, Event, Never> {
33+
let realUpdate = Update<Model, Event, Never> { model, event in
34+
return .next(update(model, event))
35+
}
36+
37+
let effectHandler = AnyConnectable<Never, Event> { _ in
38+
return Connection(
39+
acceptClosure: { _ in },
40+
disposeClosure: {}
41+
)
42+
}
43+
return loop(update: realUpdate, effectHandler: effectHandler)
44+
}
45+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Copyright (c) 2020 Spotify AB.
2+
//
3+
// Licensed to the Apache Software Foundation (ASF) under one
4+
// or more contributor license agreements. See the NOTICE file
5+
// distributed with this work for additional information
6+
// regarding copyright ownership. The ASF licenses this file
7+
// to you under the Apache License, Version 2.0 (the
8+
// "License"); you may not use this file except in compliance
9+
// with the License. You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing,
14+
// software distributed under the License is distributed on an
15+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
// KIND, either express or implied. See the License for the
17+
// specific language governing permissions and limitations
18+
// under the License.
19+
20+
import MobiusCore
21+
import MobiusExtras
22+
import XCTest
23+
24+
/// Test cases that reproduce the Getting Started section of the GitHub wiki
25+
class WikiTutorialTest: XCTestCase {
26+
// swiftlint:disable function_body_length
27+
28+
func testWikiCreatingALoop() {
29+
// Dummy implementation of print()
30+
var printedValues: [String] = []
31+
func print(_ value: Any) {
32+
printedValues.append(String(describing: value))
33+
}
34+
35+
// ---8<--- Wiki content start
36+
enum MyEvent {
37+
case up
38+
case down
39+
}
40+
41+
func update(counter: Int, event: MyEvent) -> Int {
42+
switch event {
43+
case .up:
44+
return counter + 1
45+
case .down:
46+
return counter > 0
47+
? counter - 1
48+
: counter
49+
}
50+
}
51+
52+
let loop = Mobius.beginnerLoop(update: update)
53+
.start(from: 2)
54+
55+
loop.addObserver { counter in print(counter) }
56+
57+
loop.dispatchEvent(.down) // prints "1"
58+
loop.dispatchEvent(.down) // prints "0"
59+
loop.dispatchEvent(.down) // prints "0"
60+
loop.dispatchEvent(.up) // prints "1"
61+
loop.dispatchEvent(.up) // prints "2"
62+
loop.dispatchEvent(.down) // prints "1"
63+
64+
loop.dispose()
65+
// ---8<--- Wiki content end
66+
67+
XCTAssertEqual(printedValues, ["2", "1", "0", "0", "1", "2", "1"])
68+
}
69+
70+
func testWikiCreatingALoop_addingEffects() {
71+
// Dummy implementation of print()
72+
var printedValues: [String] = []
73+
func print(_ value: Any) {
74+
printedValues.append(String(describing: value))
75+
}
76+
77+
// Carried over from previous example
78+
enum MyEvent {
79+
case up
80+
case down
81+
}
82+
83+
// ---8<--- Wiki content start
84+
enum MyEffect {
85+
case reportErrorNegative
86+
}
87+
88+
func update1(model: Int, event: MyEvent) -> Next<Int, MyEffect> {
89+
switch event {
90+
case .up:
91+
return .next(model + 1)
92+
case .down:
93+
return model > 0
94+
? .next(model - 1)
95+
: .next(model)
96+
}
97+
}
98+
99+
func update2(model: Int, event: MyEvent) -> Next<Int, MyEffect> {
100+
switch event {
101+
case .up:
102+
return .next(model + 1)
103+
case .down:
104+
return model > 0
105+
? .next(model - 1)
106+
: .next(model, effects: [.reportErrorNegative])
107+
}
108+
}
109+
110+
func update(model: Int, event: MyEvent) -> Next<Int, MyEffect> {
111+
switch event {
112+
case .up:
113+
return .next(model + 1)
114+
case .down:
115+
return model > 0
116+
? .next(model - 1)
117+
: .dispatchEffects([.reportErrorNegative])
118+
}
119+
}
120+
121+
func handleReportErrorNegative() {
122+
print("error!")
123+
}
124+
125+
let effectHandler = EffectRouter<MyEffect, MyEvent>()
126+
.routeCase(MyEffect.reportErrorNegative).to(handleReportErrorNegative)
127+
.asConnectable
128+
129+
let loop = Mobius.loop(update: update, effectHandler: effectHandler)
130+
.start(from: 2)
131+
132+
loop.addObserver { counter in print(counter) }
133+
134+
loop.dispatchEvent(.down) // prints "1"
135+
loop.dispatchEvent(.down) // prints "0"
136+
loop.dispatchEvent(.down) // followed by "error!"
137+
loop.dispatchEvent(.up) // prints "1"
138+
loop.dispatchEvent(.up) // prints "2"
139+
loop.dispatchEvent(.down) // prints "1"
140+
141+
loop.dispose()
142+
// ---8<--- Wiki content end
143+
144+
XCTAssertEqual(printedValues, ["2", "1", "0", "error!", "1", "2", "1"])
145+
}
146+
}

0 commit comments

Comments
 (0)