-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement the ability to fetch a room
Part of #19. References to spec are based on [1] at commit aa7455d. I’ve decided to, for now, throw ably-cocoa’s ARTErrorInfo for consistency with JS; created #32 to revisit this later. We have decided (see [3]) that we’re going to try using actors as our mechanism for concurrency-safe management of mutable state. We accept that this will lead to more of the public API needing to be annotated as `async` (as has happened to Rooms.get here), which in some cases might lead to weird-looking API, and have chosen to accept this compromise in order to get the safety checking offered to us by the compiler, and because of developers’ aversion to writing "@unchecked Sendable". We might not have needed to make this compromise if we had access to Swift 6 / iOS 18’s Mutex type, which allows for synchronous management of mutable state in a way that the compiler is happy with. But, none of the decisions here need to be final; we can see how we feel about the API as it evolves and as our knowledge of the language grows. [1] ably/specification#200 [2] ably/ably-cocoa#1962 [3] #33 (comment)
- Loading branch information
1 parent
3c35f4f
commit c8c85ce
Showing
6 changed files
with
219 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
@testable import AblyChat | ||
import XCTest | ||
|
||
class DefaultRoomsTests: XCTestCase { | ||
// @spec CHA-RC1a | ||
func test_get_returnsRoomWithGivenID() async throws { | ||
// Given: an instance of DefaultRooms | ||
let realtime = MockRealtime.create() | ||
let rooms = DefaultRooms(realtime: realtime, clientOptions: .init()) | ||
|
||
// When: get(roomID:options:) is called | ||
let roomID = "basketball" | ||
let options = RoomOptions() | ||
let room = try await rooms.get(roomID: roomID, options: options) | ||
|
||
// Then: It returns a DefaultRoom instance that uses the same Realtime instance, with the given ID and options | ||
let defaultRoom = try XCTUnwrap(room as? DefaultRoom) | ||
XCTAssertIdentical(defaultRoom.realtime, realtime) | ||
XCTAssertEqual(defaultRoom.roomID, roomID) | ||
XCTAssertEqual(defaultRoom.options, options) | ||
} | ||
|
||
// @spec CHA-RC1b | ||
func test_get_returnsExistingRoomWithGivenID() async throws { | ||
// Given: an instance of DefaultRooms, on which get(roomID:options:) has already been called with a given ID | ||
let realtime = MockRealtime.create() | ||
let rooms = DefaultRooms(realtime: realtime, clientOptions: .init()) | ||
|
||
let roomID = "basketball" | ||
let options = RoomOptions() | ||
let firstRoom = try await rooms.get(roomID: roomID, options: options) | ||
|
||
// When: get(roomID:options:) is called with the same room ID | ||
let secondRoom = try await rooms.get(roomID: roomID, options: options) | ||
|
||
// Then: It returns the same room object | ||
XCTAssertIdentical(secondRoom, firstRoom) | ||
} | ||
|
||
// @spec CHA-RC1c | ||
func test_get_throwsErrorWhenOptionsDoNotMatch() async throws { | ||
// Given: an instance of DefaultRooms, on which get(roomID:options:) has already been called with a given ID and options | ||
let realtime = MockRealtime.create() | ||
let rooms = DefaultRooms(realtime: realtime, clientOptions: .init()) | ||
|
||
let roomID = "basketball" | ||
let options = RoomOptions() | ||
_ = try await rooms.get(roomID: roomID, options: options) | ||
|
||
// When: get(roomID:options:) is called with the same ID but different options | ||
let differentOptions = RoomOptions(presence: .init(subscribe: false)) | ||
|
||
let caughtError: Error? | ||
do { | ||
_ = try await rooms.get(roomID: roomID, options: differentOptions) | ||
caughtError = nil | ||
} catch { | ||
caughtError = error | ||
} | ||
|
||
// Then: It throws an inconsistentRoomOptions error | ||
try assertIsChatError(caughtError, withCode: .inconsistentRoomOptions) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import Ably | ||
@testable import AblyChat | ||
import XCTest | ||
|
||
/** | ||
Asserts that a given optional `Error` is an `ARTErrorInfo` in the chat error domain with a given code. | ||
*/ | ||
func assertIsChatError(_ maybeError: (any Error)?, withCode code: AblyChat.ErrorCode, file: StaticString = #filePath, line: UInt = #line) throws { | ||
let error = try XCTUnwrap(maybeError, "Expected an error", file: file, line: line) | ||
let ablyError = try XCTUnwrap(error as? ARTErrorInfo, "Expected an ARTErrorInfo", file: file, line: line) | ||
|
||
XCTAssertEqual(ablyError.domain, AblyChat.errorDomain as String, file: file, line: line) | ||
XCTAssertEqual(ablyError.code, code.rawValue, file: file, line: line) | ||
XCTAssertEqual(ablyError.statusCode, code.statusCode, file: file, line: line) | ||
} |