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-4952] Start implementing room lifecycle spec #5

Closed
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
49 changes: 49 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,52 @@ To check formatting and code quality, run `swift run BuildTool lint`. Run with `
- We should aim to make it easy for consumers of the SDK to be able to mock out the SDK in the tests for their own code. A couple of things that will aid with this:
- Describe the SDK’s functionality via protocols (when doing so would still be sufficiently idiomatic to Swift).
- When defining a `struct` that is emitted by the public API of the library, make sure to define a public memberwise initializer so that users can create one to be emitted by their mocks. (There is no way to make Swift’s autogenerated memberwise initializer public, so you will need to write one yourself. In Xcode, you can do this by clicking at the start of the type declaration and doing Editor → Refactor → Generate Memberwise Initializer.)
- When writing code that implements behaviour specified by the Chat SDK features spec, add a comment that references the identifier of the relevant spec item.

### Testing guidelines

#### Attributing tests to a spec point

When writing a test that relates to a spec point from the Chat SDK features spec, add a comment that contains one of the following tags:

- `@spec <spec-item-id>` — The test case directly tests all the functionality documented in the spec item.
- `@specOneOf(m/n) <spec-item-id>` — The test case is the m<sup>th</sup> of n test cases which, together, test all the functionality documented in the spec item.
- `@specPartial <spec-item-id>` — The test case tests some, but not all, of the functionality documented in the spec item. This is different to `@specOneOf` in that it implies that the test suite does not fully test this spec item.

The `<spec-item-id>` parameter should be a spec item identifier such as `CHA-RL3g`.

Each of the above tags can optionally be followed by a hyphen and an explanation of how the test relates to the given spec item.

Examples:

```swift
// @spec CHA-EX3f
func test1 { … }
```
Comment on lines +49 to +52

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (documentation): Consider adding language-specific syntax highlighting to code blocks

Using syntax highlighting for Swift code blocks (swift instead of just ) could improve readability and make the documentation more polished.


```swift
// @specOneOf(1/2) CHA-EX2h — Tests the case where the room is FAILED
func test2 { … }

