diff --git a/Mobius.xcodeproj/project.pbxproj b/Mobius.xcodeproj/project.pbxproj index d5754dba..6252b0a5 100644 --- a/Mobius.xcodeproj/project.pbxproj +++ b/Mobius.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 025BB529244DA65100E80BD2 /* ConnectableMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025BB528244DA65100E80BD2 /* ConnectableMap.swift */; }; + 025BB52C244DA6BF00E80BD2 /* ConnectableMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025BB52A244DA68500E80BD2 /* ConnectableMapTests.swift */; }; + 025BB52D244DA6C400E80BD2 /* ConnectableMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025BB528244DA65100E80BD2 /* ConnectableMap.swift */; }; 02BED1BB21DD20D20093FB47 /* ConnectableContramap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9CE80421197FE000DB79A7 /* ConnectableContramap.swift */; }; 02C4061D2373078400BD7ED8 /* EffectExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C4061C2373078400BD7ED8 /* EffectExecutor.swift */; }; 02C40621237422EF00BD7ED8 /* EffectRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C40620237422EF00BD7ED8 /* EffectRouter.swift */; }; @@ -292,6 +295,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 025BB528244DA65100E80BD2 /* ConnectableMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectableMap.swift; sourceTree = ""; }; + 025BB52A244DA68500E80BD2 /* ConnectableMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectableMapTests.swift; sourceTree = ""; }; 02C4061C2373078400BD7ED8 /* EffectExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectExecutor.swift; sourceTree = ""; }; 02C40620237422EF00BD7ED8 /* EffectRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectRouter.swift; sourceTree = ""; }; 02C40622237425E100BD7ED8 /* EffectRouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectRouterTests.swift; sourceTree = ""; }; @@ -754,6 +759,7 @@ 5BB287E8209995420043B530 /* SimpleLogger.swift */, DD748322212DB525008EEECD /* Copyable.swift */, 5B1F104C21105CAD0067193C /* EventSource+Extensions.swift */, + 025BB528244DA65100E80BD2 /* ConnectableMap.swift */, ); path = Source; sourceTree = ""; @@ -847,6 +853,7 @@ 5B9CE80621199D4400DB79A7 /* ConnectableContramapTests.swift */, DD748324212DEEC1008EEECD /* CopyableTests.swift */, 5B1F104F21105CC00067193C /* EventSource+ExtensionsTests.swift */, + 025BB52A244DA68500E80BD2 /* ConnectableMapTests.swift */, 2DA1E89D2449F1ED00D240B7 /* WikiTutorialTest.swift */, ); path = Test; @@ -1252,6 +1259,7 @@ 3EE5AF052110BF2E00CF8CA8 /* ConnectableClass.swift in Sources */, 2DF4C42420DBDF1B00A4B6DE /* SimpleLogger.swift in Sources */, 02BED1BB21DD20D20093FB47 /* ConnectableContramap.swift in Sources */, + 025BB52D244DA6C400E80BD2 /* ConnectableMap.swift in Sources */, 5B1F104E21105CB10067193C /* EventSource+Extensions.swift in Sources */, 2D3A696D2355AED90053C95E /* Lock.swift in Sources */, ); @@ -1357,6 +1365,7 @@ 5B9CE80521197FE000DB79A7 /* ConnectableContramap.swift in Sources */, 5B1F104D21105CAD0067193C /* EventSource+Extensions.swift in Sources */, DDA64A6720A0AD2D00150355 /* SimpleLogger.swift in Sources */, + 025BB529244DA65100E80BD2 /* ConnectableMap.swift in Sources */, 5BCF5F0120F3620700721C0D /* ConnectableClass.swift in Sources */, 2D3A696E2355AED90053C95E /* Lock.swift in Sources */, ); @@ -1419,6 +1428,7 @@ buildActionMask = 2147483647; files = ( DD748325212DEEC1008EEECD /* CopyableTests.swift in Sources */, + 025BB52C244DA6BF00E80BD2 /* ConnectableMapTests.swift in Sources */, 5B1F105121105CC50067193C /* EventSource+ExtensionsTests.swift in Sources */, 5BCF5F0420F3636800721C0D /* ConnectableClassTests.swift in Sources */, 2DA1E8A12449FBC500D240B7 /* WikiTutorialTest.swift in Sources */, diff --git a/MobiusExtras/Source/ConnectableContramap.swift b/MobiusExtras/Source/ConnectableContramap.swift index 05e0d2bb..3d8473d1 100644 --- a/MobiusExtras/Source/ConnectableContramap.swift +++ b/MobiusExtras/Source/ConnectableContramap.swift @@ -20,22 +20,18 @@ import MobiusCore public extension Connectable { - func contramap(_ map: @escaping (NewInput) -> Input) -> AnyConnectable { - let newConnectClosure = { (consumer: @escaping Consumer) -> Connection in - let connection = self.connect(consumer) - let mappedAcceptFunction = { (newTypeInput: NewInput) in - let oldTypeInput = map(newTypeInput) - connection.accept(oldTypeInput) - } + /// Transform the input type of this `Connectable` by applying the `transform` function to each input. + /// + /// - Parameter transform: The function which should be used to transform the input to this `Connectable` + /// - Returns: A `Connectable` which applies `transform` to each input value before handling it. + func contramap(_ transform: @escaping (NewInput) -> Input) -> AnyConnectable { + return AnyConnectable { dispatch in + let connection = self.connect(dispatch) - return Connection( - acceptClosure: mappedAcceptFunction, + return Connection( + acceptClosure: { connection.accept(transform($0)) }, disposeClosure: connection.dispose ) } - - let contramapped = AnyConnectable(newConnectClosure) - - return contramapped } } diff --git a/MobiusExtras/Source/ConnectableMap.swift b/MobiusExtras/Source/ConnectableMap.swift new file mode 100644 index 00000000..ebc27ffa --- /dev/null +++ b/MobiusExtras/Source/ConnectableMap.swift @@ -0,0 +1,35 @@ +// 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 Connectable { + /// Transform the output type of this `Connectable` by applying the `transform` function to each output. + /// + /// - Parameter transform: The function which should be used to transform the output of this `Connectable` + /// - Returns: A `Connectable` which applies `transform` to each output value. + func map(_ transform: @escaping (Output) -> NewOutput) -> AnyConnectable { + return AnyConnectable { dispatch in + return self.connect { output in + let newOutput = transform(output) + dispatch(newOutput) + } + } + } +} diff --git a/MobiusExtras/Test/ConnectableMapTests.swift b/MobiusExtras/Test/ConnectableMapTests.swift new file mode 100644 index 00000000..9a252809 --- /dev/null +++ b/MobiusExtras/Test/ConnectableMapTests.swift @@ -0,0 +1,68 @@ +// 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 Nimble +import Quick + +class ConnectableMapTests: QuickSpec { + override func spec() { + context("Connectable Map") { + it("applies the `transform` function to the output") { + var output: [Int?] = [] + let connection = TestConnectable() + .map { Int($0) } + .connect { + output.append($0) + } + + connection.accept("1") + connection.accept("2") + connection.accept("3") + + expect(output).to(equal([1, 2, 3])) + + connection.dispose() + } + + it("preserves the connectable's `Disposable` conformance") { + let testConnectable = TestConnectable() + expect(testConnectable.isDisposed).to(beFalse()) + + testConnectable + .connect { _ in } + .dispose() + + expect(testConnectable.isDisposed).to(beTrue()) + } + } + } +} + +private final class TestConnectable: Connectable { + var isDisposed = false + + func connect(_ consumer: @escaping Consumer) -> Connection { + return Connection( + acceptClosure: consumer, + disposeClosure: { self.isDisposed = true } + ) + } +}