From c03c9f4cc57a6a6d113aabe38da54a5e93d67168 Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 12 Feb 2026 13:08:22 -0600 Subject: [PATCH 1/4] Run swiftformat feature/format-and-lint --- .../FileCabinetDetailView.swift | 6 +- .../FileCabinetsView.swift | 4 +- .../CoreDataBatchError.swift | 2 +- .../CoreDataRepository+Aggregate.swift | 75 +++++++++---------- .../CoreDataRepository+Create_Batch.swift | 3 +- .../CoreDataRepository+Delete_Batch.swift | 4 +- .../CoreDataRepository+Fetch.swift | 4 +- .../CoreDataRepository+Read.swift | 12 +-- .../CoreDataRepository+Read_Batch.swift | 4 +- .../CoreDataRepository.swift | 4 +- .../Internal/AggregateSubscription.swift | 5 +- .../AggregateThrowingSubscription.swift | 7 +- ...ArrayOfNSDictionary+AggregateHelpers.swift | 2 +- .../Internal/CountSubscription.swift | 5 +- .../Internal/CountThrowingSubscription.swift | 7 +- .../Internal/Subscription.swift | 4 +- .../Internal/ThrowingSubscription.swift | 4 +- .../NSManagedObject+Helpers.swift | 2 +- .../CoreDataTestSuite.swift | 8 +- .../CoreDataTestSuiteTests.swift | 6 +- .../CoreDataRepositoryTests/CustomTests.swift | 3 +- .../CoreDataRepositoryTests/DeleteTests.swift | 6 +- 22 files changed, 89 insertions(+), 88 deletions(-) diff --git a/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetDetailView.swift b/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetDetailView.swift index 95e826f..09046ba 100644 --- a/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetDetailView.swift +++ b/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetDetailView.swift @@ -21,7 +21,7 @@ struct FileCabinetDetailView: View { ) } - @ViewBuilder @MainActor + @MainActor private func sidebar() -> some View { VStack { Text(viewModel.state.fileCabinet.id.uuidString) @@ -44,12 +44,12 @@ struct FileCabinetDetailView: View { } } - @ViewBuilder @MainActor + @MainActor private func content() -> some View { EmptyView() } - @ViewBuilder @MainActor + @MainActor private func detail() -> some View { EmptyView() } diff --git a/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetsView.swift b/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetsView.swift index 80f018f..5e9e04c 100644 --- a/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetsView.swift +++ b/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetsView.swift @@ -21,7 +21,7 @@ struct FileCabinetsView: View { ) } - @ViewBuilder @MainActor + @MainActor private func sidebar() -> some View { VStack { Button( @@ -55,7 +55,7 @@ struct FileCabinetsView: View { } } - @ViewBuilder @MainActor + @MainActor private func detail() -> some View { EmptyView() } diff --git a/Sources/CoreDataRepository/CoreDataBatchError.swift b/Sources/CoreDataRepository/CoreDataBatchError.swift index 6e5aa21..b74a16b 100644 --- a/Sources/CoreDataRepository/CoreDataBatchError.swift +++ b/Sources/CoreDataRepository/CoreDataBatchError.swift @@ -11,7 +11,7 @@ import Foundation /// Batch operations that do not use `NSBatch*Request` are not atomic. Some operations may succeed while others fail. If /// multiple errors are returned, it would /// be helpful if each error is associated with the input data for the operation. -public struct CoreDataBatchError: Error where T: Sendable { +public struct CoreDataBatchError: Error { /// The input data used for the batched operation. Usually an ``UnmanagedModel`` instance or URL encoded /// NSManagedObjectID. public let item: T diff --git a/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift b/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift index 08307f7..e3b4156 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift @@ -19,14 +19,14 @@ extension CoreDataRepository { } @inlinable - public func aggregate( + public func aggregate( function: AggregateFunction, predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as valueType: Value.Type - ) async -> Result where Value: Numeric, Value: Sendable { + ) async -> Result { switch function { case .count: await count(predicate: predicate, entityDesc: entityDesc, as: valueType) @@ -46,13 +46,13 @@ extension CoreDataRepository { /// Get the average of a managed object's numeric property for all instances that satisfy the predicate. @inlinable - public func average( + public func average( predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as _: Value.Type - ) async -> Result where Value: Numeric, Value: Sendable { + ) async -> Result { await Self.send( function: .average, context: Transaction.current?.context ?? context, @@ -65,13 +65,13 @@ extension CoreDataRepository { /// Subscribe to the average of a managed object's numeric property for all instances that satisfy the predicate. @inlinable - public func averageSubscription( + public func averageSubscription( predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as _: Value.Type - ) -> AsyncStream> where Value: Numeric, Value: Sendable { + ) -> AsyncStream> { AsyncStream { continuation in let subscription = AggregateSubscription( function: .average, @@ -91,13 +91,13 @@ extension CoreDataRepository { /// Subscribe to the average of a managed object's numeric property for all instances that satisfy the predicate. @inlinable - public func averageThrowingSubscription( + public func averageThrowingSubscription( predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as _: Value.Type - ) -> AsyncThrowingStream where Value: Numeric, Value: Sendable { + ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in let subscription = AggregateThrowingSubscription( function: .average, @@ -119,11 +119,11 @@ extension CoreDataRepository { /// Get the count or quantity of managed object instances that satisfy the predicate. @inlinable - public func count( + public func count( predicate: NSPredicate, entityDesc: NSEntityDescription, as _: Value.Type - ) async -> Result where Value: Numeric, Value: Sendable { + ) async -> Result { await context.performInChild { scratchPad in do { let request = try NSFetchRequest @@ -140,11 +140,11 @@ extension CoreDataRepository { /// Subscribe to the count or quantity of managed object instances that satisfy the predicate. @inlinable - public func countSubscription( + public func countSubscription( predicate: NSPredicate, entityDesc: NSEntityDescription, as _: Value.Type - ) -> AsyncStream> where Value: Numeric, Value: Sendable { + ) -> AsyncStream> { AsyncStream { continuation in let subscription = CountSubscription( context: context.childContext(), @@ -161,11 +161,11 @@ extension CoreDataRepository { /// Subscribe to the count or quantity of managed object instances that satisfy the predicate. @inlinable - public func countThrowingSubscription( + public func countThrowingSubscription( predicate: NSPredicate, entityDesc: NSEntityDescription, as _: Value.Type - ) -> AsyncThrowingStream where Value: Numeric, Value: Sendable { + ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in let subscription = CountThrowingSubscription( context: context.childContext(), @@ -184,13 +184,13 @@ extension CoreDataRepository { /// Get the max or maximum of a managed object's numeric property for all instances that satisfy the predicate. @inlinable - public func max( + public func max( predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as _: Value.Type - ) async -> Result where Value: Numeric, Value: Sendable { + ) async -> Result { await Self.send( function: .max, context: Transaction.current?.context ?? context, @@ -204,13 +204,13 @@ extension CoreDataRepository { /// Subscribe to the max or maximum of a managed object's numeric property for all instances that satisfy the /// predicate. @inlinable - public func maxSubscription( + public func maxSubscription( predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as _: Value.Type - ) -> AsyncStream> where Value: Numeric, Value: Sendable { + ) -> AsyncStream> { AsyncStream { continuation in let subscription = AggregateSubscription( function: .max, @@ -231,13 +231,13 @@ extension CoreDataRepository { /// Subscribe to the max or maximum of a managed object's numeric property for all instances that satisfy the /// predicate. @inlinable - public func maxThrowingSubscription( + public func maxThrowingSubscription( predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as _: Value.Type - ) -> AsyncThrowingStream where Value: Numeric, Value: Sendable { + ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in let subscription = AggregateThrowingSubscription( function: .max, @@ -259,13 +259,13 @@ extension CoreDataRepository { /// Get the min or minimum of a managed object's numeric property for all instances that satisfy the predicate. @inlinable - public func min( + public func min( predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as _: Value.Type - ) async -> Result where Value: Numeric, Value: Sendable { + ) async -> Result { await Self.send( function: .min, context: Transaction.current?.context ?? context, @@ -279,13 +279,13 @@ extension CoreDataRepository { /// Subscribe to the min or minimum of a managed object's numeric property for all instances that satisfy the /// predicate. @inlinable - public func minSubscription( + public func minSubscription( predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as _: Value.Type - ) -> AsyncStream> where Value: Numeric, Value: Sendable { + ) -> AsyncStream> { AsyncStream { continuation in let subscription = AggregateSubscription( function: .min, @@ -306,13 +306,13 @@ extension CoreDataRepository { /// Subscribe to the min or minimum of a managed object's numeric property for all instances that satisfy the /// predicate. @inlinable - public func minThrowingSubscription( + public func minThrowingSubscription( predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as _: Value.Type - ) -> AsyncThrowingStream where Value: Numeric, Value: Sendable { + ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in let subscription = AggregateThrowingSubscription( function: .min, @@ -334,13 +334,13 @@ extension CoreDataRepository { /// Get the sum of a managed object's numeric property for all instances that satisfy the predicate. @inlinable - public func sum( + public func sum( predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as _: Value.Type - ) async -> Result where Value: Numeric, Value: Sendable { + ) async -> Result { await Self.send( function: .sum, context: Transaction.current?.context ?? context, @@ -353,13 +353,13 @@ extension CoreDataRepository { /// Subscribe to the sum of a managed object's numeric property for all instances that satisfy the predicate. @inlinable - public func sumSubscription( + public func sumSubscription( predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as _: Value.Type - ) -> AsyncStream> where Value: Numeric, Value: Sendable { + ) -> AsyncStream> { AsyncStream { continuation in let subscription = AggregateSubscription( function: .sum, @@ -379,13 +379,13 @@ extension CoreDataRepository { /// Subscribe to the sum of a managed object's numeric property for all instances that satisfy the predicate. @inlinable - public func sumThrowingSubscription( + public func sumThrowingSubscription( predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, as _: Value.Type - ) -> AsyncThrowingStream where Value: Numeric, Value: Sendable { + ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in let subscription = AggregateThrowingSubscription( function: .sum, @@ -405,10 +405,10 @@ extension CoreDataRepository { // MARK: Internals - private static func aggregate( + private static func aggregate( context: NSManagedObjectContext, request: NSFetchRequest - ) throws -> Value where Value: Numeric, Value: Sendable { + ) throws -> Value { let result = try context.fetch(request) guard let value: Value = result.asAggregateValue() else { throw CoreDataError.fetchedObjectFailedToCastToExpectedType @@ -417,14 +417,14 @@ extension CoreDataRepository { } @usableFromInline - static func send( + static func send( function: AggregateFunction, context: NSManagedObjectContext, predicate: NSPredicate, entityDesc: NSEntityDescription, attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil - ) async -> Result where Value: Numeric, Value: Sendable { + ) async -> Result { guard entityDesc == attributeDesc.entity else { return .failure(.propertyDoesNotMatchEntity) } @@ -437,8 +437,7 @@ extension CoreDataRepository { groupBy: groupBy ) do { - let value: Value = try Self.aggregate(context: scratchPad, request: request) - return value + return try Self.aggregate(context: scratchPad, request: request) } catch let error as CocoaError { throw CoreDataError.cocoa(error) } catch { diff --git a/Sources/CoreDataRepository/CoreDataRepository+Create_Batch.swift b/Sources/CoreDataRepository/CoreDataRepository+Create_Batch.swift index a8ac057..da90583 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Create_Batch.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Create_Batch.swift @@ -41,8 +41,7 @@ extension CoreDataRepository { return await context.performInScratchPad(schedule: .enqueued) { [context] scratchPad in scratchPad.transactionAuthor = transactionAuthor let objects = try items.map { item in - let object = try item.asManagedModel(in: scratchPad) - return object + try item.asManagedModel(in: scratchPad) } try scratchPad.save() if notTransaction { diff --git a/Sources/CoreDataRepository/CoreDataRepository+Delete_Batch.swift b/Sources/CoreDataRepository/CoreDataRepository+Delete_Batch.swift index fe93894..969a9fa 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Delete_Batch.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Delete_Batch.swift @@ -53,10 +53,10 @@ extension CoreDataRepository { /// /// This operation is non-atomic. Each instance may succeed or fail individually. @inlinable - public func delete( + public func delete( _ items: [Model], transactionAuthor: String? = nil - ) async -> (success: [Model], failed: [CoreDataBatchError]) where Model: ReadableUnmanagedModel { + ) async -> (success: [Model], failed: [CoreDataBatchError]) { var successes = [Model]() var failures = [CoreDataBatchError]() for item in items { diff --git a/Sources/CoreDataRepository/CoreDataRepository+Fetch.swift b/Sources/CoreDataRepository/CoreDataRepository+Fetch.swift index c9a5b67..433c8a6 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Fetch.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Fetch.swift @@ -60,10 +60,10 @@ extension CoreDataRepository { /// Fetch items from the store with a ``NSFetchRequest`` and transform the results. @inlinable - public func fetch( + public func fetch( request: NSFetchRequest, operation: @escaping (_ results: [Managed]) throws -> Output - ) async -> Result where Managed: NSManagedObject { + ) async -> Result { let context = Transaction.current?.context ?? context return await context.performInChild { fetchContext in try operation(fetchContext.fetch(request)) diff --git a/Sources/CoreDataRepository/CoreDataRepository+Read.swift b/Sources/CoreDataRepository/CoreDataRepository+Read.swift index 75f439d..9204a15 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Read.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Read.swift @@ -21,10 +21,10 @@ extension CoreDataRepository { /// Read an instance from the store. @inlinable - public func read( + public func read( _ id: Model.UnmanagedId, of _: Model.Type - ) async -> Result where Model: IdentifiedUnmanagedModel { + ) async -> Result { let context = Transaction.current?.context ?? context return await context.performInChild(schedule: .enqueued) { readContext in let managed = try Model.readManaged(id: id, from: readContext) @@ -34,10 +34,10 @@ extension CoreDataRepository { /// Read an instance from the store. @inlinable - public func read( + public func read( _ managedId: NSManagedObjectID, of _: Model.Type - ) async -> Result where Model: FetchableUnmanagedModel { + ) async -> Result { let context = Transaction.current?.context ?? context return await context.performInChild(schedule: .enqueued) { readContext in let object = try readContext.notDeletedObject(for: managedId) @@ -48,10 +48,10 @@ extension CoreDataRepository { /// Read an instance from the store. @inlinable - public func read( + public func read( _ managedIdUrl: URL, of _: Model.Type - ) async -> Result where Model: FetchableUnmanagedModel { + ) async -> Result { let context = Transaction.current?.context ?? context return await context.performInChild(schedule: .enqueued) { readContext in let id = try readContext.objectId(from: managedIdUrl).get() diff --git a/Sources/CoreDataRepository/CoreDataRepository+Read_Batch.swift b/Sources/CoreDataRepository/CoreDataRepository+Read_Batch.swift index 67af1ef..e52cd5d 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Read_Batch.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Read_Batch.swift @@ -32,9 +32,9 @@ extension CoreDataRepository { /// /// This operation is non-atomic. Each instance may succeed or fail individually. @inlinable - public func read( + public func read( _ items: some Sequence - ) async -> (success: [Model], failed: [CoreDataBatchError]) where Model: ReadableUnmanagedModel { + ) async -> (success: [Model], failed: [CoreDataBatchError]) { var successes = [Model]() var failures = [CoreDataBatchError]() for item in items { diff --git a/Sources/CoreDataRepository/CoreDataRepository.swift b/Sources/CoreDataRepository/CoreDataRepository.swift index 2453f49..b9fbd38 100644 --- a/Sources/CoreDataRepository/CoreDataRepository.swift +++ b/Sources/CoreDataRepository/CoreDataRepository.swift @@ -55,11 +55,11 @@ public final class CoreDataRepository: @unchecked Sendable { /// - Returns: The result of the block execution. /// - Throws: ``CoreDataError`` if the transaction fails. @inlinable - public func withTransaction( + public func withTransaction( continuing existingTransaction: Transaction? = nil, transactionAuthor: String? = nil, _ block: (Transaction) async throws(E) -> T - ) async throws(CoreDataError) -> T where E: Error { + ) async throws(CoreDataError) -> T { let transaction = existingTransaction ?? Transaction(context: context.scratchPadContext()) let scratchPad = transaction.context return try await CoreDataError.catching { diff --git a/Sources/CoreDataRepository/Internal/AggregateSubscription.swift b/Sources/CoreDataRepository/Internal/AggregateSubscription.swift index 02aa2ee..5838677 100644 --- a/Sources/CoreDataRepository/Internal/AggregateSubscription.swift +++ b/Sources/CoreDataRepository/Internal/AggregateSubscription.swift @@ -9,9 +9,8 @@ import Foundation /// Subscription provider that sends updates when an aggregate fetch request changes @usableFromInline -final class AggregateSubscription: Subscription, - @unchecked Sendable where Value: Numeric, - Value: Sendable +final class AggregateSubscription: Subscription, + @unchecked Sendable { @usableFromInline override func fetch() { diff --git a/Sources/CoreDataRepository/Internal/AggregateThrowingSubscription.swift b/Sources/CoreDataRepository/Internal/AggregateThrowingSubscription.swift index d8deb4e..e60694e 100644 --- a/Sources/CoreDataRepository/Internal/AggregateThrowingSubscription.swift +++ b/Sources/CoreDataRepository/Internal/AggregateThrowingSubscription.swift @@ -9,9 +9,12 @@ import Foundation /// Subscription provider that sends updates when an aggregate fetch request changes @usableFromInline -final class AggregateThrowingSubscription: ThrowingSubscription, +final class AggregateThrowingSubscription: ThrowingSubscription< + Value, + NSDictionary, + NSManagedObject +>, @unchecked Sendable - where Value: Numeric, Value: Sendable { @usableFromInline override func fetch() { diff --git a/Sources/CoreDataRepository/Internal/ArrayOfNSDictionary+AggregateHelpers.swift b/Sources/CoreDataRepository/Internal/ArrayOfNSDictionary+AggregateHelpers.swift index b7f753e..451c220 100644 --- a/Sources/CoreDataRepository/Internal/ArrayOfNSDictionary+AggregateHelpers.swift +++ b/Sources/CoreDataRepository/Internal/ArrayOfNSDictionary+AggregateHelpers.swift @@ -8,7 +8,7 @@ import Foundation extension [NSDictionary] { /// Helper function to convert the result of a CoreData aggregate fetch to a numeric value - func asAggregateValue() -> Value? where Value: Numeric { + func asAggregateValue() -> Value? { ((self as? [[String: Value]]) ?? []).first?.values.first } } diff --git a/Sources/CoreDataRepository/Internal/CountSubscription.swift b/Sources/CoreDataRepository/Internal/CountSubscription.swift index 8cb67ef..085a865 100644 --- a/Sources/CoreDataRepository/Internal/CountSubscription.swift +++ b/Sources/CoreDataRepository/Internal/CountSubscription.swift @@ -9,9 +9,8 @@ import Foundation /// Subscription provider that sends updates when a count fetch request changes @usableFromInline -final class CountSubscription: Subscription, - @unchecked Sendable where Value: Numeric, - Value: Sendable +final class CountSubscription: Subscription, + @unchecked Sendable { @usableFromInline override func fetch() { diff --git a/Sources/CoreDataRepository/Internal/CountThrowingSubscription.swift b/Sources/CoreDataRepository/Internal/CountThrowingSubscription.swift index f466eff..6bc0a8e 100644 --- a/Sources/CoreDataRepository/Internal/CountThrowingSubscription.swift +++ b/Sources/CoreDataRepository/Internal/CountThrowingSubscription.swift @@ -9,9 +9,12 @@ import Foundation /// Subscription provider that sends updates when a count fetch request changes @usableFromInline -final class CountThrowingSubscription: ThrowingSubscription, +final class CountThrowingSubscription: ThrowingSubscription< + Value, + NSDictionary, + NSManagedObject +>, @unchecked Sendable - where Value: Numeric, Value: Sendable { @usableFromInline override func fetch() { diff --git a/Sources/CoreDataRepository/Internal/Subscription.swift b/Sources/CoreDataRepository/Internal/Subscription.swift index a6ac90d..8162505 100644 --- a/Sources/CoreDataRepository/Internal/Subscription.swift +++ b/Sources/CoreDataRepository/Internal/Subscription.swift @@ -10,10 +10,10 @@ import Foundation /// Base class for other subscriptions. @usableFromInline class Subscription< - Output, + Output: Sendable, RequestResult: NSFetchRequestResult, ControllerResult: NSFetchRequestResult ->: BaseSubscription, @unchecked Sendable where Output: Sendable { +>: BaseSubscription, @unchecked Sendable { let continuation: AsyncStream>.Continuation @usableFromInline diff --git a/Sources/CoreDataRepository/Internal/ThrowingSubscription.swift b/Sources/CoreDataRepository/Internal/ThrowingSubscription.swift index b95fe86..16734f2 100644 --- a/Sources/CoreDataRepository/Internal/ThrowingSubscription.swift +++ b/Sources/CoreDataRepository/Internal/ThrowingSubscription.swift @@ -10,10 +10,10 @@ import Foundation /// Base class for other subscriptions. @usableFromInline class ThrowingSubscription< - Output, + Output: Sendable, RequestResult: NSFetchRequestResult, ControllerResult: NSFetchRequestResult ->: BaseSubscription, @unchecked Sendable where Output: Sendable { +>: BaseSubscription, @unchecked Sendable { private let continuation: AsyncThrowingStream.Continuation @usableFromInline diff --git a/Sources/CoreDataRepository/NSManagedObject+Helpers.swift b/Sources/CoreDataRepository/NSManagedObject+Helpers.swift index 1cdb9e8..df5c938 100644 --- a/Sources/CoreDataRepository/NSManagedObject+Helpers.swift +++ b/Sources/CoreDataRepository/NSManagedObject+Helpers.swift @@ -10,7 +10,7 @@ import Foundation extension NSManagedObject { /// Helper function to handle casting ``NSManagedObject`` to a sub-class. @inlinable - public func asManagedModel() throws -> T where T: NSManagedObject { + public func asManagedModel() throws -> T { guard let repoManaged = self as? T else { throw CoreDataError.fetchedObjectFailedToCastToExpectedType } diff --git a/Tests/CoreDataRepositoryTests/CoreDataTestSuite.swift b/Tests/CoreDataRepositoryTests/CoreDataTestSuite.swift index f0e32fe..4702ed8 100644 --- a/Tests/CoreDataRepositoryTests/CoreDataTestSuite.swift +++ b/Tests/CoreDataRepositoryTests/CoreDataTestSuite.swift @@ -45,7 +45,7 @@ extension CoreDataTestSuite { // empty by default } - func verify(_ item: T) async throws where T: FetchableUnmanagedModel, T: Equatable { + func verify(_ item: T) async throws { repositoryContext.performAndWait { [repositoryContext] in var managed: T.ManagedModel? do { @@ -64,7 +64,7 @@ extension CoreDataTestSuite { } } - func verify(_ item: T) async throws where T: ReadableUnmanagedModel, T: Equatable { + func verify(_ item: T) async throws { try repositoryContext.performAndWait { [repositoryContext] in var _managed: T.ManagedModel? do { @@ -96,7 +96,7 @@ extension CoreDataTestSuite { } } - func verifyDoesNotExist(_ item: T) async throws where T: FetchableUnmanagedModel, T: Equatable { + func verifyDoesNotExist(_ item: T) async throws { repositoryContext.performAndWait { [repositoryContext] in var _managed: T.ManagedModel? do { @@ -111,7 +111,7 @@ extension CoreDataTestSuite { } } - func verifyDoesNotExist(_ item: T) async throws where T: ReadableUnmanagedModel, T: Equatable { + func verifyDoesNotExist(_ item: T) async throws { repositoryContext.performAndWait { [repositoryContext] in var _managed: T.ManagedModel? do { diff --git a/Tests/CoreDataRepositoryTests/CoreDataTestSuiteTests.swift b/Tests/CoreDataRepositoryTests/CoreDataTestSuiteTests.swift index 1f22af9..1bd95ea 100644 --- a/Tests/CoreDataRepositoryTests/CoreDataTestSuiteTests.swift +++ b/Tests/CoreDataRepositoryTests/CoreDataTestSuiteTests.swift @@ -32,7 +32,7 @@ extension CoreDataRepositoryTests { } @Test - func verify_Fetchable_Failure() async throws { + func verify_Fetchable_Failure() async { let modelType = FetchableModel_UuidId.self let _value = modelType.seeded(1) @@ -57,7 +57,7 @@ extension CoreDataRepositoryTests { } @Test - func verify_Readable_Failure() async throws { + func verify_Readable_Failure() async { let modelType = IdentifiableModel_UuidId.self let _value = modelType.seeded(1) @@ -135,7 +135,7 @@ extension CoreDataRepositoryTests { } @Test - func verifyHistory_Failure_NoHistory() async throws { + func verifyHistory_Failure_NoHistory() { let author: String = #function let date = Date.now diff --git a/Tests/CoreDataRepositoryTests/CustomTests.swift b/Tests/CoreDataRepositoryTests/CustomTests.swift index 8bee281..622f0e1 100644 --- a/Tests/CoreDataRepositoryTests/CustomTests.swift +++ b/Tests/CoreDataRepositoryTests/CustomTests.swift @@ -72,7 +72,7 @@ extension CoreDataRepositoryTests { let result: Result<(FetchableModel_UuidId, FetchableModel_UuidId), CoreDataError> = if inTransaction { await { do { - let result = try await repository.withTransaction(transactionAuthor: transactionAuthor) { _ in + return try await repository.withTransaction(transactionAuthor: transactionAuthor) { _ in await repository.custom { context, scratchPad in let object1 = modelType.ManagedModel(context: scratchPad) try _value1.updating(managed: object1) @@ -87,7 +87,6 @@ extension CoreDataRepositoryTests { return try (modelType.init(managed: object1), modelType.init(managed: object2)) } } - return result } catch { return .failure(error as! CoreDataError) } diff --git a/Tests/CoreDataRepositoryTests/DeleteTests.swift b/Tests/CoreDataRepositoryTests/DeleteTests.swift index f670f22..9449a6a 100644 --- a/Tests/CoreDataRepositoryTests/DeleteTests.swift +++ b/Tests/CoreDataRepositoryTests/DeleteTests.swift @@ -37,7 +37,7 @@ extension CoreDataRepositoryTests { } @Test - func delete_Identifiable_Failure() async throws { + func delete_Identifiable_Failure() async { let modelType = IdentifiableModel_UuidId.self let _value = modelType.seeded(1) let result = await repository @@ -105,7 +105,7 @@ extension CoreDataRepositoryTests { } @Test - func delete_ManagedIdReferencable_NoManagedId_Failure() async throws { + func delete_ManagedIdReferencable_NoManagedId_Failure() async { let modelType = ManagedIdModel_UuidId.self let _value = modelType.seeded(1) @@ -226,7 +226,7 @@ extension CoreDataRepositoryTests { } @Test - func delete_ManagedIdUrlReferencable_NoManagedIdUrl_Failure() async throws { + func delete_ManagedIdUrlReferencable_NoManagedIdUrl_Failure() async { let modelType = ManagedIdUrlModel_UuidId.self let _value = modelType.seeded(1) From e78d33577fc1e8943701ac7663911abe2daebbd3 Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 12 Feb 2026 16:46:55 -0600 Subject: [PATCH 2/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 0d4a5365fca3ee532e3969ea1a19ead99a3a9bbc Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 12 Feb 2026 16:47:51 -0600 Subject: [PATCH 3/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 7f51d148fe20e883970c569299018baa354955ad Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 12 Feb 2026 16:48:00 -0600 Subject: [PATCH 4/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,