// @specOneOf(2/2) CHA-EX2h — Tests the case where the room is SUSPENDED
func test3 { … }
```

```swift
// @specPartial CHA-EX1h4 - Tests that we retry, but not the retry attempt limit because we’ve not implemented it yet
func test4 { … }
```

In [#46](https://github.com/ably-labs/ably-chat-swift/issues/46), we’ll write a script that uses these tags to generate a report about how much of the feature spec we’ve implemented.

#### Marking a spec point as untested

In addition to the above, you can add the following as a comment anywhere in the test suite:

- `@specUntested <spec-item-id> - <explanation>` — This indicates that the SDK implements the given spec point, but that there are no automated tests for it. This should be used sparingly; only use it when there is no way to test a spec point. It must be accompanied by an explanation of why this spec point is not tested.

Example:

```swift
// @specUntested CHA-EX2b - I was unable to find a way to test this spec point in an environment in which concurrency is being used; there is no obvious moment at which to stop observing the emitted state changes in order to be sure that FAILED has not been emitted twice.
```
137 changes: 134 additions & 3 deletions Sources/AblyChat/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,163 @@ public enum ErrorCode: Int {
/// TODO this code is a guess, revisit in https://github.com/ably-labs/ably-chat-swift/issues/32
case inconsistentRoomOptions = 1

case messagesAttachmentFailed = 102_001
case presenceAttachmentFailed = 102_002
case reactionsAttachmentFailed = 102_003
case occupancyAttachmentFailed = 102_004
case typingAttachmentFailed = 102_005

case messagesDetachmentFailed = 102_050
case presenceDetachmentFailed = 102_051
case reactionsDetachmentFailed = 102_052
case occupancyDetachmentFailed = 102_053
case typingDetachmentFailed = 102_054

case roomInFailedState = 102_101
case roomIsReleasing = 102_102
case roomIsReleased = 102_103

/// 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
// TODO: These are currently a guess, revisit once outstanding spec question re status codes is answered (https://github.com/ably/specification/pull/200#discussion_r1755222945), and also revisit in https://github.com/ably-labs/ably-chat-swift/issues/32
switch self {
case .inconsistentRoomOptions:
case .inconsistentRoomOptions,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Simplify the switch statement in the statusCode property

Consider grouping the cases that return the same value to reduce repetition and improve readability. For example, you could use a single case with a pattern that matches all relevant error codes.

.messagesDetachmentFailed,
.presenceDetachmentFailed,
.reactionsDetachmentFailed,
.occupancyDetachmentFailed,
.typingDetachmentFailed,
.roomInFailedState,
.roomIsReleasing,
.roomIsReleased:
400
case .messagesAttachmentFailed,
.presenceAttachmentFailed,
.reactionsAttachmentFailed,
.occupancyAttachmentFailed,
.typingAttachmentFailed:
// TODO: This is currently a best guess based on the limited status code information given in the spec at time of writing (i.e. CHA-RL1h4); it's not clear to me whether these error codes are always meant to have the same status code. Revisit once aforementioned spec question re status codes answered.
500
}
}
}

/**
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`.
This type exists in addition to ``ErrorCode`` to allow us to attach metadata which can be incorporated into the error’s `localizedDescription` and `cause`.
*/
internal enum ChatError {
case inconsistentRoomOptions(requested: RoomOptions, existing: RoomOptions)
case attachmentFailed(feature: RoomFeature, underlyingError: ARTErrorInfo)
case detachmentFailed(feature: RoomFeature, underlyingError: ARTErrorInfo)
case roomInFailedState
case roomIsReleasing
case roomIsReleased

/// The ``ARTErrorInfo.code`` that should be returned for this error.
internal var code: ErrorCode {
switch self {
case .inconsistentRoomOptions:
.inconsistentRoomOptions
case let .attachmentFailed(feature, _):
switch feature {
case .messages:
.messagesAttachmentFailed
case .occupancy:
.occupancyAttachmentFailed
case .presence:
.presenceAttachmentFailed
case .reactions:
.reactionsAttachmentFailed
case .typing:
.typingAttachmentFailed
}
case let .detachmentFailed(feature, _):
switch feature {
case .messages:
.messagesDetachmentFailed
case .occupancy:
.occupancyDetachmentFailed
case .presence:
.presenceDetachmentFailed
case .reactions:
.reactionsDetachmentFailed
case .typing:
.typingDetachmentFailed
}
case .roomInFailedState:
.roomInFailedState
case .roomIsReleasing:
.roomIsReleasing
case .roomIsReleased:
.roomIsReleased
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Extract repetitive code in localizedDescription into a separate method

The switch statements for attachment and detachment failures contain similar code. Consider extracting this logic into a separate method to reduce duplication and improve maintainability.


/// A helper type for parameterising the construction of error messages.
private enum AttachOrDetach {
case attach
case detach
}

private static func localizedDescription(
forFailureOfOperation operation: AttachOrDetach,
feature: RoomFeature
) -> String {
let featureDescription = switch feature {
case .messages:
"messages"
case .occupancy:
"occupancy"
case .presence:
"presence"
case .reactions:
"reactions"
case .typing:
"typing"
}

let operationDescription = switch operation {
case .attach:
"attach"
case .detach:
"detach"
}

return "The \(featureDescription) feature failed to \(operationDescription)."
}

/// 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)"
case let .attachmentFailed(feature, _):
Self.localizedDescription(forFailureOfOperation: .attach, feature: feature)
case let .detachmentFailed(feature, _):
Self.localizedDescription(forFailureOfOperation: .detach, feature: feature)
case .roomInFailedState:
"Cannot perform operation because the room is in a failed state."
case .roomIsReleasing:
"Cannot perform operation because the room is in a releasing state."
case .roomIsReleased:
"Cannot perform operation because the room is in a released state."
}
}

/// The ``ARTErrorInfo.cause`` that should be returned for this error.
internal var cause: ARTErrorInfo? {
switch self {
case let .attachmentFailed(_, underlyingError):
underlyingError
case let .detachmentFailed(_, underlyingError):
underlyingError
case .inconsistentRoomOptions,
.roomInFailedState,
.roomIsReleasing,
.roomIsReleased:
nil
}
}
}
Expand All @@ -58,6 +184,11 @@ internal extension ARTErrorInfo {
userInfo["ARTErrorInfoStatusCode"] = chatError.code.statusCode
userInfo[NSLocalizedDescriptionKey] = chatError.localizedDescription

// TODO: This is kind of an implementation detail (that NSUnderlyingErrorKey is what populates `cause`); consider documenting in ably-cocoa as part of https://github.com/ably-labs/ably-chat-swift/issues/32.
if let cause = chatError.cause {
userInfo[NSUnderlyingErrorKey] = cause
}

self.init(
domain: errorDomain,
code: chatError.code.rawValue,
Expand Down
8 changes: 8 additions & 0 deletions Sources/AblyChat/RoomFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// The features offered by a chat room.
internal enum RoomFeature {
case messages
case presence
case reactions
case occupancy
case typing
}
Loading