diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 8328e5b..b161fae 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ B5D39A0419FD00DE000E91BB /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5D39A0319FD00DE000E91BB /* UIKit.framework */; }; B5D5E0CF1A4D6AAB006468AF /* TestEntity2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D5E0CE1A4D6AAB006468AF /* TestEntity2.swift */; }; B5D8080E1A3471A500A44484 /* GCDKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5D808021A34715700A44484 /* GCDKit.framework */; }; + B5E834B91B76311F001D3D50 /* BaseDataTransaction+Importing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E834B81B76311F001D3D50 /* BaseDataTransaction+Importing.swift */; }; + B5E834BB1B7691F3001D3D50 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E834BA1B7691F3001D3D50 /* Functions.swift */; }; B5E84EDF1AFF84500064E85B /* DataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84EDB1AFF84500064E85B /* DataStack.swift */; }; B5E84EE11AFF84500064E85B /* PersistentStoreResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84EDE1AFF84500064E85B /* PersistentStoreResult.swift */; }; B5E84EE61AFF84610064E85B /* DefaultLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84EE31AFF84610064E85B /* DefaultLogger.swift */; }; @@ -126,6 +128,8 @@ B5D5E0CE1A4D6AAB006468AF /* TestEntity2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestEntity2.swift; sourceTree = ""; }; B5D806C51A34715700A44484 /* GCDKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = GCDKit.xcodeproj; sourceTree = ""; }; B5D9C8F61B160ED200E64F0E /* CoreStore.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = CoreStore.podspec; sourceTree = SOURCE_ROOT; }; + B5E834B81B76311F001D3D50 /* BaseDataTransaction+Importing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BaseDataTransaction+Importing.swift"; sourceTree = ""; }; + B5E834BA1B7691F3001D3D50 /* Functions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; B5E84ED81AFF82360064E85B /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; B5E84ED91AFF82360064E85B /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = SOURCE_ROOT; }; B5E84EDB1AFF84500064E85B /* DataStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataStack.swift; sourceTree = ""; }; @@ -221,6 +225,7 @@ B5E84EDA1AFF84500064E85B /* Setting Up */, B5E84EE21AFF84610064E85B /* Logging */, B5E84EE91AFF846E0064E85B /* Saving and Processing */, + B5E834B61B7630BD001D3D50 /* Importing Data */, B5E84EFD1AFF847B0064E85B /* Fetching and Querying */, B5E84F191AFF84860064E85B /* Observing */, B56964D11B22FF700075EE4A /* Migrating */, @@ -309,6 +314,14 @@ name = Products; sourceTree = ""; }; + B5E834B61B7630BD001D3D50 /* Importing Data */ = { + isa = PBXGroup; + children = ( + B5E834B81B76311F001D3D50 /* BaseDataTransaction+Importing.swift */, + ); + path = "Importing Data"; + sourceTree = ""; + }; B5E84EDA1AFF84500064E85B /* Setting Up */ = { isa = PBXGroup; children = ( @@ -414,6 +427,7 @@ B5E84F331AFF85470064E85B /* NSManagedObjectContext+Transaction.swift */, B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */, B5E84F2D1AFF849C0064E85B /* WeakObject.swift */, + B5E834BA1B7691F3001D3D50 /* Functions.swift */, ); path = Internal; sourceTree = ""; @@ -578,6 +592,7 @@ B5FAD6AC1B51285300714891 /* MigrationManager.swift in Sources */, B5E84EF61AFF846E0064E85B /* DataStack+Transaction.swift in Sources */, B5E84EDF1AFF84500064E85B /* DataStack.swift in Sources */, + B5E834BB1B7691F3001D3D50 /* Functions.swift in Sources */, B5E84F231AFF84860064E85B /* ListMonitor.swift in Sources */, B5E84EF71AFF846E0064E85B /* DetachedDataTransaction.swift in Sources */, B56964D41B22FFAD0075EE4A /* DataStack+Migration.swift in Sources */, @@ -597,6 +612,7 @@ B5E84EF81AFF846E0064E85B /* CoreStore+Transaction.swift in Sources */, B5E84F301AFF849C0064E85B /* NSManagedObjectContext+CoreStore.swift in Sources */, B5E84F211AFF84860064E85B /* CoreStore+Observing.swift in Sources */, + B5E834B91B76311F001D3D50 /* BaseDataTransaction+Importing.swift in Sources */, B5E84EE61AFF84610064E85B /* DefaultLogger.swift in Sources */, B5E84EF41AFF846E0064E85B /* AsynchronousDataTransaction.swift in Sources */, B5E84F151AFF847B0064E85B /* CoreStore+Querying.swift in Sources */, diff --git a/CoreStore/Importing Data/BaseDataTransaction+Importing.swift b/CoreStore/Importing Data/BaseDataTransaction+Importing.swift new file mode 100644 index 0000000..58d8be2 --- /dev/null +++ b/CoreStore/Importing Data/BaseDataTransaction+Importing.swift @@ -0,0 +1,245 @@ +// +// BaseDataTransaction+Importing.swift +// CoreStore +// +// Copyright (c) 2015 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import CoreData + + +public protocol ImportableObject: class { + + typealias ImportSource + + static func shouldImportFromSource(source: ImportSource) -> Bool + + func didInsertFromImportSource(source: ImportSource) throws + + func updateFromImportSource(source: ImportSource) throws +} + +public extension ImportableObject { + + static func shouldImportFromSource(source: ImportSource) -> Bool { + + return true + } +} + + +public protocol ImportableUniqueObject: ImportableObject { + + typealias UniqueIDType: NSObject + + static var uniqueIDKeyPath: String { get } + + var uniqueIDValue: UniqueIDType { get set } + + static func uniqueIDFromImportSource(source: ImportSource) throws -> (key: String, value: UniqueIDType!) +} + + +public extension BaseDataTransaction { + + func importObject( + into: Into, + source: T.ImportSource) throws -> T? { + + CoreStore.assert( + self.bypassesQueueing || self.transactionQueue.isCurrentExecutionContext(), + "Attempted to import an object of type \(typeName(into.entityClass)) outside the transaction's designated queue." + ) + + var returnValue: T? + try autoreleasepool { + + if !T.shouldImportFromSource(source) { + + return + } + + let object = self.create(into) + try object.didInsertFromImportSource(source) + returnValue = object + } + return returnValue + } + + func importUniqueObject( + into: Into, + source: T.ImportSource) throws -> T? { + + CoreStore.assert( + self.bypassesQueueing || self.transactionQueue.isCurrentExecutionContext(), + "Attempted to import an object of type \(typeName(into.entityClass)) outside the transaction's designated queue." + ) + + var returnValue: T? + try autoreleasepool { + + if !T.shouldImportFromSource(source) { + + return + } + + let uniqueIDKeyPath = T.uniqueIDKeyPath + let uniqueIDFromImportSource = try T.uniqueIDFromImportSource(source) + let uniqueIDValue = uniqueIDFromImportSource.value + + if let object = self.fetchOne(From(T), Where(uniqueIDKeyPath, isEqualTo: uniqueIDValue)) { + + try object.updateFromImportSource(source) + returnValue = object + } + else { + + let object = self.create(into) + object.uniqueIDValue = uniqueIDValue + try object.didInsertFromImportSource(source) + returnValue = object + } + } + return returnValue + } + + func importUniqueObjects( + into: Into, + sourceArray: [T.ImportSource], + preProcess: ((mapping: [T.UniqueIDType: T.ImportSource]) throws -> Void)? = nil) throws { + + CoreStore.assert( + self.bypassesQueueing || self.transactionQueue.isCurrentExecutionContext(), + "Attempted to import an object of type \(typeName(into.entityClass)) outside the transaction's designated queue." + ) + + try autoreleasepool { + + var mapping = Dictionary() + for source in sourceArray { + + try autoreleasepool { + + if !T.shouldImportFromSource(source) { + + return + } + + let uniqueIDValue = try T.uniqueIDFromImportSource(source).value + mapping[uniqueIDValue] = source + } + } + + if let preProcess = preProcess { + + try autoreleasepool { + + try preProcess(mapping: mapping) + } + } + + for object in self.fetchAll(From(T), Where("%K IN %@", T.uniqueIDKeyPath, mapping.keys.array)) ?? [] { + + try autoreleasepool { + + let uniqueIDValue = object.uniqueIDValue + try object.updateFromImportSource(mapping.removeValueForKey(uniqueIDValue)!) + } + } + + for (uniqueIDValue, source) in mapping { + + try autoreleasepool { + + let object = self.create(into) + object.uniqueIDValue = uniqueIDValue + try object.didInsertFromImportSource(source) + } + } + } + } + + func importUniqueObjects( + into: Into, + sourceArray: [T.ImportSource], + preProcess: ((mapping: [T.UniqueIDType: T.ImportSource]) throws -> Void)? = nil, + postProcess: (sorted: [T]) -> Void) throws { + + CoreStore.assert( + self.bypassesQueueing || self.transactionQueue.isCurrentExecutionContext(), + "Attempted to import an object of type \(typeName(into.entityClass)) outside the transaction's designated queue." + ) + + try autoreleasepool { + + var sortedIDs = Array() + var mapping = Dictionary() + for source in sourceArray { + + try autoreleasepool { + + if !T.shouldImportFromSource(source) { + + return + } + + let uniqueIDValue = try T.uniqueIDFromImportSource(source).value + mapping[uniqueIDValue] = source + sortedIDs.append(uniqueIDValue) + } + } + + if let preProcess = preProcess { + + try autoreleasepool { + + try preProcess(mapping: mapping) + } + } + + var objects = Dictionary() + for object in self.fetchAll(From(T), Where("%K IN %@", T.uniqueIDKeyPath, mapping.keys.array)) ?? [] { + + try autoreleasepool { + + let uniqueIDValue = object.uniqueIDValue + try object.updateFromImportSource(mapping.removeValueForKey(uniqueIDValue)!) + objects[uniqueIDValue] = object + } + } + + for (uniqueIDValue, source) in mapping { + + try autoreleasepool { + + let object = self.create(into) + object.uniqueIDValue = uniqueIDValue + try object.didInsertFromImportSource(source) + + objects[uniqueIDValue] = object + } + } + + postProcess(sorted: sortedIDs.flatMap { objects[$0] }) + } + } +} diff --git a/CoreStore/Internal/Functions.swift b/CoreStore/Internal/Functions.swift new file mode 100644 index 0000000..c221e2e --- /dev/null +++ b/CoreStore/Internal/Functions.swift @@ -0,0 +1,47 @@ +// +// Functions.swift +// CoreStore +// +// Copyright (c) 2014 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +internal func autoreleasepool(@noescape closure: () throws -> Void) throws { + + var closureError: ErrorType? + ObjectiveC.autoreleasepool { + + do { + + try closure() + } + catch { + + closureError = error + } + } + + if let closureError = closureError { + + throw closureError + } +}