OSDN Git Service

Doutakuを導入
[kcd/KCD.git] / KCD / JSONMapper.swift
1 //
2 //  JSONMapper.swift
3 //  KCD
4 //
5 //  Created by Hori,Masaki on 2017/02/22.
6 //  Copyright © 2017年 Hori,Masaki. All rights reserved.
7 //
8
9 import Cocoa
10 import SwiftyJSON
11 import Doutaku
12
13 struct MappingConfiguration<T: NSManagedObject> {
14     
15     let entity: Entity<T>
16     let dataKeys: [String]
17     let primaryKeys: [String]
18     let editorStore: CoreDataAccessor
19     let ignoreKeys: [String]
20     
21     init(entity: Entity<T>,
22          dataKeys: [String] = ["api_data"],
23          primaryKeys: [String] = ["id"],
24          editorStore: CoreDataAccessor,
25          ignoreKeys: [String] = []) {
26         
27         self.entity = entity
28         self.dataKeys = dataKeys
29         self.primaryKeys = primaryKeys
30         self.editorStore = editorStore
31         self.ignoreKeys = ignoreKeys
32     }
33 }
34
35 protocol JSONMapper {
36     
37     associatedtype ObjectType: NSManagedObject
38     
39     init(_ apiResponse: APIResponse)
40     
41     var apiResponse: APIResponse { get }
42     var configuration: MappingConfiguration<ObjectType> { get }
43     
44     func registerElement(_ element: JSON, to object: ObjectType)
45     func commit()
46     func beginRegister(_ object: ObjectType)
47     func handleExtraValue(_ value: JSON, forKey key: String, to object: ObjectType) -> Bool
48     func finishOperating()
49 }
50
51 extension String {
52     // delete api_ prefix.
53     func keyByDeletingPrefix() -> String {
54         
55         if self.count < 5 { return self }
56         
57         return String(self[index(startIndex, offsetBy: 4)...])
58     }
59 }
60
61 extension JSONMapper {
62     
63     var data: JSON { return apiResponse.json[configuration.dataKeys] }
64     
65     private func isEqual(_ lhs: AnyObject?, _ rhs: AnyObject?) -> Bool {
66         
67         if lhs == nil, rhs == nil { return true }
68         if let lhs = lhs, let rhs = rhs { return lhs.isEqual(rhs) }
69         
70         return false
71     }
72     
73     func setValueIfNeeded(_ value: JSON, to object: ObjectType, forKey key: String) {
74         
75         var validValue = value.object as AnyObject?
76         do {
77             
78             try object.validateValue(&validValue, forKey: key)
79             
80         } catch {
81             
82             return
83         }
84         
85         let old = object.value(forKey: key)
86         if !isEqual(old as AnyObject?, validValue) {
87             
88             object.setValue(validValue, forKey: key)
89         }
90     }
91     
92     func registerElement(_ element: JSON, to object: ObjectType) {
93         
94         beginRegister(object)
95         element.forEach { (key, value) in
96             
97             if configuration.ignoreKeys.contains(key) { return }
98             if handleExtraValue(value, forKey: key, to: object) { return }
99             
100             switch value.type {
101             case .array:
102                 value.array?.enumerated().forEach {
103                     
104                     let newKey = "\(key)_\($0.offset)"
105                     setValueIfNeeded($0.element, to: object, forKey: newKey)
106                 }
107                 
108             case .dictionary:
109                 value.forEach { (subKey: String, subValue) in
110                     
111                     let newKey = "\(key)_D_\(subKey.keyByDeletingPrefix())"
112                     setValueIfNeeded(subValue, to: object, forKey: newKey)
113                 }
114             default:
115                 
116                 setValueIfNeeded(value, to: object, forKey: key)
117             }
118         }
119     }
120     
121     private var sortDescriptors: [NSSortDescriptor] {
122         
123         return configuration.primaryKeys.map { NSSortDescriptor(key: $0, ascending: true) }
124     }
125     
126     private func objectSearch(_ objects: [ObjectType], _ element: JSON) -> ObjectType? {
127         
128         let keyPiar = configuration.primaryKeys.map { (key: $0, apiKey: "api_\($0)") }
129         
130         return objects.binarySearch {
131             
132             // TODO: replace to forEach
133             for piar in keyPiar {
134                 
135                 guard let v1 = $0.value(forKey: piar.key) else { return .orderedAscending }
136                 
137                 if element[piar.apiKey].type == .null { return .orderedDescending }
138                 
139                 let v2 = element[piar.apiKey].object
140                 
141                 return (v1 as AnyObject).compare(v2)
142             }
143             
144             return .orderedDescending
145         }
146     }
147     
148     private func sortedObjects<T>(_ entity: Entity<T>) -> [T] {
149         
150         let store = configuration.editorStore
151         
152         guard let objects = try? store.objects(of: configuration.entity) else {
153             
154             Logger.shared.log("Can not get entity named \(configuration.entity)")
155             return []
156         }
157         
158         return (objects as NSArray).sortedArray(using: sortDescriptors) as? [T] ?? []
159     }
160     private func commintInContext() {
161         
162         let store = configuration.editorStore
163         let objects = sortedObjects(configuration.entity)
164         
165         let list = (data.type == .array ? data.arrayValue : [data])
166         list.forEach {
167             
168             if let object = objectSearch(objects, $0) {
169                 
170                 registerElement($0, to: object)
171                 
172             } else if let new = store.insertNewObject(for: configuration.entity) {
173                 
174                 registerElement($0, to: new)
175                 
176             } else {
177                 
178                 fatalError("Can not get entity named \(configuration.entity)")
179             }
180         }
181         
182         finishOperating()
183         store.save()
184     }
185     
186     func commit() {
187         
188         configuration.editorStore
189             .sync {
190                 self.commintInContext()
191         }
192     }
193     
194     func beginRegister(_ object: ObjectType) {}
195     
196     func handleExtraValue(_ value: JSON, forKey key: String, to object: ObjectType) -> Bool {
197         
198         return false
199     }
200     
201     func finishOperating() {}
202 }