Skip to content

Commit 96808a6

Browse files
Implement the LiveObject.canApplyOperation method
Based on [1] at 2b5651e. Development approach as described in 4494033. [1] ably/specification#343
1 parent 79c94da commit 96808a6

File tree

2 files changed

+141
-0
lines changed

2 files changed

+141
-0
lines changed

Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
internal import AblyPlugin
2+
13
/// This is the equivalent of the `LiveObject` abstract class described in RTLO.
24
///
35
/// ``DefaultLiveCounter`` and ``DefaultLiveMap`` include it by composition.
@@ -8,4 +10,27 @@ internal struct LiveObjectMutableState {
810
internal var siteTimeserials: [String: String] = [:]
911
// RTLO3c
1012
internal var createOperationIsMerged = false
13+
14+
/// Indicates whether an operation described by an `ObjectMessage` should be applied or discarded, per RTLO4a.
15+
internal func canApplyOperation(from objectMessage: InboundObjectMessage, logger: Logger) -> Bool {
16+
// RTLO4a3: Both ObjectMessage.serial and ObjectMessage.siteCode must be non-empty strings
17+
guard let serial = objectMessage.serial, !serial.isEmpty,
18+
let siteCode = objectMessage.siteCode, !siteCode.isEmpty
19+
else {
20+
// RTLO4a3: Otherwise, log a warning that the object operation message has invalid serial values
21+
logger.log("Object operation message has invalid serial values: serial=\(objectMessage.serial ?? "nil"), siteCode=\(objectMessage.siteCode ?? "nil")", level: .warn)
22+
return false
23+
}
24+
25+
// RTLO4a4: Get the siteSerial value stored for this LiveObject in the siteTimeserials map using the key ObjectMessage.siteCode
26+
let siteSerial = siteTimeserials[siteCode]
27+
28+
// RTLO4a5: If the siteSerial for this LiveObject is null or an empty string, return true
29+
guard let siteSerial, !siteSerial.isEmpty else {
30+
return true
31+
}
32+
33+
// RTLO4a6: If the siteSerial for this LiveObject is not an empty string, return true if ObjectMessage.serial is greater than siteSerial when compared lexicographically
34+
return serial > siteSerial
35+
}
1136
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import Ably
2+
@testable import AblyLiveObjects
3+
import AblyPlugin
4+
import Testing
5+
6+
/// Tests for `LiveObjectMutableState`.
7+
struct LiveObjectMutableStateTests {
8+
/// Tests for `LiveObjectMutableState.canApplyOperation`, covering RTLO4 specification points.
9+
struct CanApplyOperationTests {
10+
/// Test case data for canApplyOperation tests
11+
struct TestCase {
12+
let description: String
13+
let objectMessageSerial: String?
14+
let objectMessageSiteCode: String?
15+
let siteTimeserials: [String: String]
16+
let expectedResult: Bool
17+
}
18+
19+
// @spec RTLO4a3
20+
// @spec RTLO4a4
21+
// @spec RTLO4a5
22+
// @spec RTLO4a6
23+
@Test(arguments: [
24+
// RTLO4a3: Both ObjectMessage.serial and ObjectMessage.siteCode must be non-empty strings
25+
TestCase(
26+
description: "serial is nil, siteCode is valid - should return false",
27+
objectMessageSerial: nil,
28+
objectMessageSiteCode: "site1",
29+
siteTimeserials: [:],
30+
expectedResult: false,
31+
),
32+
TestCase(
33+
description: "serial is empty string, siteCode is valid - should return false",
34+
objectMessageSerial: "",
35+
objectMessageSiteCode: "site1",
36+
siteTimeserials: [:],
37+
expectedResult: false,
38+
),
39+
TestCase(
40+
description: "serial is valid, siteCode is nil - should return false",
41+
objectMessageSerial: "serial1",
42+
objectMessageSiteCode: nil,
43+
siteTimeserials: [:],
44+
expectedResult: false,
45+
),
46+
TestCase(
47+
description: "serial is valid, siteCode is empty string - should return false",
48+
objectMessageSerial: "serial1",
49+
objectMessageSiteCode: "",
50+
siteTimeserials: [:],
51+
expectedResult: false,
52+
),
53+
TestCase(
54+
description: "both serial and siteCode are invalid - should return false",
55+
objectMessageSerial: nil,
56+
objectMessageSiteCode: "",
57+
siteTimeserials: [:],
58+
expectedResult: false,
59+
),
60+
61+
// RTLO4a5: If the siteSerial for this LiveObject is null or an empty string, return true
62+
TestCase(
63+
description: "siteSerial is nil (siteCode doesn't exist) - should return true",
64+
objectMessageSerial: "serial2",
65+
objectMessageSiteCode: "site1",
66+
siteTimeserials: ["site2": "serial1"], // i.e. only has an entry for a different siteCode
67+
expectedResult: true,
68+
),
69+
TestCase(
70+
description: "siteSerial is empty string - should return true",
71+
objectMessageSerial: "serial2",
72+
objectMessageSiteCode: "site1",
73+
siteTimeserials: ["site1": "", "site2": "serial1"],
74+
expectedResult: true,
75+
),
76+
77+
// RTLO4a6: If the siteSerial for this LiveObject is not an empty string, return true if ObjectMessage.serial is greater than siteSerial when compared lexicographically
78+
TestCase(
79+
description: "serial is greater than siteSerial lexicographically - should return true",
80+
objectMessageSerial: "serial2",
81+
objectMessageSiteCode: "site1",
82+
siteTimeserials: ["site1": "serial1"],
83+
expectedResult: true,
84+
),
85+
TestCase(
86+
description: "serial is less than siteSerial lexicographically - should return false",
87+
objectMessageSerial: "serial1",
88+
objectMessageSiteCode: "site1",
89+
siteTimeserials: ["site1": "serial2"],
90+
expectedResult: false,
91+
),
92+
TestCase(
93+
description: "serial equals siteSerial - should return false",
94+
objectMessageSerial: "serial1",
95+
objectMessageSiteCode: "site1",
96+
siteTimeserials: ["site1": "serial1"],
97+
expectedResult: false,
98+
),
99+
])
100+
func canApplyOperation(testCase: TestCase) {
101+
let state = LiveObjectMutableState(
102+
objectID: "test:object@123",
103+
siteTimeserials: testCase.siteTimeserials,
104+
)
105+
let objectMessage = TestFactories.inboundObjectMessage(
106+
serial: testCase.objectMessageSerial,
107+
siteCode: testCase.objectMessageSiteCode,
108+
)
109+
let logger = TestLogger()
110+
111+
let result = state.canApplyOperation(from: objectMessage, logger: logger)
112+
113+
#expect(result == testCase.expectedResult, "Expected \(testCase.expectedResult) for case: \(testCase.description)")
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)