// // JSONMapper.swift // KCD // // Created by Hori,Masaki on 2017/02/22. // Copyright © 2017年 Hori,Masaki. All rights reserved. // import Cocoa import SwiftyJSON import Doutaku struct MappingConfiguration { let entity: T.Type let dataKeys: [String] let primaryKeys: [String] let editorStore: CoreDataAccessor let ignoreKeys: Set init(entity: T.Type, dataKeys: [String] = ["api_data"], primaryKeys: [String] = ["id"], editorStore: CoreDataAccessor, ignoreKeys: Set = []) { self.entity = entity self.dataKeys = dataKeys self.primaryKeys = primaryKeys self.editorStore = editorStore self.ignoreKeys = ignoreKeys } } protocol JSONMapper { associatedtype ObjectType: Entity init(_ apiResponse: APIResponse) var apiResponse: APIResponse { get } var configuration: MappingConfiguration { get } func registerElement(_ element: JSON, to object: ObjectType) func commit() func beginRegister(_ object: ObjectType) func handleExtraValue(_ value: JSON, forKey key: String, to object: ObjectType) -> Bool func finishOperating() } extension String { // delete api_ prefix. func keyByDeletingPrefix() -> String { if self.count < 5 { return self } return String(self[index(startIndex, offsetBy: 4)...]) } } extension JSONMapper { var data: JSON { return apiResponse.json[configuration.dataKeys] } private func isEqual(_ lhs: AnyObject?, _ rhs: AnyObject?) -> Bool { if lhs == nil, rhs == nil { return true } if let lhs = lhs, let rhs = rhs { return lhs.isEqual(rhs) } return false } func setValueIfNeeded(_ value: JSON, to object: ObjectType, forKey key: String) { var validValue = value.object as AnyObject? do { try object.validateValue(&validValue, forKey: key) } catch { return } let old = object.value(forKey: key) if !isEqual(old as AnyObject?, validValue) { object.setValue(validValue, forKey: key) } } func registerElement(_ element: JSON, to object: ObjectType) { beginRegister(object) element.forEach { (key, value) in if configuration.ignoreKeys.contains(key) { return } if handleExtraValue(value, forKey: key, to: object) { return } switch value.type { case .array: value.array?.enumerated().forEach { let newKey = "\(key)_\($0.offset)" setValueIfNeeded($0.element, to: object, forKey: newKey) } case .dictionary: value.forEach { (subKey: String, subValue) in let newKey = "\(key)_D_\(subKey.keyByDeletingPrefix())" setValueIfNeeded(subValue, to: object, forKey: newKey) } default: setValueIfNeeded(value, to: object, forKey: key) } } } private var sortDescriptors: [NSSortDescriptor] { return configuration.primaryKeys.map { NSSortDescriptor(key: $0, ascending: true) } } private func objectSearch(_ objects: [ObjectType], _ element: JSON) -> ObjectType? { let keyPiar = configuration.primaryKeys.map { (key: $0, apiKey: "api_\($0)") } return objects.binarySearch { // TODO: replace to forEach for piar in keyPiar { guard let v1 = $0.value(forKey: piar.key) else { return .orderedAscending } if element[piar.apiKey].type == .null { return .orderedDescending } let v2 = element[piar.apiKey].object return (v1 as AnyObject).compare(v2) } return .orderedDescending } } private func sortedObjects(_ entity: ResultType.Type) -> [ResultType] { let store = configuration.editorStore guard let objects = try? store.objects(of: configuration.entity) else { Logger.shared.log("Can not get entity named \(configuration.entity)") return [] } return (objects as NSArray).sortedArray(using: sortDescriptors) as? [ResultType] ?? [] } private func commintInContext() { let store = configuration.editorStore let objects = sortedObjects(configuration.entity) let list = (data.type == .array ? data.arrayValue : [data]) list.forEach { if let object = objectSearch(objects, $0) { registerElement($0, to: object) } else if let new = store.insertNewObject(for: configuration.entity) { registerElement($0, to: new) } else { fatalError("Can not get entity named \(configuration.entity)") } } finishOperating() store.save() } func commit() { configuration.editorStore .sync { self.commintInContext() } } func beginRegister(_ object: ObjectType) {} func handleExtraValue(_ value: JSON, forKey key: String, to object: ObjectType) -> Bool { return false } func finishOperating() {} }