Skip to content
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
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ To check formatting and code quality, run `swift run BuildTool lint`. Run with `
### Throwing errors

- The public API of the SDK should use typed throws, and the thrown errors should be of type `ErrorInfo`.
- Currently, we throw the `InternalError` type everywhere internally, and then convert it to `ErrorInfo` at the public API.
- In order to throw a Chat-specific error (i.e. if you're not just re-throwing an ably-cocoa error) then you should create an `InternalError` and then call its `toErrorInfo()`, as opposed to using `ErrorInfo`'s memberwise initializer.

If you haven't worked with typed throws before, be aware of a few sharp edges:

- Some of the Swift standard library does not (yet?) interact as nicely with typed throws as you might hope.
- It is not currently possible to create a `Task`, `CheckedContinuation`, or `AsyncThrowingStream` with a specific error type. You will need to instead return a `Result` and then call its `.get()` method.
- `Dictionary.mapValues` does not support typed throws. We have our own extension `ablyChat_mapValuesWithTypedThrow` which does; use this.
- There are times when the compiler struggles to infer the type of the error thrown within a `do` block. In these cases, you can disable type inference for a `do` block and explicitly specify the type of the thrown error, like: `do throws(InternalError) { … }`.
- The compiler will never infer the type of the error thrown by a closure; you will need to specify this yourself; e.g. `let items = try jsonValues.map { jsonValue throws(InternalError) in … }`.
- There are times when the compiler struggles to infer the type of the error thrown within a `do` block. In these cases, you can disable type inference for a `do` block and explicitly specify the type of the thrown error, like: `do throws(ErrorInfo) { … }`.
- The compiler will never infer the type of the error thrown by a closure; you will need to specify this yourself; e.g. `let items = try jsonValues.map { jsonValue throws(ErrorInfo) in … }`.

### Swift concurrency rough edges

Expand Down
146 changes: 97 additions & 49 deletions Sources/AblyChat/AblyCocoaExtensions/InternalAblyCocoaTypes.swift

Large diffs are not rendered by default.

34 changes: 17 additions & 17 deletions Sources/AblyChat/ChatAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ internal final class ChatAPI {
}

// (CHA-M6) Messages should be queryable from a paginated REST API.
internal func getMessages(roomName: String, params: HistoryParams) async throws(InternalError) -> some PaginatedResult<Message> {
internal func getMessages(roomName: String, params: HistoryParams) async throws(ErrorInfo) -> some PaginatedResult<Message> {
let endpoint = roomUrl(roomName: roomName, suffix: "/messages")
return try await makePaginatedRequest(endpoint, params: params.asQueryItems())
}

// (CHA-M13) Get a single message by its serial
internal func getMessage(roomName: String, serial: String) async throws(InternalError) -> Message {
internal func getMessage(roomName: String, serial: String) async throws(ErrorInfo) -> Message {
let endpoint = messageUrl(roomName: roomName, serial: serial)
return try await makeRequest(endpoint, method: "GET")
}
Expand All @@ -53,14 +53,14 @@ internal final class ChatAPI {
internal struct MessageReactionResponse: JSONObjectDecodable {
internal let serial: String

internal init(jsonObject: [String: JSONValue]) throws(InternalError) {
internal init(jsonObject: [String: JSONValue]) throws(ErrorInfo) {
serial = try jsonObject.stringValueForKey("serial")
}
}

// (CHA-M3) Messages are sent to Ably via the Chat REST API, using the send method.
// (CHA-M3a) When a message is sent successfully, the caller shall receive a struct representing the Message in response (as if it were received via Realtime event).
internal func sendMessage(roomName: String, params: SendMessageParams) async throws(InternalError) -> Message {
internal func sendMessage(roomName: String, params: SendMessageParams) async throws(ErrorInfo) -> Message {
let endpoint = roomUrl(roomName: roomName, suffix: "/messages")
var body: [String: JSONValue] = ["text": .string(params.text)]

Expand All @@ -79,7 +79,7 @@ internal final class ChatAPI {

// (CHA-M8) A client must be able to update a message in a room.
// (CHA-M8a) A client may update a message via the Chat REST API by calling the update method.
internal func updateMessage(roomName: String, serial: String, updateParams: UpdateMessageParams, details: OperationDetails?) async throws(InternalError) -> Message {
internal func updateMessage(roomName: String, serial: String, updateParams: UpdateMessageParams, details: OperationDetails?) async throws(ErrorInfo) -> Message {
let endpoint = messageUrl(roomName: roomName, serial: serial)
var body: [String: JSONValue] = [:]

Expand Down Expand Up @@ -114,7 +114,7 @@ internal final class ChatAPI {

// (CHA-M9) A client must be able to delete a message in a room.
// (CHA-M9a) A client may delete a message via the Chat REST API by calling the delete method.
internal func deleteMessage(roomName: String, serial: String, details: OperationDetails?) async throws(InternalError) -> Message {
internal func deleteMessage(roomName: String, serial: String, details: OperationDetails?) async throws(ErrorInfo) -> Message {
let endpoint = messageUrl(roomName: roomName, serial: serial, suffix: "/delete")
var body: [String: JSONValue] = [:]

Expand All @@ -131,16 +131,16 @@ internal final class ChatAPI {
return try await makeRequest(endpoint, method: "POST", body: .jsonObject(body))
}

internal func getOccupancy(roomName: String) async throws(InternalError) -> OccupancyData {
internal func getOccupancy(roomName: String) async throws(ErrorInfo) -> OccupancyData {
let endpoint = roomUrl(roomName: roomName, suffix: "/occupancy")
return try await makeRequest(endpoint, method: "GET")
}

// (CHA-MR4) Users should be able to send a reaction to a message via the `send` method of the `MessagesReactions` object
internal func sendReactionToMessage(_ messageSerial: String, roomName: String, params: SendMessageReactionParams) async throws(InternalError) -> MessageReactionResponse {
internal func sendReactionToMessage(_ messageSerial: String, roomName: String, params: SendMessageReactionParams) async throws(ErrorInfo) -> MessageReactionResponse {
// (CHA-MR4a1) If the serial passed to this method is invalid: undefined, null, empty string, an error with code 40000 must be thrown.
guard !messageSerial.isEmpty else {
throw ChatError.messageReactionInvalidMessageSerial.toInternalError()
throw ChatError.messageReactionInvalidMessageSerial.toErrorInfo()
}

let endpoint = messageUrl(roomName: roomName, serial: messageSerial, suffix: "/reactions")
Expand All @@ -155,10 +155,10 @@ internal final class ChatAPI {
}

// (CHA-MR11) Users should be able to delete a reaction from a message via the `delete` method of the `MessagesReactions` object
internal func deleteReactionFromMessage(_ messageSerial: String, roomName: String, params: DeleteMessageReactionParams) async throws(InternalError) -> MessageReactionResponse {
internal func deleteReactionFromMessage(_ messageSerial: String, roomName: String, params: DeleteMessageReactionParams) async throws(ErrorInfo) -> MessageReactionResponse {
// (CHA-MR11a1) If the serial passed to this method is invalid: undefined, null, empty string, an error with code 40000 must be thrown.
guard !messageSerial.isEmpty else {
throw ChatError.messageReactionInvalidMessageSerial.toInternalError()
throw ChatError.messageReactionInvalidMessageSerial.toErrorInfo()
}

let endpoint = messageUrl(roomName: roomName, serial: messageSerial, suffix: "/reactions")
Expand All @@ -172,7 +172,7 @@ internal final class ChatAPI {
}

// CHA-MR13
internal func getClientReactions(forMessageWithSerial messageSerial: String, roomName: String, clientID: String?) async throws(InternalError) -> MessageReactionSummary {
internal func getClientReactions(forMessageWithSerial messageSerial: String, roomName: String, clientID: String?) async throws(ErrorInfo) -> MessageReactionSummary {
// CHA-MR13b
let endpoint = messageUrl(roomName: roomName, serial: messageSerial, suffix: "/client-reactions")

Expand All @@ -188,12 +188,12 @@ internal final class ChatAPI {
internal struct MessageReactionSummaryResponse: JSONObjectDecodable {
internal let reactions: MessageReactionSummary

internal init(jsonObject: [String: JSONValue]) throws(InternalError) {
internal init(jsonObject: [String: JSONValue]) throws(ErrorInfo) {
reactions = MessageReactionSummary(values: jsonObject)
}
}

private func makeRequest<Response: JSONDecodable>(_ url: String, method: String, params: [String: String]? = nil, body: RequestBody? = nil) async throws(InternalError) -> Response {
private func makeRequest<Response: JSONDecodable>(_ url: String, method: String, params: [String: String]? = nil, body: RequestBody? = nil) async throws(ErrorInfo) -> Response {
let ablyCocoaBody: Any? = if let body {
switch body {
case let .jsonObject(jsonObject):
Expand All @@ -209,7 +209,7 @@ internal final class ChatAPI {
let paginatedResponse = try await realtime.request(method, path: url, params: params, body: ablyCocoaBody, headers: [:])

guard let firstItem = paginatedResponse.items.first else {
throw ChatError.noItemInResponse.toInternalError()
throw ChatError.noItemInResponse.toErrorInfo()
}

let jsonValue = JSONValue(ablyCocoaData: firstItem)
Expand All @@ -219,10 +219,10 @@ internal final class ChatAPI {
private func makePaginatedRequest<Response: JSONDecodable & Sendable & Equatable>(
_ url: String,
params: [String: String]? = nil,
) async throws(InternalError) -> some PaginatedResult<Response> {
) async throws(ErrorInfo) -> some PaginatedResult<Response> {
let paginatedResponse = try await realtime.request("GET", path: url, params: params, body: nil, headers: [:])
let jsonValues = paginatedResponse.items.map { JSONValue(ablyCocoaData: $0) }
let items = try jsonValues.map { jsonValue throws(InternalError) in
let items = try jsonValues.map { jsonValue throws(ErrorInfo) in
try Response(jsonValue: jsonValue)
}
return paginatedResponse.toPaginatedResult(items: items)
Expand Down
4 changes: 2 additions & 2 deletions Sources/AblyChat/DefaultConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal final class DefaultConnection: Connection {

// (CHA-CS2b) The chat client must expose the latest error, if any, associated with its current status.
internal var error: ErrorInfo? {
.init(optionalAblyCocoaError: realtime.connection.errorReason)
realtime.connection.errorReason
}

internal init(realtime: any InternalRealtimeClientProtocol) {
Expand All @@ -36,7 +36,7 @@ internal final class DefaultConnection: Connection {
let statusChange = ConnectionStatusChange(
current: currentState,
previous: previousState,
error: .init(optionalAblyCocoaError: stateChange.reason),
error: stateChange.reason,
// TODO: Actually emit `nil` when appropriate (we can't currently since ably-cocoa's corresponding property is mis-typed): https://github.com/ably/ably-chat-swift/issues/394
retryIn: stateChange.retryIn,
)
Expand Down
59 changes: 23 additions & 36 deletions Sources/AblyChat/DefaultMessageReactions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,34 @@ internal final class DefaultMessageReactions: MessageReactions {

// (CHA-MR4) Users should be able to send a reaction to a message via the `send` method of the `MessagesReactions` object
internal func send(forMessageWithSerial messageSerial: String, params: SendMessageReactionParams) async throws(ErrorInfo) {
do {
var count = params.count
if params.type == .multiple, params.count == nil {
count = 1
}
var count = params.count
if params.type == .multiple, params.count == nil {
count = 1
}

let apiParams: ChatAPI.SendMessageReactionParams = .init(
type: params.type ?? options.defaultMessageReactionType,
name: params.name,
count: count,
)
let response = try await chatAPI.sendReactionToMessage(messageSerial, roomName: roomName, params: apiParams)
let apiParams: ChatAPI.SendMessageReactionParams = .init(
type: params.type ?? options.defaultMessageReactionType,
name: params.name,
count: count,
)
let response = try await chatAPI.sendReactionToMessage(messageSerial, roomName: roomName, params: apiParams)

logger.log(message: "Added message reaction (annotation serial: \(response.serial))", level: .info)
} catch {
throw error.toErrorInfo()
}
logger.log(message: "Added message reaction (annotation serial: \(response.serial))", level: .info)
}

// (CHA-MR11) Users should be able to delete a reaction from a message via the `delete` method of the `MessagesReactions` object
internal func delete(fromMessageWithSerial messageSerial: String, params: DeleteMessageReactionParams) async throws(ErrorInfo) {
let reactionType = params.type ?? options.defaultMessageReactionType
if reactionType != .unique, params.name == nil {
throw InternalError.internallyThrown(.unableDeleteReactionWithoutName(reactionType: reactionType.rawValue)).toErrorInfo()
throw InternalError.unableDeleteReactionWithoutName(reactionType: reactionType.rawValue).toErrorInfo()
}
do {
let apiParams: ChatAPI.DeleteMessageReactionParams = .init(
type: reactionType,
name: reactionType != .unique ? params.name : nil,
)
let response = try await chatAPI.deleteReactionFromMessage(messageSerial, roomName: roomName, params: apiParams)
let apiParams: ChatAPI.DeleteMessageReactionParams = .init(
type: reactionType,
name: reactionType != .unique ? params.name : nil,
)
let response = try await chatAPI.deleteReactionFromMessage(messageSerial, roomName: roomName, params: apiParams)

logger.log(message: "Deleted message reaction (annotation serial: \(response.serial))", level: .info)
} catch {
throw error.toErrorInfo()
}
logger.log(message: "Deleted message reaction (annotation serial: \(response.serial))", level: .info)
}

// (CHA-MR6) Users must be able to subscribe to message reaction summaries via the subscribe method of the MessagesReactions object. The events emitted will be of type MessageReactionSummaryEvent.
Expand Down Expand Up @@ -148,18 +140,13 @@ internal final class DefaultMessageReactions: MessageReactions {

// CHA-MR13
internal func clientReactions(forMessageWithSerial messageSerial: String, clientID: String?) async throws(ErrorInfo) -> MessageReactionSummary {
do {
logger.log(message: "Fetching client reactions for message serial: \(messageSerial), clientId: \(clientID ?? "current client")", level: .debug)
logger.log(message: "Fetching client reactions for message serial: \(messageSerial), clientId: \(clientID ?? "current client")", level: .debug)

// CHA-MR13b
let summary = try await chatAPI.getClientReactions(forMessageWithSerial: messageSerial, roomName: roomName, clientID: clientID)
// CHA-MR13b, CHA-MR13c
let summary = try await chatAPI.getClientReactions(forMessageWithSerial: messageSerial, roomName: roomName, clientID: clientID)

logger.log(message: "Fetched client reactions for message serial: \(messageSerial)", level: .info)
logger.log(message: "Fetched client reactions for message serial: \(messageSerial)", level: .info)

return summary
} catch {
// CHA-MR13c
throw error.toErrorInfo()
}
return summary
}
}
Loading