From 9f1cbd92c0456ccca486a6efedfceb572a0d7bb2 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Thu, 21 Nov 2024 14:26:44 +0100 Subject: [PATCH] Added new fields to the `ARTMessage`. --- Source/ARTBaseMessage.m | 1 + Source/ARTJsonLikeEncoder.m | 43 ++++++-- Source/ARTMessage.m | 9 +- .../PrivateHeaders/Ably/ARTJsonLikeEncoder.h | 4 +- Source/include/Ably/ARTMessage.h | 48 ++++++++ Test/Tests/RestClientChannelTests.swift | 2 +- Test/Tests/UtilitiesTests.swift | 104 ++++++++++++++++++ 7 files changed, 197 insertions(+), 14 deletions(-) diff --git a/Source/ARTBaseMessage.m b/Source/ARTBaseMessage.m index 48676a629..7201f09e8 100644 --- a/Source/ARTBaseMessage.m +++ b/Source/ARTBaseMessage.m @@ -21,6 +21,7 @@ - (id)copyWithZone:(NSZone *)zone { message->_data = [self.data copy]; message->_connectionId = self.connectionId; message->_encoding = self.encoding; + message->_extras = self.extras; return message; } diff --git a/Source/ARTJsonLikeEncoder.m b/Source/ARTJsonLikeEncoder.m index d63c08439..4e56ff422 100644 --- a/Source/ARTJsonLikeEncoder.m +++ b/Source/ARTJsonLikeEncoder.m @@ -75,11 +75,11 @@ - (NSString *)formatAsString { } - (ARTMessage *)decodeMessage:(NSData *)data error:(NSError **)error { - return [self messageFromDictionary:[self decodeDictionary:data error:error]]; + return [self messageFromDictionary:[self decodeDictionary:data error:error] protocolMessage:nil]; } - (NSArray *)decodeMessages:(NSData *)data error:(NSError **)error { - return [self messagesFromArray:[self decodeArray:data error:error]]; + return [self messagesFromArray:[self decodeArray:data error:error] protocolMessage:nil]; } - (NSData *)encodeMessage:(ARTMessage *)message error:(NSError **)error { @@ -266,7 +266,15 @@ - (NSArray *)decodeStats:(NSData *)data error:(NSError **)error { return [self statsFromArray:[self decodeArray:data error:error]]; } -- (ARTMessage *)messageFromDictionary:(NSDictionary *)input { +- (nullable NSString *)versionFromInput:(NSDictionary *)input withProtocolMessage:(nullable ARTProtocolMessage *)protocolMessage { + if (protocolMessage.channelSerial == nil) { + return nil; + } + int index = [[input artNumber:@"_index"] intValue]; + return [NSString stringWithFormat:@"%@:%03d", protocolMessage.channelSerial, index]; // TM2p : +} + +- (ARTMessage *)messageFromDictionary:(NSDictionary *)input protocolMessage:(ARTProtocolMessage *)protocolMessage { ARTLogVerbose(_logger, @"RS:%p ARTJsonLikeEncoder<%@>: messageFromDictionary %@", _rest, [_delegate formatAsString], input); if (![input isKindOfClass:[NSDictionary class]]) { return nil; @@ -275,24 +283,34 @@ - (ARTMessage *)messageFromDictionary:(NSDictionary *)input { ARTMessage *message = [[ARTMessage alloc] init]; message.id = [input artString:@"id"]; message.name = [input artString:@"name"]; + message.action = ([input artNumber:@"action"] ?: [[NSNumber alloc] initWithInt:ARTMessageActionCreate]).integerValue; + message.version = [input artString:@"version"] ?: [self versionFromInput:input withProtocolMessage:protocolMessage]; // TM2p + message.serial = [input artString:@"serial"]; + if (!message.serial && message.action == ARTMessageActionCreate) { // TM2k + message.serial = message.version; + } message.clientId = [input artString:@"clientId"]; message.data = [input objectForKey:@"data"]; - message.encoding = [input artString:@"encoding"];; + message.encoding = [input artString:@"encoding"]; message.timestamp = [input artTimestamp:@"timestamp"]; + message.createdAt = [input artTimestamp:@"createdAt"]; + if (!message.createdAt && message.action == ARTMessageActionCreate) { // TM2o + message.createdAt = message.timestamp; + } message.connectionId = [input artString:@"connectionId"]; message.extras = [input objectForKey:@"extras"]; return message; } -- (NSArray *)messagesFromArray:(NSArray *)input { +- (NSArray *)messagesFromArray:(NSArray *)input protocolMessage:(ARTProtocolMessage *)protocolMessage { if (![input isKindOfClass:[NSArray class]]) { return nil; } NSMutableArray *output = [NSMutableArray array]; for (NSDictionary *item in input) { - ARTMessage *message = [self messageFromDictionary:item]; + ARTMessage *message = [self messageFromDictionary:item protocolMessage:protocolMessage]; if (!message) { return nil; } @@ -734,8 +752,6 @@ - (ARTProtocolMessage *)protocolMessageFromDictionary:(NSDictionary *)input { message.id = [input artString:@"id"]; message.msgSerial = [input artNumber:@"msgSerial"]; message.timestamp = [input artTimestamp:@"timestamp"]; - message.messages = [self messagesFromArray:[input objectForKey:@"messages"]]; - message.presence = [self presenceMessagesFromArray:[input objectForKey:@"presence"]]; message.connectionKey = [input artString:@"connectionKey"]; message.flags = [[input artNumber:@"flags"] longLongValue]; message.connectionDetails = [self connectionDetailsFromDictionary:[input valueForKey:@"connectionDetails"]]; @@ -747,6 +763,17 @@ - (ARTProtocolMessage *)protocolMessageFromDictionary:(NSDictionary *)input { message.error = [ARTErrorInfo createWithCode:[[error artNumber:@"code"] intValue] status:[[error artNumber:@"statusCode"] intValue] message:[error artString:@"message"]]; } + NSMutableArray *messages = [[input objectForKey:@"messages"] mutableCopy]; + + // There is probably a better way to do this, but I have limited time to implement TM2p + for (int i = 0; i < messages.count; i++) { + NSMutableDictionary *msgDict = [messages[i] mutableCopy]; + msgDict[@"_index"] = @(i); + messages[i] = msgDict; + } + message.messages = [self messagesFromArray:messages protocolMessage:message]; + message.presence = [self presenceMessagesFromArray:[input objectForKey:@"presence"]]; + return message; } diff --git a/Source/ARTMessage.m b/Source/ARTMessage.m index 0ebf065da..16de50b74 100644 --- a/Source/ARTMessage.m +++ b/Source/ARTMessage.m @@ -40,7 +40,10 @@ - (NSString *)description { - (id)copyWithZone:(NSZone *)zone { ARTMessage *message = [super copyWithZone:zone]; message.name = self.name; - message.extras = self.extras; + message.action = self.action; + message.serial = self.serial; + message.version = self.version; + message.createdAt = self.createdAt; return message; } @@ -67,7 +70,7 @@ + (instancetype)fromEncoded:(NSDictionary *)jsonObject channelOptions:(ARTChanne return nil; } - ARTMessage *message = [jsonEncoder messageFromDictionary:jsonObject]; + ARTMessage *message = [jsonEncoder messageFromDictionary:jsonObject protocolMessage:nil]; NSError *decodeError = nil; message = [message decodeWithEncoder:decoder error:&decodeError]; @@ -97,7 +100,7 @@ + (instancetype)fromEncoded:(NSDictionary *)jsonObject channelOptions:(ARTChanne return nil; } - NSArray *messages = [jsonEncoder messagesFromArray:jsonArray]; + NSArray *messages = [jsonEncoder messagesFromArray:jsonArray protocolMessage:nil]; NSMutableArray *decodedMessages = [NSMutableArray array]; for (ARTMessage *message in messages) { diff --git a/Source/PrivateHeaders/Ably/ARTJsonLikeEncoder.h b/Source/PrivateHeaders/Ably/ARTJsonLikeEncoder.h index 10d5a5a5a..112fdf35e 100644 --- a/Source/PrivateHeaders/Ably/ARTJsonLikeEncoder.h +++ b/Source/PrivateHeaders/Ably/ARTJsonLikeEncoder.h @@ -30,8 +30,8 @@ NS_ASSUME_NONNULL_BEGIN @interface ARTJsonLikeEncoder () -- (nullable ARTMessage *)messageFromDictionary:(NSDictionary *)input; -- (nullable NSArray *)messagesFromArray:(NSArray *)input; +- (nullable ARTMessage *)messageFromDictionary:(NSDictionary *)input protocolMessage:(nullable ARTProtocolMessage *)protocolMessage; +- (nullable NSArray *)messagesFromArray:(NSArray *)input protocolMessage:(nullable ARTProtocolMessage *)protocolMessage; - (nullable ARTPresenceMessage *)presenceMessageFromDictionary:(NSDictionary *)input; - (nullable NSArray *)presenceMessagesFromArray:(NSArray *)input; diff --git a/Source/include/Ably/ARTMessage.h b/Source/include/Ably/ARTMessage.h index 88d145170..5e2b89ebf 100644 --- a/Source/include/Ably/ARTMessage.h +++ b/Source/include/Ably/ARTMessage.h @@ -4,6 +4,41 @@ #import #import +/** + * The namespace containing the different types of message actions. + */ +NS_SWIFT_SENDABLE +typedef NS_ENUM(NSUInteger, ARTMessageAction) { + /** + * Message action has not been set. + */ + ARTMessageActionUnset, + /** + * Message action for a newly created message. + */ + ARTMessageActionCreate, + /** + * Message action for an updated message. + */ + ARTMessageActionUpdate, + /** + * Message action for a deleted message. + */ + ARTMessageActionDelete, + /** + * Message action for a newly created annotation. + */ + ARTMessageActionAnnotationCreate, + /** + * Message action for a deleted annotation. + */ + ARTMessageActionAnnotationDelete, + /** + * Message action for a meta-message that contains channel occupancy information. + */ + ARTMessageActionMetaOccupancy, +}; + NS_ASSUME_NONNULL_BEGIN /** @@ -14,6 +49,19 @@ NS_ASSUME_NONNULL_BEGIN /// The event name, if available @property (nullable, readwrite, nonatomic) NSString *name; +/// The action type of the message, one of the `ARTMessageAction` enum values. +@property (readwrite, nonatomic) ARTMessageAction action; + +/// The version of the message, lexicographically-comparable with other versions (that share the same serial). +/// Will differ from the serial only if the message has been updated or deleted. +@property (nullable, readwrite, nonatomic) NSString *version; + +/// This message's unique serial (an identifier that will be the same in all future updates of this message). +@property (nullable, readwrite, nonatomic) NSString *serial; + +/// The timestamp of the very first version of a given message. +@property (nonatomic, nullable) NSDate *createdAt; + /** * Construct an `ARTMessage` object with an event name and payload. * diff --git a/Test/Tests/RestClientChannelTests.swift b/Test/Tests/RestClientChannelTests.swift index 10513784c..d9ad0ac21 100644 --- a/Test/Tests/RestClientChannelTests.swift +++ b/Test/Tests/RestClientChannelTests.swift @@ -568,7 +568,7 @@ class RestClientChannelTests: XCTestCase { XCTAssertTrue((client.internal.encoders["application/json"] as! ARTJsonLikeEncoder).message(from: [ "data": "foo", "extras": ["push": ["notification": ["title": "Hello from Ably!"]]], - ])?.extras == extras) + ], protocolMessage: nil)?.extras == extras) waitUntil(timeout: testTimeout) { done in channel.publish("name", data: "some data", extras: extras) { error in diff --git a/Test/Tests/UtilitiesTests.swift b/Test/Tests/UtilitiesTests.swift index ba9955c00..3c9be70ab 100644 --- a/Test/Tests/UtilitiesTests.swift +++ b/Test/Tests/UtilitiesTests.swift @@ -484,4 +484,108 @@ class UtilitiesTests: XCTestCase { let expectedSize = "{\"test\":\"test\"}".count + "{\"push\":{\"key\":\"value\"}}".count + clientId.count + message.name!.count XCTAssertEqual(message.messageSize(), expectedSize) } + + // TM2k + func test__025_Utilities__message_received_with_action_create_does_not_contain_a_serial_takes_it_from_version() throws { + beforeEach__Utilities__JSON_Encoder() + var json = """ + { + "messages": [ + { + "action": 1, + "version": "123" + } + ] + } + """ + var data = json.data(using: .utf8)! + var pm = try jsonEncoder.decodeProtocolMessage(data) + var messages = try XCTUnwrap(pm.messages) + XCTAssert(messages[0].action == .create) + XCTAssert(messages[0].version == "123") + XCTAssert(messages[0].serial == "123") + + // should only apply to creates + json = """ + { + "messages": [ + { + "action": 0, + "version": "123" + } + ] + } + """ + data = json.data(using: .utf8)! + pm = try jsonEncoder.decodeProtocolMessage(data) + messages = try XCTUnwrap(pm.messages) + XCTAssert(messages[0].action == .unset) + XCTAssert(messages[0].version == "123") + XCTAssertNil(messages[0].serial) + } + + // TM2p + func test__026_Utilities__message_received_over_a_realtime_transport_does_not_contain_a_version() throws { + beforeEach__Utilities__JSON_Encoder() + let json = """ + { + "channelSerial": "foo", + "messages": [ + { + "serial": "12345" + }, + { + "serial": "123456" + } + ] + } + """ + let data = json.data(using: .utf8)! + let pm = try jsonEncoder.decodeProtocolMessage(data) + let messages = try XCTUnwrap(pm.messages) + XCTAssert(messages[0].action == .create) + XCTAssert(messages[0].version == "foo:000") + XCTAssert(messages[0].serial == "12345") + XCTAssert(messages[1].action == .create) + XCTAssert(messages[1].version == "foo:001") + XCTAssert(messages[1].serial == "123456") + } + + // TM2o + func test__027_Utilities__message_received_with_action_create_does_not_contain_createdAt() throws { + beforeEach__Utilities__JSON_Encoder() + var json = """ + { + "messages": [ + { + "action": 1, + "timestamp": "1234512345", + } + ] + } + """ + var data = json.data(using: .utf8)! + var pm = try jsonEncoder.decodeProtocolMessage(data) + var messages = try XCTUnwrap(pm.messages) + XCTAssert(messages[0].action == .create) + XCTAssertNotNil(messages[0].createdAt) + XCTAssertEqual(messages[0].createdAt, messages[0].timestamp) + + // should only apply to creates + json = """ + { + "messages": [ + { + "action": 0, + "timestamp": "1234512345", + } + ] + } + """ + data = json.data(using: .utf8)! + pm = try jsonEncoder.decodeProtocolMessage(data) + messages = try XCTUnwrap(pm.messages) + XCTAssert(messages[0].action == .unset) + XCTAssertNil(messages[0].createdAt) + } }