Skip to content

Commit a1e16c6

Browse files
Implement the ability to fetch a room
Part of #19. References to spec are based on [1] at commit aa7455d. The @preconcurrency imports of ably-cocoa are temporary and will be removed once [2] is done; created #31 for tracking. I’ve decided to, for now, throw ably-cocoa’s ARTErrorInfo for consistency with JS; created #32 to revisit this later. [1] ably/specification#200 [2] ably/ably-cocoa#1962
1 parent bc06747 commit a1e16c6

File tree

8 files changed

+268
-6
lines changed

8 files changed

+268
-6
lines changed

Sources/AblyChat/Errors.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import Ably
2+
3+
/**
4+
The error domain used for the ``Ably.ARTErrorInfo`` error instances thrown by the Ably Chat SDK.
5+
6+
See ``ErrorCode`` for the possible ``ARTErrorInfo.code`` values.
7+
*/
8+
public let errorDomain: NSString = "AblyChatErrorDomain"
9+
10+
/**
11+
The error codes for errors in the ``errorDomain`` error domain.
12+
*/
13+
public enum ErrorCode: Int {
14+
/// ``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:)``.
15+
///
16+
/// TODO this code is a guess, revisit in https://github.com/ably-labs/ably-chat-swift/issues/32
17+
case inconsistentRoomOptions = 1
18+
19+
/// The ``ARTErrorInfo.statusCode`` that should be returned for this error.
20+
internal var statusCode: Int {
21+
// TODO: These are currently a guess, revisit in https://github.com/ably-labs/ably-chat-swift/issues/32
22+
switch self {
23+
case .inconsistentRoomOptions:
24+
400
25+
}
26+
}
27+
}
28+
29+
/**
30+
The errors thrown by the Chat SDK.
31+
32+
This type exists in addition to ``ErrorCode`` to allow us to attach metadata which can be incorporated into the error’s `localizedDescription`.
33+
*/
34+
internal enum ChatError {
35+
case inconsistentRoomOptions(requested: RoomOptions, existing: RoomOptions)
36+
37+
/// The ``ARTErrorInfo.code`` that should be returned for this error.
38+
internal var code: ErrorCode {
39+
switch self {
40+
case .inconsistentRoomOptions:
41+
.inconsistentRoomOptions
42+
}
43+
}
44+
45+
/// The ``ARTErrorInfo.localizedDescription`` that should be returned for this error.
46+
internal var localizedDescription: String {
47+
switch self {
48+
case let .inconsistentRoomOptions(requested, existing):
49+
"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)"
50+
}
51+
}
52+
}
53+
54+
internal extension ARTErrorInfo {
55+
convenience init(chatError: ChatError) {
56+
var userInfo: [String: Any] = [:]
57+
// 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.
58+
userInfo["ARTErrorInfoStatusCode"] = chatError.code.statusCode
59+
userInfo[NSLocalizedDescriptionKey] = chatError.localizedDescription
60+
61+
self.init(
62+
domain: errorDomain as String,
63+
code: chatError.code.rawValue,
64+
userInfo: userInfo
65+
)
66+
}
67+
}

