Skip to content

Commit 6c0ffcf

Browse files
authored
Merge pull request #1048 from ably/ECO-5139/parse-action-and-serial
[ECO-5139] feat: add `action` and `serial` fields
2 parents 1ba7723 + 0c65aff commit 6c0ffcf

File tree

7 files changed

+225
-3
lines changed

7 files changed

+225
-3
lines changed

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.ably.lib.types.DeltaExtras;
2626
import io.ably.lib.types.ErrorInfo;
2727
import io.ably.lib.types.Message;
28+
import io.ably.lib.types.MessageAction;
2829
import io.ably.lib.types.MessageDecodeException;
2930
import io.ably.lib.types.MessageSerializer;
3031
import io.ably.lib.types.PaginatedResult;
@@ -843,6 +844,12 @@ private void onMessage(final ProtocolMessage protocolMessage) {
843844
if(msg.connectionId == null) msg.connectionId = protocolMessage.connectionId;
844845
if(msg.timestamp == 0) msg.timestamp = protocolMessage.timestamp;
845846
if(msg.id == null) msg.id = protocolMessage.id + ':' + i;
847+
// (TM2p)
848+
if(msg.version == null) msg.version = String.format("%s:%03d", protocolMessage.channelSerial, i);
849+
// (TM2k)
850+
if(msg.serial == null && msg.action == MessageAction.MESSAGE_CREATE) msg.serial = msg.version;
851+
// (TM2o)
852+
if(msg.createdAt == null && msg.action == MessageAction.MESSAGE_CREATE) msg.createdAt = msg.timestamp;
846853

847854
try {
848855
msg.decode(options, decodingContext);

lib/src/main/java/io/ably/lib/types/BaseMessage.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,20 @@ protected Long readLong(final JsonObject map, final String key) {
278278
return element.getAsLong();
279279
}
280280

281+
/**
282+
* Read an optional numerical value.
283+
* @return The value, or null if the key was not present in the map.
284+
* @throws ClassCastException if an element exists for that key and that element is not a {@link JsonPrimitive}
285+
* or is not a valid int value.
286+
*/
287+
protected Integer readInt(final JsonObject map, final String key) {
288+
final JsonElement element = map.get(key);
289+
if (null == element || element instanceof JsonNull) {
290+
return null;
291+
}
292+
return element.getAsInt();
293+
}
294+
281295
/* Msgpack processing */
282296
boolean readField(MessageUnpacker unpacker, String fieldName, MessageFormat fieldType) throws IOException {
283297
boolean result = true;

lib/src/main/java/io/ably/lib/types/Message.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,41 @@ public class Message extends BaseMessage {
4646
*/
4747
public String connectionKey;
4848

49+
/**
50+
* (TM2k) serial string – an opaque string that uniquely identifies the message. If a message received from Ably
51+
* (whether over realtime or REST, eg history) with an action of MESSAGE_CREATE does not contain a serial,
52+
* the SDK must set it equal to its version.
53+
*/
54+
public String serial;
55+
56+
/**
57+
* (TM2p) version string – an opaque string that uniquely identifies the message, and is different for different versions.
58+
* If a message received from Ably over a realtime transport does not contain a version,
59+
* the SDK must set it to <channelSerial>:<padded_index> from the channelSerial field of the enclosing ProtocolMessage,
60+
* and padded_index is the index of the message inside the messages array of the ProtocolMessage,
61+
* left-padded with 0s to three digits (for example, the second entry might be foo:001)
62+
*/
63+
public String version;
64+
65+
/**
66+
* (TM2j) action enum
67+
*/
68+
public MessageAction action;
69+
70+
/**
71+
* (TM2o) createdAt time in milliseconds since epoch. If a message received from Ably
72+
* (whether over realtime or REST, eg history) with an action of MESSAGE_CREATE does not contain a createdAt,
73+
* the SDK must set it equal to the TM2f timestamp.
74+
*/
75+
public Long createdAt;
76+
4977
private static final String NAME = "name";
5078
private static final String EXTRAS = "extras";
5179
private static final String CONNECTION_KEY = "connectionKey";
80+
private static final String SERIAL = "serial";
81+
private static final String VERSION = "version";
82+
private static final String ACTION = "action";
83+
private static final String CREATED_AT = "createdAt";
5284

5385
/**
5486
* Default constructor
@@ -128,6 +160,10 @@ void writeMsgpack(MessagePacker packer) throws IOException {
128160
int fieldCount = super.countFields();
129161
if(name != null) ++fieldCount;
130162
if(extras != null) ++fieldCount;
163+
if(serial != null) ++fieldCount;
164+
if(version != null) ++fieldCount;
165+
if(action != null) ++fieldCount;
166+
if(createdAt != null) ++fieldCount;
131167
packer.packMapHeader(fieldCount);
132168
super.writeFields(packer);
133169
if(name != null) {
@@ -138,6 +174,22 @@ void writeMsgpack(MessagePacker packer) throws IOException {
138174
packer.packString(EXTRAS);
139175
extras.write(packer);
140176
}
177+
if(serial != null) {
178+
packer.packString(SERIAL);
179+
packer.packString(serial);
180+
}
181+
if(version != null) {
182+
packer.packString(VERSION);
183+
packer.packString(version);
184+
}
185+
if(action != null) {
186+
packer.packString(ACTION);
187+
packer.packInt(action.ordinal());
188+
}
189+
if(createdAt != null) {
190+
packer.packString(CREATED_AT);
191+
packer.packLong(createdAt);
192+
}
141193
}
142194

143195
Message readMsgpack(MessageUnpacker unpacker) throws IOException {
@@ -157,6 +209,14 @@ Message readMsgpack(MessageUnpacker unpacker) throws IOException {
157209
name = unpacker.unpackString();
158210
} else if (fieldName.equals(EXTRAS)) {
159211
extras = MessageExtras.read(unpacker);
212+
} else if (fieldName.equals(SERIAL)) {
213+
serial = unpacker.unpackString();
214+
} else if (fieldName.equals(VERSION)) {
215+
version = unpacker.unpackString();
216+
} else if (fieldName.equals(ACTION)) {
217+
action = MessageAction.tryFindByOrdinal(unpacker.unpackInt());
218+
} else if (fieldName.equals(CREATED_AT)) {
219+
createdAt = unpacker.unpackLong();
160220
} else {
161221
Log.v(TAG, "Unexpected field: " + fieldName);
162222
unpacker.skipValue();
@@ -313,6 +373,12 @@ protected void read(final JsonObject map) throws MessageDecodeException {
313373
}
314374
extras = MessageExtras.read((JsonObject) extrasElement);
315375
}
376+
377+
serial = readString(map, SERIAL);
378+
version = readString(map, VERSION);
379+
Integer actionOrdinal = readInt(map, ACTION);
380+
action = actionOrdinal == null ? null : MessageAction.tryFindByOrdinal(actionOrdinal);
381+
createdAt = readLong(map, CREATED_AT);
316382
}
317383

318384
public static class Serializer implements JsonSerializer<Message>, JsonDeserializer<Message> {
@@ -328,6 +394,18 @@ public JsonElement serialize(Message message, Type typeOfMessage, JsonSerializat
328394
if (message.connectionKey != null) {
329395
json.addProperty(CONNECTION_KEY, message.connectionKey);
330396
}
397+
if (message.serial != null) {
398+
json.addProperty(SERIAL, message.serial);
399+
}
400+
if (message.version != null) {
401+
json.addProperty(VERSION, message.version);
402+
}
403+
if (message.action != null) {
404+
json.addProperty(ACTION, message.action.ordinal());
405+
}
406+
if (message.createdAt != null) {
407+
json.addProperty(CREATED_AT, message.createdAt);
408+
}
331409
return json;
332410
}
333411

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.ably.lib.types;
2+
3+
public enum MessageAction {
4+
MESSAGE_UNSET, // 0
5+
MESSAGE_CREATE, // 1
6+
MESSAGE_UPDATE, // 2
7+
MESSAGE_DELETE, // 3
8+
ANNOTATION_CREATE, // 4
9+
ANNOTATION_DELETE, // 5
10+
META_OCCUPANCY; // 6
11+
12+
static MessageAction tryFindByOrdinal(int ordinal) {
13+
return values().length <= ordinal ? null: values()[ordinal];
14+
}
15+
}

lib/src/test/java/io/ably/lib/test/realtime/RealtimeMessageTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import static org.junit.Assert.assertArrayEquals;
44
import static org.junit.Assert.assertEquals;
55
import static org.junit.Assert.assertNotEquals;
6+
import static org.junit.Assert.assertNotNull;
7+
import static org.junit.Assert.assertNull;
68
import static org.junit.Assert.assertTrue;
79
import static org.junit.Assert.fail;
810

@@ -17,7 +19,9 @@
1719
import com.google.gson.JsonElement;
1820
import com.google.gson.JsonObject;
1921
import com.google.gson.JsonPrimitive;
22+
import io.ably.lib.types.MessageAction;
2023
import io.ably.lib.types.MessageExtras;
24+
import io.ably.lib.types.Param;
2125
import io.ably.lib.util.Serialisation;
2226
import org.junit.Ignore;
2327
import org.junit.Rule;
@@ -970,4 +974,40 @@ public void opaque_message_extras() throws AblyException {
970974
}
971975
}
972976
}
977+
978+
/**
979+
* Check that important chat SDK fields are populated (serial, action, createdAt)
980+
*/
981+
@Test
982+
public void should_have_serial_action_createdAt() throws AblyException {
983+
ClientOptions opts = createOptions(testVars.keys[7].keyStr);
984+
opts.clientId = "chat";
985+
try (AblyRealtime realtime = new AblyRealtime(opts)) {
986+
final Channel channel = realtime.channels.get("foo::$chat::$chatMessages");
987+
CompletionWaiter msgComplete = new CompletionWaiter();
988+
channel.subscribe(message -> {
989+
assertNotNull(message.serial);
990+
assertNotNull(message.version);
991+
assertNotNull(message.createdAt);
992+
assertEquals(MessageAction.MESSAGE_CREATE, message.action);
993+
assertEquals("chat.message", message.name);
994+
assertEquals("hello world!", ((JsonObject)message.data).get("text").getAsString());
995+
msgComplete.onSuccess();
996+
});
997+
998+
/* publish to the channel */
999+
JsonObject chatMessage = new JsonObject();
1000+
chatMessage.addProperty("text", "hello world!");
1001+
realtime.request(
1002+
"POST",
1003+
"/chat/v2/rooms/foo/messages",
1004+
new Param[] { new Param("v", 3) },
1005+
HttpUtils.requestBodyFromGson(chatMessage, opts.useBinaryProtocol),
1006+
null
1007+
);
1008+
1009+
// wait until we get message on the channel
1010+
assertNull(msgComplete.waitFor(1, 10_000));
1011+
}
1012+
}
9731013
}

lib/src/test/java/io/ably/lib/types/MessageTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,69 @@ public void serialize_message_with_name_and_data() {
4646
assertEquals("test-data", serializedObject.get("data").getAsString());
4747
assertEquals("test-name", serializedObject.get("name").getAsString());
4848
}
49+
50+
@Test
51+
public void serialize_message_with_serial() {
52+
// Given
53+
Message message = new Message("test-name", "test-data");
54+
message.clientId = "test-client-id";
55+
message.connectionKey = "test-key";
56+
message.action = MessageAction.MESSAGE_CREATE;
57+
message.serial = "01826232498871-001@abcdefghij:001";
58+
59+
// When
60+
JsonElement serializedElement = serializer.serialize(message, null, null);
61+
62+
// Then
63+
JsonObject serializedObject = serializedElement.getAsJsonObject();
64+
assertEquals("test-client-id", serializedObject.get("clientId").getAsString());
65+
assertEquals("test-key", serializedObject.get("connectionKey").getAsString());
66+
assertEquals("test-data", serializedObject.get("data").getAsString());
67+
assertEquals("test-name", serializedObject.get("name").getAsString());
68+
assertEquals(1, serializedObject.get("action").getAsInt());
69+
assertEquals("01826232498871-001@abcdefghij:001", serializedObject.get("serial").getAsString());
70+
}
71+
72+
@Test
73+
public void deserialize_message_with_serial() throws Exception {
74+
// Given
75+
JsonObject jsonObject = new JsonObject();
76+
jsonObject.addProperty("clientId", "test-client-id");
77+
jsonObject.addProperty("data", "test-data");
78+
jsonObject.addProperty("name", "test-name");
79+
jsonObject.addProperty("action", 1);
80+
jsonObject.addProperty("serial", "01826232498871-001@abcdefghij:001");
81+
82+
// When
83+
Message message = Message.fromEncoded(jsonObject, new ChannelOptions());
84+
85+
// Then
86+
assertEquals("test-client-id", message.clientId);
87+
assertEquals("test-data", message.data);
88+
assertEquals("test-name", message.name);
89+
assertEquals(MessageAction.MESSAGE_CREATE, message.action);
90+
assertEquals("01826232498871-001@abcdefghij:001", message.serial);
91+
}
92+
93+
94+
@Test
95+
public void deserialize_message_with_unknown_action() throws Exception {
96+
// Given
97+
JsonObject jsonObject = new JsonObject();
98+
jsonObject.addProperty("clientId", "test-client-id");
99+
jsonObject.addProperty("data", "test-data");
100+
jsonObject.addProperty("name", "test-name");
101+
jsonObject.addProperty("action", 10);
102+
jsonObject.addProperty("serial", "01826232498871-001@abcdefghij:001");
103+
104+
// When
105+
Message message = Message.fromEncoded(jsonObject, new ChannelOptions());
106+
107+
// Then
108+
assertEquals("test-client-id", message.clientId);
109+
assertEquals("test-data", message.data);
110+
assertEquals("test-name", message.name);
111+
assertNull(message.action);
112+
assertEquals("01826232498871-001@abcdefghij:001", message.serial);
113+
}
49114
}

lib/src/test/resources/local/testAppSpec.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@
1919
},
2020
{
2121
"capability": "{\"persisted:text_protocol:channel0\":[\"publish\",\"subscribe\",\"history\"],\"persisted:text_protocol:channel1\":[\"publish\",\"subscribe\",\"history\"],\"persisted:binary_protocol:channel0\":[\"publish\",\"subscribe\",\"history\"],\"persisted:binary_protocol:channel1\":[\"publish\",\"subscribe\",\"history\"],\"persisted:*\":[\"subscribe\",\"history\"]}"
22-
}
23-
],
22+
},
23+
{
24+
"capability": "{ \"[*]*\":[\"*\"] }"
25+
}
26+
],
2427
"namespaces": [
2528
{
2629
"id": "persisted",
@@ -78,4 +81,4 @@
7881
]
7982
}
8083
]
81-
}
84+
}

0 commit comments

Comments
 (0)