diff --git a/.github/workflows/swift-integration.yml b/.github/workflows/swift-integration.yml index 14f50e4e..b320ce6a 100644 --- a/.github/workflows/swift-integration.yml +++ b/.github/workflows/swift-integration.yml @@ -15,7 +15,7 @@ jobs: xcode-version: '14.3.1' - uses: actions/checkout@v3 - name: Setup Docker on macOS using Colima, Lima-VM, and Homebrew. - uses: 7hong13/setup-docker-macos-action@fix_formula_paths + uses: douglascamata/setup-docker-macos-action@fix-python-dep id: docker - run: docker-compose -f docker/docker-compose-ci.yml up --build -d - name: Run tests diff --git a/Sources/API/Converter.swift b/Sources/API/Converter.swift index 6eb32d06..6cdeb02c 100644 --- a/Sources/API/Converter.swift +++ b/Sources/API/Converter.swift @@ -89,21 +89,13 @@ extension Converter { /** * `toPresence` converts the given model to Protobuf format. */ - static func toPresence(presence: PresenceData) -> PbPresence { + static func toPresence(presence: StringValueTypeDictionary) -> PbPresence { var pbPresence = PbPresence() for (key, value) in presence { - if value is AnyValueTypeDictionary || value is [Any], - let jsonObject = try? JSONSerialization.data(withJSONObject: value), - let jsonString = String(data: jsonObject, encoding: .utf8) { - pbPresence.data[key] = jsonString - } else if value is String { - pbPresence.data[key] = "\"\(value)\"" - } else { - pbPresence.data[key] = "\(value)" - } + pbPresence.data[key] = value } - + return pbPresence } @@ -127,35 +119,11 @@ extension Converter { /** * `fromPresence` converts the given Protobuf format to model format. */ - static func fromPresence(pbPresence: PbPresence) -> PresenceData { - var data = PresenceData() + static func fromPresence(pbPresence: PbPresence) -> StringValueTypeDictionary { + var data = StringValueTypeDictionary() pbPresence.data.forEach { (key, value) in - if let dataValue = value.data(using: .utf8), let jsonValue = try? JSONSerialization.jsonObject(with: dataValue) as? AnyValueTypeDictionary { - data[key] = jsonValue - } else { - if value.first == "\"" && value.last == "\"" { - // Sring. - data[key] = value.substring(from: 1, to: value.count - 2) - } else if value.first == "[", value.last == "]", - let dataValue = "{\"\(key)\":\(value)}".data(using: .utf8), let jsonValue = try? JSONSerialization.jsonObject(with: dataValue) as? AnyValueTypeDictionary { - // Array. eg. "[\"a\", \"b\"]" - data.merge(jsonValue, uniquingKeysWith: { _, last in last }) - } else { - if let intValue = Int(value) { - data[key] = intValue - } else if let doubleValue = Double(value) { - data[key] = doubleValue - } else if "\(true)" == value.lowercased() { - data[key] = true - } else if "\(false)" == value.lowercased() { - data[key] = false - } else { - data[key] = value - assertionFailure("Invalid Presence Value [\(key)]:[\(value)]") - } - } - } + data[key] = value } return data @@ -178,7 +146,7 @@ extension Converter { /** * `fromPresences` converts the given Protobuf format to model format. */ - static func fromPresences(_ pbPresences: [String: PbPresence]) -> [ActorID: PresenceData] { + static func fromPresences(_ pbPresences: [String: PbPresence]) -> [ActorID: StringValueTypeDictionary] { pbPresences.mapValues { fromPresence(pbPresence: $0) } } } @@ -1166,7 +1134,7 @@ extension Converter { /** * `bytesToSnapshot` creates a Snapshot from the given byte array. */ - static func bytesToSnapshot(bytes: Data) throws -> (root: CRDTObject, presences: [ActorID: PresenceData]) { + static func bytesToSnapshot(bytes: Data) throws -> (root: CRDTObject, presences: [ActorID: StringValueTypeDictionary]) { if bytes.isEmpty { return (CRDTObject(createdAt: TimeTicket.initial), [:]) } diff --git a/Sources/Core/Client.swift b/Sources/Core/Client.swift index 9eaaa9b1..e2f14783 100644 --- a/Sources/Core/Client.swift +++ b/Sources/Core/Client.swift @@ -732,9 +732,9 @@ public actor Client { // NOTE(chacha912): There is no presence, when PresenceChange(clear) is applied before unwatching. // In that case, the 'unwatched' event is triggered while handling the PresenceChange. let presence = await self.attachmentMap[docKey]?.doc.getPresence(publisher) - + await self.attachmentMap[docKey]?.doc.removeOnlineClient(publisher) - + if let presence { await self.attachmentMap[docKey]?.doc.publishPresenceEvent(.unwatched, publisher, presence) } diff --git a/Sources/Document/CRDT/CRDTTree.swift b/Sources/Document/CRDT/CRDTTree.swift index bd2b0393..be7daac7 100644 --- a/Sources/Document/CRDT/CRDTTree.swift +++ b/Sources/Document/CRDT/CRDTTree.swift @@ -187,7 +187,7 @@ extension CRDTTreeNodeID { /** * `CRDTTreePosStruct` represents the structure of CRDTTreePos. */ -struct CRDTTreePosStruct { +struct CRDTTreePosStruct: Codable { let parentID: CRDTTreeNodeIDStruct let leftSiblingID: CRDTTreeNodeIDStruct } @@ -196,7 +196,7 @@ struct CRDTTreePosStruct { * `CRDTTreeNodeIDStruct` represents the structure of CRDTTreePos. * It is used to serialize and deserialize the CRDTTreePos. */ -struct CRDTTreeNodeIDStruct { +struct CRDTTreeNodeIDStruct: Codable { let createdAt: TimeTicketStruct let offset: Int32 } @@ -323,17 +323,17 @@ final class CRDTTreeNode: IndexTreeNode { * `clone` clones this node with the given offset. */ func clone(offset: Int32) -> CRDTTreeNode { - let clone = CRDTTreeNode(id: CRDTTreeNodeID(createdAt: self.id.createdAt, offset: offset), type: self.type, attributes: self.attrs) + let clone = CRDTTreeNode(id: CRDTTreeNodeID(createdAt: self.id.createdAt, offset: offset), type: self.type, attributes: self.attrs) clone.removedAt = self.removedAt clone.value = self.value clone.size = self.size clone.innerChildren = self.innerChildren.compactMap { let childClone = $0.deepcopy() childClone?.parent = clone - + return childClone } - + return clone } @@ -432,7 +432,7 @@ final class CRDTTreeNode: IndexTreeNode { } return JSONTreeElementNode(type: self.type, - attributes: attrs.anyValueTypeDictionary, + attributes: attrs.toJSONObejct, children: self.children.compactMap { $0.toJSONTreeNode }) } } @@ -460,7 +460,7 @@ class CRDTTree: CRDTGCElement { self.nodeMapByID.put(node.id, node) } } - + /** * `findFloorNode` finds node of given id. */ @@ -518,7 +518,7 @@ class CRDTTree: CRDTGCElement { if index <= parentNode.innerChildren.count { for idx in index ..< parentNode.innerChildren.count { let next = parentNode.innerChildren[idx] - + if next.id.createdAt.after(editedAt) { leftSiblingNode = next } else { @@ -614,30 +614,22 @@ class CRDTTree: CRDTGCElement { if node.isText == false, contain != .all { return } - + guard let actorID = node.createdAt.actorID else { throw YorkieError.unexpected(message: "Can't get actorID") } - + let latestCreatedAt = latestCreatedAtMapByActor.isEmpty == false ? latestCreatedAtMapByActor[actorID] ?? TimeTicket.initial : TimeTicket.max - + if node.canDelete(editedAt, latestCreatedAt) { let latestCreatedAt = latestCreatedAtMap[actorID] let createdAt = node.createdAt - + if latestCreatedAt == nil || createdAt.after(latestCreatedAt!) { latestCreatedAtMap[actorID] = createdAt } - - toBeRemoveds.append(node) - } - } - for node in toBeRemoveds { - node.remove(editedAt) - - if node.isRemoved { - self.removedNodeMap[node.id.toIDString] = node + toBeRemoveds.append(node) } } @@ -685,13 +677,14 @@ class CRDTTree: CRDTGCElement { _ fromLeft: CRDTTreeNode, _ toParent: CRDTTreeNode, _ toLeft: CRDTTreeNode, - callback: @escaping (CRDTTreeNode, TagContained) throws -> Void) throws { - let fromIdx = try self.toIndex(fromParent, fromLeft) - let toIdx = try self.toIndex(toParent, toLeft) + callback: @escaping (CRDTTreeNode, TagContained) throws -> Void) throws + { + let fromIdx = try self.toIndex(fromParent, fromLeft) + let toIdx = try self.toIndex(toParent, toLeft) + + return try self.indexTree.nodesBetween(fromIdx, toIdx, callback) + } - return try self.indexTree.nodesBetween(fromIdx, toIdx, callback) - } - /** * `editByIndex` edits the given range with the given value. * This method uses indexes instead of a pair of TreePos for testing. diff --git a/Sources/Document/CRDT/ElementRHT.swift b/Sources/Document/CRDT/ElementRHT.swift index fac644fc..51c09309 100644 --- a/Sources/Document/CRDT/ElementRHT.swift +++ b/Sources/Document/CRDT/ElementRHT.swift @@ -150,7 +150,7 @@ class ElementRHT { * `get` returns the value of the given key. */ func get(key: String) -> CRDTElement? { - nodeMapByKey[key]?.value + self.nodeMapByKey[key]?.value } } diff --git a/Sources/Document/CRDT/RGATreeSplit.swift b/Sources/Document/CRDT/RGATreeSplit.swift index 9a8d8036..519c41f6 100644 --- a/Sources/Document/CRDT/RGATreeSplit.swift +++ b/Sources/Document/CRDT/RGATreeSplit.swift @@ -40,7 +40,7 @@ protocol RGATreeSplitValue { * `RGATreeSplitPosStruct` is a structure represents the meta data of the node pos. * It is used to serialize and deserialize the node pos. */ -public struct RGATreeSplitPosStruct { +public struct RGATreeSplitPosStruct: Codable { let id: RGATreeSplitNodeIDStruct let relativeOffset: Int32 } @@ -49,7 +49,7 @@ public struct RGATreeSplitPosStruct { * `RGATreeSplitNodeIDStruct` is a structure represents the meta data of the node id. * It is used to serialize and deserialize the node id. */ -public struct RGATreeSplitNodeIDStruct { +public struct RGATreeSplitNodeIDStruct: Codable { let createdAt: TimeTicketStruct let offset: Int32 } diff --git a/Sources/Document/Change/Change.swift b/Sources/Document/Change/Change.swift index fe99f55f..07d6be53 100644 --- a/Sources/Document/Change/Change.swift +++ b/Sources/Document/Change/Change.swift @@ -57,7 +57,7 @@ public struct Change { * `execute` executes the operations of this change to the given root. */ @discardableResult - func execute(root: CRDTRoot, presences: inout [ActorID: PresenceData]) throws -> [any OperationInfo] { + func execute(root: CRDTRoot, presences: inout [ActorID: StringValueTypeDictionary]) throws -> [any OperationInfo] { let opInfos = try self.operations.flatMap { try $0.execute(root: root) } diff --git a/Sources/Document/DocEvent.swift b/Sources/Document/DocEvent.swift index 083c287f..63cfcd3f 100644 --- a/Sources/Document/DocEvent.swift +++ b/Sources/Document/DocEvent.swift @@ -131,7 +131,7 @@ public struct RemoteChangeEvent: ChangeEvent { /** * `PeersChangedValue` represents the value of the PeersChanged event. */ -public typealias PeerElement = (clientID: ActorID, presence: PresenceData) +public typealias PeerElement = (clientID: ActorID, presence: [String: Any]) public struct InitializedEvent: DocEvent { /** diff --git a/Sources/Document/Document.swift b/Sources/Document/Document.swift index f3de40ac..40826b09 100644 --- a/Sources/Document/Document.swift +++ b/Sources/Document/Document.swift @@ -64,7 +64,7 @@ public actor Document { private var localChanges: [Change] private var root: CRDTRoot - private var clone: (root: CRDTRoot, presences: [ActorID: PresenceData])? + private var clone: (root: CRDTRoot, presences: [ActorID: StringValueTypeDictionary])? private var defaultSubscribeCallback: SubscribeCallback? private var subscribeCallbacks: [String: SubscribeCallback] @@ -78,7 +78,7 @@ public actor Document { /** * `presences` is a map of client IDs to their presence information. */ - private var presences: [ActorID: PresenceData] + private var presences: [ActorID: StringValueTypeDictionary] public init(key: String) { self.key = key @@ -136,7 +136,7 @@ public actor Document { self.publish(changeEvent) } - if change.presenceChange != nil, let presence = self.presences[actorID] { + if change.presenceChange != nil, let presence = self.getPresence(actorID) { self.publish(PresenceChangedEvent(value: (actorID, presence))) } @@ -384,9 +384,9 @@ public actor Document { // their presence was initially absent, we can consider that we have // received their initial presence, so trigger the 'watched' event if self.onlineClients.contains(actorID) { - let peer = (actorID, presence) + let peer = (actorID, presence.toJSONObejct) - if self.presences[actorID] != nil { + if self.getPresence(actorID) != nil { presenceEvent = PresenceChangedEvent(value: peer) } else { presenceEvent = WatchedEvent(value: peer) @@ -487,7 +487,7 @@ public actor Document { "[\(self.key)]" } - func publishPresenceEvent(_ eventType: DocEventType, _ peerActorID: ActorID? = nil, _ presence: PresenceData? = nil) { + func publishPresenceEvent(_ eventType: DocEventType, _ peerActorID: ActorID? = nil, _ presence: [String: Any]? = nil) { switch eventType { case .initialized: self.publish(InitializedEvent(value: self.getPresences())) @@ -618,30 +618,30 @@ public actor Document { /** * `getMyPresence` returns the presence of the current client. */ - public func getMyPresence() -> PresenceData? { + public func getMyPresence() -> [String: Any]? { guard self.status == .attached, let id = self.changeID.getActorID() else { return nil } - - return self.presences[id] + + return self.presences[id]?.mapValues { $0.toJSONObject } } /** * `getPresence` returns the presence of the given clientID. */ - public func getPresence(_ clientID: ActorID) -> PresenceData? { + public func getPresence(_ clientID: ActorID) -> [String: Any]? { guard self.onlineClients.contains(clientID) else { return nil } - return self.presences[clientID] + return self.presences[clientID]?.mapValues { $0.toJSONObject } } /** * `getPresenceForTest` returns the presence of the given clientID. */ - public func getPresenceForTest(_ clientID: ActorID) -> PresenceData? { - self.presences[clientID] + public func getPresenceForTest(_ clientID: ActorID) -> [String: Any]? { + self.presences[clientID]?.mapValues { $0.toJSONObject } } /** @@ -651,7 +651,7 @@ public actor Document { var presences = [PeerElement]() for clientID in self.onlineClients { - if let presence = self.presences[clientID] { + if let presence = getPresence(clientID) { presences.append((clientID, presence)) } } diff --git a/Sources/Document/Json/JSONTree.swift b/Sources/Document/Json/JSONTree.swift index a03c3bde..ba3d5753 100644 --- a/Sources/Document/Json/JSONTree.swift +++ b/Sources/Document/Json/JSONTree.swift @@ -243,7 +243,7 @@ public class JSONTree { /** * `styleByPath` sets the attributes to the elements of the given path. */ - public func styleByPath(_ path: [Int], _ attributes: [String: Any]) throws { + public func styleByPath(_ path: [Int], _ attributes: [String: Codable]) throws { guard let context, let tree else { throw YorkieError.unexpected(message: "it is not initialized yet") } @@ -271,7 +271,7 @@ public class JSONTree { /** * `style` sets the attributes to the elements of the given range. */ - public func style(_ fromIdx: Int, _ toIdx: Int, _ attributes: [String: Any]) throws { + public func style(_ fromIdx: Int, _ toIdx: Int, _ attributes: [String: Codable]) throws { guard let context, let tree else { throw YorkieError.unexpected(message: "it is not initialized yet") } @@ -500,7 +500,7 @@ public class JSONTree { * `TimeTicketStruct` is a structure represents the meta data of the ticket. * It is used to serialize and deserialize the ticket. */ -struct TimeTicketStruct { +struct TimeTicketStruct: Codable { let lamport: String let delimiter: UInt32 let actorID: ActorID? diff --git a/Sources/Document/Operation/Operation.swift b/Sources/Document/Operation/Operation.swift index 8a0446c4..18e90100 100644 --- a/Sources/Document/Operation/Operation.swift +++ b/Sources/Document/Operation/Operation.swift @@ -150,7 +150,7 @@ public struct TreeStyleOpInfo: OperationInfo { public let from: Int public let to: Int public let fromPath: [Int] - public let value: [String: Any] + public let value: [String: Codable] public static func == (lhs: TreeStyleOpInfo, rhs: TreeStyleOpInfo) -> Bool { if lhs.type != lhs.type { diff --git a/Sources/Document/Operation/TreeEditOperation.swift b/Sources/Document/Operation/TreeEditOperation.swift index 996494ff..24f53e01 100644 --- a/Sources/Document/Operation/TreeEditOperation.swift +++ b/Sources/Document/Operation/TreeEditOperation.swift @@ -72,9 +72,13 @@ struct TreeEditOperation: Operation { } return changes.compactMap { change in - guard case .nodes(let nodes) = change.value else { - return nil - } + let value: [TreeNode] = { + if case .nodes(let nodes) = change.value { + return nodes + } else { + return [] + } + }() return TreeEditOpInfo( path: path, @@ -82,7 +86,7 @@ struct TreeEditOperation: Operation { to: change.to, fromPath: change.fromPath, toPath: change.toPath, - value: nodes + value: value ) } } diff --git a/Sources/Document/Operation/TreeSytleOperation.swift b/Sources/Document/Operation/TreeSytleOperation.swift index a9ac0b8b..236aaeda 100644 --- a/Sources/Document/Operation/TreeSytleOperation.swift +++ b/Sources/Document/Operation/TreeSytleOperation.swift @@ -67,16 +67,20 @@ class TreeStyleOperation: Operation { } return changes.compactMap { change in - guard case .attributes(let attributes) = change.value else { - return nil - } + let attributes: [String: Codable] = { + if case .attributes(let attributes) = change.value { + return attributes + } else { + return [:] + } + }() return TreeStyleOpInfo( path: path, from: change.from, to: change.to, fromPath: change.fromPath, - value: attributes.anyValueTypeDictionary + value: attributes ) } } diff --git a/Sources/Document/Presence/Presence.swift b/Sources/Document/Presence/Presence.swift index cbc4eed1..94571155 100644 --- a/Sources/Document/Presence/Presence.swift +++ b/Sources/Document/Presence/Presence.swift @@ -20,7 +20,7 @@ import Foundation * PresenceData key, value dictionary * Similar to an Indexable in JS SDK */ -public typealias PresenceData = [String: Any] +public typealias PresenceData = [String: Codable] /** * `PresenceChangeType` represents the type of presence change. @@ -31,7 +31,7 @@ enum PresenceChangeType { } enum PresenceChange { - case put(presence: PresenceData) + case put(presence: StringValueTypeDictionary) case clear } @@ -40,9 +40,9 @@ enum PresenceChange { */ public class Presence { private var changeContext: ChangeContext - private(set) var presence: PresenceData + private(set) var presence: StringValueTypeDictionary - init(changeContext: ChangeContext, presence: PresenceData) { + init(changeContext: ChangeContext, presence: StringValueTypeDictionary) { self.changeContext = changeContext self.presence = presence } @@ -50,9 +50,9 @@ public class Presence { /** * `set` updates the presence based on the partial presence. */ - func set(_ presence: PresenceData) { + public func set(_ presence: PresenceData) { for (key, value) in presence { - self.presence[key] = value + self.presence[key] = value.toJSONString ?? "" } let presenceChange = PresenceChange.put(presence: self.presence) @@ -62,8 +62,12 @@ public class Presence { /** * `get` returns the presence value of the given key. */ - public func get(_ key: PresenceData.Key) -> Any? { - self.presence[key] + public func get(_ key: PresenceData.Key) -> T? { + if let data = self.presence[key]?.data(using: .utf8) { + return try? JSONDecoder().decode(T.self, from: data) + } + + return nil } /** diff --git a/Sources/Util/Codable+Extension.swift b/Sources/Util/Codable+Extension.swift index a17e930c..285016d2 100644 --- a/Sources/Util/Codable+Extension.swift +++ b/Sources/Util/Codable+Extension.swift @@ -27,6 +27,17 @@ extension Encodable { return (try? JSONSerialization.jsonObject(with: data) as? [String: Any]) ?? [:] } + + var toJSONString: String? { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + + guard let data = try? encoder.encode(self) else { + return nil + } + + return String(data: data, encoding: .utf8) + } } extension Encodable where Self == String { diff --git a/Sources/Util/Dictionary+Extension.swift b/Sources/Util/Dictionary+Extension.swift index 3aa7254e..26119545 100644 --- a/Sources/Util/Dictionary+Extension.swift +++ b/Sources/Util/Dictionary+Extension.swift @@ -27,21 +27,11 @@ extension AnyValueTypeDictionary { var convertedDictionary: [String: String] = [:] for (key, value) in dictionary { - if let stringValue = value as? String { - let jsonString = "\"\(stringValue)\"" - convertedDictionary[key] = jsonString - } else if let convertibleToString = value as? CustomStringConvertible { - let jsonString = "\(convertibleToString.description)" - convertedDictionary[key] = jsonString - } else if let nestedDictionary = value as? [String: Any] { - // If the value is a nested dictionary, recursively convert it - let nestedConverted = self.convertToDictionaryStringValues(nestedDictionary) - // Convert the nested dictionary to its JSON representation - if let jsonString = try? JSONSerialization.data(withJSONObject: nestedConverted) { - convertedDictionary[key] = String(data: jsonString, encoding: .utf8) - } else { - convertedDictionary[key] = "null" - } + if let value = value as? Encodable, + let jsonData = try? JSONEncoder().encode(value), + let stringValue = String(data: jsonData, encoding: .utf8) + { + convertedDictionary[key] = stringValue } else { print("Warning: Skipping non-convertible value for key '\(key)': \(value)") } @@ -99,4 +89,32 @@ extension StringValueTypeDictionary { return result } + + var toJSONObejct: [String: Any] { + self.compactMapValues { $0.toJSONObject } + } +} + +typealias CodableValueTypeDictionary = [String: Codable] + +extension CodableValueTypeDictionary { + var stringValueTypeDictionary: [String: String] { + self.convertToDictionaryStringValues(self) + } + + func convertToDictionaryStringValues(_ dictionary: [String: Codable]) -> [String: String] { + var convertedDictionary: [String: String] = [:] + + for (key, value) in dictionary { + if let jsonData = try? JSONEncoder().encode(value), + let stringValue = String(data: jsonData, encoding: .utf8) + { + convertedDictionary[key] = stringValue + } else { + print("Warning: Skipping non-convertible value for key '\(key)': \(value)") + } + } + + return convertedDictionary + } } diff --git a/Sources/Util/String+Extensions.swift b/Sources/Util/String+Extensions.swift index 10c4aaff..33e85f43 100644 --- a/Sources/Util/String+Extensions.swift +++ b/Sources/Util/String+Extensions.swift @@ -37,4 +37,12 @@ extension String { return regex?.stringByReplacingMatches(in: lower, options: [], range: NSRange(0 ..< lower.count), withTemplate: "-").substring(from: 0, to: 119) ?? "" } + + var toJSONObject: Any { + if let data = self.data(using: .utf8) { + return (try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])) ?? self + } + + return self + } } diff --git a/Tests/Integration/ClientIntegrationTests.swift b/Tests/Integration/ClientIntegrationTests.swift index 5fe479ca..25d15250 100644 --- a/Tests/Integration/ClientIntegrationTests.swift +++ b/Tests/Integration/ClientIntegrationTests.swift @@ -317,12 +317,4 @@ final class ClientIntegrationTests: XCTestCase { try await c2.deactivate() try await c3.deactivate() } - - private func decodePresence(_ dictionary: [String: Any]) -> T? { - guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else { - return nil - } - - return try? JSONDecoder().decode(T.self, from: data) - } } diff --git a/Tests/Integration/GCTests.swift b/Tests/Integration/GCTests.swift index bcb13dcc..27e2aa6e 100644 --- a/Tests/Integration/GCTests.swift +++ b/Tests/Integration/GCTests.swift @@ -321,10 +321,10 @@ class GCTests: XCTestCase { do { try (root.t as? JSONTree)?.editByPath([0], [1], [JSONTreeElementNode(type: "p", - children: [ - JSONTreeElementNode(type: "tn", - children: [JSONTreeTextNode(value: "ab")]) - ])]) + children: [ + JSONTreeElementNode(type: "tn", + children: [JSONTreeTextNode(value: "ab")]) + ])]) } catch { assertionFailure("Can't editByPath") } @@ -360,8 +360,8 @@ class GCTests: XCTestCase { try await client1.activate() try await client2.activate() - try await client1.attach(doc1) - try await client2.attach(doc2) + try await client1.attach(doc1, [:], false) + try await client2.attach(doc2, [:], false) try await doc1.update { root, _ in root.t = JSONTree(initialRoot: diff --git a/Tests/Integration/PresenceTests.swift b/Tests/Integration/PresenceTests.swift index 48c49880..bf76d111 100644 --- a/Tests/Integration/PresenceTests.swift +++ b/Tests/Integration/PresenceTests.swift @@ -231,7 +231,7 @@ final class PresenceTests: XCTestCase { try await c2.attach(doc2, ["counter": 0], false) try await doc1.update { _, presence in - if let counter = presence.get("counter") as? Int { + if let counter: Int = presence.get("counter") { presence.set(["counter": counter + 1]) } } diff --git a/Tests/Integration/TreeIntegrationTests.swift b/Tests/Integration/TreeIntegrationTests.swift index 95ffff4d..649b80b4 100644 --- a/Tests/Integration/TreeIntegrationTests.swift +++ b/Tests/Integration/TreeIntegrationTests.swift @@ -736,11 +736,11 @@ final class TreeIntegrationEditTests: XCTestCase { ])) } } - + func test_delete_nodes_in_a_multi_level_range_test() async throws { let docKey = "\(self.description)-\(Date().description)".toDocKey let doc = Document(key: docKey) - + try await doc.update { root, _ in root.t = JSONTree(initialRoot: JSONTreeElementNode(type: "doc", children: [ @@ -777,10 +777,10 @@ final class TreeIntegrationEditTests: XCTestCase { try await doc.update { root, _ in try (root.t as? JSONTree)?.edit(2, 18) } - + docXML = await(doc.getRoot().t as? JSONTree)?.toXML() XCTAssertEqual(docXML, /* html */ "

a

f

") - + // TODO(sejongk): Use the below assertion after implementing Tree.Move. // assert.equal(doc.getRoot().t.toXML(), /*html*/ `

af

`); } @@ -896,32 +896,32 @@ final class TreeIntegrationStyleTests: XCTestCase { XCTAssertEqual(d2XML, /* html */ "

hello

") } } - + func test_style_node_with_element_attributes_test() async throws { let docKey = "\(self.description)-\(Date().description)".toDocKey let doc = Document(key: docKey) - + try await doc.update { root, _ in root.t = JSONTree(initialRoot: - JSONTreeElementNode(type: "doc", children: [ - JSONTreeElementNode(type: "p", - children: [ - JSONTreeTextNode(value: "ab") - ]), - JSONTreeElementNode(type: "p", - children: [ - JSONTreeTextNode(value: "cd") - ]) - ]) + JSONTreeElementNode(type: "doc", children: [ + JSONTreeElementNode(type: "p", + children: [ + JSONTreeTextNode(value: "ab") + ]), + JSONTreeElementNode(type: "p", + children: [ + JSONTreeTextNode(value: "cd") + ]) + ]) ) - + XCTAssertEqual((root.t as? JSONTree)?.toXML(), /* html */ "

ab

cd

") - + // 01. style attributes to an element node. // style attributes with opening tag try (root.t as? JSONTree)?.style(0, 1, ["weight": "bold"]) XCTAssertEqual((root.t as? JSONTree)?.toXML(), /* html */ "

ab

cd

") - + // style attributes with closing tag try (root.t as? JSONTree)?.style(3, 4, ["color": "red"]) XCTAssertEqual((root.t as? JSONTree)?.toXML(), /* html */ "

ab

cd

") @@ -929,7 +929,7 @@ final class TreeIntegrationStyleTests: XCTestCase { // style attributes with the whole try (root.t as? JSONTree)?.style(0, 4, ["size": "small"]) XCTAssertEqual((root.t as? JSONTree)?.toXML(), /* html */ "

ab

cd

") - + // 02. style attributes to elements. try (root.t as? JSONTree)?.style(0, 5, ["style": "italic"]) XCTAssertEqual((root.t as? JSONTree)?.toXML(), /* html */ "

ab

cd

") diff --git a/Tests/Unit/API/V1/ConverterTests.swift b/Tests/Unit/API/V1/ConverterTests.swift index 1937121c..6d4d3128 100644 --- a/Tests/Unit/API/V1/ConverterTests.swift +++ b/Tests/Unit/API/V1/ConverterTests.swift @@ -359,7 +359,9 @@ class ConverterTests: XCTestCase { func test_presence() { let samplePresence: PresenceData = ["a": "str", "b": 10, "c": 0.5, "d": false, "e": ["a", "b", "c"], "f": ""] - let converted = Converter.fromPresence(pbPresence: Converter.toPresence(presence: samplePresence)) + let stringPresence = samplePresence.mapValues { $0.toJSONString ?? "" } + + let converted = Converter.fromPresence(pbPresence: Converter.toPresence(presence: stringPresence)) XCTAssert(!(samplePresence == converted)) } diff --git a/Tests/Unit/Document/CRDT/CRDTObjectTests.swift b/Tests/Unit/Document/CRDT/CRDTObjectTests.swift index 8c8ea274..aabf166b 100644 --- a/Tests/Unit/Document/CRDT/CRDTObjectTests.swift +++ b/Tests/Unit/Document/CRDT/CRDTObjectTests.swift @@ -92,7 +92,7 @@ class CRDTObjectTests: XCTestCase { target.set(key: "K3", value: a3) try target.deleteByKey(key: targetKey, - executedAt: TimeTicket(lamport: 4, delimiter: 0, actorID: self.actorId)) + executedAt: TimeTicket(lamport: 4, delimiter: 0, actorID: self.actorId)) XCTAssertFalse(target.has(key: targetKey)) diff --git a/Tests/Unit/Document/CRDT/CRDTTreeTests.swift b/Tests/Unit/Document/CRDT/CRDTTreeTests.swift index db3da9c2..789f80ee 100644 --- a/Tests/Unit/Document/CRDT/CRDTTreeTests.swift +++ b/Tests/Unit/Document/CRDT/CRDTTreeTests.swift @@ -219,42 +219,42 @@ final class CRDTTreeTests: XCTestCase { final class CRDTTreeMoveTests: XCTestCase { func test_can_delete_nodes_between_element_nodes_with_edit() async throws { - // 01. Create a tree with 2 paragraphs. - // 0 1 2 3 4 5 6 7 8 - //

a b

c d

+ // 01. Create a tree with 2 paragraphs. + // 0 1 2 3 4 5 6 7 8 + //

a b

c d

let tree = CRDTTree(root: CRDTTreeNode(id: issuePos(), type: DefaultTreeNodeType.root.rawValue), createdAt: issueTime) try tree.editByIndex((0, 0), [CRDTTreeNode(id: issuePos(), type: "p")], issueTime) try tree.editByIndex((1, 1), - [CRDTTreeNode(id: issuePos(), type: DefaultTreeNodeType.text.rawValue, value: "ab")], - issueTime) + [CRDTTreeNode(id: issuePos(), type: DefaultTreeNodeType.text.rawValue, value: "ab")], + issueTime) try tree.editByIndex((4, 4), [CRDTTreeNode(id: issuePos(), type: "p")], issueTime) try tree.editByIndex((5, 5), [CRDTTreeNode(id: issuePos(), type: DefaultTreeNodeType.text.rawValue, value: "cd")], issueTime) - - XCTAssertEqual(tree.toXML(), /*html*/ "

ab

cd

") - // 02. delete b, c and first paragraph. - // 0 1 2 3 4 - //

a d

+ XCTAssertEqual(tree.toXML(), /* html */ "

ab

cd

") + + // 02. delete b, c and first paragraph. + // 0 1 2 3 4 + //

a d

try tree.editByIndex((2, 6), nil, issueTime) - XCTAssertEqual(tree.toXML(), /*html*/ "

a

d

") - - // TODO(sejongk): Use the below assertion after implementing Tree.Move. - // assert.deepEqual(tree.toXML(), /*html*/ `

ad

`); - - // const treeNode = tree.toTestTreeNode(); - // assert.equal(treeNode.size, 4); // root - // assert.equal(treeNode.children![0].size, 2); // p - // assert.equal(treeNode.children![0].children![0].size, 1); // a - // assert.equal(treeNode.children![0].children![1].size, 1); // d - - // // 03. insert a new text node at the start of the first paragraph. - // tree.editByIndex( - // [1, 1], - // [new CRDTTreeNode(issuePos(), 'text', '@')], - // issueTime(), - // ); - // assert.deepEqual(tree.toXML(), /*html*/ `

@ad

`); + XCTAssertEqual(tree.toXML(), /* html */ "

a

d

") + + // TODO(sejongk): Use the below assertion after implementing Tree.Move. + // assert.deepEqual(tree.toXML(), /*html*/ `

ad

`); + + // const treeNode = tree.toTestTreeNode(); + // assert.equal(treeNode.size, 4); // root + // assert.equal(treeNode.children![0].size, 2); // p + // assert.equal(treeNode.children![0].children![0].size, 1); // a + // assert.equal(treeNode.children![0].children![1].size, 1); // d + + // // 03. insert a new text node at the start of the first paragraph. + // tree.editByIndex( + // [1, 1], + // [new CRDTTreeNode(issuePos(), 'text', '@')], + // issueTime(), + // ); + // assert.deepEqual(tree.toXML(), /*html*/ `

@ad

`); } } diff --git a/Tests/Unit/Document/Change/ChangeTests.swift b/Tests/Unit/Document/Change/ChangeTests.swift index 7e1e58a9..fc1a0c81 100644 --- a/Tests/Unit/Document/Change/ChangeTests.swift +++ b/Tests/Unit/Document/Change/ChangeTests.swift @@ -82,7 +82,7 @@ class ChangeTests: XCTestCase { {"k-a1":"a1","k-a3":{"k-b1":"b1"}} """) - var presences = [ActorID: PresenceData]() + var presences = [ActorID: StringValueTypeDictionary]() try target.execute(root: root, presences: &presences) diff --git a/Tests/Unit/Util/IndexTreeTests.swift b/Tests/Unit/Util/IndexTreeTests.swift index fc45fee8..12f2904f 100644 --- a/Tests/Unit/Util/IndexTreeTests.swift +++ b/Tests/Unit/Util/IndexTreeTests.swift @@ -158,7 +158,8 @@ final class IndexTreeTests: XCTestCase { "text.b:All", "p:Closing", "text.cde:All", - "p:Opening"]) + "p:Opening" + ]) try self.nodesBetweenEqual(tree, 0, 1, ["p:Opening"]) try self.nodesBetweenEqual(tree, 3, 4, ["p:Closing"]) try self.nodesBetweenEqual(tree, 3, 5, ["p:Closing", "p:Opening"])