Skip to content

Commit 315d162

Browse files
Merge pull request #199 from ably/198-reaction-headers-and-metadata
[ECO-5181] Fix sending and receiving of reaction metadata and headers
2 parents 35350c4 + 6035a1e commit 315d162

11 files changed

+465
-46
lines changed

Sources/AblyChat/DefaultMessages.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,12 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities {
7777
throw ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message without clientId")
7878
}
7979

80-
let metadata: Metadata? = if let metadataJSONObject = data["metadata"]?.objectValue {
80+
let metadata: Metadata? = if let metadataJSONObject = try data.optionalObjectValueForKey("metadata") {
8181
try metadataJSONObject.mapValues { try MetadataValue(jsonValue: $0) }
8282
} else {
8383
nil
8484
}
85-
let headers: Headers? = if let headersJSONObject = extras["headers"]?.objectValue {
85+
let headers: Headers? = if let headersJSONObject = try extras.optionalObjectValueForKey("headers") {
8686
try headersJSONObject.mapValues { try HeadersValue(jsonValue: $0) }
8787
} else {
8888
nil

Sources/AblyChat/DefaultRoomReactions.swift

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,14 @@ internal final class DefaultRoomReactions: RoomReactions, EmitsDiscontinuities {
2323
// (CHA-ER3a) Reactions are sent on the channel using a message in a particular format - see spec for format.
2424
internal func send(params: SendReactionParams) async throws {
2525
logger.log(message: "Sending reaction with params: \(params)", level: .debug)
26-
let extras = ["headers": params.headers ?? [:]] as ARTJsonCompatible
27-
channel.publish(RoomReactionEvents.reaction.rawValue, data: params.asJSONObject(), extras: extras)
26+
27+
let dto = RoomReactionDTO(type: params.type, metadata: params.metadata, headers: params.headers)
28+
29+
channel.publish(
30+
RoomReactionEvents.reaction.rawValue,
31+
data: dto.data.toJSONValue.toAblyCocoaData,
32+
extras: dto.extras.toJSONObject.toARTJsonCompatible
33+
)
2834
}
2935

3036
// (CHA-ER4) A user may subscribe to reaction events in Realtime.
@@ -38,10 +44,8 @@ internal final class DefaultRoomReactions: RoomReactions, EmitsDiscontinuities {
3844
logger.log(message: "Received roomReaction message: \(message)", level: .debug)
3945
Task {
4046
do {
41-
guard let data = message.data as? [String: Any],
42-
let reactionType = data["type"] as? String
43-
else {
44-
throw ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message without data or text")
47+
guard let ablyCocoaData = message.data else {
48+
throw ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message without data")
4549
}
4650

4751
guard let messageClientID = message.clientId else {
@@ -52,18 +56,20 @@ internal final class DefaultRoomReactions: RoomReactions, EmitsDiscontinuities {
5256
throw ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message without timestamp")
5357
}
5458

55-
guard let extras = try message.extras?.toJSON() else {
59+
guard let ablyCocoaExtras = try message.extras?.toJSON() else {
5660
throw ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message without extras")
5761
}
5862

59-
let metadata = data["metadata"] as? Metadata
60-
let headers = extras["headers"] as? Headers
63+
let dto = try RoomReactionDTO(
64+
data: .init(jsonValue: .init(ablyCocoaData: ablyCocoaData)),
65+
extras: .init(jsonValue: .init(ablyCocoaData: ablyCocoaExtras))
66+
)
6167

6268
// (CHA-ER4d) Realtime events that are malformed (unknown fields should be ignored) shall not be emitted to listeners.
6369
let reaction = Reaction(
64-
type: reactionType,
65-
metadata: metadata ?? .init(),
66-
headers: headers ?? .init(),
70+
type: dto.type,
71+
metadata: dto.metadata ?? [:],
72+
headers: dto.headers ?? [:],
6773
createdAt: timestamp,
6874
clientID: messageClientID,
6975
isSelf: messageClientID == clientID

Sources/AblyChat/JSONCodable.swift

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,224 @@ internal protocol JSONDecodable {
77
}
88

99
internal typealias JSONCodable = JSONDecodable & JSONEncodable
10+
11+
internal protocol JSONObjectEncodable: JSONEncodable {
12+
var toJSONObject: [String: JSONValue] { get }
13+
}
14+
15+
// Default implementation of `JSONEncodable` conformance for `JSONObjectEncodable`
16+
internal extension JSONObjectEncodable {
17+
var toJSONValue: JSONValue {
18+
.object(toJSONObject)
19+
}
20+
}
21+
22+
internal protocol JSONObjectDecodable: JSONDecodable {
23+
init(jsonObject: [String: JSONValue]) throws
24+
}
25+
26+
internal enum JSONValueDecodingError: Error {
27+
case valueIsNotObject
28+
case noValueForKey(String)
29+
case wrongTypeForKey(String, actualValue: JSONValue)
30+
}
31+
32+
// Default implementation of `JSONDecodable` conformance for `JSONObjectDecodable`
33+
internal extension JSONObjectDecodable {
34+
init(jsonValue: JSONValue) throws {
35+
guard case let .object(jsonObject) = jsonValue else {
36+
throw JSONValueDecodingError.valueIsNotObject
37+
}
38+
39+
self = try .init(jsonObject: jsonObject)
40+
}
41+
}
42+
43+
internal typealias JSONObjectCodable = JSONObjectDecodable & JSONObjectEncodable
44+
45+
// MARK: - Extracting values from a dictionary
46+
47+
/// This extension adds some helper methods for extracting values from a dictionary of `JSONValue` values; you may find them helpful when implementing `JSONCodable`.
48+
internal extension [String: JSONValue] {
49+
/// If this dictionary contains a value for `key`, and this value has case `object`, this returns the associated value.
50+
///
51+
/// - Throws:
52+
/// - `JSONValueDecodingError.noValueForKey` if the key is absent
53+
/// - `JSONValueDecodingError.wrongTypeForKey` if the value does not have case `object`
54+
func objectValueForKey(_ key: String) throws -> [String: JSONValue] {
55+
guard let value = self[key] else {
56+
throw JSONValueDecodingError.noValueForKey(key)
57+
}
58+
59+
guard case let .object(objectValue) = value else {
60+
throw JSONValueDecodingError.wrongTypeForKey(key, actualValue: value)
61+
}
62+
63+
return objectValue
64+
}
65+
66+
/// If this dictionary contains a value for `key`, and this value has case `object`, this returns the associated value. If this dictionary does not contain a value for `key`, or if the value for key has case `null`, it returns `nil`.
67+
///
68+
/// - Throws: `JSONValueDecodingError.wrongTypeForKey` if the value does not have case `object` or `null`
69+
func optionalObjectValueForKey(_ key: String) throws -> [String: JSONValue]? {
70+
guard let value = self[key] else {
71+
return nil
72+
}
73+
74+
if case .null = value {
75+
return nil
76+
}
77+
78+
guard case let .object(objectValue) = value else {
79+
throw JSONValueDecodingError.wrongTypeForKey(key, actualValue: value)
80+
}
81+
82+
return objectValue
83+
}
84+
85+
/// If this dictionary contains a value for `key`, and this value has case `array`, this returns the associated value.
86+
///
87+
/// - Throws:
88+
/// - `JSONValueDecodingError.noValueForKey` if the key is absent
89+
/// - `JSONValueDecodingError.wrongTypeForKey` if the value does not have case `array`
90+
func arrayValueForKey(_ key: String) throws -> [JSONValue] {
91+
guard let value = self[key] else {
92+
throw JSONValueDecodingError.noValueForKey(key)
93+
}
94+
95+
guard case let .array(arrayValue) = value else {
96+
throw JSONValueDecodingError.wrongTypeForKey(key, actualValue: value)
97+
}
98+
99+
return arrayValue
100+
}
101+
102+
/// If this dictionary contains a value for `key`, and this value has case `array`, this returns the associated value. If this dictionary does not contain a value for `key`, or if the value for key has case `null`, it returns `nil`.
103+
///
104+
/// - Throws: `JSONValueDecodingError.wrongTypeForKey` if the value does not have case `array` or `null`
105+
func optionalArrayValueForKey(_ key: String) throws -> [JSONValue]? {
106+
guard let value = self[key] else {
107+
return nil
108+
}
109+
110+
if case .null = value {
111+
return nil
112+
}
113+
114+
guard case let .array(arrayValue) = value else {
115+
throw JSONValueDecodingError.wrongTypeForKey(key, actualValue: value)
116+
}
117+
118+
return arrayValue
119+
}
120+
121+
/// If this dictionary contains a value for `key`, and this value has case `string`, this returns the associated value.
122+
///
123+
/// - Throws:
124+
/// - `JSONValueDecodingError.noValueForKey` if the key is absent
125+
/// - `JSONValueDecodingError.wrongTypeForKey` if the value does not have case `string`
126+
func stringValueForKey(_ key: String) throws -> String {
127+
guard let value = self[key] else {
128+
throw JSONValueDecodingError.noValueForKey(key)
129+
}
130+
131+
guard case let .string(stringValue) = value else {
132+
throw JSONValueDecodingError.wrongTypeForKey(key, actualValue: value)
133+
}
134+
135+
return stringValue
136+
}
137+
138+
/// If this dictionary contains a value for `key`, and this value has case `string`, this returns the associated value. If this dictionary does not contain a value for `key`, or if the value for key has case `null`, it returns `nil`.
139+
///
140+
/// - Throws: `JSONValueDecodingError.wrongTypeForKey` if the value does not have case `string` or `null`
141+
func optionalStringValueForKey(_ key: String) throws -> String? {
142+
guard let value = self[key] else {
143+
return nil
144+
}
145+
146+
if case .null = value {
147+
return nil
148+
}
149+
150+
guard case let .string(stringValue) = value else {
151+
throw JSONValueDecodingError.wrongTypeForKey(key, actualValue: value)
152+
}
153+
154+
return stringValue
155+
}
156+
157+
/// If this dictionary contains a value for `key`, and this value has case `number`, this returns the associated value.
158+
///
159+
/// - Throws:
160+
/// - `JSONValueDecodingError.noValueForKey` if the key is absent
161+
/// - `JSONValueDecodingError.wrongTypeForKey` if the value does not have case `number`
162+
func numberValueForKey(_ key: String) throws -> Double {
163+
guard let value = self[key] else {
164+
throw JSONValueDecodingError.noValueForKey(key)
165+
}
166+
167+
guard case let .number(numberValue) = value else {
168+
throw JSONValueDecodingError.wrongTypeForKey(key, actualValue: value)
169+
}
170+
171+
return numberValue
172+
}
173+
174+
/// If this dictionary contains a value for `key`, and this value has case `number`, this returns the associated value. If this dictionary does not contain a value for `key`, or if the value for key has case `null`, it returns `nil`.
175+
///
176+
/// - Throws: `JSONValueDecodingError.wrongTypeForKey` if the value does not have case `number` or `null`
177+
func optionalNumberValueForKey(_ key: String) throws -> Double? {
178+
guard let value = self[key] else {
179+
return nil
180+
}
181+
182+
if case .null = value {
183+
return nil
184+
}
185+
186+
guard case let .number(numberValue) = value else {
187+
throw JSONValueDecodingError.wrongTypeForKey(key, actualValue: value)
188+
}
189+
190+
return numberValue
191+
}
192+
193+
/// If this dictionary contains a value for `key`, and this value has case `bool`, this returns the associated value.
194+
///
195+
/// - Throws:
196+
/// - `JSONValueDecodingError.noValueForKey` if the key is absent
197+
/// - `JSONValueDecodingError.wrongTypeForKey` if the value does not have case `bool`
198+
func boolValueForKey(_ key: String) throws -> Bool {
199+
guard let value = self[key] else {
200+
throw JSONValueDecodingError.noValueForKey(key)
201+
}
202+
203+
guard case let .bool(boolValue) = value else {
204+
throw JSONValueDecodingError.wrongTypeForKey(key, actualValue: value)
205+
}
206+
207+
return boolValue
208+
}
209+
210+
/// If this dictionary contains a value for `key`, and this value has case `bool`, this returns the associated value.
211+
///
212+
/// - Throws:
213+
/// - `JSONValueDecodingError.noValueForKey` if the key is absent
214+
/// - `JSONValueDecodingError.wrongTypeForKey` if the value does not have case `bool`
215+
func optionalBoolValueForKey(_ key: String) throws -> Bool? {
216+
guard let value = self[key] else {
217+
return nil
218+
}
219+
220+
if case .null = value {
221+
return nil
222+
}
223+
224+
guard case let .bool(boolValue) = value else {
225+
throw JSONValueDecodingError.wrongTypeForKey(key, actualValue: value)
226+
}
227+
228+
return boolValue
229+
}
230+
}

Sources/AblyChat/JSONValue.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Ably
12
import Foundation
23

34
/// A JSON value (where "value" has the meaning defined by the [JSON specification](https://www.json.org)).
@@ -166,10 +167,11 @@ internal extension JSONValue {
166167
///
167168
/// - `ARTPresenceMessage`’s `data` property
168169
/// - the `data` argument that’s passed to `ARTRealtime`’s `request(…)` method
170+
/// - the `data` argument that’s passed to `ARTRealtime`’s `publish(…)` method
169171
var toAblyCocoaData: Any {
170172
switch self {
171173
case let .object(underlying):
172-
underlying.mapValues(\.toAblyCocoaData)
174+
underlying.toAblyCocoaDataDictionary
173175
case let .array(underlying):
174176
underlying.map(\.toAblyCocoaData)
175177
case let .string(underlying):
@@ -183,3 +185,21 @@ internal extension JSONValue {
183185
}
184186
}
185187
}
188+
189+
internal extension [String: JSONValue] {
190+
/// Creates an ably-cocoa deserialized JSON object from a dictionary that has string keys and `JSONValue` values.
191+
///
192+
/// Specifically, the value of this property can be used as:
193+
///
194+
/// - `ARTPresenceMessage`’s `data` property
195+
/// - the `data` argument that’s passed to `ARTRealtime`’s `request(…)` method
196+
/// - the `data` argument that’s passed to `ARTRealtime`’s `publish(…)` method
197+
var toAblyCocoaDataDictionary: [String: Any] {
198+
mapValues(\.toAblyCocoaData)
199+
}
200+
201+
/// Creates an ably-cocoa `ARTJsonCompatible` object from a dictionary that has string keys and `JSONValue` values.
202+
var toARTJsonCompatible: ARTJsonCompatible {
203+
toAblyCocoaDataDictionary as ARTJsonCompatible
204+
}
205+
}

Sources/AblyChat/PresenceDataDTO.swift

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,24 @@ internal struct PresenceDataDTO: Equatable {
33
internal var userCustomData: PresenceData?
44
}
55

6-
// MARK: - JSONCodable
6+
// MARK: - JSONObjectCodable
77

8-
extension PresenceDataDTO: JSONCodable {
8+
extension PresenceDataDTO: JSONObjectCodable {
99
internal enum JSONKey: String {
1010
case userCustomData
1111
}
1212

13-
internal enum DecodingError: Error {
14-
case valueHasWrongType(key: JSONKey)
15-
}
16-
17-
internal init(jsonValue: JSONValue) throws {
18-
guard case let .object(jsonObject) = jsonValue else {
19-
throw DecodingError.valueHasWrongType(key: .userCustomData)
20-
}
21-
13+
internal init(jsonObject: [String: JSONValue]) throws {
2214
userCustomData = jsonObject[JSONKey.userCustomData.rawValue]
2315
}
2416

25-
internal var toJSONValue: JSONValue {
17+
internal var toJSONObject: [String: JSONValue] {
2618
var result: [String: JSONValue] = [:]
2719

2820
if let userCustomData {
2921
result[JSONKey.userCustomData.rawValue] = userCustomData
3022
}
3123

32-
return .object(result)
24+
return result
3325
}
3426
}

0 commit comments

Comments
 (0)