From 2a8e63287bbbd778d0e2d867c60a7e500f6848ee Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 12 Feb 2026 16:46:55 -0600 Subject: [PATCH 1/4] Add descriptions to some error cases so they include some context of where they were thrown feature/identified-description --- Sources/CoreDataRepository/CoreDataError.swift | 12 ++++++------ .../CoreDataRepository+Aggregate.swift | 15 +++++++++++++-- .../CoreDataRepository+BatchRequest.swift | 6 +++--- .../CoreDataRepository+Delete.swift | 2 +- .../CoreDataRepository+Delete_Batch.swift | 3 ++- .../CoreDataRepository+Read_Batch.swift | 10 ++++++---- .../FetchableUnmanagedModel.swift | 8 ++++++++ .../IdentifiedUnmanagedModel.swift | 8 ++++++-- .../Internal/AggregateSubscription.swift | 17 +++++++++++++++-- .../AggregateThrowingSubscription.swift | 17 +++++++++++++++-- .../NSManagedObject+Helpers.swift | 2 +- .../NSManagedObjectContext+Helpers.swift | 2 +- .../ReadableUnmanagedModel.swift | 4 ++-- .../ModelsWithIntId/IdentifiableModel_Int.swift | 4 ++++ .../IdentifiableModel_Uuid.swift | 4 ++++ Tests/CoreDataRepositoryTests/DeleteTests.swift | 10 +++++++--- .../Delete_BatchTests.swift | 10 +++++++--- Tests/CoreDataRepositoryTests/ReadTests.swift | 16 ++++++++++++---- .../Read_BatchTests.swift | 10 +++++++--- Tests/CoreDataRepositoryTests/UpdateTests.swift | 10 +++++++--- .../Update_BatchTests.swift | 6 +++++- 21 files changed, 132 insertions(+), 44 deletions(-) diff --git a/Sources/CoreDataRepository/CoreDataError.swift b/Sources/CoreDataRepository/CoreDataError.swift index a291cd3..1ab5211 100644 --- a/Sources/CoreDataRepository/CoreDataError.swift +++ b/Sources/CoreDataRepository/CoreDataError.swift @@ -21,15 +21,15 @@ public enum CoreDataError: Error, Hashable, Sendable { /// against the correct property. /// If the `NSAttributeDescription` is not for the correct or expected `NSEntityDescription`, this error is /// returned. - case propertyDoesNotMatchEntity + case propertyDoesNotMatchEntity(description: String?) /// CoreData may return a value of a related type to what is actually needed. If casting the value CoreData returns /// to the required type fails, this error is returned. - case fetchedObjectFailedToCastToExpectedType + case fetchedObjectFailedToCastToExpectedType(description: String?) /// It's possible for a persisted object to be flagged as deleted but still be fetched. If that happens, this error /// is returned. - case fetchedObjectIsFlaggedAsDeleted + case fetchedObjectIsFlaggedAsDeleted(description: String) /// If CoreData throws a `CocoaError`, it is embedded here. case cocoa(CocoaError) @@ -48,13 +48,13 @@ public enum CoreDataError: Error, Hashable, Sendable { /// If a ``ManagedIdUrlReferencable`` value is used in a transaction where it is expected to already be persisted /// but has no `URL` /// representing the ``NSManagedObjectID``, this error is returned. - case noUrlOnItemToMapToObjectId + case noUrlOnItemToMapToObjectId(description: String) /// If a ``ManagedIdReferencable`` value is used in a transaction where it is expected to already be persisted but /// has no `NSManagedObjectID`, this error is returned. - case noObjectIdOnItem + case noObjectIdOnItem(description: String) - case noMatchFoundWhenReadingItem + case noMatchFoundWhenReadingItem(description: String) public var localizedDescription: String { switch self { diff --git a/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift b/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift index e3b4156..77c4bc5 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift @@ -411,7 +411,7 @@ extension CoreDataRepository { ) throws -> Value { let result = try context.fetch(request) guard let value: Value = result.asAggregateValue() else { - throw CoreDataError.fetchedObjectFailedToCastToExpectedType + throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: nil) } return value } @@ -426,7 +426,18 @@ extension CoreDataRepository { groupBy: NSAttributeDescription? = nil ) async -> Result { guard entityDesc == attributeDesc.entity else { - return .failure(.propertyDoesNotMatchEntity) + guard let entityName = entityDesc.name ?? entityDesc.managedObjectClassName else { + return .failure(.propertyDoesNotMatchEntity(description: nil)) + } + guard let attributeEntityName = attributeDesc.entity.name ?? attributeDesc.entity.managedObjectClassName + else { + return .failure(.propertyDoesNotMatchEntity(description: entityName)) + } + return .failure( + .propertyDoesNotMatchEntity( + description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)" + ) + ) } return await context.performInChild { scratchPad in let request = try NSFetchRequest.request( diff --git a/Sources/CoreDataRepository/CoreDataRepository+BatchRequest.swift b/Sources/CoreDataRepository/CoreDataRepository+BatchRequest.swift index b3efc2d..68b98dd 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+BatchRequest.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+BatchRequest.swift @@ -18,7 +18,7 @@ extension CoreDataRepository { context.transactionAuthor = transactionAuthor guard let result = try scratchPad.execute(request) as? NSBatchDeleteResult else { context.transactionAuthor = nil - throw CoreDataError.fetchedObjectFailedToCastToExpectedType + throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: request.description) } context.transactionAuthor = nil return result @@ -36,7 +36,7 @@ extension CoreDataRepository { context.transactionAuthor = transactionAuthor guard let result = try scratchPad.execute(request) as? NSBatchInsertResult else { context.transactionAuthor = nil - throw CoreDataError.fetchedObjectFailedToCastToExpectedType + throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: request.description) } context.transactionAuthor = nil return result @@ -54,7 +54,7 @@ extension CoreDataRepository { context.transactionAuthor = transactionAuthor guard let result = try scratchPad.execute(request) as? NSBatchUpdateResult else { context.transactionAuthor = nil - throw CoreDataError.fetchedObjectFailedToCastToExpectedType + throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: request.description) } context.transactionAuthor = nil return result diff --git a/Sources/CoreDataRepository/CoreDataRepository+Delete.swift b/Sources/CoreDataRepository/CoreDataRepository+Delete.swift index 66136a6..569078c 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Delete.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Delete.swift @@ -70,7 +70,7 @@ extension CoreDataRepository { scratchPad.transactionAuthor = transactionAuthor let object = try item.readManaged(from: scratchPad) guard !object.isDeleted else { - throw CoreDataError.fetchedObjectIsFlaggedAsDeleted + throw CoreDataError.fetchedObjectIsFlaggedAsDeleted(description: item.errorDescription) } object.prepareForDeletion() scratchPad.delete(object) diff --git a/Sources/CoreDataRepository/CoreDataRepository+Delete_Batch.swift b/Sources/CoreDataRepository/CoreDataRepository+Delete_Batch.swift index 969a9fa..a1fa944 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Delete_Batch.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Delete_Batch.swift @@ -138,7 +138,8 @@ extension CoreDataRepository { for item in items { let object = try item.readManaged(from: scratchPad) guard !object.isDeleted else { - throw CoreDataError.fetchedObjectIsFlaggedAsDeleted + throw CoreDataError + .fetchedObjectIsFlaggedAsDeleted(description: item.errorDescription) } object.prepareForDeletion() scratchPad.delete(object) diff --git a/Sources/CoreDataRepository/CoreDataRepository+Read_Batch.swift b/Sources/CoreDataRepository/CoreDataRepository+Read_Batch.swift index e52cd5d..8aa8b8c 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Read_Batch.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Read_Batch.swift @@ -103,7 +103,8 @@ extension CoreDataRepository { try ids.map { id in let managed = try Model.readManaged(id: id, from: readContext) guard !managed.isDeleted else { - throw CoreDataError.fetchedObjectIsFlaggedAsDeleted + throw CoreDataError + .fetchedObjectIsFlaggedAsDeleted(description: Model.errorDescription(for: id)) } return try Model(managed: managed) } @@ -122,7 +123,8 @@ extension CoreDataRepository { try items.map { item in let managed = try item.readManaged(from: readContext) guard !managed.isDeleted else { - throw CoreDataError.fetchedObjectIsFlaggedAsDeleted + throw CoreDataError + .fetchedObjectIsFlaggedAsDeleted(description: item.errorDescription) } return try Model(managed: managed) } @@ -142,7 +144,7 @@ extension CoreDataRepository { try managedIds.map { managedId in let _managed = try readContext.notDeletedObject(for: managedId) guard let managed = _managed as? Model.ManagedModel else { - throw CoreDataError.fetchedObjectFailedToCastToExpectedType + throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: "\(Model.self)") } return try Model(managed: managed) } @@ -163,7 +165,7 @@ extension CoreDataRepository { let managedId = try readContext.objectId(from: managedIdUrl).get() let _managed = try readContext.notDeletedObject(for: managedId) guard let managed = _managed as? Model.ManagedModel else { - throw CoreDataError.fetchedObjectFailedToCastToExpectedType + throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: "\(Model.self)") } return try Model(managed: managed) } diff --git a/Sources/CoreDataRepository/FetchableUnmanagedModel.swift b/Sources/CoreDataRepository/FetchableUnmanagedModel.swift index 6be3536..a177eb9 100644 --- a/Sources/CoreDataRepository/FetchableUnmanagedModel.swift +++ b/Sources/CoreDataRepository/FetchableUnmanagedModel.swift @@ -67,6 +67,9 @@ public protocol FetchableUnmanagedModel: Sendable { /// ``NSFetchRequest`` for ``ManagedModel`` with a strongly typed ``NSFetchRequest.ResultType`` static func managedFetchRequest() -> NSFetchRequest + + /// A description of the context from where an error is thrown + var errorDescription: String { get } } extension FetchableUnmanagedModel { @@ -77,4 +80,9 @@ extension FetchableUnmanagedModel { .managedObjectClassName ) } + + @inlinable + public var errorDescription: String { + "\(Self.self)" + } } diff --git a/Sources/CoreDataRepository/IdentifiedUnmanagedModel.swift b/Sources/CoreDataRepository/IdentifiedUnmanagedModel.swift index 8a1f8bf..9899b71 100644 --- a/Sources/CoreDataRepository/IdentifiedUnmanagedModel.swift +++ b/Sources/CoreDataRepository/IdentifiedUnmanagedModel.swift @@ -10,6 +10,8 @@ public protocol IdentifiedUnmanagedModel: ReadableUnmanagedModel { associatedtype UnmanagedId: Equatable var unmanagedId: UnmanagedId { get } static var unmanagedIdExpression: NSExpression { get } + /// Enables including ``UnmanagedId`` in ``errorDescription`` + static func errorDescription(for unmanagedId: UnmanagedId) -> String } extension IdentifiedUnmanagedModel { @@ -29,10 +31,12 @@ extension IdentifiedUnmanagedModel { ) let fetchResult = try context.fetch(request) guard let managed = fetchResult.first, fetchResult.count == 1 else { - throw CoreDataError.noMatchFoundWhenReadingItem + throw CoreDataError + .noMatchFoundWhenReadingItem(description: "\(Self.self) -- id: \(errorDescription(for: id))") } guard !managed.isDeleted else { - throw CoreDataError.fetchedObjectIsFlaggedAsDeleted + throw CoreDataError + .fetchedObjectIsFlaggedAsDeleted(description: "\(Self.self) -- id: \(errorDescription(for: id))") } return managed } diff --git a/Sources/CoreDataRepository/Internal/AggregateSubscription.swift b/Sources/CoreDataRepository/Internal/AggregateSubscription.swift index 5838677..0ecb058 100644 --- a/Sources/CoreDataRepository/Internal/AggregateSubscription.swift +++ b/Sources/CoreDataRepository/Internal/AggregateSubscription.swift @@ -32,7 +32,7 @@ final class AggregateSubscription: Subscription: Subscription: ThrowingSu } guard let value: Value = result.asAggregateValue() else { - self?.fail(.fetchedObjectFailedToCastToExpectedType) + self?.fail(.fetchedObjectFailedToCastToExpectedType(description: nil)) return } self?.send(value) @@ -97,7 +97,20 @@ final class AggregateThrowingSubscription: ThrowingSu context: context, continuation: continuation ) - fail(.propertyDoesNotMatchEntity) + guard let entityName = entityDesc.name ?? entityDesc.managedObjectClassName else { + fail(.propertyDoesNotMatchEntity(description: nil)) + return + } + guard let attributeEntityName = attributeDesc.entity.name ?? attributeDesc.entity.managedObjectClassName + else { + fail(.propertyDoesNotMatchEntity(description: entityName)) + return + } + fail( + .propertyDoesNotMatchEntity( + description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)" + ) + ) return } self.init(request: request, context: context, continuation: continuation) diff --git a/Sources/CoreDataRepository/NSManagedObject+Helpers.swift b/Sources/CoreDataRepository/NSManagedObject+Helpers.swift index df5c938..7e5d9ff 100644 --- a/Sources/CoreDataRepository/NSManagedObject+Helpers.swift +++ b/Sources/CoreDataRepository/NSManagedObject+Helpers.swift @@ -12,7 +12,7 @@ extension NSManagedObject { @inlinable public func asManagedModel() throws -> T { guard let repoManaged = self as? T else { - throw CoreDataError.fetchedObjectFailedToCastToExpectedType + throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: "\(Self.self) -> \(T.self)") } return repoManaged } diff --git a/Sources/CoreDataRepository/NSManagedObjectContext+Helpers.swift b/Sources/CoreDataRepository/NSManagedObjectContext+Helpers.swift index 96ef305..11751ea 100644 --- a/Sources/CoreDataRepository/NSManagedObjectContext+Helpers.swift +++ b/Sources/CoreDataRepository/NSManagedObjectContext+Helpers.swift @@ -22,7 +22,7 @@ extension NSManagedObjectContext { public func notDeletedObject(for id: NSManagedObjectID) throws -> NSManagedObject { let object: NSManagedObject = try existingObject(with: id) guard !object.isDeleted else { - throw CoreDataError.fetchedObjectIsFlaggedAsDeleted + throw CoreDataError.fetchedObjectIsFlaggedAsDeleted(description: id.description) } return object } diff --git a/Sources/CoreDataRepository/ReadableUnmanagedModel.swift b/Sources/CoreDataRepository/ReadableUnmanagedModel.swift index f669481..e79d65c 100644 --- a/Sources/CoreDataRepository/ReadableUnmanagedModel.swift +++ b/Sources/CoreDataRepository/ReadableUnmanagedModel.swift @@ -80,7 +80,7 @@ extension ReadableUnmanagedModel where Self: ManagedIdReferencable { @inlinable public func readManaged(from context: NSManagedObjectContext) throws -> ManagedModel { guard let managedId else { - throw CoreDataError.noObjectIdOnItem + throw CoreDataError.noObjectIdOnItem(description: "\(Self.self)") } return try context.notDeletedObject(for: managedId).asManagedModel() } @@ -90,7 +90,7 @@ extension ReadableUnmanagedModel where Self: ManagedIdUrlReferencable { @inlinable public func readManaged(from context: NSManagedObjectContext) throws -> ManagedModel { guard let managedIdUrl else { - throw CoreDataError.noUrlOnItemToMapToObjectId + throw CoreDataError.noUrlOnItemToMapToObjectId(description: "\(Self.self)") } let managedId = try context.objectId(from: managedIdUrl).get() return try context.notDeletedObject(for: managedId).asManagedModel() diff --git a/Sources/Internal/ModelsWithIntId/IdentifiableModel_Int.swift b/Sources/Internal/ModelsWithIntId/IdentifiableModel_Int.swift index bf0a287..15cd66f 100644 --- a/Sources/Internal/ModelsWithIntId/IdentifiableModel_Int.swift +++ b/Sources/Internal/ModelsWithIntId/IdentifiableModel_Int.swift @@ -105,6 +105,10 @@ extension IdentifiableModel_IntId: IdentifiedUnmanagedModel { } package nonisolated(unsafe) static let unmanagedIdExpression = NSExpression(forKeyPath: \ManagedModel_IntId.id) + + package static func errorDescription(for unmanagedId: Int) -> String { + unmanagedId.description + } } extension IdentifiableModel_IntId: WritableUnmanagedModel { diff --git a/Sources/Internal/ModelsWithUuidId/IdentifiableModel_Uuid.swift b/Sources/Internal/ModelsWithUuidId/IdentifiableModel_Uuid.swift index 71f805f..6bc34fd 100644 --- a/Sources/Internal/ModelsWithUuidId/IdentifiableModel_Uuid.swift +++ b/Sources/Internal/ModelsWithUuidId/IdentifiableModel_Uuid.swift @@ -105,6 +105,10 @@ extension IdentifiableModel_UuidId: IdentifiedUnmanagedModel { } package nonisolated(unsafe) static let unmanagedIdExpression = NSExpression(forKeyPath: \ManagedModel_UuidId.id) + + package static func errorDescription(for unmanagedId: UUID) -> String { + unmanagedId.uuidString + } } extension IdentifiableModel_UuidId: WritableUnmanagedModel { diff --git a/Tests/CoreDataRepositoryTests/DeleteTests.swift b/Tests/CoreDataRepositoryTests/DeleteTests.swift index 9449a6a..7914afc 100644 --- a/Tests/CoreDataRepositoryTests/DeleteTests.swift +++ b/Tests/CoreDataRepositoryTests/DeleteTests.swift @@ -46,7 +46,11 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noMatchFoundWhenReadingItem): + case .failure( + .noMatchFoundWhenReadingItem( + description: "\(modelType) -- id: \(modelType.seeded(1).unmanagedId.uuidString)" + ) + ): return case let .failure(error): Issue.record("Unexpected error: \(error)") @@ -116,7 +120,7 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noObjectIdOnItem): + case .failure(.noObjectIdOnItem(description: "\(modelType)")): return case let .failure(error): Issue.record("Unexpected error: \(error)") @@ -237,7 +241,7 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noUrlOnItemToMapToObjectId): + case .failure(.noUrlOnItemToMapToObjectId(description: "\(modelType)")): return case let .failure(error): Issue.record("Unexpected error: \(error)") diff --git a/Tests/CoreDataRepositoryTests/Delete_BatchTests.swift b/Tests/CoreDataRepositoryTests/Delete_BatchTests.swift index 10126e5..191f796 100644 --- a/Tests/CoreDataRepositoryTests/Delete_BatchTests.swift +++ b/Tests/CoreDataRepositoryTests/Delete_BatchTests.swift @@ -548,7 +548,11 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noMatchFoundWhenReadingItem): + case .failure( + .noMatchFoundWhenReadingItem( + description: "\(modelType) -- id: \(modelType.seeded(1).unmanagedId.uuidString)" + ) + ): break case let .failure(error): Issue.record("Unexpected error: \(error)") @@ -671,7 +675,7 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noObjectIdOnItem): + case .failure(.noObjectIdOnItem(description: "\(modelType)")): break case let .failure(error): Issue.record("Unexpected error: \(error)") @@ -892,7 +896,7 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noUrlOnItemToMapToObjectId): + case .failure(.noUrlOnItemToMapToObjectId(description: "\(modelType)")): break case let .failure(error): Issue.record("Unexpected error: \(error)") diff --git a/Tests/CoreDataRepositoryTests/ReadTests.swift b/Tests/CoreDataRepositoryTests/ReadTests.swift index 4055677..6c632fb 100644 --- a/Tests/CoreDataRepositoryTests/ReadTests.swift +++ b/Tests/CoreDataRepositoryTests/ReadTests.swift @@ -62,7 +62,11 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noMatchFoundWhenReadingItem): + case .failure( + .noMatchFoundWhenReadingItem( + description: "\(modelType) -- id: \(modelType.seeded(1).unmanagedId.uuidString)" + ) + ): return case let .failure(error): Issue.record("Unexpected error: \(error)") @@ -114,7 +118,11 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noMatchFoundWhenReadingItem): + case .failure( + .noMatchFoundWhenReadingItem( + description: "\(modelType) -- id: \(modelType.seeded(1).unmanagedId.uuidString)" + ) + ): return case let .failure(error): Issue.record("Unexpected error: \(error)") @@ -547,7 +555,7 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noObjectIdOnItem): + case .failure(.noObjectIdOnItem(description: "\(modelType)")): return case let .failure(error): Issue.record("Unexpected error: \(error)") @@ -891,7 +899,7 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noUrlOnItemToMapToObjectId): + case .failure(.noUrlOnItemToMapToObjectId(description: "\(modelType)")): return case let .failure(error): Issue.record("Unexpected error: \(error)") diff --git a/Tests/CoreDataRepositoryTests/Read_BatchTests.swift b/Tests/CoreDataRepositoryTests/Read_BatchTests.swift index a6e2499..5186944 100644 --- a/Tests/CoreDataRepositoryTests/Read_BatchTests.swift +++ b/Tests/CoreDataRepositoryTests/Read_BatchTests.swift @@ -496,7 +496,11 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noMatchFoundWhenReadingItem): + case .failure( + .noMatchFoundWhenReadingItem( + description: "\(modelType) -- id: \(modelType.seeded(1).unmanagedId.uuidString)" + ) + ): break case let .failure(error): Issue.record("Unexpected error: \(error)") @@ -608,7 +612,7 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noObjectIdOnItem): + case .failure(.noObjectIdOnItem(description: "\(modelType)")): break case let .failure(error): Issue.record("Unexpected error: \(error)") @@ -805,7 +809,7 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noUrlOnItemToMapToObjectId): + case .failure(.noUrlOnItemToMapToObjectId(description: "\(modelType)")): break case let .failure(error): Issue.record("Unexpected error: \(error)") diff --git a/Tests/CoreDataRepositoryTests/UpdateTests.swift b/Tests/CoreDataRepositoryTests/UpdateTests.swift index dc2be19..0a99796 100644 --- a/Tests/CoreDataRepositoryTests/UpdateTests.swift +++ b/Tests/CoreDataRepositoryTests/UpdateTests.swift @@ -70,7 +70,11 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noMatchFoundWhenReadingItem): + case .failure( + .noMatchFoundWhenReadingItem( + description: "\(modelType) -- id: \(modelType.seeded(1).unmanagedId.uuidString)" + ) + ): return case let .failure(error): Issue.record("Unexpected error: \(error)") @@ -129,7 +133,7 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noObjectIdOnItem): + case .failure(.noObjectIdOnItem(description: "\(modelType)")): return case let .failure(error): Issue.record("Unexpected error: \(error)") @@ -272,7 +276,7 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noUrlOnItemToMapToObjectId): + case .failure(.noUrlOnItemToMapToObjectId(description: "\(modelType)")): return case let .failure(error): Issue.record("Unexpected error: \(error)") diff --git a/Tests/CoreDataRepositoryTests/Update_BatchTests.swift b/Tests/CoreDataRepositoryTests/Update_BatchTests.swift index 932216f..62ad03e 100644 --- a/Tests/CoreDataRepositoryTests/Update_BatchTests.swift +++ b/Tests/CoreDataRepositoryTests/Update_BatchTests.swift @@ -154,7 +154,11 @@ extension CoreDataRepositoryTests { switch result { case .success: Issue.record("Not expecting success") - case .failure(.noMatchFoundWhenReadingItem): + case .failure( + .noMatchFoundWhenReadingItem( + description: "\(modelType) -- id: \(modelType.seeded(1).unmanagedId.uuidString)" + ) + ): break case let .failure(error): Issue.record("Unexpected error: \(error)") From c39e92d96357602e4612f240ff2f4ccb66110167 Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 12 Feb 2026 16:47:51 -0600 Subject: [PATCH 2/4] Fix error description localizations feature/identified-description --- .../CoreDataRepository/CoreDataError.swift | 71 ++++++++++--------- .../Resources/Localizable.xcstrings | 15 ++-- 2 files changed, 47 insertions(+), 39 deletions(-) diff --git a/Sources/CoreDataRepository/CoreDataError.swift b/Sources/CoreDataRepository/CoreDataError.swift index 1ab5211..d00f53c 100644 --- a/Sources/CoreDataRepository/CoreDataError.swift +++ b/Sources/CoreDataRepository/CoreDataError.swift @@ -56,32 +56,38 @@ public enum CoreDataError: Error, Hashable, Sendable { case noMatchFoundWhenReadingItem(description: String) + private static var noErrorDescription: String { + String( + localized: "no description", + bundle: .module, + comment: "Placeholder for when an error description is nil." + ) + } + + // swiftlint:disable line_length public var localizedDescription: String { switch self { case .failedToGetObjectIdFromUrl: - NSLocalizedString( - "No NSManagedObjectID found that correlates to the provided URL.", + String( + localized: "No NSManagedObjectID found that correlates to the provided URL.", bundle: .module, comment: "Error for when an ObjectID can't be found for the provided URL." ) - case .propertyDoesNotMatchEntity: - NSLocalizedString( - "There is a mismatch between a provided NSPropertyDescrption's entity and a NSEntityDescription. " - + "When a property description is provided, it must match any related entity descriptions.", + case let .propertyDoesNotMatchEntity(description: description): + String( + localized: "There is a mismatch between a provided NSPropertyDescrption's entity and a NSEntityDescription. When a property description is provided, it must match any related entity descriptions: \(description ?? Self.noErrorDescription)", bundle: .module, - comment: "Error for when the developer does not provide a valid pair of NSAttributeDescription " - + "and NSPropertyDescription (or any of their child types)." + comment: "Error for when the developer does not provide a valid pair of NSAttributeDescription and NSPropertyDescription (or any of their child types)." ) - case .fetchedObjectFailedToCastToExpectedType: - NSLocalizedString( - "The object corresponding to the provided NSManagedObjectID is an incorrect Entity or " - + "NSManagedObject subtype. It failed to cast to the requested type.", + case let .fetchedObjectFailedToCastToExpectedType(description: description): + String( + localized: "The object corresponding to the provided NSManagedObjectID is an incorrect Entity or NSManagedObject subtype. It failed to cast to the requested type: \(description ?? Self.noErrorDescription)", bundle: .module, comment: "Error for when an object is found for a given ObjectID but it is not the expected type." ) - case .fetchedObjectIsFlaggedAsDeleted: - NSLocalizedString( - "The object corresponding to the provided NSManagedObjectID is deleted and cannot be fetched.", + case let .fetchedObjectIsFlaggedAsDeleted(description: description): + String( + localized: "The object corresponding to the provided NSManagedObjectID is deleted and cannot be fetched: \(description)", bundle: .module, comment: "Error for when an object is fetched but is flagged as deleted and is no longer usable." ) @@ -90,41 +96,40 @@ public enum CoreDataError: Error, Hashable, Sendable { case let .unknown(error): error.localizedDescription case .noEntityNameFound: - NSLocalizedString( - "The managed object entity description does not have a name.", + String( + localized: "The managed object entity description does not have a name.", bundle: .module, comment: "Error for when the NSEntityDescription does not have a name." ) case .atLeastOneAttributeDescRequired: - NSLocalizedString( - "The managed object entity has no attribute description. An attribute description is required for " - + "aggregate operations.", + String( + localized: "The managed object entity has no attribute description. An attribute description is required for aggregate operations.", bundle: .module, comment: "Error for when the NSEntityDescription has no NSAttributeDescription but one is required." ) - case .noUrlOnItemToMapToObjectId: - NSLocalizedString( - "No object ID URL found on the model for an operation against an existing managed object.", + case let .noUrlOnItemToMapToObjectId(description: description): + String( + localized: "No object ID URL found on the model for an operation against an existing managed object: \(description)", bundle: .module, - comment: "Error for performing an operation against an existing NSManagedObject but the " - + "ManagedIdUrlReferencable instance has no managedIdUrl for looking up the NSManagedOjbectID." + comment: "Error for performing an operation against an existing NSManagedObject but the ManagedIdUrlReferencable instance has no managedIdUrl for looking up the NSManagedOjbectID." ) - case .noObjectIdOnItem: - NSLocalizedString( - "No object ID found on the model for an operation against an existing managed object.", + case let .noObjectIdOnItem(description: description): + String( + localized: "No object ID found on the model for an operation against an existing managed object: \(description)", bundle: .module, - comment: "Error for performing an operation against an existing NSManagedObject but the " - + "ManagedIdReferencable instance has no managedId." + comment: "Error for performing an operation against an existing NSManagedObject but the ManagedIdReferencable instance has no managedId." ) - case .noMatchFoundWhenReadingItem: - NSLocalizedString( - "No match found when attempting to read an instance from CoreData.", + case let .noMatchFoundWhenReadingItem(description: description): + String( + localized: "No match found when attempting to read an instance from CoreData: \(description)", bundle: .module, comment: "Error for reading an instance from CoreData but no instance was found." ) } } + // swiftlint:enable line_length + @usableFromInline static func catching(block: () async throws -> T) async throws(Self) -> T { do { diff --git a/Sources/CoreDataRepository/Resources/Localizable.xcstrings b/Sources/CoreDataRepository/Resources/Localizable.xcstrings index cd50090..ee1b231 100644 --- a/Sources/CoreDataRepository/Resources/Localizable.xcstrings +++ b/Sources/CoreDataRepository/Resources/Localizable.xcstrings @@ -1,16 +1,19 @@ { "sourceLanguage" : "en", "strings" : { - "No match found when attempting to read an instance from CoreData." : { + "no description" : { + "comment" : "Placeholder for when an error description is nil." + }, + "No match found when attempting to read an instance from CoreData: %@" : { "comment" : "Error for reading an instance from CoreData but no instance was found." }, "No NSManagedObjectID found that correlates to the provided URL." : { "comment" : "Error for when an ObjectID can't be found for the provided URL." }, - "No object ID found on the model for an operation against an existing managed object." : { + "No object ID found on the model for an operation against an existing managed object: %@" : { "comment" : "Error for performing an operation against an existing NSManagedObject but the ManagedIdReferencable instance has no managedId." }, - "No object ID URL found on the model for an operation against an existing managed object." : { + "No object ID URL found on the model for an operation against an existing managed object: %@" : { "comment" : "Error for performing an operation against an existing NSManagedObject but the ManagedIdUrlReferencable instance has no managedIdUrl for looking up the NSManagedOjbectID." }, "The managed object entity description does not have a name." : { @@ -19,13 +22,13 @@ "The managed object entity has no attribute description. An attribute description is required for aggregate operations." : { "comment" : "Error for when the NSEntityDescription has no NSAttributeDescription but one is required." }, - "The object corresponding to the provided NSManagedObjectID is an incorrect Entity or NSManagedObject subtype. It failed to cast to the requested type." : { + "The object corresponding to the provided NSManagedObjectID is an incorrect Entity or NSManagedObject subtype. It failed to cast to the requested type: %@" : { "comment" : "Error for when an object is found for a given ObjectID but it is not the expected type." }, - "The object corresponding to the provided NSManagedObjectID is deleted and cannot be fetched." : { + "The object corresponding to the provided NSManagedObjectID is deleted and cannot be fetched: %@" : { "comment" : "Error for when an object is fetched but is flagged as deleted and is no longer usable." }, - "There is a mismatch between a provided NSPropertyDescrption's entity and a NSEntityDescription. When a property description is provided, it must match any related entity descriptions." : { + "There is a mismatch between a provided NSPropertyDescrption's entity and a NSEntityDescription. When a property description is provided, it must match any related entity descriptions: %@" : { "comment" : "Error for when the developer does not provide a valid pair of NSAttributeDescription and NSPropertyDescription (or any of their child types)." } }, From c3dbce3b30daf5657872842aaf9bcc5a5af43246 Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 12 Feb 2026 16:48:00 -0600 Subject: [PATCH 3/4] Fix lint warnings feature/identified-description --- Sources/CoreDataRepository/Internal/AggregateSubscription.swift | 1 + .../Internal/AggregateThrowingSubscription.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Sources/CoreDataRepository/Internal/AggregateSubscription.swift b/Sources/CoreDataRepository/Internal/AggregateSubscription.swift index 0ecb058..e395ee9 100644 --- a/Sources/CoreDataRepository/Internal/AggregateSubscription.swift +++ b/Sources/CoreDataRepository/Internal/AggregateSubscription.swift @@ -40,6 +40,7 @@ final class AggregateSubscription: Subscription: ThrowingSu } @usableFromInline + // swiftlint:disable:next function_body_length convenience init( function: CoreDataRepository.AggregateFunction, context: NSManagedObjectContext, From c46b111c31354e1cef9b4af15252aeaa85721b35 Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 26 Feb 2026 12:01:28 -0600 Subject: [PATCH 4/4] Fix PR feedback feature/identified-description --- Sources/CoreDataRepository/ReadableUnmanagedModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CoreDataRepository/ReadableUnmanagedModel.swift b/Sources/CoreDataRepository/ReadableUnmanagedModel.swift index e79d65c..2a5eb0a 100644 --- a/Sources/CoreDataRepository/ReadableUnmanagedModel.swift +++ b/Sources/CoreDataRepository/ReadableUnmanagedModel.swift @@ -66,7 +66,7 @@ import Foundation /// ) /// let fetchResult = try context.fetch(request) /// guard let managed = fetchResult.first, fetchResult.count == 1 else { -/// throw CoreDataError.noMatchFoundWhenReadingItem +/// throw CoreDataError.noMatchFoundWhenReadingItem(description: errorDescription) /// } /// return managed /// }