Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECO-4936] Implement the ability to fetch a room #33

Merged
merged 3 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
maratal marked this conversation as resolved.
Show resolved Hide resolved
// CHA-RC1b
if let existingRoom = rooms[roomID] {
if existingRoom.options != options {
throw ARTErrorInfo(
maratal marked this conversation as resolved.
Show resolved Hide resolved
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")
}
lawrence-forooghian marked this conversation as resolved.
Show resolved Hide resolved
}
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