Sources/AblyChat/Room.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@preconcurrency import Ably
2+
13
public protocol Room: AnyObject, Sendable {
24
var roomID: String { get }
35
var messages: any Messages { get }
@@ -14,3 +16,49 @@ public protocol Room: AnyObject, Sendable {
1416
func detach() async throws
1517
var options: RoomOptions { get }
1618
}
19+
20+
public final class DefaultRoom: Room {
21+
public let roomID: String
22+
public let options: RoomOptions
23+
24+
// Exposed for testing.
25+
internal let realtime: any ARTRealtimeProtocol
26+
27+
internal init(realtime: any ARTRealtimeProtocol, roomID: String, options: RoomOptions) {
28+
self.realtime = realtime
29+
self.roomID = roomID
30+
self.options = options
31+
}
32+
33+
public var messages: any Messages {
34+
fatalError("Not yet implemented")
35+
}
36+
37+
public var presence: any Presence {
38+
fatalError("Not yet implemented")
39+
}
40+
41+
public var reactions: any RoomReactions {
42+
fatalError("Not yet implemented")
43+
}
44+
45+
public var typing: any Typing {
46+
fatalError("Not yet implemented")
47+
}
48+
49+
public var occupancy: any Occupancy {
50+
fatalError("Not yet implemented")
51+
}
52+
53+
public var status: any RoomStatus {
54+
fatalError("Not yet implemented")
55+
}
56+
57+
public func attach() async throws {
58+
fatalError("Not yet implemented")
59+
}
60+
61+
public func detach() async throws {
62+
fatalError("Not yet implemented")
63+
}
64+
}

Sources/AblyChat/RoomOptions.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
22

3-
public struct RoomOptions: Sendable {
3+
public struct RoomOptions: Sendable, Equatable {
44
public var presence: PresenceOptions?
55
public var typing: TypingOptions?
66
public var reactions: RoomReactionsOptions?
@@ -14,7 +14,7 @@ public struct RoomOptions: Sendable {
1414
}
1515
}
1616

17-
public struct PresenceOptions: Sendable {
17+
public struct PresenceOptions: Sendable, Equatable {
1818
public var enter = true
1919
public var subscribe = true
2020

@@ -24,18 +24,18 @@ public struct PresenceOptions: Sendable {
2424
}
2525
}
2626

27-
public struct TypingOptions: Sendable {
27+
public struct TypingOptions: Sendable, Equatable {
2828
public var timeout: TimeInterval = 10
2929

3030
public init(timeout: TimeInterval = 10) {
3131
self.timeout = timeout
3232
}
3333
}
3434

35-
public struct RoomReactionsOptions: Sendable {
35+
public struct RoomReactionsOptions: Sendable, Equatable {
3636
public init() {}
3737
}
3838

39-
public struct OccupancyOptions: Sendable {
39+
public struct OccupancyOptions: Sendable, Equatable {
4040
public init() {}
4141
}

Sources/AblyChat/Rooms.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,64 @@
1+
@preconcurrency import Ably
2+
13
public protocol Rooms: AnyObject, Sendable {
24
func get(roomID: String, options: RoomOptions) throws -> any Room
35
func release(roomID: String) async throws
46
var clientOptions: ClientOptions { get }
57
}
8+
9+
internal final class DefaultRooms: Rooms {
10+
private let realtime: ARTRealtimeProtocol
11+
private let storage = Storage()
12+
13+
internal init(realtime: ARTRealtimeProtocol) {
14+
self.realtime = realtime
15+
}
16+
17+
/// Thread-safe storage of the set of rooms.
18+
private class Storage: @unchecked Sendable {
19+
/// Mutex used to synchronize access to ``rooms``.
20+
private let lock = NSLock()
21+
22+
/// The set of rooms, keyed by room ID. Only access whilst holding ``lock``.
23+
private var rooms: [String: DefaultRoom] = [:]
24+
25+
/// If there is an existing room with the given ID, returns it. Else creates a new room with the given ID and options.
26+
public func getOrCreate(realtime: any ARTRealtimeProtocol, roomID: String, options: RoomOptions) -> DefaultRoom {
27+
let room: DefaultRoom
28+
lock.lock()
29+
// CHA-RC1b
30+
if let existingRoom = rooms[roomID] {
31+
room = existingRoom
32+
} else {
33+
room = DefaultRoom(realtime: realtime, roomID: roomID, options: options)
34+
rooms[roomID] = room
35+
}
36+
lock.unlock()
37+
return room
38+
}
39+
}
40+
41+
internal func get(roomID: String, options: RoomOptions) throws -> any Room {
42+
let room = storage.getOrCreate(
43+
realtime: realtime,
44+
roomID: roomID,
45+
options: options
46+
)
47+
48+
if room.options != options {
49+
throw ARTErrorInfo(
50+
chatError: .inconsistentRoomOptions(requested: options, existing: room.options)
51+
)
52+
}
53+
54+
return room
55+
}
56+
57+
internal func release(roomID _: String) async throws {
58+
fatalError("Not yet implemented")
59+
}
60+
61+
internal var clientOptions: ClientOptions {
62+
fatalError("Not yet implemented")
63+
}
64+
}

Tests/AblyChatTests/AblyChatTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import XCTest
33

44
final class AblyChatTests: XCTestCase {
55
func testExample() throws {
6-
XCTAssertNoThrow(DefaultChatClient(realtime: MockRealtime(key: ""), clientOptions: ClientOptions()))
6+
XCTAssertNoThrow(DefaultChatClient(realtime: MockRealtime.create(), clientOptions: ClientOptions()))
77
}
88
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
@testable import AblyChat
2+
import XCTest
3+
4+
class DefaultRoomsTests: XCTestCase {
5+
// @spec CHA-RC1a
6+
func test_get_returnsRoomWithGivenID() throws {
7+
// Given: an instance of DefaultRooms
8+
let realtime = MockRealtime.create()
9+
let rooms = DefaultRooms(realtime: realtime)
10+
11+
// When: get(roomID:options:) is called
12+
let roomID = "basketball"
13+
let options = RoomOptions()
14+
let room = try rooms.get(roomID: roomID, options: options)
15+
16+
// Then: It returns a DefaultRoom instance that uses the same Realtime instance, with the given ID and options
17+
let defaultRoom = try XCTUnwrap(room as? DefaultRoom)
18+
XCTAssertIdentical(defaultRoom.realtime, realtime)
19+
XCTAssertEqual(defaultRoom.roomID, roomID)
20+
XCTAssertEqual(defaultRoom.options, options)
21+
}
22+
23+
// @spec CHA-RC1b
24+
func test_get_returnsExistingRoomWithGivenID() throws {
25+
// Given: an instance of DefaultRooms, on which get(roomID:options:) has already been called with a given ID
26+
let realtime = MockRealtime.create()
27+
let rooms = DefaultRooms(realtime: realtime)
28+
29+
let roomID = "basketball"
30+
let options = RoomOptions()
31+
let firstRoom = try rooms.get(roomID: roomID, options: options)
32+
33+
// When: get(roomID:options:) is called with the same room ID
34+
let secondRoom = try rooms.get(roomID: roomID, options: options)
35+
36+
// Then: It returns the same room object
37+
XCTAssertIdentical(secondRoom, firstRoom)
38+
}
39+
40+
// @spec CHA-RC1c
41+
func test_get_throwsErrorWhenOptionsDoNotMatch() throws {
42+
// Given: an instance of DefaultRooms, on which get(roomID:options:) has already been called with a given ID and options
43+
let realtime = MockRealtime.create()
44+
let rooms = DefaultRooms(realtime: realtime)
45+
46+
let roomID = "basketball"
47+
let options = RoomOptions()
48+
_ = try rooms.get(roomID: roomID, options: options)
49+
50+
// When: get(roomID:options:) is called with the same ID but different options
51+
let differentOptions = RoomOptions(presence: .init(subscribe: false))
52+
53+
let caughtError: Error?
54+
do {
55+
_ = try rooms.get(roomID: roomID, options: differentOptions)
56+
caughtError = nil
57+
} catch {
58+
caughtError = error
59+
}
60+
61+
// Then: It throws an inconsistentRoomOptions error
62+
try assertIsChatError(caughtError, withCode: .inconsistentRoomOptions)
63+
}
64+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Ably
2+
@testable import AblyChat
3+
import XCTest
4+
5+
/**
6+
Asserts that a given optional `Error` is an `ARTErrorInfo` in the chat error domain with a given code.
7+
*/
8+
func assertIsChatError(_ maybeError: (any Error)?, withCode code: AblyChat.ErrorCode, file: StaticString = #filePath, line: UInt = #line) throws {
9+
let error = try XCTUnwrap(maybeError, "Expected an error", file: file, line: line)
10+
let ablyError = try XCTUnwrap(error as? ARTErrorInfo, "Expected an ARTErrorInfo", file: file, line: line)
11+
12+
XCTAssertEqual(ablyError.domain, AblyChat.errorDomain as String, file: file, line: line)
13+
XCTAssertEqual(ablyError.code, code.rawValue, file: file, line: line)
14+
XCTAssertEqual(ablyError.statusCode, code.statusCode, file: file, line: line)
15+
}

Tests/AblyChatTests/Mocks/MockRealtime.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ class MockRealtime: NSObject, ARTRealtimeProtocol {
1515

1616
required init(token _: String) {}
1717

18+
/**
19+
Creates an instance of MockRealtime.
20+
21+
This exists to give a convenient way to create an instance, because `init` is marked as unavailable in `ARTRealtimeProtocol`.
22+
*/
23+
static func create() -> MockRealtime {
24+
MockRealtime(key: "")
25+
}
26+
1827
func time(_: @escaping ARTDateTimeCallback) {
1928
fatalError("Not implemented")
2029
}

0 commit comments

Comments
 (0)