diff --git a/Example/AblyChatExample/ContentView.swift b/Example/AblyChatExample/ContentView.swift index 750c754d..a0de7edf 100644 --- a/Example/AblyChatExample/ContentView.swift +++ b/Example/AblyChatExample/ContentView.swift @@ -4,7 +4,7 @@ import SwiftUI struct ContentView: View { /// Just used to check that we can successfully import and use the AblyChat library. TODO remove this once we start building the library @State private var ablyChatClient = DefaultChatClient( - realtime: MockRealtime(key: ""), + realtime: MockRealtime.create(), clientOptions: ClientOptions() ) diff --git a/Example/AblyChatExample/Mocks/MockRealtime.swift b/Example/AblyChatExample/Mocks/MockRealtime.swift index 8aa0d015..21a1a347 100644 --- a/Example/AblyChatExample/Mocks/MockRealtime.swift +++ b/Example/AblyChatExample/Mocks/MockRealtime.swift @@ -1,12 +1,15 @@ import Ably +import Foundation /// A mock implementation of `ARTRealtimeProtocol`. It only exists so that we can construct an instance of `DefaultChatClient` without needing to create a proper `ARTRealtime` instance (which we can’t yet do because we don’t have a method for inserting an API key into the example app). TODO remove this once we start building the example app -class MockRealtime: NSObject, ARTRealtimeProtocol { +final class MockRealtime: NSObject, ARTRealtimeProtocol, Sendable { var device: ARTLocalDevice { fatalError("Not implemented") } - var clientId: String? + var clientId: String? { + fatalError("Not implemented") + } required init(options _: ARTClientOptions) {} @@ -14,6 +17,15 @@ class MockRealtime: NSObject, ARTRealtimeProtocol { required init(token _: String) {} + /** + Creates an instance of MockRealtime. + + This exists to give a convenient way to create an instance, because `init` is marked as unavailable in `ARTRealtimeProtocol`. + */ + static func create() -> MockRealtime { + MockRealtime(key: "") + } + func time(_: @escaping ARTDateTimeCallback) { fatalError("Not implemented") } diff --git a/Sources/AblyChat/ChatClient.swift b/Sources/AblyChat/ChatClient.swift index aa919a4d..87c79f28 100644 --- a/Sources/AblyChat/ChatClient.swift +++ b/Sources/AblyChat/ChatClient.swift @@ -1,4 +1,4 @@ -import Ably +@preconcurrency import Ably public protocol ChatClient: AnyObject, Sendable { var rooms: any Rooms { get } @@ -8,28 +8,22 @@ public protocol ChatClient: AnyObject, Sendable { var clientOptions: ClientOptions { get } } -public final class DefaultChatClient: ChatClient { - public init(realtime _: ARTRealtimeProtocol, clientOptions _: ClientOptions?) { - // This one doesn’t do `fatalError`, so that I can call it in the example app - } - - public var rooms: any Rooms { - fatalError("Not yet implemented") - } - - public var connection: any Connection { - fatalError("Not yet implemented") - } +public actor DefaultChatClient: ChatClient { + public let realtime: ARTRealtimeProtocol + public nonisolated let clientOptions: ClientOptions + public nonisolated let rooms: Rooms - public var clientID: String { - fatalError("Not yet implemented") + public init(realtime: ARTRealtimeProtocol, clientOptions: ClientOptions?) { + self.realtime = realtime + self.clientOptions = clientOptions ?? .init() + rooms = DefaultRooms(realtime: realtime, clientOptions: self.clientOptions) } - public var realtime: any ARTRealtimeProtocol { + public nonisolated var connection: any Connection { fatalError("Not yet implemented") } - public var clientOptions: ClientOptions { + public nonisolated var clientID: String { fatalError("Not yet implemented") } } @@ -42,4 +36,9 @@ public struct ClientOptions: Sendable { self.logHandler = logHandler self.logLevel = logLevel } + + /// Used for comparing these instances in tests without having to make this Equatable, which I’m not yet sure makes sense (we’ll decide in https://github.com/ably-labs/ably-chat-swift/issues/10) + internal func isEqualForTestPurposes(_ other: ClientOptions) -> Bool { + logHandler === other.logHandler && logLevel == other.logLevel + } } diff --git a/Sources/AblyChat/Rooms.swift b/Sources/AblyChat/Rooms.swift index f09478a4..c3e48e5b 100644 --- a/Sources/AblyChat/Rooms.swift +++ b/Sources/AblyChat/Rooms.swift @@ -1,5 +1,26 @@ +import Ably + public protocol Rooms: AnyObject, Sendable { func get(roomID: String, options: RoomOptions) throws -> any Room func release(roomID: String) async throws var clientOptions: ClientOptions { get } } + +internal actor DefaultRooms: Rooms { + /// Exposed so that we can test it. + internal nonisolated let realtime: ARTRealtimeProtocol + internal nonisolated let clientOptions: ClientOptions + + internal init(realtime: ARTRealtimeProtocol, clientOptions: ClientOptions) { + self.realtime = realtime + self.clientOptions = clientOptions + } + + internal nonisolated func get(roomID _: String, options _: RoomOptions) throws -> any Room { + fatalError("Not yet implemented") + } + + internal func release(roomID _: String) async throws { + fatalError("Not yet implemented") + } +} diff --git a/Tests/AblyChatTests/AblyChatTests.swift b/Tests/AblyChatTests/AblyChatTests.swift deleted file mode 100644 index c8c5c0bc..00000000 --- a/Tests/AblyChatTests/AblyChatTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -@testable import AblyChat -import XCTest - -final class AblyChatTests: XCTestCase { - func testExample() throws { - XCTAssertNoThrow(DefaultChatClient(realtime: MockRealtime(key: ""), clientOptions: ClientOptions())) - } -} diff --git a/Tests/AblyChatTests/DefaultChatClientTests.swift b/Tests/AblyChatTests/DefaultChatClientTests.swift new file mode 100644 index 00000000..9798d50d --- /dev/null +++ b/Tests/AblyChatTests/DefaultChatClientTests.swift @@ -0,0 +1,27 @@ +@testable import AblyChat +import XCTest + +class DefaultChatClientTests: XCTestCase { + func test_init_withoutClientOptions() { + // Given: An instance of DefaultChatClient is created with nil clientOptions + let client = DefaultChatClient(realtime: MockRealtime.create(), clientOptions: nil) + + // Then: It uses the default client options + let defaultOptions = ClientOptions() + XCTAssertTrue(client.clientOptions.isEqualForTestPurposes(defaultOptions)) + } + + func test_rooms() throws { + // Given: An instance of DefaultChatClient + let realtime = MockRealtime.create() + let options = ClientOptions() + let client = DefaultChatClient(realtime: realtime, clientOptions: options) + + // Then: Its `rooms` property returns an instance of DefaultRooms with the same realtime client and client options + let rooms = client.rooms + + let defaultRooms = try XCTUnwrap(rooms as? DefaultRooms) + XCTAssertIdentical(defaultRooms.realtime, realtime) + XCTAssertTrue(defaultRooms.clientOptions.isEqualForTestPurposes(options)) + } +} diff --git a/Tests/AblyChatTests/Mocks/MockRealtime.swift b/Tests/AblyChatTests/Mocks/MockRealtime.swift index f49b06c7..d0f8c4a9 100644 --- a/Tests/AblyChatTests/Mocks/MockRealtime.swift +++ b/Tests/AblyChatTests/Mocks/MockRealtime.swift @@ -2,12 +2,14 @@ import Ably import Foundation /// A mock implementation of `ARTRealtimeProtocol`. Copied from the class of the same name in the example app. We’ll figure out how to do mocking in tests properly in https://github.com/ably-labs/ably-chat-swift/issues/5. -class MockRealtime: NSObject, ARTRealtimeProtocol { +final class MockRealtime: NSObject, ARTRealtimeProtocol, Sendable { var device: ARTLocalDevice { fatalError("Not implemented") } - var clientId: String? + var clientId: String? { + fatalError("Not implemented") + } required init(options _: ARTClientOptions) {} @@ -15,6 +17,15 @@ class MockRealtime: NSObject, ARTRealtimeProtocol { required init(token _: String) {} + /** + Creates an instance of MockRealtime. + + This exists to give a convenient way to create an instance, because `init` is marked as unavailable in `ARTRealtimeProtocol`. + */ + static func create() -> MockRealtime { + MockRealtime(key: "") + } + func time(_: @escaping ARTDateTimeCallback) { fatalError("Not implemented") }