yorkie-ios-sdk/Sources/API/Converter.swift

914 lines
36 KiB
Swift

/*
* 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: PbValueType, data: Data) throws -> PrimitiveValue {
switch valueType {
case .null:
return .null
case .boolean:
return .boolean(data[0] == 1)
case .integer:
let result = Int32(littleEndian: data.withUnsafeBytes { $0.load(as: Int32.self) })
return .integer(result)
case .double:
let result = Double(bitPattern: UInt64(littleEndian: data.withUnsafeBytes { $0.load(as: UInt64.self) }))
return .double(result)
case .string:
return .string(String(decoding: data, as: UTF8.self))
case .long:
let result = Int64(littleEndian: data.withUnsafeBytes { $0.load(as: Int64.self) })
return .long(result)
case .bytes:
return .bytes(data)
case .date:
let milliseconds = Int64(littleEndian: data.withUnsafeBytes { $0.load(as: Int64.self) })
return .date(Date(timeIntervalSince1970: TimeInterval(Double(milliseconds) / 1000)))
default:
throw YorkieError.unimplemented(message: String(describing: valueType))
}
}
static func countValueFrom(_ valueType: PbValueType, data: Data) throws -> any YorkieCountable {
switch valueType {
case .integerCnt:
return Int32(littleEndian: data.withUnsafeBytes { $0.load(as: Int32.self) })
case .longCnt:
return Int64(littleEndian: data.withUnsafeBytes { $0.load(as: Int64.self) })
default:
throw YorkieError.unimplemented(message: String(describing: valueType))
}
}
/**
* `toValueType` converts the given model to Protobuf format.
*/
static func toValueType(_ valueType: PrimitiveValue) -> PbValueType {
switch valueType {
case .null:
return .null
case .boolean:
return .boolean
case .integer:
return .integer
case .long:
return .long
case .double:
return .double
case .string:
return .string
case .bytes:
return .bytes
case .date:
return .date
}
}
}
// MARK: Presence
extension Converter {
/**
* `fromPresence` converts the given Protobuf format to model format.
*/
static func fromPresence(pbPresence: PbPresence) -> PresenceInfo {
var data = [String: Any]()
pbPresence.data.forEach { (key, value) in
if let dataValue = value.data(using: .utf8), let jsonValue = try? JSONSerialization.jsonObject(with: dataValue) {
data[key] = jsonValue
} else {
if value.first == "\"" && value.last == "\"" {
data[key] = value.substring(from: 1, to: value.count - 2)
} 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 {
assertionFailure("Invalid Presence Value [\(key)]:[\(value)")
}
}
}
}
return PresenceInfo(clock: pbPresence.clock, data: data)
}
/**
* `toClient` converts the given model to Protobuf format.
*/
static func toClient(id: String, presence: PresenceInfo) -> PbClient {
var pbPresence = PbPresence()
pbPresence.clock = presence.clock
presence.data.forEach { (key, value) in
if JSONSerialization.isValidJSONObject(value), let jsonData = try? JSONSerialization.data(withJSONObject: value) {
pbPresence.data[key] = String(bytes: jsonData, encoding: .utf8)
} else {
// emulate JSON.stringify() in JavaScript.
pbPresence.data[key] = value is String ? "\"\(value)\"" : "\(value)"
}
}
var pbClient = PbClient()
pbClient.id = id.toData ?? Data()
pbClient.presence = pbPresence
return pbClient
}
}
// MARK: Checkpoint
extension Converter {
/**
* `toCheckpoint` converts the given model to Protobuf format.
*/
static func toCheckpoint(_ checkpoint: Checkpoint) -> PbCheckpoint {
var pbCheckpoint = PbCheckpoint()
pbCheckpoint.serverSeq = checkpoint.getServerSeq()
pbCheckpoint.clientSeq = checkpoint.getClientSeq()
return pbCheckpoint
}
/**
* `fromCheckpoint` converts the given Protobuf format to model format.
*/
static func fromCheckpoint(_ pbCheckpoint: PbCheckpoint) -> Checkpoint {
Checkpoint(serverSeq: pbCheckpoint.serverSeq, clientSeq: pbCheckpoint.clientSeq)
}
}
// MARK: ChangeID
extension Converter {
/**
* `toChangeID` converts the given model to Protobuf format.
*/
static func toChangeID(_ changeID: ChangeID) -> PbChangeID {
var pbChangeID = PbChangeID()
pbChangeID.clientSeq = changeID.getClientSeq()
pbChangeID.lamport = changeID.getLamport()
pbChangeID.actorID = changeID.getActorID()?.toData ?? Data()
return pbChangeID
}
/**
* `fromChangeID` converts the given Protobuf format to model format.
*/
static func fromChangeID(_ pbChangeID: PbChangeID) -> ChangeID {
ChangeID(clientSeq: pbChangeID.clientSeq,
lamport: pbChangeID.lamport,
actor: pbChangeID.actorID.toHexString,
serverSeq: pbChangeID.serverSeq)
}
}
// MARK: TimeTicket
extension Converter {
/**
* `toTimeTicket` converts the given model to Protobuf format.
*/
static func toTimeTicket(_ ticket: TimeTicket) -> PbTimeTicket {
var pbTimeTicket = PbTimeTicket()
pbTimeTicket.lamport = ticket.lamport
pbTimeTicket.delimiter = ticket.delimiter
pbTimeTicket.actorID = ticket.actorID?.toData ?? Data()
return pbTimeTicket
}
/**
* `fromTimeTicket` converts the given Protobuf format to model format.
*/
static func fromTimeTicket(_ pbTimeTicket: PbTimeTicket) -> TimeTicket {
TimeTicket(lamport: pbTimeTicket.lamport,
delimiter: pbTimeTicket.delimiter,
actorID: pbTimeTicket.actorID.isEmpty ? nil : pbTimeTicket.actorID.toHexString)
}
}
// MARK: CounterType
extension Converter {
/**
* `toCounterType` converts the given model to Protobuf format.
*/
static func toCounterType(_ valueType: any YorkieCountable) -> PbValueType {
if valueType is Int32 {
return .integerCnt
} else {
return .longCnt
}
}
}
// MARK: ElementSimple
extension Converter {
/**
* `toElementSimple` converts the given model to Protobuf format.
*/
static func toElementSimple(_ element: CRDTElement) -> PbJSONElementSimple {
var pbElementSimple = PbJSONElementSimple()
if element is CRDTObject {
pbElementSimple.type = .jsonObject
} else if element is CRDTArray {
pbElementSimple.type = .jsonArray
} else if element is CRDTText {
pbElementSimple.type = .text
} else if let element = element as? Primitive {
let primitive = element.value
pbElementSimple.type = toValueType(primitive)
pbElementSimple.value = element.toBytes()
} else if let counter = element as? CRDTCounter<Int32> {
pbElementSimple.type = .integerCnt
pbElementSimple.value = counter.toBytes()
} else if let counter = element as? CRDTCounter<Int64> {
pbElementSimple.type = .longCnt
pbElementSimple.value = counter.toBytes()
}
pbElementSimple.createdAt = toTimeTicket(element.createdAt)
return pbElementSimple
}
/**
* `fromElementSimple` converts the given Protobuf format to model format.
*/
static func fromElementSimple(pbElementSimple: PbJSONElementSimple) throws -> CRDTElement {
switch pbElementSimple.type {
case .jsonObject:
return CRDTObject(createdAt: fromTimeTicket(pbElementSimple.createdAt))
case .jsonArray:
return CRDTArray(createdAt: fromTimeTicket(pbElementSimple.createdAt))
case .text:
return CRDTText(rgaTreeSplit: RGATreeSplit(), createdAt: fromTimeTicket(pbElementSimple.createdAt))
case .null, .boolean, .integer, .long, .double, .string, .bytes, .date:
return Primitive(value: try valueFrom(pbElementSimple.type, data: pbElementSimple.value), createdAt: fromTimeTicket(pbElementSimple.createdAt))
case .integerCnt:
guard let value = try countValueFrom(pbElementSimple.type, data: pbElementSimple.value) as? Int32 else {
throw YorkieError.unexpected(message: "unexpected counter value type")
}
return CRDTCounter<Int32>(value: value, createdAt: fromTimeTicket(pbElementSimple.createdAt))
case .longCnt:
guard let value = try countValueFrom(pbElementSimple.type, data: pbElementSimple.value) as? Int64 else {
throw YorkieError.unexpected(message: "unexpected counter value type")
}
return CRDTCounter<Int64>(value: value, createdAt: fromTimeTicket(pbElementSimple.createdAt))
default:
throw YorkieError.unimplemented(message: "unimplemented element: \(pbElementSimple)")
}
}
}
// MARK: TextNodeID
extension Converter {
/**
* `toTextNodeID` converts the given model to Protobuf format.
*/
static func toTextNodeID(id: RGATreeSplitNodeID) -> PbTextNodeID {
var pbTextNodeID = PbTextNodeID()
pbTextNodeID.createdAt = toTimeTicket(id.createdAt)
pbTextNodeID.offset = id.offset
return pbTextNodeID
}
/**
* `fromTextNodeID` converts the given Protobuf format to model format.
*/
static func fromTextNodeID(_ pbTextNodeID: PbTextNodeID) -> RGATreeSplitNodeID {
RGATreeSplitNodeID(Self.fromTimeTicket(pbTextNodeID.createdAt), pbTextNodeID.offset)
}
}
// MARK: TextNodePos
extension Converter {
/**
* `toTextNodePos` converts the given model to Protobuf format.
*/
static func toTextNodePos(pos: RGATreeSplitNodePos) -> PbTextNodePos {
var pbTextNodePos = PbTextNodePos()
pbTextNodePos.createdAt = toTimeTicket(pos.id.createdAt)
pbTextNodePos.offset = pos.id.offset
pbTextNodePos.relativeOffset = pos.relativeOffset
return pbTextNodePos
}
/**
* `fromTextNodePos` converts the given Protobuf format to model format.
*/
static func fromTextNodePos(_ pbTextNodePos: PbTextNodePos) -> RGATreeSplitNodePos {
RGATreeSplitNodePos(RGATreeSplitNodeID(Self.fromTimeTicket(pbTextNodePos.createdAt), pbTextNodePos.offset), pbTextNodePos.relativeOffset)
}
}
// MARK: Operation
extension Converter {
/**
* `toOperation` converts the given model to Protobuf format.
*/
static func toOperation(_ operation: Operation) throws -> PbOperation {
var pbOperation = PbOperation()
if let setOperation = operation as? SetOperation {
var pbSetOperation = PbOperation.Set()
pbSetOperation.parentCreatedAt = toTimeTicket(setOperation.parentCreatedAt)
pbSetOperation.key = setOperation.key
pbSetOperation.value = toElementSimple(setOperation.value)
pbSetOperation.executedAt = toTimeTicket(setOperation.executedAt)
pbOperation.set = pbSetOperation
} else if let addOperation = operation as? AddOperation {
var pbAddOperation = PbOperation.Add()
pbAddOperation.parentCreatedAt = toTimeTicket(addOperation.parentCreatedAt)
pbAddOperation.prevCreatedAt = toTimeTicket(addOperation.previousCreatedAt)
pbAddOperation.value = toElementSimple(addOperation.value)
pbAddOperation.executedAt = toTimeTicket(addOperation.executedAt)
pbOperation.add = pbAddOperation
} else if let moveOperation = operation as? MoveOperation {
var pbMoveOperation = PbOperation.Move()
pbMoveOperation.parentCreatedAt = toTimeTicket(moveOperation.parentCreatedAt)
pbMoveOperation.prevCreatedAt = toTimeTicket(moveOperation.previousCreatedAt)
pbMoveOperation.createdAt = toTimeTicket(moveOperation.createdAt)
pbMoveOperation.executedAt = toTimeTicket(moveOperation.executedAt)
pbOperation.move = pbMoveOperation
} else if let removeOperation = operation as? RemoveOperation {
var pbRemoveOperation = PbOperation.Remove()
pbRemoveOperation.parentCreatedAt = toTimeTicket(removeOperation.parentCreatedAt)
pbRemoveOperation.createdAt = toTimeTicket(removeOperation.createdAt)
pbRemoveOperation.executedAt = toTimeTicket(removeOperation.executedAt)
pbOperation.remove = pbRemoveOperation
} else if let editOperation = operation as? EditOperation {
var pbEditOperation = PbOperation.Edit()
pbEditOperation.parentCreatedAt = toTimeTicket(editOperation.parentCreatedAt)
pbEditOperation.from = toTextNodePos(pos: editOperation.fromPos)
pbEditOperation.to = toTextNodePos(pos: editOperation.toPos)
editOperation.maxCreatedAtMapByActor.forEach {
pbEditOperation.createdAtMapByActor[$0.key] = toTimeTicket($0.value)
}
pbEditOperation.content = editOperation.content
editOperation.attributes?.forEach {
pbEditOperation.attributes[$0.key] = $0.value
}
pbEditOperation.executedAt = toTimeTicket(editOperation.executedAt)
pbOperation.edit = pbEditOperation
} else if let selectOperaion = operation as? SelectOperation {
var pbSelectOperation = PbOperation.Select()
pbSelectOperation.parentCreatedAt = toTimeTicket(selectOperaion.parentCreatedAt)
pbSelectOperation.from = toTextNodePos(pos: selectOperaion.fromPos)
pbSelectOperation.to = toTextNodePos(pos: selectOperaion.toPos)
pbSelectOperation.executedAt = toTimeTicket(selectOperaion.executedAt)
pbOperation.select = pbSelectOperation
} else if let styleOperation = operation as? StyleOperation {
var pbStyleOperation = PbOperation.Style()
pbStyleOperation.parentCreatedAt = toTimeTicket(styleOperation.parentCreatedAt)
pbStyleOperation.from = toTextNodePos(pos: styleOperation.fromPos)
pbStyleOperation.to = toTextNodePos(pos: styleOperation.toPos)
styleOperation.attributes.forEach {
pbStyleOperation.attributes[$0.key] = $0.value
}
pbStyleOperation.executedAt = toTimeTicket(styleOperation.executedAt)
pbOperation.style = pbStyleOperation
} else if let increaseOperation = operation as? IncreaseOperation {
var pbIncreaseOperation = PbOperation.Increase()
pbIncreaseOperation.parentCreatedAt = toTimeTicket(increaseOperation.parentCreatedAt)
pbIncreaseOperation.value = toElementSimple(increaseOperation.value)
pbIncreaseOperation.executedAt = toTimeTicket(increaseOperation.executedAt)
pbOperation.increase = pbIncreaseOperation
} else {
throw YorkieError.unimplemented(message: "unimplemented operation \(operation)")
}
return pbOperation
}
/**
* `toOperations` converts the given model to Protobuf format.
*/
static func toOperations(_ operations: [Operation]) -> [PbOperation] {
operations.compactMap { try? toOperation($0) }
}
/**
* `fromOperations` converts the given Protobuf format to model format.
*/
static func fromOperations(_ pbOperations: [PbOperation]) throws -> [Operation] {
try pbOperations.compactMap { pbOperation in
if case let .set(pbSetOperation) = pbOperation.body {
return SetOperation(key: pbSetOperation.key,
value: try fromElementSimple(pbElementSimple: pbSetOperation.value),
parentCreatedAt: fromTimeTicket(pbSetOperation.parentCreatedAt),
executedAt: fromTimeTicket(pbSetOperation.executedAt))
} else if case let .add(pbAddOperation) = pbOperation.body {
return AddOperation(parentCreatedAt: fromTimeTicket(pbAddOperation.parentCreatedAt),
previousCreatedAt: fromTimeTicket(pbAddOperation.prevCreatedAt),
value: try fromElementSimple(pbElementSimple: pbAddOperation.value),
executedAt: fromTimeTicket(pbAddOperation.executedAt))
} else if case let .move(pbMoveOperation) = pbOperation.body {
return MoveOperation(parentCreatedAt: fromTimeTicket(pbMoveOperation.parentCreatedAt),
previousCreatedAt: fromTimeTicket(pbMoveOperation.prevCreatedAt),
createdAt: fromTimeTicket(pbMoveOperation.createdAt),
executedAt: fromTimeTicket(pbMoveOperation.executedAt))
} else if case let .remove(pbRemoveOperation) = pbOperation.body {
return RemoveOperation(parentCreatedAt: fromTimeTicket(pbRemoveOperation.parentCreatedAt),
createdAt: fromTimeTicket(pbRemoveOperation.createdAt),
executedAt: fromTimeTicket(pbRemoveOperation.executedAt))
} else if case let .edit(pbEditOperation) = pbOperation.body {
let createdAtMapByActor = pbEditOperation.createdAtMapByActor.mapValues { fromTimeTicket($0) }
return EditOperation(parentCreatedAt: fromTimeTicket(pbEditOperation.parentCreatedAt),
fromPos: fromTextNodePos(pbEditOperation.from),
toPos: fromTextNodePos(pbEditOperation.to),
maxCreatedAtMapByActor: createdAtMapByActor,
content: pbEditOperation.content,
attributes: pbEditOperation.attributes,
executedAt: fromTimeTicket(pbEditOperation.executedAt))
} else if case let .select(pbSelectOperation) = pbOperation.body {
return SelectOperation(parentCreatedAt: fromTimeTicket(pbSelectOperation.parentCreatedAt),
fromPos: fromTextNodePos(pbSelectOperation.from),
toPos: fromTextNodePos(pbSelectOperation.to),
executedAt: fromTimeTicket(pbSelectOperation.executedAt))
} else if case let .style(pbStyleOperation) = pbOperation.body {
return StyleOperation(parentCreatedAt: fromTimeTicket(pbStyleOperation.parentCreatedAt),
fromPos: fromTextNodePos(pbStyleOperation.from),
toPos: fromTextNodePos(pbStyleOperation.to),
attributes: pbStyleOperation.attributes,
executedAt: fromTimeTicket(pbStyleOperation.executedAt))
} else if case let .increase(pbIncreaseOperation) = pbOperation.body {
return IncreaseOperation(parentCreatedAt: fromTimeTicket(pbIncreaseOperation.parentCreatedAt),
value: try fromElementSimple(pbElementSimple: pbIncreaseOperation.value),
executedAt: fromTimeTicket(pbIncreaseOperation.executedAt))
} else {
throw YorkieError.unimplemented(message: "unimplemented operation \(pbOperation)")
}
}
}
}
// MARK: RHTNode
extension Converter {
/**
* `toRHTNodes` converts the given model to Protobuf format.
*/
static func toRHTNodes(rht: ElementRHT) -> [PbRHTNode] {
rht.compactMap {
guard let element = try? toElement($0.value) else {
return nil
}
var pbRHTNode = PbRHTNode()
pbRHTNode.key = $0.key
pbRHTNode.element = element
return pbRHTNode
}
}
}
// MARK: RGNNodes
extension Converter {
/**
* `toRGANodes` converts the given model to Protobuf format.
*/
static func toRGANodes(_ rgaTreeList: RGATreeList) -> [PbRGANode] {
rgaTreeList.compactMap {
guard let element = try? toElement($0.value) else {
return nil
}
var pbRGANode = PbRGANode()
pbRGANode.element = element
return pbRGANode
}
}
}
// MARK: JSONElement
extension Converter {
/**
* `toObject` converts the given model to Protobuf format.
*/
static func toObject(_ obj: CRDTObject) -> PbJSONElement {
var pbObject = PbJSONElement.JSONObject()
pbObject.nodes = toRHTNodes(rht: obj.rht)
pbObject.createdAt = toTimeTicket(obj.createdAt)
if let ticket = obj.movedAt {
pbObject.movedAt = toTimeTicket(ticket)
} else {
pbObject.clearMovedAt()
}
if let ticket = obj.removedAt {
pbObject.removedAt = toTimeTicket(ticket)
} else {
pbObject.clearRemovedAt()
}
var pbElement = PbJSONElement()
pbElement.jsonObject = pbObject
return pbElement
}
/**
* `fromObject` converts the given Protobuf format to model format.
*/
static func fromObject(_ pbObject: PbJSONElement.JSONObject) throws -> CRDTObject {
let rht = ElementRHT()
try pbObject.nodes.forEach { pbRHTNode in
rht.set(key: pbRHTNode.key, value: try fromElement(pbElement: pbRHTNode.element))
}
let obj = CRDTObject(createdAt: fromTimeTicket(pbObject.createdAt), memberNodes: rht)
obj.movedAt = pbObject.hasMovedAt ? fromTimeTicket(pbObject.movedAt) : nil
obj.removedAt = pbObject.hasRemovedAt ? fromTimeTicket(pbObject.removedAt) : nil
return obj
}
/**
* `toArray` converts the given model to Protobuf format.
*/
static func toArray(_ arr: CRDTArray) -> PbJSONElement {
var pbArray = PbJSONElement.JSONArray()
pbArray.nodes = toRGANodes(arr.getElements())
pbArray.createdAt = toTimeTicket(arr.createdAt)
if let ticket = arr.movedAt {
pbArray.movedAt = toTimeTicket(ticket)
} else {
pbArray.clearMovedAt()
}
if let ticket = arr.removedAt {
pbArray.removedAt = toTimeTicket(ticket)
} else {
pbArray.clearRemovedAt()
}
var pbElement = PbJSONElement()
pbElement.jsonArray = pbArray
return pbElement
}
/**
* `fromArray` converts the given Protobuf format to model format.
*/
static func fromArray(_ pbArray: PbJSONElement.JSONArray) throws -> CRDTArray {
let rgaTreeList = RGATreeList()
try pbArray.nodes.forEach { pbRGANode in
try rgaTreeList.insert(fromElement(pbElement: pbRGANode.element))
}
let arr = CRDTArray(createdAt: fromTimeTicket(pbArray.createdAt), elements: rgaTreeList)
arr.movedAt = pbArray.hasMovedAt ? fromTimeTicket(pbArray.movedAt) : nil
arr.removedAt = pbArray.hasRemovedAt ? fromTimeTicket(pbArray.removedAt) : nil
return arr
}
/**
* `toPrimitive` converts the given model to Protobuf format.
*/
static func toPrimitive(_ primitive: Primitive) -> PbJSONElement {
var pbPrimitive = PbJSONElement.Primitive()
pbPrimitive.type = toValueType(primitive.value)
pbPrimitive.value = primitive.toBytes()
pbPrimitive.createdAt = toTimeTicket(primitive.createdAt)
if let ticket = primitive.movedAt {
pbPrimitive.movedAt = toTimeTicket(ticket)
} else {
pbPrimitive.clearMovedAt()
}
if let ticket = primitive.removedAt {
pbPrimitive.removedAt = toTimeTicket(ticket)
} else {
pbPrimitive.clearRemovedAt()
}
var pbElement = PbJSONElement()
pbElement.primitive = pbPrimitive
return pbElement
}
/**
* `fromPrimitive` converts the given Protobuf format to model format.
*/
static func fromPrimitive(_ pbPrimitive: PbJSONElement.Primitive) throws -> Primitive {
let primitive = Primitive(value: try valueFrom(pbPrimitive.type, data: pbPrimitive.value), createdAt: fromTimeTicket(pbPrimitive.createdAt))
primitive.movedAt = pbPrimitive.hasMovedAt ? fromTimeTicket(pbPrimitive.movedAt) : nil
primitive.removedAt = pbPrimitive.hasRemovedAt ? fromTimeTicket(pbPrimitive.removedAt) : nil
return primitive
}
/**
* `toText` converts the given model to Protobuf format.
*/
static func toText(_ text: CRDTText) -> PbJSONElement {
var pbText = PbJSONElement.Text()
pbText.nodes = toTextNodes(text.rgaTreeSplit)
pbText.createdAt = toTimeTicket(text.createdAt)
if let ticket = text.movedAt {
pbText.movedAt = toTimeTicket(ticket)
}
if let ticket = text.removedAt {
pbText.removedAt = toTimeTicket(ticket)
}
var pbElement = PbJSONElement()
pbElement.text = pbText
return pbElement;
}
/**
* `fromText` converts the given Protobuf format to model format.
*/
static func fromText(_ pbText: PbJSONElement.Text) -> CRDTText {
let rgaTreeSplit = RGATreeSplit<TextValue>()
var prev = rgaTreeSplit.head
pbText.nodes.forEach { pbNode in
let current = rgaTreeSplit.insertAfter(prev, fromTextNode(pbNode))
if pbNode.hasInsPrevID {
current.setInsPrev(rgaTreeSplit.findNode(fromTextNodeID(pbNode.insPrevID)))
}
prev = current
}
let text = CRDTText(rgaTreeSplit: rgaTreeSplit, createdAt: fromTimeTicket(pbText.createdAt))
text.movedAt = fromTimeTicket(pbText.movedAt)
text.removedAt = pbText.hasRemovedAt ? fromTimeTicket(pbText.removedAt) : nil
return text
}
/**
* `toCounter` converts the given model to Protobuf format.
*/
static func toCounter<T: YorkieCountable>(_ counter: CRDTCounter<T>) -> PbJSONElement {
var pbCounter = PbJSONElement.Counter()
pbCounter.type = toCounterType(counter.value)
pbCounter.value = counter.toBytes()
pbCounter.createdAt = toTimeTicket(counter.createdAt)
if let ticket = counter.movedAt {
pbCounter.movedAt = toTimeTicket(ticket)
} else {
pbCounter.clearMovedAt()
}
if let ticket = counter.removedAt {
pbCounter.removedAt = toTimeTicket(ticket)
} else {
pbCounter.clearRemovedAt()
}
var pbElement = PbJSONElement()
pbElement.counter = pbCounter
return pbElement
}
/**
* `fromCounter` converts the given Protobuf format to model format.
*/
static func fromCounter(_ pbCounter: PbJSONElement.Counter) throws -> CRDTElement {
let value = try countValueFrom(pbCounter.type, data: pbCounter.value)
switch pbCounter.type {
case .integerCnt:
guard let value = value as? Int32 else {
throw YorkieError.unexpected(message: "[\(pbCounter.type)] value is not Int32.")
}
let counter = CRDTCounter<Int32>(value: value, createdAt: fromTimeTicket(pbCounter.createdAt))
counter.movedAt = pbCounter.hasMovedAt ? fromTimeTicket(pbCounter.movedAt) : nil
counter.removedAt = pbCounter.hasRemovedAt ? fromTimeTicket(pbCounter.removedAt) : nil
return counter
case .longCnt:
guard let value = value as? Int64 else {
throw YorkieError.unexpected(message: "[\(pbCounter.type)] value is not Int64.")
}
let counter = CRDTCounter<Int64>(value: value, createdAt: fromTimeTicket(pbCounter.createdAt))
counter.movedAt = pbCounter.hasMovedAt ? fromTimeTicket(pbCounter.movedAt) : nil
counter.removedAt = pbCounter.hasRemovedAt ? fromTimeTicket(pbCounter.removedAt) : nil
return counter
default:
throw YorkieError.unimplemented(message: "\(pbCounter.type) is not implemented.")
}
}
/**
* `toElement` converts the given model to Protobuf format.
*/
static func toElement(_ element: CRDTElement) throws -> PbJSONElement {
if let element = element as? CRDTObject {
return toObject(element)
} else if let element = element as? CRDTArray {
return toArray(element)
} else if let element = element as? Primitive {
return toPrimitive(element)
} else if let element = element as? CRDTText {
return toText(element);
} else if let element = element as? CRDTCounter<Int32> {
return toCounter(element)
} else if let element = element as? CRDTCounter<Int64> {
return toCounter(element)
} else {
throw YorkieError.unimplemented(message: "unimplemented element: \(element)")
}
}
/**
* `fromElement` converts the given Protobuf format to model format.
*/
static func fromElement(pbElement: PbJSONElement) throws -> CRDTElement {
if case let .jsonObject(element) = pbElement.body {
return try fromObject(element)
} else if case let .jsonArray(element) = pbElement.body {
return try fromArray(element)
} else if case let .primitive(element) = pbElement.body {
return try fromPrimitive(element)
} else if case let .text(element) = pbElement.body {
return fromText(element)
} else if case let .counter(element) = pbElement.body {
return try fromCounter(element)
} else {
throw YorkieError.unimplemented(message: "unimplemented element: \(pbElement)")
}
}
}
// MARK: TextNode
extension Converter {
/**
* `toTextNodes` converts the given model to Protobuf format.
*/
static func toTextNodes(_ rgaTreeSplit: RGATreeSplit<TextValue>) -> [PbTextNode] {
var pbTextNodes = [PbTextNode]()
for textNode in rgaTreeSplit {
var pbTextNode = PbTextNode()
pbTextNode.id = toTextNodeID(id: textNode.id)
pbTextNode.value = String(describing: textNode.value.content)
textNode.value.getAttributes().forEach { key, value in
var attr = PbTextNodeAttr()
attr.value = value.value
attr.updatedAt = toTimeTicket(value.updatedAt)
pbTextNode.attributes[key] = attr
}
if let removedAt = textNode.removedAt {
pbTextNode.removedAt = toTimeTicket(removedAt)
}
pbTextNodes.append(pbTextNode)
}
return pbTextNodes
}
/**
* `fromTextNode` converts the given Protobuf format to model format.
*/
static func fromTextNode(_ pbTextNode: PbTextNode) -> RGATreeSplitNode<TextValue> {
let textValue = TextValue(pbTextNode.value)
pbTextNode.attributes.forEach {
textValue.setAttr(key: $0.key, value: $0.value.value, updatedAt: fromTimeTicket($0.value.updatedAt))
}
let textNode = RGATreeSplitNode(fromTextNodeID(pbTextNode.id), textValue)
if pbTextNode.hasRemovedAt {
textNode.remove(fromTimeTicket(pbTextNode.removedAt))
}
return textNode
}
}
// MARK: ChangePack
extension Converter {
/**
* `toChangePack` converts the given model to Protobuf format.
*/
static func toChangePack(pack: ChangePack) -> PbChangePack {
var pbChangePack = PbChangePack()
pbChangePack.documentKey = pack.getDocumentKey()
pbChangePack.checkpoint = toCheckpoint(pack.getCheckpoint())
pbChangePack.changes = toChanges(pack.getChanges())
pbChangePack.snapshot = pack.getSnapshot() ?? Data()
if let minSyncedTicket = pack.getMinSyncedTicket() {
pbChangePack.minSyncedTicket = toTimeTicket(minSyncedTicket)
} else {
pbChangePack.clearMinSyncedTicket()
}
pbChangePack.isRemoved = pack.isRemoved
return pbChangePack
}
/**
* `fromChangePack` converts the given Protobuf format to model format.
*/
static func fromChangePack(_ pbPack: PbChangePack) throws -> ChangePack {
ChangePack(key: pbPack.documentKey,
checkpoint: fromCheckpoint(pbPack.checkpoint),
changes: try fromChanges(pbPack.changes),
snapshot: pbPack.snapshot.isEmpty ? nil : pbPack.snapshot,
minSyncedTicket: pbPack.hasMinSyncedTicket ? fromTimeTicket(pbPack.minSyncedTicket) : nil,
isRemoved: pbPack.isRemoved)
}
}
// MARK: Change
extension Converter {
/**
* `toChange` converts the given model to Protobuf format.
*/
static func toChange(_ change: Change) -> PbChange {
var pbChange = PbChange()
pbChange.id = toChangeID(change.id)
pbChange.message = change.message ?? ""
pbChange.operations = toOperations(change.operations)
return pbChange
}
/**
* `toChanges` converts the given model to Protobuf format.
*/
static func toChanges(_ changes: [Change]) -> [PbChange] {
changes.map {
toChange($0)
}
}
/**
* `fromChanges` converts the given Protobuf format to model format.
*/
static func fromChanges(_ pbChanges: [PbChange]) throws -> [Change] {
try pbChanges.compactMap {
Change(id: fromChangeID($0.id),
operations: try fromOperations($0.operations),
message: $0.message.isEmpty ? nil : $0.message)
}
}
}
// MARK: Bytes.
extension Converter {
/**
* `bytesToObject` creates an JSONObject from the given byte array.
*/
static func bytesToObject(bytes: Data) throws -> CRDTObject {
guard bytes.isEmpty == false else {
return CRDTObject(createdAt: TimeTicket.initial)
}
let pbElement = try PbJSONElement(serializedData: bytes)
return try fromObject(pbElement.jsonObject)
}
/**
* `objectToBytes` converts the given JSONObject to byte array.
*/
static func objectToBytes(obj: CRDTObject) throws -> Data {
try toElement(obj).serializedData()
}
}
// MARK: Hex
extension Data {
var toHexString: String {
self.map { String(format: "%02x", $0) }.joined()
}
}
extension String {
// Same as toUint8Array in JS
var toData: Data? {
guard self.count % 2 == 0 else {
return nil
}
var data = Data()
for index in stride(from: 0, to: self.count, by: 2) {
let pair = self.substring(from: index, to: index + 1)
guard let value = UInt8(pair, radix: 16) else {
return nil
}
data.append(value)
}
return data
}
}