Skip to content

Commit

Permalink
Merge pull request #33 from ably-labs/19-fetch-room
Browse files Browse the repository at this point in the history
[ECO-4936] Implement the ability to fetch a room
  • Loading branch information
lawrence-forooghian authored Sep 2, 2024
2 parents bc06747 + 7d6acde commit 0e9f703
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 35 deletions.
2 changes: 1 addition & 1 deletion Example/AblyChatExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)

Expand Down
15 changes: 13 additions & 2 deletions Example/AblyChatExample/Mocks/MockRealtime.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import Ably

/// 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) {}

required init(key _: String) {}

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")
}
Expand Down
33 changes: 17 additions & 16 deletions Sources/AblyChat/ChatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,28 @@ public protocol ChatClient: AnyObject, Sendable {
var rooms: any Rooms { get }
var connection: any Connection { get }
var clientID: String { get }
var realtime: any ARTRealtimeProtocol { get }
var realtime: RealtimeClient { get }
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 typealias RealtimeClient = any(ARTRealtimeProtocol & Sendable)

public var rooms: any Rooms {
fatalError("Not yet implemented")
}
public actor DefaultChatClient: ChatClient {
public let realtime: RealtimeClient
public nonisolated let clientOptions: ClientOptions
public nonisolated let rooms: Rooms

public var connection: any Connection {
fatalError("Not yet implemented")
public init(realtime: RealtimeClient, clientOptions: ClientOptions?) {
self.realtime = realtime
self.clientOptions = clientOptions ?? .init()
rooms = DefaultRooms(realtime: realtime, clientOptions: self.clientOptions)
}

public var clientID: String {
public nonisolated var connection: any Connection {
fatalError("Not yet implemented")
}

public var realtime: any ARTRealtimeProtocol {
fatalError("Not yet implemented")
}

public var clientOptions: ClientOptions {
public nonisolated var clientID: String {
fatalError("Not yet implemented")
}
}
Expand All @@ -42,4 +38,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
}
}
67 changes: 67 additions & 0 deletions Sources/AblyChat/Errors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Ably

/**
The error domain used for the ``Ably.ARTErrorInfo`` error instances thrown by the Ably Chat SDK.
See ``ErrorCode`` for the possible ``ARTErrorInfo.code`` values.
*/
public let errorDomain = "AblyChatErrorDomain"

/**
The error codes for errors in the ``errorDomain`` error domain.
*/
public enum ErrorCode: Int {
/// ``Rooms.get(roomID:options:)`` was called with a different set of room options than was used on a previous call. You must first release the existing room instance using ``Rooms.release(roomID:)``.
///
/// TODO this code is a guess, revisit in https://github.com/ably-labs/ably-chat-swift/issues/32
case inconsistentRoomOptions = 1

/// The ``ARTErrorInfo.statusCode`` that should be returned for this error.
internal var statusCode: Int {
// TODO: These are currently a guess, revisit in https://github.com/ably-labs/ably-chat-swift/issues/32
switch self {
case .inconsistentRoomOptions:
400
}
}
}

/**
The errors thrown by the Chat SDK.
This type exists in addition to ``ErrorCode`` to allow us to attach metadata which can be incorporated into the error’s `localizedDescription`.
*/
internal enum ChatError {
case inconsistentRoomOptions(requested: RoomOptions, existing: RoomOptions)

/// The ``ARTErrorInfo.code`` that should be returned for this error.
internal var code: ErrorCode {
switch self {
case .inconsistentRoomOptions:
.inconsistentRoomOptions
}
}

/// The ``ARTErrorInfo.localizedDescription`` that should be returned for this error.
internal var localizedDescription: String {
switch self {
case let .inconsistentRoomOptions(requested, existing):
"Rooms.get(roomID:options:) was called with a different set of room options than was used on a previous call. You must first release the existing room instance using Rooms.release(roomID:). Requested options: \(requested), existing options: \(existing)"
}
}
}

internal extension ARTErrorInfo {
convenience init(chatError: ChatError) {
var userInfo: [String: Any] = [:]
// TODO: copied and pasted from implementation of -[ARTErrorInfo createWithCode:status:message:requestId:] because there’s no way to pass domain; revisit in https://github.com/ably-labs/ably-chat-swift/issues/32. Also the ARTErrorInfoStatusCode variable in ably-cocoa is not public.
userInfo["ARTErrorInfoStatusCode"] = chatError.code.statusCode
userInfo[NSLocalizedDescriptionKey] = chatError.localizedDescription

self.init(
domain: errorDomain,
code: chatError.code.rawValue,
userInfo: userInfo
)
}
}
48 changes: 48 additions & 0 deletions Sources/AblyChat/Room.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Ably

public protocol Room: AnyObject, Sendable {
var roomID: String { get }
var messages: any Messages { get }
Expand All @@ -14,3 +16,49 @@ public protocol Room: AnyObject, Sendable {
func detach() async throws
var options: RoomOptions { get }
}

internal actor DefaultRoom: Room {
internal nonisolated let roomID: String
internal nonisolated let options: RoomOptions

// Exposed for testing.
internal nonisolated let realtime: RealtimeClient

internal init(realtime: RealtimeClient, roomID: String, options: RoomOptions) {
self.realtime = realtime
self.roomID = roomID
self.options = options
}

public nonisolated var messages: any Messages {
fatalError("Not yet implemented")
}

public nonisolated var presence: any Presence {
fatalError("Not yet implemented")
}

public nonisolated var reactions: any RoomReactions {
fatalError("Not yet implemented")
}

public nonisolated var typing: any Typing {
fatalError("Not yet implemented")
}

public nonisolated var occupancy: any Occupancy {
fatalError("Not yet implemented")
}

public nonisolated var status: any RoomStatus {
fatalError("Not yet implemented")
}

public func attach() async throws {
fatalError("Not yet implemented")
}

public func detach() async throws {
fatalError("Not yet implemented")
}
}
10 changes: 5 additions & 5 deletions Sources/AblyChat/RoomOptions.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public struct RoomOptions: Sendable {
public struct RoomOptions: Sendable, Equatable {
public var presence: PresenceOptions?
public var typing: TypingOptions?
public var reactions: RoomReactionsOptions?
Expand All @@ -14,7 +14,7 @@ public struct RoomOptions: Sendable {
}
}

public struct PresenceOptions: Sendable {
public struct PresenceOptions: Sendable, Equatable {
public var enter = true
public var subscribe = true

Expand All @@ -24,18 +24,18 @@ public struct PresenceOptions: Sendable {
}
}

public struct TypingOptions: Sendable {
public struct TypingOptions: Sendable, Equatable {
public var timeout: TimeInterval = 10

public init(timeout: TimeInterval = 10) {
self.timeout = timeout
}
}

public struct RoomReactionsOptions: Sendable {
public struct RoomReactionsOptions: Sendable, Equatable {
public init() {}
}

public struct OccupancyOptions: Sendable {
public struct OccupancyOptions: Sendable, Equatable {
public init() {}
}
39 changes: 38 additions & 1 deletion Sources/AblyChat/Rooms.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
import Ably

public protocol Rooms: AnyObject, Sendable {
func get(roomID: String, options: RoomOptions) throws -> any Room
func get(roomID: String, options: RoomOptions) async 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: RealtimeClient
internal nonisolated let clientOptions: ClientOptions

/// The set of rooms, keyed by room ID.
private var rooms: [String: DefaultRoom] = [:]

internal init(realtime: RealtimeClient, clientOptions: ClientOptions) {
self.realtime = realtime
self.clientOptions = clientOptions
}

internal func get(roomID: String, options: RoomOptions) throws -> any Room {
// CHA-RC1b
if let existingRoom = rooms[roomID] {
if existingRoom.options != options {
throw ARTErrorInfo(
chatError: .inconsistentRoomOptions(requested: options, existing: existingRoom.options)
)
}

return existingRoom
} else {
let room = DefaultRoom(realtime: realtime, roomID: roomID, options: options)
rooms[roomID] = room
return room
}
}

internal func release(roomID _: String) async throws {
fatalError("Not yet implemented")
}
}
8 changes: 0 additions & 8 deletions Tests/AblyChatTests/AblyChatTests.swift

This file was deleted.

27 changes: 27 additions & 0 deletions Tests/AblyChatTests/DefaultChatClientTests.swift
Original file line number Diff line number Diff line change
@@ -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))
}
}
Loading

0 comments on commit 0e9f703

Please sign in to comment.