From 86446ee7acd4b797e383a6ccd3ec79e23e1c382c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=95=EC=8B=9D?= Date: Tue, 27 Sep 2022 18:08:33 +0900 Subject: [PATCH] Implement basic types for CRDT data structure (#10) This commit implements primitives for creating CRDT datatypes such as TimeTicket, ActorID, CRDTElement, and so on. We found a problem with the JS SDK using lamport as Uint64 and this commit fixed it with Int64. --- Sources/API/Converter.swift | 51 ++++++ .../Ticket.swift => API/GRPCTypeAlias.swift} | 11 +- Sources/Core/Logger.swift | 5 + Sources/Document/CRDT/CRDTElement.swift | 161 ++++++++++++++++++ Sources/Document/CRDT/Primitive.swift | 157 +++++++++++++++++ Sources/Document/Json/Strings.swift | 38 +++++ Sources/Document/Time/ActorId.swift | 36 ++++ Sources/Document/Time/TimeTicket.swift | 141 +++++++++++++++ .../CRDT/Root.swift => Util/Errors.swift} | 7 +- Sources/Util/SplayTree.swift | 8 +- Tests/API/V1/GRPCTests.swift | 12 +- Tests/Document/CRDT/CRDTElementTests.swift | 89 ++++++++++ Tests/Document/CRDT/PrimitiveTests.swift | 111 ++++++++++++ Tests/Document/Time/TimeTicketTests.swift | 41 +++++ Tests/Util/SplayTreeTests.swift | 24 +-- Yorkie.xcodeproj/project.pbxproj | 76 ++++++++- 16 files changed, 936 insertions(+), 32 deletions(-) create mode 100644 Sources/API/Converter.swift rename Sources/{Document/Time/Ticket.swift => API/GRPCTypeAlias.swift} (55%) create mode 100644 Sources/Document/CRDT/CRDTElement.swift create mode 100644 Sources/Document/CRDT/Primitive.swift create mode 100644 Sources/Document/Json/Strings.swift create mode 100644 Sources/Document/Time/ActorId.swift create mode 100644 Sources/Document/Time/TimeTicket.swift rename Sources/{Document/CRDT/Root.swift => Util/Errors.swift} (85%) create mode 100644 Tests/Document/CRDT/CRDTElementTests.swift create mode 100644 Tests/Document/CRDT/PrimitiveTests.swift create mode 100644 Tests/Document/Time/TimeTicketTests.swift diff --git a/Sources/API/Converter.swift b/Sources/API/Converter.swift new file mode 100644 index 0000000..b092500 --- /dev/null +++ b/Sources/API/Converter.swift @@ -0,0 +1,51 @@ +/* + * Copyright 2022 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +enum Converter { + + /** + * parses the given bytes into value. + */ + static func valueFrom(valueType: ValueType, data: Data) throws -> PrimitiveValue { + switch valueType { + case .null: + return .null + case .boolean: + return .boolean(data[0] == 1) + case .integer: + let result = data.withUnsafeBytes { $0.load(as: Int32.self) } + return .integer(result) + case .double: + let result = data.withUnsafeBytes { $0.load(as: Double.self) } + return .double(result) + case .string: + return .string(String(decoding: data, as: UTF8.self)) + case .long: + let result = data.withUnsafeBytes { $0.load(as: Int64.self) } + return .long(result) + case .bytes: + return .bytes(data) + case .date: + let milliseconds = data.withUnsafeBytes { $0.load(as: Double.self) } + return .date(Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)) + default: + throw YorkieError.unimplemented(message: String(describing: valueType)) + } + } + +} diff --git a/Sources/Document/Time/Ticket.swift b/Sources/API/GRPCTypeAlias.swift similarity index 55% rename from Sources/Document/Time/Ticket.swift rename to Sources/API/GRPCTypeAlias.swift index db2f172..68e4a28 100644 --- a/Sources/Document/Time/Ticket.swift +++ b/Sources/API/GRPCTypeAlias.swift @@ -1,7 +1,7 @@ /* * Copyright 2022 The Yorkie Authors. All rights reserved. * - * Licensed under the Apache License, Version 2.0 (the "License"); + * Licensed under the Apache License, Version 2.0 (the "License") * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * @@ -14,4 +14,13 @@ * limitations under the License. */ +// Use a Swift typealias to remap the type names of Protobuf locally +// Swift Protobuf Guide: https://github.com/apple/swift-protobuf/blob/main/Documentation/API.md#generated-struct-name import Foundation + +typealias ValueType = Yorkie_V1_ValueType +typealias YorkieServiceNIOClient = Yorkie_V1_YorkieServiceNIOClient +typealias ActivateClientRequest = Yorkie_V1_ActivateClientRequest +typealias DeactivateClientRequest = Yorkie_V1_DeactivateClientRequest +typealias YorkieServiceAsyncClient = Yorkie_V1_YorkieServiceAsyncClient + diff --git a/Sources/Core/Logger.swift b/Sources/Core/Logger.swift index 14abdcc..d94d657 100644 --- a/Sources/Core/Logger.swift +++ b/Sources/Core/Logger.swift @@ -21,6 +21,7 @@ enum LogLevel: String { case info = "INFO" case warn = "WARN" case error = "ERROR" + case fatal = "FATAL" } enum Logger { @@ -40,6 +41,10 @@ enum Logger { self.log(level: .error, message, error: error, filename: filename, function: function, line: line) } + static func fatal(_ message: String, error: Error? = nil, filename: String = #file, function: String = #function, line: Int = #line) { + self.log(level: .fatal, message, error: error, filename: filename, function: function, line: line) + } + static func log(level: LogLevel, _ message: String, error: Error? = nil, filename: String = #file, function: String = #function, line: Int = #line) { let file = URL(fileURLWithPath: filename).lastPathComponent let log = message + (error?.localizedDescription ?? "") diff --git a/Sources/Document/CRDT/CRDTElement.swift b/Sources/Document/CRDT/CRDTElement.swift new file mode 100644 index 0000000..2b2e38d --- /dev/null +++ b/Sources/Document/CRDT/CRDTElement.swift @@ -0,0 +1,161 @@ +/* + * Copyright 2022 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/** + * `CRDTElement` represents element type containing logical clock. + * + * @internal + */ +class CRDTElement { + private var createdAt: TimeTicket + private var movedAt: TimeTicket? + private var removedAt: TimeTicket? + + init(createdAt: TimeTicket) { + self.createdAt = createdAt + } + + /** + * `getCreatedAt` returns the creation time of this element. + */ + func getCreatedAt() -> TimeTicket { + return self.createdAt + } + + /** + * `getID` returns the creation time of this element. + */ + func getID() -> TimeTicket { + return self.createdAt + } + + /** + * `getMovedAt` returns the move time of this element. + */ + func getMovedAt() -> TimeTicket? { + return self.movedAt + } + + /** + * `getRemovedAt` returns the removal time of this element. + */ + func getRemovedAt() -> TimeTicket? { + return self.removedAt + } + + /** + * `setMovedAt` sets the move time of this element. + */ + @discardableResult + func setMovedAt(_ movedAt: TimeTicket?) -> Bool { + guard let currentMoveAt = self.movedAt else { + self.movedAt = movedAt + return true + } + + if let movedAt = movedAt, movedAt.after(currentMoveAt) { + self.movedAt = movedAt + return true + } + + return false + } + + /** + * `setRemovedAt` sets the remove time of this element. + */ + func setRemovedAt(_ removedAt: TimeTicket?) { + self.removedAt = removedAt + } + + /** + * `remove` removes this element. + */ + func remove(_ removedAt: TimeTicket?) -> Bool { + guard let removedAt = removedAt, removedAt.after(self.createdAt) else { + return false + } + + if self.removedAt == nil { + self.removedAt = removedAt + return true + } + + if let currentRemovedAt = self.removedAt, removedAt.after(currentRemovedAt) { + self.removedAt = removedAt + return true + } + + return false + } + + /** + * `isRemoved` check if this element was removed. + */ + func isRemoved() -> Bool { + return self.removedAt != nil + } + + func toJSON() -> String { + fatalError("Must be implemented.") + } + + func toSortedJSON() -> String { + fatalError("Must be implemented.") + } + + func deepcopy() -> CRDTElement { + fatalError("Must be implemented.") + } +} + +/** + * + * `CRDTContainer` represents CRDTArray or CRDtObject. + * @internal + */ +class CRDTContainer: CRDTElement { + func keyOf(createdAt: TimeTicket) -> String? { + fatalError("Must be implemented.") + } + + func purge(element: CRDTElement) { + fatalError("Must be implemented.") + } + + func delete(createdAt: TimeTicket, executedAt: TimeTicket) -> CRDTElement { + fatalError("Must be implemented.") + } + + func getDescendants(callback: (_ elem: CRDTElement, _ parent: CRDTContainer) -> Bool) { + fatalError("Must be implemented.") + } +} + +/** + * `CRDTTextElement` represents CRDTText or CRDTRichText. + */ +class CRDTTextElement: CRDTElement { + func getRemovedNodesLen() -> Int { + fatalError("Must be implemented.") + } + + func purgeTextNodesWithGarbage(ticket: TimeTicket) -> Int { + fatalError("Must be implemented.") + } +} diff --git a/Sources/Document/CRDT/Primitive.swift b/Sources/Document/CRDT/Primitive.swift new file mode 100644 index 0000000..0b0c2ff --- /dev/null +++ b/Sources/Document/CRDT/Primitive.swift @@ -0,0 +1,157 @@ +/* + * Copyright 2022 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +enum PrimitiveValue { + case null + case boolean(Bool) + case integer(Int32) + case long(Int64) + case double(Double) + case string(String) + case bytes(Data) + case date(Date) +} + +/** + * `Primitive` represents primitive data type including logical clock. + * It has a type and a value. + */ +class Primitive: CRDTElement { + let value: PrimitiveValue + + init(value: PrimitiveValue, createdAt: TimeTicket) { + self.value = value + super.init(createdAt: createdAt) + } + + /** + * `toJSON` returns the JSON encoding of the value. + * + * TODOs: We need to consider the case where the value is + * a byte array and a date. + */ + var toJSON: String { + switch self.value { + case .null: + return "null" + case .boolean(let value): + return "\(value)" + case .integer(let value): + return "\(value)" + case .double(let value): + return "\(value)" + case .string(let value): + return "\(value.escaped())" + case .long(let value): + return "\(value)" + case .bytes(let value): + return "\(value)" + case .date(let value): + return "\(value.timeIntervalSince1970 * 1000)" + } + } + + /** + * `toSortedJSON` returns the sorted JSON encoding of the value. + */ + var toSortedJSON: String { + return self.toJSON + } + + /** + * `deepcopy` copies itself deeply. + */ + var deepcopy: Primitive { + let primitive = Primitive(value: self.value, createdAt: self.getCreatedAt()) + primitive.setMovedAt(self.getMovedAt()) + return primitive + } + + /** + * `getPrimitiveType` returns the primitive type of the value. + */ + static func type(of value: Any?) -> PrimitiveValue? { + guard let value = value else { + return .null + } + + switch value { + case let casted as Bool: + return .boolean(casted) + case let casted as Int32: + return .integer(casted) + case let casted as Int64: + return .long(casted) + case let casted as Double: + return .double(casted) + case let casted as String: + return .string(casted) + case let casted as Data: + return .bytes(casted) + case let casted as Date: + return .date(casted) + default: + return nil + } + } + + /** + * `isSupport` check if the given value is supported type. + */ + static func isSupport(value: Any) -> Bool { + return Primitive.type(of: value) != nil + } + + /** + * `isNumericType` checks numeric type by JSONPrimitive + */ + var isNumericType: Bool { + switch self.value { + case .integer, .long, .double: + return true + default: + return false + } + } + + /** + * `toBytes` creates an array representing the value. + */ + func toBytes() throws -> Data { + switch self.value { + case .null: + return Data() + case .boolean(let value): + var valueInInt = Int(exactly: NSNumber(value: value)) + return Data(bytes: &valueInInt, count: MemoryLayout.size(ofValue: valueInInt)) + case .integer(let value): + return withUnsafeBytes(of: value) { Data($0) } + case .double(let value): + return withUnsafeBytes(of: value) { Data($0) } + case .string(let value): + return value.data(using: .utf8) ?? Data() + case .long(let value): + return withUnsafeBytes(of: value) { Data($0) } + case .bytes(let value): + return value + case .date(let value): + let milliseconds = value.timeIntervalSince1970 * 1000 + return withUnsafeBytes(of: milliseconds) { Data($0) } + } + } +} diff --git a/Sources/Document/Json/Strings.swift b/Sources/Document/Json/Strings.swift new file mode 100644 index 0000000..9545bc7 --- /dev/null +++ b/Sources/Document/Json/Strings.swift @@ -0,0 +1,38 @@ +/* + * Copyright 2022 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation + +/** + * `EscapeString` escapes string. + */ +extension String { + static let escapeSequences = [ + (original: "\\", escaped: "\\\\"), + (original: "\"", escaped: "\\\""), + (original: "'", escaped: "\\'"), + (original: "\n", escaped: "\\n"), + (original: "\r", escaped: "\\r"), + (original: "\t", escaped: "\\t"), + (original: "\u{2028}", escaped: "\\u{2028}"), + (original: "\u{2029}", escaped: "\\u{2029}") + ] + + func escaped() -> String { + return String.escapeSequences.reduce(self) { string, seq in + string.replacingOccurrences(of: seq.original, with: seq.escaped) + } + } +} diff --git a/Sources/Document/Time/ActorId.swift b/Sources/Document/Time/ActorId.swift new file mode 100644 index 0000000..80c11e5 --- /dev/null +++ b/Sources/Document/Time/ActorId.swift @@ -0,0 +1,36 @@ +/* + * Copyright 2020 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/** + * `ActorID` is used to identify who is making changes to the document. + * It is hexadecimal string and should be generated by unique value. + * + */ +typealias ActorID = String + +enum ActorIds { + /** + * `InitialActorID` is the initial value of ActorID. + */ + static let initialActorID = "000000000000000000000000" + + /** + * `MaxActorID` is the maximum value of ActorID. + */ + static let maxActorID = "FFFFFFFFFFFFFFFFFFFFFFFF" +} diff --git a/Sources/Document/Time/TimeTicket.swift b/Sources/Document/Time/TimeTicket.swift new file mode 100644 index 0000000..30657b5 --- /dev/null +++ b/Sources/Document/Time/TimeTicket.swift @@ -0,0 +1,141 @@ +/* + * Copyright 2022 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/** + * `TimeTicket` is a timestamp of the logical clock. Ticket is immutable. + * It is created by `ChangeID`. + */ +class TimeTicket { + private enum InitialValue { + static let initialDelimiter: UInt32 = 0 + static let maxDelemiter: UInt32 = .max + static let maxLamport: Int64 = .max + } + + static let initialTimeTicket = TimeTicket(lamport: 0, delimiter: InitialValue.initialDelimiter, actorID: ActorIds.initialActorID) + static let maxTimeTicket = TimeTicket(lamport: InitialValue.maxLamport, delimiter: InitialValue.maxDelemiter, actorID: ActorIds.maxActorID) + + private var lamport: Int64 + private var delimiter: UInt32 + private var actorID: ActorID? + + /** @hideconstructor */ + init(lamport: Int64, delimiter: UInt32, actorID: ActorID?) { + self.lamport = lamport + self.delimiter = delimiter + self.actorID = actorID + } + + /** + * `of` creates an instance of Ticket. + */ + public static func of(lamport: Int64, delimiter: UInt32, actorID: ActorID?) -> TimeTicket { + return TimeTicket(lamport: lamport, delimiter: delimiter, actorID: actorID) + } + + /** + * `toIDString` returns the lamport string for this Ticket. + */ + func toIDString() -> String { + guard let actorID = self.actorID else { + return "\(self.lamport):nil:\(self.delimiter)" + } + return "\(self.lamport):\(actorID):\(self.delimiter)" + } + + /** + * `getStructureAsString` returns a string containing the meta data of the ticket + * for debugging purpose. + */ + func getStructureAsString() -> String { + guard let actorID = self.actorID else { + return "\(self.lamport):nil:\(self.delimiter)" + } + return "\(self.lamport):\(actorID):\(self.delimiter)" + } + + /** + * `setActor` creates a new instance of Ticket with the given actorID. + */ + func setActor(actorID: ActorID) -> TimeTicket { + return TimeTicket(lamport: self.lamport, delimiter: self.delimiter, actorID: actorID) + } + + /** + * `getLamportAsString` returns the lamport string. + */ + func getLamportAsString() -> String { + return "\(self.lamport)" + } + + /** + * `getDelimiter` returns delimiter. + */ + func getDelimiter() -> UInt32 { + return self.delimiter + } + + /** + * `getActorID` returns actorID. + */ + func getActorID() -> String? { + return self.actorID + } + + /** + * `after` returns whether the given ticket was created later. + */ + func after(_ other: TimeTicket) -> Bool { + return self.compare(other) == .orderedDescending + } + + /** + * `equals` returns whether the given ticket was created. + */ + func equals(other: TimeTicket) -> Bool { + return self.compare(other) == .orderedSame + } + + /** + * `compare` returns an integer comparing two Ticket. + * The result will be 0 if id==other, -1 if `id < other`, and +1 if `id > other`. + * If the receiver or argument is nil, it would panic at runtime. + */ + func compare(_ other: TimeTicket) -> ComparisonResult { + if self.lamport > other.lamport { + return .orderedDescending + } else if self.lamport < other.lamport { + return .orderedAscending + } + + if let actorID = actorID, let otherActorID = other.actorID { + let compare = actorID.localizedCompare(otherActorID) + if compare != .orderedSame { + return compare + } + } + + if self.delimiter > other.delimiter { + return .orderedDescending + } else if other.delimiter > self.delimiter { + return .orderedAscending + } + + return .orderedSame + } +} diff --git a/Sources/Document/CRDT/Root.swift b/Sources/Util/Errors.swift similarity index 85% rename from Sources/Document/CRDT/Root.swift rename to Sources/Util/Errors.swift index db2f172..182371a 100644 --- a/Sources/Document/CRDT/Root.swift +++ b/Sources/Util/Errors.swift @@ -1,7 +1,7 @@ /* * Copyright 2022 The Yorkie Authors. All rights reserved. * - * Licensed under the Apache License, Version 2.0 (the "License"); + * Licensed under the Apache License, Version 2.0 (the "License") * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * @@ -15,3 +15,8 @@ */ import Foundation + +enum YorkieError: Error { + case unexpected(message: String) + case unimplemented(message: String) +} diff --git a/Sources/Util/SplayTree.swift b/Sources/Util/SplayTree.swift index f7d5aff..62d4490 100644 --- a/Sources/Util/SplayTree.swift +++ b/Sources/Util/SplayTree.swift @@ -33,8 +33,7 @@ class SplayNode { } func getLength() -> Int { - assertionFailure("Must be implemented.") - return 0 + fatalError("Must be implemented.") } /** @@ -248,6 +247,7 @@ class SplayTree { /** * `insertAfter` inserts the node after the given previous node. */ + @discardableResult func insertAfter(_ target: SplayNode?, newNode: SplayNode) -> SplayNode { guard let target = target else { self.root = newNode @@ -404,10 +404,10 @@ class SplayTree { } /** - * `getAnnotatedString` returns a string containing the meta data of the Node + * `getStructureAsString` returns a string containing the meta data of the Node * for debugging purpose. */ - func getAnnotatedString() -> String { + func getStructureAsString() -> String { var metaString: [SplayNode] = [] self.traverseInorder(self.root!, stack: &metaString) return metaString diff --git a/Tests/API/V1/GRPCTests.swift b/Tests/API/V1/GRPCTests.swift index 95cb7af..0485449 100644 --- a/Tests/API/V1/GRPCTests.swift +++ b/Tests/API/V1/GRPCTests.swift @@ -35,8 +35,8 @@ class GRPCTests: XCTestCase { return } - let client = Yorkie_V1_YorkieServiceNIOClient(channel: channel) - var activateRequest = Yorkie_V1_ActivateClientRequest() + let client = YorkieServiceNIOClient(channel: channel) + var activateRequest = ActivateClientRequest() activateRequest.clientKey = testClientKey let activateResponse = try? client.activateClient(activateRequest, callOptions: nil).response.wait() guard let activateResponse = activateResponse else { @@ -46,7 +46,7 @@ class GRPCTests: XCTestCase { XCTAssertEqual(activateResponse.clientKey, testClientKey) - var deactivateRequest = Yorkie_V1_DeactivateClientRequest() + var deactivateRequest = DeactivateClientRequest() deactivateRequest.clientID = activateResponse.clientID guard let deactivatedResponse = try? client.deactivateClient(deactivateRequest).response.wait() else { XCTFail("The response of deactivate is nil.") @@ -71,8 +71,8 @@ class GRPCTests: XCTestCase { return } - let client = Yorkie_V1_YorkieServiceAsyncClient(channel: channel) - var activateRequest = Yorkie_V1_ActivateClientRequest() + let client = YorkieServiceAsyncClient(channel: channel) + var activateRequest = ActivateClientRequest() activateRequest.clientKey = testClientKey let activateResponse = try? await client.activateClient(activateRequest, callOptions: nil) guard let activateResponse = activateResponse else { @@ -82,7 +82,7 @@ class GRPCTests: XCTestCase { XCTAssertEqual(activateResponse.clientKey, testClientKey) - var deactivateRequest = Yorkie_V1_DeactivateClientRequest() + var deactivateRequest = DeactivateClientRequest() deactivateRequest.clientID = activateResponse.clientID guard let deactivatedResponse = try? await client.deactivateClient(deactivateRequest) else { XCTFail("The response of deactivate is nil.") diff --git a/Tests/Document/CRDT/CRDTElementTests.swift b/Tests/Document/CRDT/CRDTElementTests.swift new file mode 100644 index 0000000..14ea088 --- /dev/null +++ b/Tests/Document/CRDT/CRDTElementTests.swift @@ -0,0 +1,89 @@ +/* + * Copyright 2022 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import XCTest +@testable import Yorkie + +class CRDTElementTests: XCTestCase { + func test_can_set_smaller_movedAt() { + let small = TimeTicket.initialTimeTicket + let big = TimeTicket.maxTimeTicket + + let target = CRDTElement(createdAt: big) + let movedResult = target.setMovedAt(small) + + XCTAssertEqual(movedResult, true) + XCTAssertEqual(target.getMovedAt()?.compare(small), .orderedSame) + } + + func test_can_set_bigger_movedAt_when_movedAt_is_nil() { + let small = TimeTicket.initialTimeTicket + let big = TimeTicket.maxTimeTicket + + let target = CRDTElement(createdAt: small) + let movedResult = target.setMovedAt(big) + + XCTAssertEqual(movedResult, true) + XCTAssertEqual(target.getMovedAt()?.compare(big), .orderedSame) + } + + func test_can_not_set_bigger_movedAt_when_movedAt_is_non_nil() { + let small = TimeTicket.initialTimeTicket + let big = TimeTicket.maxTimeTicket + + let target = CRDTElement(createdAt: small) + target.setMovedAt(big) + + let timeTicket = TimeTicket(lamport: 10, delimiter: 10, actorID: ActorIds.initialActorID) + let movedResult = target.setMovedAt(timeTicket) + + XCTAssertEqual(movedResult, false) + XCTAssertEqual(target.getMovedAt()?.compare(small), .orderedDescending) + } + + func test_can_not_remove_when_nil() { + let target = CRDTElement(createdAt: TimeTicket.initialTimeTicket) + + XCTAssertEqual(target.remove(nil), false) + } + + func test_can_not_remove_when_removeAt_is_before_createdAt() { + let target = CRDTElement(createdAt: TimeTicket.maxTimeTicket) + + XCTAssertEqual(target.remove(TimeTicket.initialTimeTicket), false) + } + + func test_can_remove_when_current_removeAt_is_nil() { + let target = CRDTElement(createdAt: TimeTicket.initialTimeTicket) + + XCTAssertEqual(target.remove(TimeTicket.maxTimeTicket), true) + } + + func test_can_remove_when_current_removeAt_is_not_nil_and_samll() { + let target = CRDTElement(createdAt: TimeTicket.initialTimeTicket) + target.setRemovedAt(TimeTicket.initialTimeTicket) + + XCTAssertEqual(target.remove(TimeTicket.maxTimeTicket), true) + } + + func test_can_not_remove_when_current_removeAt_is_not_nil_and_big() { + let target = CRDTElement(createdAt: TimeTicket.initialTimeTicket) + target.setRemovedAt(TimeTicket.maxTimeTicket) + + let timeTicket = TimeTicket(lamport: 10, delimiter: 10, actorID: ActorIds.initialActorID) + XCTAssertEqual(target.remove(timeTicket), false) + } +} diff --git a/Tests/Document/CRDT/PrimitiveTests.swift b/Tests/Document/CRDT/PrimitiveTests.swift new file mode 100644 index 0000000..2e087f4 --- /dev/null +++ b/Tests/Document/CRDT/PrimitiveTests.swift @@ -0,0 +1,111 @@ +/* + * Copyright 2022 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import XCTest +@testable import Yorkie + +class PrimitiveTests: XCTestCase { + func test_value_is_null() throws { + let primitiveValue = Primitive(value: .null, createdAt: TimeTicket.initialTimeTicket) + let valueFromData = try Converter.valueFrom(valueType: .null, data: primitiveValue.toBytes()) + switch valueFromData { + case .null: + () + default: + XCTFail("Type error.") + } + } + + func test_value_is_bool() throws { + let primitiveValue = Primitive(value: .boolean(true), createdAt: TimeTicket.initialTimeTicket) + let valueFromData = try Converter.valueFrom(valueType: .boolean, data: primitiveValue.toBytes()) + switch valueFromData { + case .boolean(let value): + XCTAssertEqual(value, true) + default: + XCTFail("Type error.") + } + } + + func test_value_is_integer() throws { + let primitiveValue = Primitive(value: .integer(12345), createdAt: TimeTicket.initialTimeTicket) + let valueFromData = try Converter.valueFrom(valueType: .integer, data: primitiveValue.toBytes()) + switch valueFromData { + case .integer(let value): + XCTAssertEqual(value, 12345) + default: + XCTFail("Type error.") + } + } + + func test_value_is_long() throws { + let primitiveValue = Primitive(value: .long(1_234_567_890), createdAt: TimeTicket.initialTimeTicket) + let valueFromData = try Converter.valueFrom(valueType: .long, data: primitiveValue.toBytes()) + switch valueFromData { + case .long(let value): + XCTAssertEqual(value, 1_234_567_890) + default: + XCTFail("Type error.") + } + } + + func test_value_is_double() throws { + let primitiveValue = Primitive(value: .double(-123_456_789), createdAt: TimeTicket.initialTimeTicket) + let valueFromData = try Converter.valueFrom(valueType: .double, data: primitiveValue.toBytes()) + switch valueFromData { + case .double(let value): + XCTAssertEqual(value, -123_456_789) + default: + XCTFail("Type error.") + } + } + + func test_value_is_string() throws { + let primitiveValue = Primitive(value: .string("ABCDEFG"), createdAt: TimeTicket.initialTimeTicket) + let valueFromData = try Converter.valueFrom(valueType: .string, data: primitiveValue.toBytes()) + switch valueFromData { + case .string(let value): + XCTAssertEqual(value, "ABCDEFG") + default: + XCTFail("Type error.") + } + } + + func test_value_is_bytes() throws { + let testData = "abcdefg".data(using: .utf8)! + let primitiveValue = Primitive(value: .bytes(testData), createdAt: TimeTicket.initialTimeTicket) + let valueFromData = try Converter.valueFrom(valueType: .bytes, data: primitiveValue.toBytes()) + switch valueFromData { + case .bytes(let value): + XCTAssertEqual(String(decoding: value, as: UTF8.self), "abcdefg") + default: + XCTFail("Type error.") + } + } + + func test_value_is_date() throws { + let testDate = Date() + print(testDate.timeIntervalSince1970) + let primitiveValue = Primitive(value: .date(testDate), createdAt: TimeTicket.initialTimeTicket) + let valueFromData = try Converter.valueFrom(valueType: .date, data: primitiveValue.toBytes()) + switch valueFromData { + case .date(let value): + XCTAssertEqual(value.timeIntervalSince1970, testDate.timeIntervalSince1970) + default: + XCTFail("Type error.") + } + } +} diff --git a/Tests/Document/Time/TimeTicketTests.swift b/Tests/Document/Time/TimeTicketTests.swift new file mode 100644 index 0000000..1352b1f --- /dev/null +++ b/Tests/Document/Time/TimeTicketTests.swift @@ -0,0 +1,41 @@ +/* + * Copyright 2022 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import XCTest +@testable import Yorkie + +class TimeTicketTests: XCTestCase { + func test_compare_with_a_big_thing() { + let small = TimeTicket.initialTimeTicket + let big = TimeTicket.maxTimeTicket + + XCTAssertEqual(small.compare(big), .orderedAscending) + } + + func test_compare_with_a_small_thing() { + let big = TimeTicket.maxTimeTicket + let small = TimeTicket.initialTimeTicket + + XCTAssertEqual(big.compare(small), .orderedDescending) + } + + func test_compare_with_a_same_thing() { + let big = TimeTicket.maxTimeTicket + let big2 = TimeTicket.maxTimeTicket + + XCTAssertEqual(big.compare(big2), .orderedSame) + } +} diff --git a/Tests/Util/SplayTreeTests.swift b/Tests/Util/SplayTreeTests.swift index 72f892b..d3840d0 100644 --- a/Tests/Util/SplayTreeTests.swift +++ b/Tests/Util/SplayTreeTests.swift @@ -40,16 +40,16 @@ class SplayTreeTests: XCTestCase { let tree = SplayTree() let nodeA = tree.insert(StringNode.create("A2")) - XCTAssertEqual(tree.getAnnotatedString(), "[2,2]A2") + XCTAssertEqual(tree.getStructureAsString(), "[2,2]A2") XCTAssertEqual(tree.getRoot()?.value, "A2") let nodeB = tree.insert(StringNode.create("B23")) - XCTAssertEqual(tree.getAnnotatedString(), "[2,2]A2[5,3]B23") + XCTAssertEqual(tree.getStructureAsString(), "[2,2]A2[5,3]B23") XCTAssertEqual(tree.getRoot()?.value, "B23") let nodeC = tree.insert(StringNode.create("C234")) - XCTAssertEqual(tree.getAnnotatedString(), "[2,2]A2[5,3]B23[9,4]C234") + XCTAssertEqual(tree.getStructureAsString(), "[2,2]A2[5,3]B23[9,4]C234") XCTAssertEqual(tree.getRoot()?.value, "C234") let nodeD = tree.insert(StringNode.create("D2345")) - XCTAssertEqual(tree.getAnnotatedString(), "[2,2]A2[5,3]B23[9,4]C234[14,5]D2345") + XCTAssertEqual(tree.getStructureAsString(), "[2,2]A2[5,3]B23[9,4]C234[14,5]D2345") XCTAssertEqual(tree.getRoot()?.value, "D2345") XCTAssertEqual(tree.indexOf(nodeA), 0) @@ -66,16 +66,16 @@ class SplayTreeTests: XCTestCase { let tree = SplayTree() let nodeH = tree.insert(StringNode.create("H")) - XCTAssertEqual(tree.getAnnotatedString(), "[1,1]H") + XCTAssertEqual(tree.getStructureAsString(), "[1,1]H") let nodeE = tree.insert(StringNode.create("E")) - XCTAssertEqual(tree.getAnnotatedString(), "[1,1]H[2,1]E") + XCTAssertEqual(tree.getStructureAsString(), "[1,1]H[2,1]E") let nodeL = tree.insert(StringNode.create("LL")) - XCTAssertEqual(tree.getAnnotatedString(), "[1,1]H[2,1]E[4,2]LL") + XCTAssertEqual(tree.getStructureAsString(), "[1,1]H[2,1]E[4,2]LL") let nodeO = tree.insert(StringNode.create("O")) - XCTAssertEqual(tree.getAnnotatedString(), "[1,1]H[2,1]E[4,2]LL[5,1]O") + XCTAssertEqual(tree.getStructureAsString(), "[1,1]H[2,1]E[4,2]LL[5,1]O") tree.delete(nodeE) - XCTAssertEqual(tree.getAnnotatedString(), "[4,1]H[3,2]LL[1,1]O") + XCTAssertEqual(tree.getStructureAsString(), "[4,1]H[3,2]LL[1,1]O") XCTAssertEqual(tree.indexOf(nodeH), 0) XCTAssertEqual(tree.indexOf(nodeE), -1) @@ -113,12 +113,12 @@ class SplayTreeTests: XCTestCase { func test_can_delete_range_between_the_given_2_boundary_nodes_first() { let testTree = self.sampleTree // check the filtering of rangeDelete - XCTAssertEqual("[1,1]A[3,2]BB[6,3]CCC[10,4]DDDD[15,5]EEEEE[19,4]FFFF[22,3]GGG[24,2]HH[25,1]I", testTree.tree.getAnnotatedString()) + XCTAssertEqual("[1,1]A[3,2]BB[6,3]CCC[10,4]DDDD[15,5]EEEEE[19,4]FFFF[22,3]GGG[24,2]HH[25,1]I", testTree.tree.getStructureAsString()) self.removeNodes(testTree.nodes, from: 7, to: 8) - XCTAssertEqual("[1,1]A[3,2]BB[6,3]CCC[10,4]DDDD[15,5]EEEEE[19,4]FFFF[22,3]GGG[24,0]HH[25,0]I", testTree.tree.getAnnotatedString()) + XCTAssertEqual("[1,1]A[3,2]BB[6,3]CCC[10,4]DDDD[15,5]EEEEE[19,4]FFFF[22,3]GGG[24,0]HH[25,0]I", testTree.tree.getStructureAsString()) testTree.tree.deleteRange(leftBoundary: testTree.nodes[6]) XCTAssertEqual(testTree.tree.indexOf(testTree.nodes[6]), 19) - XCTAssertEqual("[1,1]A[3,2]BB[6,3]CCC[10,4]DDDD[15,5]EEEEE[19,4]FFFF[22,3]GGG[0,0]HH[0,0]I", testTree.tree.getAnnotatedString()) + XCTAssertEqual("[1,1]A[3,2]BB[6,3]CCC[10,4]DDDD[15,5]EEEEE[19,4]FFFF[22,3]GGG[0,0]HH[0,0]I", testTree.tree.getStructureAsString()) XCTAssertTrue(testTree.nodes[6] === testTree.tree.getRoot()) XCTAssertEqual(testTree.nodes[6].getWeight(), 22) XCTAssertEqual(self.sumOfWeight(testTree.nodes, from: 7, to: 8), 0) diff --git a/Yorkie.xcodeproj/project.pbxproj b/Yorkie.xcodeproj/project.pbxproj index a4b09b1..5da43a7 100644 --- a/Yorkie.xcodeproj/project.pbxproj +++ b/Yorkie.xcodeproj/project.pbxproj @@ -14,12 +14,21 @@ CE3EC95228D195E4009471BC /* RedBlackTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC95028D195E0009471BC /* RedBlackTreeTests.swift */; }; CE3EC95F28D2AAA1009471BC /* SplayTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC95D28D2AA9C009471BC /* SplayTreeTests.swift */; }; CE3EC96128D2D626009471BC /* SplayTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC96028D2D626009471BC /* SplayTree.swift */; }; + CE3EC96728D30E74009471BC /* CRDTElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC96628D30E74009471BC /* CRDTElement.swift */; }; + CE3EC96928D30FEF009471BC /* ActorId.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC96828D30FEE009471BC /* ActorId.swift */; }; + CE3EC96E28D3FFF0009471BC /* TimeTicketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC96C28D3FFED009471BC /* TimeTicketTests.swift */; }; + CE3EC97328D40498009471BC /* CRDTElementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC97128D4042D009471BC /* CRDTElementTests.swift */; }; + CE3EC97528D41903009471BC /* Primitive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC97428D41903009471BC /* Primitive.swift */; }; CE6071E528C5D7EE00A8783E /* CONTRIBUTING.md in Resources */ = {isa = PBXBuildFile; fileRef = CE6071E428C5D7EE00A8783E /* CONTRIBUTING.md */; }; + CE7B996E28E142A300D56198 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7B996D28E142A300D56198 /* Errors.swift */; }; + CE7B997028E1453E00D56198 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7B996F28E1453E00D56198 /* Strings.swift */; }; + CE7B997428E1766F00D56198 /* Converter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7B997328E1766F00D56198 /* Converter.swift */; }; + CE7B997628E1773A00D56198 /* GRPCTypeAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7B997528E1773A00D56198 /* GRPCTypeAlias.swift */; }; + CE7B997828E178EF00D56198 /* PrimitiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7B997128E1750000D56198 /* PrimitiveTests.swift */; }; CE8C22EF28C9E85900432DE5 /* Change.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22EE28C9E85900432DE5 /* Change.swift */; }; - CE8C22F128C9E86A00432DE5 /* Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F028C9E86A00432DE5 /* Root.swift */; }; CE8C22F328C9E87800432DE5 /* Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F228C9E87800432DE5 /* Object.swift */; }; CE8C22F528C9E88500432DE5 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F428C9E88500432DE5 /* Operation.swift */; }; - CE8C22F728C9E89100432DE5 /* Ticket.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F628C9E89100432DE5 /* Ticket.swift */; }; + CE8C22F728C9E89100432DE5 /* TimeTicket.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F628C9E89100432DE5 /* TimeTicket.swift */; }; CE8C22F928C9E8CA00432DE5 /* Heap.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F828C9E8CA00432DE5 /* Heap.swift */; }; CE8C230528C9F1BD00432DE5 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C230428C9F1BD00432DE5 /* Client.swift */; }; CE8C230728D1514900432DE5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C230628D1514900432DE5 /* Logger.swift */; }; @@ -53,12 +62,21 @@ CE3EC95028D195E0009471BC /* RedBlackTreeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedBlackTreeTests.swift; sourceTree = ""; }; CE3EC95D28D2AA9C009471BC /* SplayTreeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplayTreeTests.swift; sourceTree = ""; }; CE3EC96028D2D626009471BC /* SplayTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplayTree.swift; sourceTree = ""; }; + CE3EC96628D30E74009471BC /* CRDTElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CRDTElement.swift; sourceTree = ""; }; + CE3EC96828D30FEE009471BC /* ActorId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActorId.swift; sourceTree = ""; }; + CE3EC96C28D3FFED009471BC /* TimeTicketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeTicketTests.swift; sourceTree = ""; }; + CE3EC97128D4042D009471BC /* CRDTElementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CRDTElementTests.swift; sourceTree = ""; }; + CE3EC97428D41903009471BC /* Primitive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Primitive.swift; sourceTree = ""; }; CE6071E428C5D7EE00A8783E /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; + CE7B996D28E142A300D56198 /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; + CE7B996F28E1453E00D56198 /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; + CE7B997128E1750000D56198 /* PrimitiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimitiveTests.swift; sourceTree = ""; }; + CE7B997328E1766F00D56198 /* Converter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Converter.swift; sourceTree = ""; }; + CE7B997528E1773A00D56198 /* GRPCTypeAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRPCTypeAlias.swift; sourceTree = ""; }; CE8C22EE28C9E85900432DE5 /* Change.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Change.swift; sourceTree = ""; }; - CE8C22F028C9E86A00432DE5 /* Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Root.swift; sourceTree = ""; }; CE8C22F228C9E87800432DE5 /* Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Object.swift; sourceTree = ""; }; CE8C22F428C9E88500432DE5 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = ""; }; - CE8C22F628C9E89100432DE5 /* Ticket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ticket.swift; sourceTree = ""; }; + CE8C22F628C9E89100432DE5 /* TimeTicket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeTicket.swift; sourceTree = ""; }; CE8C22F828C9E8CA00432DE5 /* Heap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Heap.swift; sourceTree = ""; }; CE8C230428C9F1BD00432DE5 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; CE8C230628D1514900432DE5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Logger.swift; path = Sources/Core/Logger.swift; sourceTree = SOURCE_ROOT; }; @@ -128,6 +146,7 @@ 96DA808F28C5B7B400E2C1DA /* Tests */ = { isa = PBXGroup; children = ( + CE3EC97028D40421009471BC /* Document */, CE6071EB28C5ECA900A8783E /* API */, CE8C230828D15F5200432DE5 /* Core */, CE3EC94A28D1885F009471BC /* Util */, @@ -155,10 +174,38 @@ path = Util; sourceTree = ""; }; + CE3EC96B28D3FFDB009471BC /* Time */ = { + isa = PBXGroup; + children = ( + CE3EC96C28D3FFED009471BC /* TimeTicketTests.swift */, + ); + path = Time; + sourceTree = ""; + }; + CE3EC96F28D40415009471BC /* CRDT */ = { + isa = PBXGroup; + children = ( + CE3EC97128D4042D009471BC /* CRDTElementTests.swift */, + CE7B997128E1750000D56198 /* PrimitiveTests.swift */, + ); + path = CRDT; + sourceTree = ""; + }; + CE3EC97028D40421009471BC /* Document */ = { + isa = PBXGroup; + children = ( + CE3EC96F28D40415009471BC /* CRDT */, + CE3EC96B28D3FFDB009471BC /* Time */, + ); + path = Document; + sourceTree = ""; + }; CE6071E828C5EC2500A8783E /* API */ = { isa = PBXGroup; children = ( CEEB17CF28C8493F004988DD /* V1 */, + CE7B997328E1766F00D56198 /* Converter.swift */, + CE7B997528E1773A00D56198 /* GRPCTypeAlias.swift */, ); path = API; sourceTree = ""; @@ -204,6 +251,7 @@ CE8C22F828C9E8CA00432DE5 /* Heap.swift */, CE3EC94E28D1922E009471BC /* RedBlackTree.swift */, CE3EC96028D2D626009471BC /* SplayTree.swift */, + CE7B996D28E142A300D56198 /* Errors.swift */, ); path = Util; sourceTree = ""; @@ -219,7 +267,8 @@ CE8C22E728C9E55800432DE5 /* CRDT */ = { isa = PBXGroup; children = ( - CE8C22F028C9E86A00432DE5 /* Root.swift */, + CE3EC96628D30E74009471BC /* CRDTElement.swift */, + CE3EC97428D41903009471BC /* Primitive.swift */, ); path = CRDT; sourceTree = ""; @@ -228,6 +277,7 @@ isa = PBXGroup; children = ( CE8C22F228C9E87800432DE5 /* Object.swift */, + CE7B996F28E1453E00D56198 /* Strings.swift */, ); path = Json; sourceTree = ""; @@ -243,7 +293,8 @@ CE8C22EA28C9E56B00432DE5 /* Time */ = { isa = PBXGroup; children = ( - CE8C22F628C9E89100432DE5 /* Ticket.swift */, + CE8C22F628C9E89100432DE5 /* TimeTicket.swift */, + CE3EC96828D30FEE009471BC /* ActorId.swift */, ); path = Time; sourceTree = ""; @@ -451,18 +502,24 @@ buildActionMask = 2147483647; files = ( CE3EC96128D2D626009471BC /* SplayTree.swift in Sources */, + CE3EC96928D30FEF009471BC /* ActorId.swift in Sources */, CE8C230728D1514900432DE5 /* Logger.swift in Sources */, + CE3EC97528D41903009471BC /* Primitive.swift in Sources */, CEEB17E428C84D26004988DD /* resources.pb.swift in Sources */, CE8C22F928C9E8CA00432DE5 /* Heap.swift in Sources */, CE8C22EF28C9E85900432DE5 /* Change.swift in Sources */, CEEB17E328C84D26004988DD /* yorkie.pb.swift in Sources */, CE3EC94F28D1922E009471BC /* RedBlackTree.swift in Sources */, + CE7B997028E1453E00D56198 /* Strings.swift in Sources */, CE8C230528C9F1BD00432DE5 /* Client.swift in Sources */, CE8C22F528C9E88500432DE5 /* Operation.swift in Sources */, - CE8C22F728C9E89100432DE5 /* Ticket.swift in Sources */, + CE7B997428E1766F00D56198 /* Converter.swift in Sources */, + CE8C22F728C9E89100432DE5 /* TimeTicket.swift in Sources */, + CE7B997628E1773A00D56198 /* GRPCTypeAlias.swift in Sources */, + CE7B996E28E142A300D56198 /* Errors.swift in Sources */, CE8C22F328C9E87800432DE5 /* Object.swift in Sources */, + CE3EC96728D30E74009471BC /* CRDTElement.swift in Sources */, CEEB17E528C84D26004988DD /* yorkie.grpc.swift in Sources */, - CE8C22F128C9E86A00432DE5 /* Root.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -473,6 +530,9 @@ CECCCB8428C96CD600544204 /* XCTestCase+Extension.swift in Sources */, CE3EC95F28D2AAA1009471BC /* SplayTreeTests.swift in Sources */, CE8C230B28D15FF200432DE5 /* ClientTests.swift in Sources */, + CE7B997828E178EF00D56198 /* PrimitiveTests.swift in Sources */, + CE3EC97328D40498009471BC /* CRDTElementTests.swift in Sources */, + CE3EC96E28D3FFF0009471BC /* TimeTicketTests.swift in Sources */, 96DA809128C5B7B400E2C1DA /* GRPCTests.swift in Sources */, CE3EC94D28D189EF009471BC /* HeapTests.swift in Sources */, CE3EC95228D195E4009471BC /* RedBlackTreeTests.swift in Sources */,