yorkie-ios-sdk/Sources/Document/Json/JSONObject.swift

279 lines
9.8 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
/**
* `JSONObject` represents a JSON object, but unlike regular JSON, it has time
* tickets created by a logical clock to resolve conflicts.
*/
@dynamicMemberLookup
public class JSONObject {
private static let keySeparator = "/.^/"
private let reservedCharacterForKey = JSONObject.keySeparator
private func isValidKey(_ key: String) -> Bool {
return key.contains(self.reservedCharacterForKey) == false
}
private var target: CRDTObject!
private var context: ChangeContext!
init() {}
init(target: CRDTObject, context: ChangeContext) {
self.target = target
self.context = context
}
func set(_ values: [String: Any]) {
values.forEach { (key: String, value: Any) in
self.set(key: key, value: value)
}
}
func set<T>(key: String, value: T) {
guard self.isValidKey(key) else {
Logger.error("The key \(key) doesn't have the reserved characters: \(self.reservedCharacterForKey)")
return
}
let ticket = self.context.issueTimeTicket()
if let optionalValue = value as? OptionalValue, optionalValue.isNil {
self.setValueNull(key: key, ticket: ticket)
} else if let value = value as? Bool {
self.setValue(key: key, value: value, ticket: ticket)
} else if let value = value as? Int32 {
self.setValue(key: key, value: value, ticket: ticket)
} else if let value = value as? Int64 {
self.setValue(key: key, value: value, ticket: ticket)
} else if let value = value as? Double {
self.setValue(key: key, value: value, ticket: ticket)
} else if let value = value as? String {
self.setValue(key: key, value: value, ticket: ticket)
} else if let value = value as? Data {
self.setValue(key: key, value: value, ticket: ticket)
} else if let value = value as? Date {
self.setValue(key: key, value: value, ticket: ticket)
} else if value is JSONObject {
let object = CRDTObject(createdAt: ticket)
self.setValue(key: key, value: object, ticket: ticket)
} else if let value = value as? [String: Any] {
self.set(key: key, value: JSONObject())
let jsonObject = self.get(key: key) as? JSONObject
jsonObject?.set(value)
} else if let value = value as? JSONObjectable {
self.set(key: key, value: JSONObject())
let jsonObject = self.get(key: key) as? JSONObject
jsonObject?.set(value.toJsonObject)
} else if value is JSONArray {
let array = CRDTArray(createdAt: ticket)
self.setValue(key: key, value: array, ticket: ticket)
} else if let value = value as? [Any] {
let array = CRDTArray(createdAt: ticket)
self.setValue(key: key, value: array, ticket: ticket)
let jsonArray = self.get(key: key) as? JSONArray
jsonArray?.append(values: value.toJsonArray)
} else if let element = value as? JSONCounter<Int32>, let value = element.value as? Int32 {
let counter = CRDTCounter<Int32>(value: value, createdAt: ticket)
element.initialize(context: self.context, counter: counter)
self.setValue(key: key, value: counter, ticket: ticket)
} else if let element = value as? JSONCounter<Int64>, let value = element.value as? Int64 {
let counter = CRDTCounter<Int64>(value: value, createdAt: ticket)
element.initialize(context: self.context, counter: counter)
self.setValue(key: key, value: counter, ticket: ticket)
} else if let element = value as? JSONText {
let text = CRDTText(rgaTreeSplit: RGATreeSplit(), createdAt: ticket)
element.initialize(context: self.context, text: text)
self.setValue(key: key, value: text, ticket: ticket)
} else {
Logger.error("The value is not supported. - key: \(key): value: \(value)")
assertionFailure()
}
}
private func setToCRDTObject(key: String, value: CRDTElement) {
let removed = self.target.set(key: key, value: value)
self.context.registerElement(value, parent: self.target)
if let removed {
self.context.registerRemovedElement(removed)
}
}
private func setPrimitive(key: String, value: PrimitiveValue, ticket: TimeTicket) {
let primitive = Primitive(value: value, createdAt: context.issueTimeTicket())
self.setToCRDTObject(key: key, value: primitive)
let operation = SetOperation(key: key,
value: primitive,
parentCreatedAt: self.target.createdAt,
executedAt: ticket)
self.context.push(operation: operation)
}
private func setValueNull(key: String, ticket: TimeTicket) {
self.setPrimitive(key: key, value: .null, ticket: ticket)
}
private func setValue(key: String, value: Bool, ticket: TimeTicket) {
self.setPrimitive(key: key, value: .boolean(value), ticket: ticket)
}
private func setValue(key: String, value: Int32, ticket: TimeTicket) {
self.setPrimitive(key: key, value: .integer(value), ticket: ticket)
}
private func setValue(key: String, value: Int64, ticket: TimeTicket) {
self.setPrimitive(key: key, value: .long(value), ticket: ticket)
}
private func setValue(key: String, value: Double, ticket: TimeTicket) {
self.setPrimitive(key: key, value: .double(value), ticket: ticket)
}
private func setValue(key: String, value: String, ticket: TimeTicket) {
self.setPrimitive(key: key, value: .string(value), ticket: ticket)
}
private func setValue(key: String, value: Data, ticket: TimeTicket) {
self.setPrimitive(key: key, value: .bytes(value), ticket: ticket)
}
private func setValue(key: String, value: Date, ticket: TimeTicket) {
self.setPrimitive(key: key, value: .date(value), ticket: ticket)
}
private func setValue(key: String, value: CRDTElement, ticket: TimeTicket) {
self.setToCRDTObject(key: key, value: value)
let operation = SetOperation(key: key,
value: value.deepcopy(),
parentCreatedAt: self.target.createdAt,
executedAt: ticket)
self.context.push(operation: operation)
}
public func get(key: String) -> Any? {
guard let value = try? self.target.get(key: key) else {
Logger.error("The value does not exist. - key: \(key)")
return nil
}
return toJSONElement(from: value)
}
/// Search the value by separating the key with dot and return it.
func get(keyPath: String) -> Any? {
let keys = keyPath.components(separatedBy: JSONObject.keySeparator)
var nested: JSONObject = self
for key in keys {
let value = nested.get(key: key)
if let jsonObject = value as? JSONObject {
nested = jsonObject
} else {
return value
}
}
return nested
}
public func remove(key: String) {
Logger.trace("obj[\(key)]")
let removed = try? self.target.remove(key: key, executedAt: self.context.issueTimeTicket())
guard let removed else {
return
}
let removeOperation = RemoveOperation(parentCreatedAt: self.target.createdAt,
createdAt: removed.createdAt,
executedAt: self.context.issueTimeTicket())
self.context.push(operation: removeOperation)
self.context.registerRemovedElement(removed)
}
public subscript(key: String) -> Any? {
get {
self.get(key: key)
}
set {
self.set(key: key, value: newValue)
}
}
subscript(keyPath keyPath: String) -> Any? {
self.get(keyPath: keyPath)
}
/**
* `id` returns the ID(time ticket) of this Object.
*/
public func getID() -> TimeTicket {
self.target.createdAt
}
/**
* `toJSON` returns the JSON encoding of this object.
*/
func toJson() -> String {
self.target.toJSON()
}
private func toSortedJSON() -> String {
self.target.toSortedJSON()
}
var iterator: [(key: String, value: CRDTElement)] {
return self.target.map { (key: String, value: CRDTElement) in
(key, value)
}
}
}
extension JSONObject: CustomStringConvertible {
public var description: String {
self.toJson()
}
}
extension JSONObject: CustomDebugStringConvertible {
public var debugDescription: String {
self.toSortedJSON()
}
}
extension JSONObject: JSONDatable {
var changeContext: ChangeContext {
self.context
}
var crdtElement: CRDTElement {
self.target
}
}
public extension JSONObject {
subscript(dynamicMember member: String) -> Any? {
get {
self.get(key: member)
}
set {
self.set(key: member, value: newValue)
}
}
}