Skip to content

Commit d795f9e

Browse files
Implement the LiveObject.canApplyOperation method
Based on [1] at 29276a5. Development approach as described in cb427d8. [1] ably/specification#343
1 parent 0a2e72a commit d795f9e

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed

Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift

Lines changed: 39 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,41 @@ internal struct LiveObjectMutableState {
810
internal var siteTimeserials: [String: String] = [:]
911
// RTLO3c
1012
internal var createOperationIsMerged = false
13+
14+
/// Represents parameters of an operation that `canApplyOperation` has decided can be applied to a `LiveObject`.
15+
///
16+
/// The key thing is that it offers a non-nil `serial` and `siteCode`, which will be needed when subsequently performing the operation.
17+
internal struct ApplicableOperation: Equatable {
18+
internal let objectMessageSerial: String
19+
internal let objectMessageSiteCode: String
20+
}
21+
22+
/// Indicates whether an operation described by an `ObjectMessage` should be applied or discarded, per RTLO4a.
23+
///
24+
/// Instead of returning a `Bool`, in the case where the operation can be applied it returns a non-nil `ApplicableOperation` (whose non-nil `serial` and `siteCode` will be needed as part of subsequently performing this operation).
25+
internal func canApplyOperation(objectMessageSerial: String?, objectMessageSiteCode: String?, logger: Logger) -> ApplicableOperation? {
26+
// RTLO4a3: Both ObjectMessage.serial and ObjectMessage.siteCode must be non-empty strings
27+
guard let serial = objectMessageSerial, !serial.isEmpty,
28+
let siteCode = objectMessageSiteCode, !siteCode.isEmpty
29+
else {
30+
// RTLO4a3: Otherwise, log a warning that the object operation message has invalid serial values
31+
logger.log("Object operation message has invalid serial values: serial=\(objectMessageSerial ?? "nil"), siteCode=\(objectMessageSiteCode ?? "nil")", level: .warn)
32+
return nil
33+
}
34+
35+
// RTLO4a4: Get the siteSerial value stored for this LiveObject in the siteTimeserials map using the key ObjectMessage.siteCode
36+
let siteSerial = siteTimeserials[siteCode]
37+
38+
// RTLO4a5: If the siteSerial for this LiveObject is null or an empty string, return true
39+
guard let siteSerial, !siteSerial.isEmpty else {
40+
return ApplicableOperation(objectMessageSerial: serial, objectMessageSiteCode: siteCode)
41+
}
42+
43+
// RTLO4a6: If the siteSerial for this LiveObject is not an empty string, return true if ObjectMessage.serial is greater than siteSerial when compared lexicographically
44+
if serial > siteSerial {
45+
return ApplicableOperation(objectMessageSerial: serial, objectMessageSiteCode: siteCode)
46+
}
47+
48+
return nil
49+
}
1150
}
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: LiveObjectMutableState.ApplicableOperation?
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 nil",
27+
objectMessageSerial: nil,
28+
objectMessageSiteCode: "site1",
29+
siteTimeserials: [:],
30+
expectedResult: nil,
31+
),
32+
TestCase(
33+
description: "serial is empty string, siteCode is valid - should return nil",
34+
objectMessageSerial: "",
35+
objectMessageSiteCode: "site1",
36+
siteTimeserials: [:],
37+
expectedResult: nil,
38+
),
39+
TestCase(
40+
description: "serial is valid, siteCode is nil - should return nil",
41+
objectMessageSerial: "serial1",
42+
objectMessageSiteCode: nil,
43+
siteTimeserials: [:],
44+
expectedResult: nil,
45+
),
46+
TestCase(
47+
description: "serial is valid, siteCode is empty string - should return nil",
48+
objectMessageSerial: "serial1",
49+
objectMessageSiteCode: "",
50+
siteTimeserials: [:],
51+
expectedResult: nil,
52+
),
53+
TestCase(
54+
description: "both serial and siteCode are invalid - should return nil",
55+
objectMessageSerial: nil,
56+
objectMessageSiteCode: "",
57+
siteTimeserials: [:],
58+
expectedResult: nil,
59+
),
60+
61+
// RTLO4a5: If the siteSerial for this LiveObject is null or an empty string, return ApplicableOperation
62+
TestCase(
63+
description: "siteSerial is nil (siteCode doesn't exist) - should return ApplicableOperation",
64+
objectMessageSerial: "serial2",
65+
objectMessageSiteCode: "site1",
66+
siteTimeserials: ["site2": "serial1"], // i.e. only has an entry for a different siteCode
67+
expectedResult: LiveObjectMutableState.ApplicableOperation(objectMessageSerial: "serial2", objectMessageSiteCode: "site1"),
68+
),
69+
TestCase(
70+
description: "siteSerial is empty string - should return ApplicableOperation",
71+
objectMessageSerial: "serial2",
72+
objectMessageSiteCode: "site1",
73+
siteTimeserials: ["site1": "", "site2": "serial1"],
74+
expectedResult: LiveObjectMutableState.ApplicableOperation(objectMessageSerial: "serial2", objectMessageSiteCode: "site1"),
75+
),
76+
77+
// RTLO4a6: If the siteSerial for this LiveObject is not an empty string, return ApplicableOperation if ObjectMessage.serial is greater than siteSerial when compared lexicographically
78+
TestCase(
79+
description: "serial is greater than siteSerial lexicographically - should return ApplicableOperation",
80+
objectMessageSerial: "serial2",
81+
objectMessageSiteCode: "site1",
82+
siteTimeserials: ["site1": "serial1"],
83+
expectedResult: LiveObjectMutableState.ApplicableOperation(objectMessageSerial: "serial2", objectMessageSiteCode: "site1"),
84+
),
85+
TestCase(
86+
description: "serial is less than siteSerial lexicographically - should return nil",
87+
objectMessageSerial: "serial1",
88+
objectMessageSiteCode: "site1",
89+
siteTimeserials: ["site1": "serial2"],
90+
expectedResult: nil,
91+
),
92+
TestCase(
93+
description: "serial equals siteSerial - should return nil",
94+
objectMessageSerial: "serial1",
95+
objectMessageSiteCode: "site1",
96+
siteTimeserials: ["site1": "serial1"],
97+
expectedResult: nil,
98+
),
99+
])
100+
func canApplyOperation(testCase: TestCase) {
101+
let state = LiveObjectMutableState(
102+
objectID: "test:object@123",
103+
siteTimeserials: testCase.siteTimeserials,
104+
)
105+
let logger = TestLogger()
106+
107+
let result = state.canApplyOperation(
108+
objectMessageSerial: testCase.objectMessageSerial,
109+
objectMessageSiteCode: testCase.objectMessageSiteCode,
110+
logger: logger,
111+
)
112+
113+
#expect(result == testCase.expectedResult, "Expected \(String(describing: testCase.expectedResult)) for case: \(testCase.description)")
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)