diff --git a/Mobius.xcodeproj/project.pbxproj b/Mobius.xcodeproj/project.pbxproj index 01ed2120..6252b0a5 100644 --- a/Mobius.xcodeproj/project.pbxproj +++ b/Mobius.xcodeproj/project.pbxproj @@ -42,6 +42,9 @@ 2D405B7824337E9F00A39BD4 /* MobiusThrowableAssertion.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D25B9AA24337C580077FB07 /* MobiusThrowableAssertion.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2D405B7924337EAB00A39BD4 /* module.modulemap in Headers */ = {isa = PBXBuildFile; fileRef = 2D25B9A724337C580077FB07 /* module.modulemap */; settings = {ATTRIBUTES = (Public, ); }; }; 2D587360238EC60F001F21ED /* EventRouterDisposalLogicalRaceRegressionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D58735F238EC60F001F21ED /* EventRouterDisposalLogicalRaceRegressionTest.swift */; }; + 2DA1E89F2449FBA800D240B7 /* BeginnerLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA1E89A2449EF6E00D240B7 /* BeginnerLoop.swift */; }; + 2DA1E8A02449FBA800D240B7 /* BeginnerLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA1E89A2449EF6E00D240B7 /* BeginnerLoop.swift */; }; + 2DA1E8A12449FBC500D240B7 /* WikiTutorialTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA1E89D2449F1ED00D240B7 /* WikiTutorialTest.swift */; }; 2DA9A41E23FAEA4800BF5534 /* AnyEffectHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA9A41D23FAEA4800BF5534 /* AnyEffectHandlerTests.swift */; }; 2DB61AFD23A8F485009E55DB /* NonReentrancyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB61AFC23A8F485009E55DB /* NonReentrancyTests.swift */; }; 2DDF54C0229BDB4800D05861 /* CompositeEventSourceBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF54BF229BDB4700D05861 /* CompositeEventSourceBuilder.swift */; }; @@ -319,6 +322,8 @@ 2D3EEB9523FADA9E006E478A /* AsyncStartStopStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncStartStopStateMachine.swift; sourceTree = ""; }; 2D3F26EC237B02B8004C2B75 /* AsyncDispatchQueueConnectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AsyncDispatchQueueConnectable.swift; path = ConnectableConvenienceClasses/AsyncDispatchQueueConnectable.swift; sourceTree = ""; }; 2D58735F238EC60F001F21ED /* EventRouterDisposalLogicalRaceRegressionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventRouterDisposalLogicalRaceRegressionTest.swift; sourceTree = ""; }; + 2DA1E89A2449EF6E00D240B7 /* BeginnerLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeginnerLoop.swift; sourceTree = ""; }; + 2DA1E89D2449F1ED00D240B7 /* WikiTutorialTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikiTutorialTest.swift; sourceTree = ""; }; 2DA9A41D23FAEA4800BF5534 /* AnyEffectHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEffectHandlerTests.swift; sourceTree = ""; }; 2DB61AFC23A8F485009E55DB /* NonReentrancyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonReentrancyTests.swift; sourceTree = ""; }; 2DDF54BF229BDB4700D05861 /* CompositeEventSourceBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompositeEventSourceBuilder.swift; sourceTree = ""; }; @@ -748,6 +753,7 @@ 5BB287E7209995420043B530 /* Source */ = { isa = PBXGroup; children = ( + 2DA1E89A2449EF6E00D240B7 /* BeginnerLoop.swift */, 5BCF5F0020F3620700721C0D /* ConnectableClass.swift */, 5B9CE80421197FE000DB79A7 /* ConnectableContramap.swift */, 5BB287E8209995420043B530 /* SimpleLogger.swift */, @@ -848,6 +854,7 @@ DD748324212DEEC1008EEECD /* CopyableTests.swift */, 5B1F104F21105CC00067193C /* EventSource+ExtensionsTests.swift */, 025BB52A244DA68500E80BD2 /* ConnectableMapTests.swift */, + 2DA1E89D2449F1ED00D240B7 /* WikiTutorialTest.swift */, ); path = Test; sourceTree = ""; @@ -1247,6 +1254,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2DA1E8A02449FBA800D240B7 /* BeginnerLoop.swift in Sources */, DD748326212DEEC6008EEECD /* Copyable.swift in Sources */, 3EE5AF052110BF2E00CF8CA8 /* ConnectableClass.swift in Sources */, 2DF4C42420DBDF1B00A4B6DE /* SimpleLogger.swift in Sources */, @@ -1352,6 +1360,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2DA1E89F2449FBA800D240B7 /* BeginnerLoop.swift in Sources */, DD748323212DB525008EEECD /* Copyable.swift in Sources */, 5B9CE80521197FE000DB79A7 /* ConnectableContramap.swift in Sources */, 5B1F104D21105CAD0067193C /* EventSource+Extensions.swift in Sources */, @@ -1422,6 +1431,7 @@ 025BB52C244DA6BF00E80BD2 /* ConnectableMapTests.swift in Sources */, 5B1F105121105CC50067193C /* EventSource+ExtensionsTests.swift in Sources */, 5BCF5F0420F3636800721C0D /* ConnectableClassTests.swift in Sources */, + 2DA1E8A12449FBC500D240B7 /* WikiTutorialTest.swift in Sources */, 5B9CE80721199D4400DB79A7 /* ConnectableContramapTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MobiusExtras/Source/BeginnerLoop.swift b/MobiusExtras/Source/BeginnerLoop.swift new file mode 100644 index 00000000..632bf7f9 --- /dev/null +++ b/MobiusExtras/Source/BeginnerLoop.swift @@ -0,0 +1,45 @@ +// Copyright (c) 2020 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import MobiusCore + +public extension Mobius { + + /// A simplified version of `Mobius.loop` for use in tutorials. + /// + /// This helper simplifies setting up a loop with no effects. + /// + /// - Parameter update: A function taking a model and event and returning a new model. + @inlinable + static func beginnerLoop( + update: @escaping (Model, Event) -> Model + ) -> Builder { + let realUpdate = Update { model, event in + return .next(update(model, event)) + } + + let effectHandler = AnyConnectable { _ in + return Connection( + acceptClosure: { _ in }, + disposeClosure: {} + ) + } + return loop(update: realUpdate, effectHandler: effectHandler) + } +} diff --git a/MobiusExtras/Test/WikiTutorialTest.swift b/MobiusExtras/Test/WikiTutorialTest.swift new file mode 100644 index 00000000..92a6a12c --- /dev/null +++ b/MobiusExtras/Test/WikiTutorialTest.swift @@ -0,0 +1,146 @@ +// Copyright (c) 2020 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import MobiusCore +import MobiusExtras +import XCTest + +/// Test cases that reproduce the Getting Started section of the GitHub wiki +class WikiTutorialTest: XCTestCase { + // swiftlint:disable function_body_length + + func testWikiCreatingALoop() { + // Dummy implementation of print() + var printedValues: [String] = [] + func print(_ value: Any) { + printedValues.append(String(describing: value)) + } + + // ---8<--- Wiki content start + enum MyEvent { + case up + case down + } + + func update(counter: Int, event: MyEvent) -> Int { + switch event { + case .up: + return counter + 1 + case .down: + return counter > 0 + ? counter - 1 + : counter + } + } + + let loop = Mobius.beginnerLoop(update: update) + .start(from: 2) + + loop.addObserver { counter in print(counter) } + + loop.dispatchEvent(.down) // prints "1" + loop.dispatchEvent(.down) // prints "0" + loop.dispatchEvent(.down) // prints "0" + loop.dispatchEvent(.up) // prints "1" + loop.dispatchEvent(.up) // prints "2" + loop.dispatchEvent(.down) // prints "1" + + loop.dispose() + // ---8<--- Wiki content end + + XCTAssertEqual(printedValues, ["2", "1", "0", "0", "1", "2", "1"]) + } + + func testWikiCreatingALoop_addingEffects() { + // Dummy implementation of print() + var printedValues: [String] = [] + func print(_ value: Any) { + printedValues.append(String(describing: value)) + } + + // Carried over from previous example + enum MyEvent { + case up + case down + } + + // ---8<--- Wiki content start + enum MyEffect { + case reportErrorNegative + } + + func update1(model: Int, event: MyEvent) -> Next { + switch event { + case .up: + return .next(model + 1) + case .down: + return model > 0 + ? .next(model - 1) + : .next(model) + } + } + + func update2(model: Int, event: MyEvent) -> Next { + switch event { + case .up: + return .next(model + 1) + case .down: + return model > 0 + ? .next(model - 1) + : .next(model, effects: [.reportErrorNegative]) + } + } + + func update(model: Int, event: MyEvent) -> Next { + switch event { + case .up: + return .next(model + 1) + case .down: + return model > 0 + ? .next(model - 1) + : .dispatchEffects([.reportErrorNegative]) + } + } + + func handleReportErrorNegative() { + print("error!") + } + + let effectHandler = EffectRouter() + .routeCase(MyEffect.reportErrorNegative).to(handleReportErrorNegative) + .asConnectable + + let loop = Mobius.loop(update: update, effectHandler: effectHandler) + .start(from: 2) + + loop.addObserver { counter in print(counter) } + + loop.dispatchEvent(.down) // prints "1" + loop.dispatchEvent(.down) // prints "0" + loop.dispatchEvent(.down) // followed by "error!" + loop.dispatchEvent(.up) // prints "1" + loop.dispatchEvent(.up) // prints "2" + loop.dispatchEvent(.down) // prints "1" + + loop.dispose() + // ---8<--- Wiki content end + + XCTAssertEqual(printedValues, ["2", "1", "0", "error!", "1", "2", "1"]) + } +}