diff --git a/CoreStoreTests/TransactionTests.swift b/CoreStoreTests/TransactionTests.swift index 00bd7e1..e4a6504 100644 --- a/CoreStoreTests/TransactionTests.swift +++ b/CoreStoreTests/TransactionTests.swift @@ -42,33 +42,32 @@ final class TransactionTests: BaseTestCase { do { let createExpectation = self.expectation(description: "create") - stack.beginSynchronous { (transaction) in + let hasChanges: Bool = stack.perform( + synchronous: { (transaction) in - XCTAssertEqual(transaction.context, transaction.internalContext()) - XCTAssertTrue(transaction.context.isTransactionContext) - XCTAssertFalse(transaction.context.isDataStackContext) - - let object = transaction.create(Into()) - XCTAssertEqual(object.fetchSource()?.internalContext(), transaction.context) - XCTAssertEqual(object.querySource()?.internalContext(), transaction.context) - - object.testEntityID = NSNumber(value: 1) - object.testString = "string1" - object.testNumber = 100 - object.testDate = testDate - - - switch transaction.commitAndWait() { + defer { + + createExpectation.fulfill() + } + XCTAssertEqual(transaction.context, transaction.internalContext()) + XCTAssertTrue(transaction.context.isTransactionContext) + XCTAssertFalse(transaction.context.isDataStackContext) - case .success(let hasChanges): - XCTAssertTrue(hasChanges) - createExpectation.fulfill() + let object = transaction.create(Into()) + XCTAssertEqual(object.fetchSource()?.internalContext(), transaction.context) + XCTAssertEqual(object.querySource()?.internalContext(), transaction.context) - default: - XCTFail() - } - } + object.testEntityID = NSNumber(value: 1) + object.testString = "string1" + object.testNumber = 100 + object.testDate = testDate + + return transaction.hasChanges + }, + waitForObserverNotifications: true + ) self.checkExpectationsImmediately() + XCTAssertTrue(hasChanges) XCTAssertEqual(stack.fetchCount(From()), 1) @@ -85,28 +84,28 @@ final class TransactionTests: BaseTestCase { do { let updateExpectation = self.expectation(description: "update") - stack.beginSynchronous { (transaction) in - - guard let object = transaction.fetchOne(From()) else { + let hasChanges: Bool = stack.perform( + synchronous: { (transaction) in - XCTFail() - return - } - object.testString = "string1_edit" - object.testNumber = 200 - object.testDate = Date.distantFuture - - switch transaction.commitAndWait() { + defer { + + updateExpectation.fulfill() + } + guard let object = transaction.fetchOne(From()) else { + + XCTFail() + return // TODO: convert fetch methods to throwing methods + } + object.testString = "string1_edit" + object.testNumber = 200 + object.testDate = Date.distantFuture - case .success(let hasChanges): - XCTAssertTrue(hasChanges) - updateExpectation.fulfill() - - default: - XCTFail() - } - } + return transaction.hasChanges + }, + waitForObserverNotifications: true + ) self.checkExpectationsImmediately() + XCTAssertTrue(hasChanges) XCTAssertEqual(stack.fetchCount(From()), 1) diff --git a/README.md b/README.md index 231c1f0..7e67bec 100644 --- a/README.md +++ b/README.md @@ -815,7 +815,7 @@ public protocol ImportableUniqueObject: ImportableObject { static var uniqueIDKeyPath: String { get } var uniqueIDValue: UniqueIDType { get set } - static func shouldInsert(from source: ImportSource, inn transaction: BaseDataTransaction) -> Bool + static func shouldInsert(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool static func shouldUpdate(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType? func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws diff --git a/Sources/CoreStoreError.swift b/Sources/CoreStoreError.swift index 8a87ab8..8a4a917 100644 --- a/Sources/CoreStoreError.swift +++ b/Sources/CoreStoreError.swift @@ -59,6 +59,16 @@ public enum CoreStoreError: Error, CustomNSError, Hashable { */ case internalError(NSError: NSError) + /** + The transaction was terminated by a user-thrown `Error`. + */ + case userError(error: Error) + + /** + The transaction was cancelled by the user. + */ + case userCancelled + // MARK: CustomNSError @@ -85,6 +95,12 @@ public enum CoreStoreError: Error, CustomNSError, Hashable { case .internalError: return CoreStoreErrorCode.internalError.rawValue + + case .userError: + return CoreStoreErrorCode.userError.rawValue + + case .userCancelled: + return CoreStoreErrorCode.userCancelled.rawValue } } @@ -112,10 +128,18 @@ public enum CoreStoreError: Error, CustomNSError, Hashable { "localStoreURL": localStoreURL ] - case .internalError(let NSError): + case .internalError(let nsError): return [ - "NSError": NSError + "NSError": nsError ] + + case .userError(let error): + return [ + "Error": error + ] + + case .userCancelled: + return [:] } } @@ -139,7 +163,23 @@ public enum CoreStoreError: Error, CustomNSError, Hashable { return url1 == url2 case (.internalError(let NSError1), .internalError(let NSError2)): - return NSError1 == NSError2 + return NSError1.isEqual(NSError2) + + case (.userError(let error1), .userError(let error2)): + switch (error1, error2) { + + case (let error1 as AnyHashable, let error2 as AnyHashable): + return error1 == error2 + + case (let error1 as NSError, let error2 as NSError): + return error1.isEqual(error2) + + default: + return false // shouldn't happen + } + + case (.userCancelled, .userCancelled): + return true default: return false @@ -166,8 +206,14 @@ public enum CoreStoreError: Error, CustomNSError, Hashable { case .progressiveMigrationRequired(let localStoreURL): return code.hashValue ^ localStoreURL.hashValue - case .internalError(let NSError): - return code.hashValue ^ NSError.hashValue + case .internalError(let nsError): + return code.hashValue ^ nsError.hashValue + + case .userError(let error): + return code.hashValue ^ (error as NSError).hashValue + + case .userCancelled: + return code.hashValue } } @@ -221,6 +267,16 @@ public enum CoreStoreErrorCode: Int { An internal SDK call failed with the specified "NSError" userInfo key. */ case internalError + + /** + The transaction was terminated by a user-thrown `Error` specified by "Error" userInfo key. + */ + case userError + + /** + The transaction was cancelled by the user. + */ + case userCancelled } diff --git a/Sources/Logging/CoreStore+CustomDebugStringConvertible.swift b/Sources/Logging/CoreStore+CustomDebugStringConvertible.swift index 283467c..b5413a3 100644 --- a/Sources/Logging/CoreStore+CustomDebugStringConvertible.swift +++ b/Sources/Logging/CoreStore+CustomDebugStringConvertible.swift @@ -142,6 +142,13 @@ extension CoreStoreError: CustomDebugStringConvertible, CoreStoreDebugStringConv case .internalError(let NSError): firstLine = ".internalError" info.append(("NSError", NSError)) + + case .userError(error: let error): + firstLine = ".userError" + info.append(("Error", error)) + + case .userCancelled: + firstLine = ".userCancelled" } return createFormattedString( diff --git a/Sources/ObjectiveC/CSError.swift b/Sources/ObjectiveC/CSError.swift index 39c8c83..d886a52 100644 --- a/Sources/ObjectiveC/CSError.swift +++ b/Sources/ObjectiveC/CSError.swift @@ -137,6 +137,16 @@ public enum CSErrorCode: Int { An internal SDK call failed with the specified "NSError" userInfo key. */ case internalError + + /** + The transaction was terminated by a user-thrown error with the specified "Error" userInfo key. + */ + case userError + + /** + The transaction was cancelled by the user. + */ + case userCancelled } @@ -209,12 +219,23 @@ extension CoreStoreError: CoreStoreSwiftType, _ObjectiveCBridgeableError { self = .progressiveMigrationRequired(localStoreURL: localStoreURL) case .internalError: - guard case let NSError as NSError = info["NSError"] else { + guard case let nsError as NSError = info["NSError"] else { self = .unknown return } - self = .internalError(NSError: NSError) + self = .internalError(NSError: nsError) + + case .userError: + guard case let error as Error = info["Error"] else { + + self = .unknown + return + } + self = .userError(error: error) + + case .userCancelled: + self = .userCancelled } } } diff --git a/Sources/Transactions/BaseDataTransaction.swift b/Sources/Transactions/BaseDataTransaction.swift index 6b6c0ec..d0b25e6 100644 --- a/Sources/Transactions/BaseDataTransaction.swift +++ b/Sources/Transactions/BaseDataTransaction.swift @@ -36,6 +36,12 @@ public /*abstract*/ class BaseDataTransaction { // MARK: Object management + + public func cancel() throws -> Never { + + throw CoreStoreError.userCancelled + } + /** Indicates if the transaction has pending changes */ diff --git a/Sources/Transactions/DataStack+Transaction.swift b/Sources/Transactions/DataStack+Transaction.swift index 310a34b..68e8d41 100644 --- a/Sources/Transactions/DataStack+Transaction.swift +++ b/Sources/Transactions/DataStack+Transaction.swift @@ -31,6 +31,79 @@ import CoreData public extension DataStack { + public func perform(asynchronous task: @escaping (_ transaction: AsynchronousDataTransaction) throws -> T, success: @escaping (T) -> Void, failure: @escaping (CoreStoreError) -> Void) { + + let transaction = AsynchronousDataTransaction( + mainContext: self.rootSavingContext, + queue: self.childTransactionQueue, + closure: { _ in } + ) + transaction.transactionQueue.cs_async { + + do { + + let extraInfo = try task(transaction) + transaction.commit { (result) in + + switch result { + + case .success: + success(extraInfo) + + case .failure(let error): + failure(error) + } + } + } + catch let error as CoreStoreError { + + DispatchQueue.main.async { failure(error) } + } + catch let error { + + DispatchQueue.main.async { failure(.userError(error: error)) } + } + } + } + + public func perform(synchronous task: ((_ transaction: SynchronousDataTransaction) throws -> T), waitForObserverNotifications: Bool = true) throws -> T { + + let transaction = SynchronousDataTransaction( + mainContext: self.rootSavingContext, + queue: self.childTransactionQueue, + closure: { _ in } + ) + return try transaction.transactionQueue.cs_sync { + + let extraInfo: T + do { + + extraInfo = try task(transaction) + } + catch let error as CoreStoreError { + + throw error + } + catch let error { + + throw CoreStoreError.userError(error: error) + } + + let result = waitForObserverNotifications + ? transaction.commitAndWait() + : transaction.commit() + switch result { + + case .success: + return extraInfo + + case .failure(let error): + throw error + } + } + } + + /** Begins a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made. @@ -83,7 +156,6 @@ public extension DataStack { Thread.isMainThread, "Attempted to refresh entities outside their designated queue." ) - self.mainContext.refreshAndMergeAllObjects() } }