From 6ed7e32815361424e1eb7f924c690c8357e92591 Mon Sep 17 00:00:00 2001 From: Guilherme Rambo Date: Fri, 28 Feb 2020 16:46:37 -0300 Subject: [PATCH] Initial implementation and tests --- .../contents.xcworkspacedata | 7 ++ .../MockMultipeerConnection.swift | 21 ++++++ .../Models/MultipeerMessage.swift | 65 ++++++++++++++++++ .../Internal API/MultipeerConnection.swift | 36 +++++++--- .../Internal API/MultipeerProtocol.swift | 14 ++++ .../Public API/MultipeerConfiguration.swift | 2 +- .../Public API/MultipeerReceiver.swift | 51 -------------- .../Public API/MultipeerTransceiver.swift | 68 +++++++++++++++++++ Tests/LinuxMain.swift | 7 -- .../MultipeerKitTests/MultipeerKitTests.swift | 52 ++++++++++++-- 10 files changed, 247 insertions(+), 76 deletions(-) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 Sources/MultipeerKit/Internal API/MockMultipeerConnection.swift create mode 100644 Sources/MultipeerKit/Internal API/Models/MultipeerMessage.swift create mode 100644 Sources/MultipeerKit/Internal API/MultipeerProtocol.swift delete mode 100644 Sources/MultipeerKit/Public API/MultipeerReceiver.swift create mode 100644 Sources/MultipeerKit/Public API/MultipeerTransceiver.swift delete mode 100644 Tests/LinuxMain.swift diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Sources/MultipeerKit/Internal API/MockMultipeerConnection.swift b/Sources/MultipeerKit/Internal API/MockMultipeerConnection.swift new file mode 100644 index 0000000..aaa3c6b --- /dev/null +++ b/Sources/MultipeerKit/Internal API/MockMultipeerConnection.swift @@ -0,0 +1,21 @@ +import Foundation + +final class MockMultipeerConnection: MultipeerProtocol { + + var didReceiveData: ((Data, PeerName) -> Void)? + + var isRunning = false + + func resume() { + isRunning = true + } + + func stop() { + isRunning = false + } + + func broadcast(_ data: Data) throws { + didReceiveData?(data, "MockPeer") + } + +} diff --git a/Sources/MultipeerKit/Internal API/Models/MultipeerMessage.swift b/Sources/MultipeerKit/Internal API/Models/MultipeerMessage.swift new file mode 100644 index 0000000..3935184 --- /dev/null +++ b/Sources/MultipeerKit/Internal API/Models/MultipeerMessage.swift @@ -0,0 +1,65 @@ +import Foundation + +struct MultipeerMessage: Codable { + let type: String + let payload: Any? + + init(type: String, payload: Any) { + self.type = type + self.payload = payload + } + + enum CodingKeys: String, CodingKey { + case type + case payload + } + + private typealias MessageDecoder = (KeyedDecodingContainer) throws -> Any + private typealias MessageEncoder = (Any, inout KeyedEncodingContainer) throws -> Void + + private static var decoders: [String: MessageDecoder] = [:] + private static var encoders: [String: MessageEncoder] = [:] + + static func register(_ type: T.Type, for typeName: String, closure: @escaping (T) -> Void) { + decoders[typeName] = { container in + let payload = try container.decode(T.self, forKey: .payload) + + DispatchQueue.main.async { closure(payload) } + + return payload + } + + encoders[typeName] = { payload, container in + try container.encode(payload as! T, forKey: .payload) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try container.decode(String.self, forKey: .type) + + if let decode = Self.decoders[type] { + payload = try decode(container) + } else { + payload = nil + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(type, forKey: .type) + + if let payload = self.payload { + guard let encode = Self.encoders[type] else { + let context = EncodingError.Context(codingPath: [], debugDescription: "Invalid payload type: \(type).") + throw EncodingError.invalidValue(self, context) + } + + try encode(payload, &container) + } else { + try container.encodeNil(forKey: .payload) + } + } + +} diff --git a/Sources/MultipeerKit/Internal API/MultipeerConnection.swift b/Sources/MultipeerKit/Internal API/MultipeerConnection.swift index 0712352..e9682be 100644 --- a/Sources/MultipeerKit/Internal API/MultipeerConnection.swift +++ b/Sources/MultipeerKit/Internal API/MultipeerConnection.swift @@ -2,33 +2,34 @@ import Foundation import MultipeerConnectivity import os.log -final class MultipeerConnection: NSObject { +final class MultipeerConnection: NSObject, MultipeerProtocol { - enum Mode: Int { + enum Mode: Int, CaseIterable { case receiver case transmitter } private let log = MultipeerKit.log(for: MultipeerConnection.self) - let mode: Mode + let modes: [Mode] let configuration: MultipeerConfiguration let me: MCPeerID - init(mode: Mode, configuration: MultipeerConfiguration = .default) { - self.mode = mode + init(modes: [Mode] = Mode.allCases, configuration: MultipeerConfiguration = .default) { + self.modes = modes self.configuration = configuration self.me = MCPeerID.fetchOrCreate(with: configuration) } - var didReceiveData: ((Data, MCPeerID) -> Void)? + var didReceiveData: ((Data, PeerName) -> Void)? func resume() { os_log("%{public}@", log: log, type: .debug, #function) - if mode == .receiver { + if modes.contains(.receiver) { advertiser.startAdvertisingPeer() - } else { + } + if modes.contains(.transmitter) { browser.startBrowsingForPeers() } } @@ -36,9 +37,10 @@ final class MultipeerConnection: NSObject { func stop() { os_log("%{public}@", log: log, type: .debug, #function) - if mode == .receiver { + if modes.contains(.receiver) { advertiser.stopAdvertisingPeer() - } else { + } + if modes.contains(.transmitter) { browser.stopBrowsingForPeers() } } @@ -68,6 +70,15 @@ final class MultipeerConnection: NSObject { return a }() + func broadcast(_ data: Data) throws { + guard !session.connectedPeers.isEmpty else { + os_log("Not broadcasting message: no connected peers", log: self.log, type: .error) + return + } + + try session.send(data, toPeers: session.connectedPeers, with: .reliable) + } + } // MARK: - Session delegate @@ -81,7 +92,7 @@ extension MultipeerConnection: MCSessionDelegate { func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { os_log("%{public}@", log: log, type: .debug, #function) - didReceiveData?(data, peerID) + didReceiveData?(data, peerID.displayName) } func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) { @@ -104,6 +115,9 @@ extension MultipeerConnection: MCNearbyServiceBrowserDelegate { func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) { os_log("%{public}@", log: log, type: .debug, #function) + + #warning("TODO: Add public API that can list/observe peers and customize invitation") + browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10.0) } func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { diff --git a/Sources/MultipeerKit/Internal API/MultipeerProtocol.swift b/Sources/MultipeerKit/Internal API/MultipeerProtocol.swift new file mode 100644 index 0000000..6526911 --- /dev/null +++ b/Sources/MultipeerKit/Internal API/MultipeerProtocol.swift @@ -0,0 +1,14 @@ +import Foundation + +typealias PeerName = String + +protocol MultipeerProtocol: AnyObject { + + var didReceiveData: ((Data, PeerName) -> Void)? { get set } + + func resume() + func stop() + + func broadcast(_ data: Data) throws + +} diff --git a/Sources/MultipeerKit/Public API/MultipeerConfiguration.swift b/Sources/MultipeerKit/Public API/MultipeerConfiguration.swift index e7f4313..89c36cd 100644 --- a/Sources/MultipeerKit/Public API/MultipeerConfiguration.swift +++ b/Sources/MultipeerKit/Public API/MultipeerConfiguration.swift @@ -14,7 +14,7 @@ public struct MultipeerConfiguration { } public static let `default` = MultipeerConfiguration( - serviceType: "MultipeerKit", + serviceType: "MKSVC", peerName: MCPeerID.defaultDisplayName, defaults: .standard ) diff --git a/Sources/MultipeerKit/Public API/MultipeerReceiver.swift b/Sources/MultipeerKit/Public API/MultipeerReceiver.swift deleted file mode 100644 index 4eb6c19..0000000 --- a/Sources/MultipeerKit/Public API/MultipeerReceiver.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Foundation -import MultipeerConnectivity.MCPeerID -import os.log - -struct ReceiveHandler { - let handle: (T) -> Void -} - -struct MultipeerMessage: Hashable, Codable { - let typeName: String - let payload: Data? -} - -public final class MultipeerReceiver { - - private let log = MultipeerKit.log(for: MultipeerReceiver.self) - - private let connection: MultipeerConnection - - public init(configuration: MultipeerConfiguration = .default) { - self.connection = MultipeerConnection( - mode: .receiver, - configuration: configuration - ) - - connection.didReceiveData = { [weak self] data, peer in - self?.handleDataReceived(data, from: peer) - } - } - - public func receive(_ type: T.Type, using closure: @escaping (T) -> Void) { - // Problem: need to store the handler for later use. How do we do that, given that the - // handler is generic? 🤔 - } - - public func resume() { - connection.resume() - } - - public func stop() { - connection.stop() - } - - private func handleDataReceived(_ data: Data, from peer: MCPeerID) { - os_log("%{public}@", log: log, type: .debug, #function) - // Here we don't know the type of data so that we can find the correct - // handler for it. Encapsulate every message as a `MultipeerMessage`? 🤔 - // If we do encapsulate, how do we then fetch the correct handler to call? - } - -} diff --git a/Sources/MultipeerKit/Public API/MultipeerTransceiver.swift b/Sources/MultipeerKit/Public API/MultipeerTransceiver.swift new file mode 100644 index 0000000..bddd1c4 --- /dev/null +++ b/Sources/MultipeerKit/Public API/MultipeerTransceiver.swift @@ -0,0 +1,68 @@ +import Foundation +import MultipeerConnectivity.MCPeerID +import os.log + +public final class MultipeerTransceiver { + + private let log = MultipeerKit.log(for: MultipeerTransceiver.self) + + let connection: MultipeerProtocol + + public init(configuration: MultipeerConfiguration = .default) { + self.connection = MultipeerConnection( + modes: MultipeerConnection.Mode.allCases, + configuration: configuration + ) + + configure(connection) + } + + init(connection: MultipeerProtocol) { + self.connection = connection + + configure(connection) + } + + private func configure(_ connection: MultipeerProtocol) { + connection.didReceiveData = { [weak self] data, peer in + self?.handleDataReceived(data, from: peer) + } + } + + public func receive(_ type: T.Type, using closure: @escaping (T) -> Void) { + MultipeerMessage.register(type, for: String(describing: type), closure: closure) + } + + public func resume() { + connection.resume() + } + + public func stop() { + connection.stop() + } + + public func broadcast(_ payload: T) { + do { + let message = MultipeerMessage(type: String(describing: T.self), payload: payload) + + let data = try JSONEncoder().encode(message) + + try connection.broadcast(data) + } catch { + os_log("Failed to send payload %@: %{public}@", log: self.log, type: .error, String(describing: payload), String(describing: error)) + } + } + + private func handleDataReceived(_ data: Data, from peer: PeerName) { + os_log("%{public}@", log: log, type: .debug, #function) + + do { + let message = try JSONDecoder().decode(MultipeerMessage.self, from: data) + + os_log("Received message %@", log: self.log, type: .debug, String(describing: message)) + } catch { + os_log("Failed to decode message: %{public}@", log: self.log, type: .error, String(describing: error)) + } + } + +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index e13d0fe..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import MultipeerKitTests - -var tests = [XCTestCaseEntry]() -tests += MultipeerKitTests.allTests() -XCTMain(tests) diff --git a/Tests/MultipeerKitTests/MultipeerKitTests.swift b/Tests/MultipeerKitTests/MultipeerKitTests.swift index 3d736f7..6e993a8 100644 --- a/Tests/MultipeerKitTests/MultipeerKitTests.swift +++ b/Tests/MultipeerKitTests/MultipeerKitTests.swift @@ -1,15 +1,55 @@ import XCTest @testable import MultipeerKit +fileprivate extension MultipeerTransceiver { + var mockConnection: MockMultipeerConnection { + connection as! MockMultipeerConnection + } +} + +fileprivate struct TestPayload: Hashable, Codable { + let n: Int +} + final class MultipeerKitTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(MultipeerKit().text, "Hello, World!") + + private func makeMockTransceiver() -> MultipeerTransceiver { + MultipeerTransceiver(connection: MockMultipeerConnection()) + } + + func testCallingResumeResumesConnection() { + let mock = makeMockTransceiver() + mock.resume() + XCTAssertEqual(mock.mockConnection.isRunning, true) + } + + func testCallingStopStopsConnection() { + let mock = makeMockTransceiver() + mock.resume() + mock.stop() + XCTAssertEqual(mock.mockConnection.isRunning, false) + } + + func testReceivingCustomPayload() { + let mock = makeMockTransceiver() + let tsPayload = TestPayload(n: 42) + + let expect = XCTestExpectation(description: "Receive payload") + + mock.receive(TestPayload.self) { p in + XCTAssertEqual(p, tsPayload) + + expect.fulfill() + } + + mock.broadcast(tsPayload) + + wait(for: [expect], timeout: 2) } static var allTests = [ - ("testExample", testExample), + ("testCallingResumeResumesConnection", testCallingResumeResumesConnection), + ("testCallingStopStopsConnection", testCallingStopStopsConnection), + ("testReceivingCustomPayload", testReceivingCustomPayload), ] }