Skip to content

Commit e5ec1cc

Browse files
authored
Merge pull request #44 from roanutil/feature/identified-description
Add descriptions to some error cases so they include context of where they were thrown from
2 parents 311edfe + c46b111 commit e5ec1cc

22 files changed

Lines changed: 182 additions & 84 deletions

Sources/CoreDataRepository/CoreDataError.swift

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ public enum CoreDataError: Error, Hashable, Sendable {
2121
/// against the correct property.
2222
/// If the `NSAttributeDescription` is not for the correct or expected `NSEntityDescription`, this error is
2323
/// returned.
24-
case propertyDoesNotMatchEntity
24+
case propertyDoesNotMatchEntity(description: String?)
2525

2626
/// CoreData may return a value of a related type to what is actually needed. If casting the value CoreData returns
2727
/// to the required type fails, this error is returned.
28-
case fetchedObjectFailedToCastToExpectedType
28+
case fetchedObjectFailedToCastToExpectedType(description: String?)
2929

3030
/// It's possible for a persisted object to be flagged as deleted but still be fetched. If that happens, this error
3131
/// is returned.
32-
case fetchedObjectIsFlaggedAsDeleted
32+
case fetchedObjectIsFlaggedAsDeleted(description: String)
3333

3434
/// If CoreData throws a `CocoaError`, it is embedded here.
3535
case cocoa(CocoaError)
@@ -48,40 +48,46 @@ public enum CoreDataError: Error, Hashable, Sendable {
4848
/// If a ``ManagedIdUrlReferencable`` value is used in a transaction where it is expected to already be persisted
4949
/// but has no `URL`
5050
/// representing the ``NSManagedObjectID``, this error is returned.
51-
case noUrlOnItemToMapToObjectId
51+
case noUrlOnItemToMapToObjectId(description: String)
5252

5353
/// If a ``ManagedIdReferencable`` value is used in a transaction where it is expected to already be persisted but
5454
/// has no `NSManagedObjectID`, this error is returned.
55-
case noObjectIdOnItem
55+
case noObjectIdOnItem(description: String)
5656

57-
case noMatchFoundWhenReadingItem
57+
case noMatchFoundWhenReadingItem(description: String)
5858

59+
private static var noErrorDescription: String {
60+
String(
61+
localized: "no description",
62+
bundle: .module,
63+
comment: "Placeholder for when an error description is nil."
64+
)
65+
}
66+
67+
// swiftlint:disable line_length
5968
public var localizedDescription: String {
6069
switch self {
6170
case .failedToGetObjectIdFromUrl:
62-
NSLocalizedString(
63-
"No NSManagedObjectID found that correlates to the provided URL.",
71+
String(
72+
localized: "No NSManagedObjectID found that correlates to the provided URL.",
6473
bundle: .module,
6574
comment: "Error for when an ObjectID can't be found for the provided URL."
6675
)
67-
case .propertyDoesNotMatchEntity:
68-
NSLocalizedString(
69-
"There is a mismatch between a provided NSPropertyDescrption's entity and a NSEntityDescription. "
70-
+ "When a property description is provided, it must match any related entity descriptions.",
76+
case let .propertyDoesNotMatchEntity(description: description):
77+
String(
78+
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)",
7179
bundle: .module,
72-
comment: "Error for when the developer does not provide a valid pair of NSAttributeDescription "
73-
+ "and NSPropertyDescription (or any of their child types)."
80+
comment: "Error for when the developer does not provide a valid pair of NSAttributeDescription and NSPropertyDescription (or any of their child types)."
7481
)
75-
case .fetchedObjectFailedToCastToExpectedType:
76-
NSLocalizedString(
77-
"The object corresponding to the provided NSManagedObjectID is an incorrect Entity or "
78-
+ "NSManagedObject subtype. It failed to cast to the requested type.",
82+
case let .fetchedObjectFailedToCastToExpectedType(description: description):
83+
String(
84+
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)",
7985
bundle: .module,
8086
comment: "Error for when an object is found for a given ObjectID but it is not the expected type."
8187
)
82-
case .fetchedObjectIsFlaggedAsDeleted:
83-
NSLocalizedString(
84-
"The object corresponding to the provided NSManagedObjectID is deleted and cannot be fetched.",
88+
case let .fetchedObjectIsFlaggedAsDeleted(description: description):
89+
String(
90+
localized: "The object corresponding to the provided NSManagedObjectID is deleted and cannot be fetched: \(description)",
8591
bundle: .module,
8692
comment: "Error for when an object is fetched but is flagged as deleted and is no longer usable."
8793
)
@@ -90,41 +96,40 @@ public enum CoreDataError: Error, Hashable, Sendable {
9096
case let .unknown(error):
9197
error.localizedDescription
9298
case .noEntityNameFound:
93-
NSLocalizedString(
94-
"The managed object entity description does not have a name.",
99+
String(
100+
localized: "The managed object entity description does not have a name.",
95101
bundle: .module,
96102
comment: "Error for when the NSEntityDescription does not have a name."
97103
)
98104
case .atLeastOneAttributeDescRequired:
99-
NSLocalizedString(
100-
"The managed object entity has no attribute description. An attribute description is required for "
101-
+ "aggregate operations.",
105+
String(
106+
localized: "The managed object entity has no attribute description. An attribute description is required for aggregate operations.",
102107
bundle: .module,
103108
comment: "Error for when the NSEntityDescription has no NSAttributeDescription but one is required."
104109
)
105-
case .noUrlOnItemToMapToObjectId:
106-
NSLocalizedString(
107-
"No object ID URL found on the model for an operation against an existing managed object.",
110+
case let .noUrlOnItemToMapToObjectId(description: description):
111+
String(
112+
localized: "No object ID URL found on the model for an operation against an existing managed object: \(description)",
108113
bundle: .module,
109-
comment: "Error for performing an operation against an existing NSManagedObject but the "
110-
+ "ManagedIdUrlReferencable instance has no managedIdUrl for looking up the NSManagedOjbectID."
114+
comment: "Error for performing an operation against an existing NSManagedObject but the ManagedIdUrlReferencable instance has no managedIdUrl for looking up the NSManagedOjbectID."
111115
)
112-
case .noObjectIdOnItem:
113-
NSLocalizedString(
114-
"No object ID found on the model for an operation against an existing managed object.",
116+
case let .noObjectIdOnItem(description: description):
117+
String(
118+
localized: "No object ID found on the model for an operation against an existing managed object: \(description)",
115119
bundle: .module,
116-
comment: "Error for performing an operation against an existing NSManagedObject but the "
117-
+ "ManagedIdReferencable instance has no managedId."
120+
comment: "Error for performing an operation against an existing NSManagedObject but the ManagedIdReferencable instance has no managedId."
118121
)
119-
case .noMatchFoundWhenReadingItem:
120-
NSLocalizedString(
121-
"No match found when attempting to read an instance from CoreData.",
122+
case let .noMatchFoundWhenReadingItem(description: description):
123+
String(
124+
localized: "No match found when attempting to read an instance from CoreData: \(description)",
122125
bundle: .module,
123126
comment: "Error for reading an instance from CoreData but no instance was found."
124127
)
125128
}
126129
}
127130

131+
// swiftlint:enable line_length
132+
128133
@usableFromInline
129134
static func catching<T>(block: () async throws -> T) async throws(Self) -> T {
130135
do {

Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ extension CoreDataRepository {
411411
) throws -> Value {
412412
let result = try context.fetch(request)
413413
guard let value: Value = result.asAggregateValue() else {
414-
throw CoreDataError.fetchedObjectFailedToCastToExpectedType
414+
throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: nil)
415415
}
416416
return value
417417
}
@@ -426,7 +426,18 @@ extension CoreDataRepository {
426426
groupBy: NSAttributeDescription? = nil
427427
) async -> Result<Value, CoreDataError> {
428428
guard entityDesc == attributeDesc.entity else {
429-
return .failure(.propertyDoesNotMatchEntity)
429+
guard let entityName = entityDesc.name ?? entityDesc.managedObjectClassName else {
430+
return .failure(.propertyDoesNotMatchEntity(description: nil))
431+
}
432+
guard let attributeEntityName = attributeDesc.entity.name ?? attributeDesc.entity.managedObjectClassName
433+
else {
434+
return .failure(.propertyDoesNotMatchEntity(description: entityName))
435+
}
436+
return .failure(
437+
.propertyDoesNotMatchEntity(
438+
description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)"
439+
)
440+
)
430441
}
431442
return await context.performInChild { scratchPad in
432443
let request = try NSFetchRequest<NSDictionary>.request(

Sources/CoreDataRepository/CoreDataRepository+BatchRequest.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extension CoreDataRepository {
1818
context.transactionAuthor = transactionAuthor
1919
guard let result = try scratchPad.execute(request) as? NSBatchDeleteResult else {
2020
context.transactionAuthor = nil
21-
throw CoreDataError.fetchedObjectFailedToCastToExpectedType
21+
throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: request.description)
2222
}
2323
context.transactionAuthor = nil
2424
return result
@@ -36,7 +36,7 @@ extension CoreDataRepository {
3636
context.transactionAuthor = transactionAuthor
3737
guard let result = try scratchPad.execute(request) as? NSBatchInsertResult else {
3838
context.transactionAuthor = nil
39-
throw CoreDataError.fetchedObjectFailedToCastToExpectedType
39+
throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: request.description)
4040
}
4141
context.transactionAuthor = nil
4242
return result
@@ -54,7 +54,7 @@ extension CoreDataRepository {
5454
context.transactionAuthor = transactionAuthor
5555
guard let result = try scratchPad.execute(request) as? NSBatchUpdateResult else {
5656
context.transactionAuthor = nil
57-
throw CoreDataError.fetchedObjectFailedToCastToExpectedType
57+
throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: request.description)
5858
}
5959
context.transactionAuthor = nil
6060
return result

Sources/CoreDataRepository/CoreDataRepository+Delete.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ extension CoreDataRepository {
7070
scratchPad.transactionAuthor = transactionAuthor
7171
let object = try item.readManaged(from: scratchPad)
7272
guard !object.isDeleted else {
73-
throw CoreDataError.fetchedObjectIsFlaggedAsDeleted
73+
throw CoreDataError.fetchedObjectIsFlaggedAsDeleted(description: item.errorDescription)
7474
}
7575
object.prepareForDeletion()
7676
scratchPad.delete(object)

Sources/CoreDataRepository/CoreDataRepository+Delete_Batch.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ extension CoreDataRepository {
138138
for item in items {
139139
let object = try item.readManaged(from: scratchPad)
140140
guard !object.isDeleted else {
141-
throw CoreDataError.fetchedObjectIsFlaggedAsDeleted
141+
throw CoreDataError
142+
.fetchedObjectIsFlaggedAsDeleted(description: item.errorDescription)
142143
}
143144
object.prepareForDeletion()
144145
scratchPad.delete(object)

Sources/CoreDataRepository/CoreDataRepository+Read_Batch.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ extension CoreDataRepository {
103103
try ids.map { id in
104104
let managed = try Model.readManaged(id: id, from: readContext)
105105
guard !managed.isDeleted else {
106-
throw CoreDataError.fetchedObjectIsFlaggedAsDeleted
106+
throw CoreDataError
107+
.fetchedObjectIsFlaggedAsDeleted(description: Model.errorDescription(for: id))
107108
}
108109
return try Model(managed: managed)
109110
}
@@ -122,7 +123,8 @@ extension CoreDataRepository {
122123
try items.map { item in
123124
let managed = try item.readManaged(from: readContext)
124125
guard !managed.isDeleted else {
125-
throw CoreDataError.fetchedObjectIsFlaggedAsDeleted
126+
throw CoreDataError
127+
.fetchedObjectIsFlaggedAsDeleted(description: item.errorDescription)
126128
}
127129
return try Model(managed: managed)
128130
}
@@ -142,7 +144,7 @@ extension CoreDataRepository {
142144
try managedIds.map { managedId in
143145
let _managed = try readContext.notDeletedObject(for: managedId)
144146
guard let managed = _managed as? Model.ManagedModel else {
145-
throw CoreDataError.fetchedObjectFailedToCastToExpectedType
147+
throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: "\(Model.self)")
146148
}
147149
return try Model(managed: managed)
148150
}
@@ -163,7 +165,7 @@ extension CoreDataRepository {
163165
let managedId = try readContext.objectId(from: managedIdUrl).get()
164166
let _managed = try readContext.notDeletedObject(for: managedId)
165167
guard let managed = _managed as? Model.ManagedModel else {
166-
throw CoreDataError.fetchedObjectFailedToCastToExpectedType
168+
throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: "\(Model.self)")
167169
}
168170
return try Model(managed: managed)
169171
}

Sources/CoreDataRepository/FetchableUnmanagedModel.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ public protocol FetchableUnmanagedModel: Sendable {
6767

6868
/// ``NSFetchRequest`` for ``ManagedModel`` with a strongly typed ``NSFetchRequest.ResultType``
6969
static func managedFetchRequest() -> NSFetchRequest<ManagedModel>
70+
71+
/// A description of the context from where an error is thrown
72+
var errorDescription: String { get }
7073
}
7174

7275
extension FetchableUnmanagedModel {
@@ -77,4 +80,9 @@ extension FetchableUnmanagedModel {
7780
.managedObjectClassName
7881
)
7982
}
83+
84+
@inlinable
85+
public var errorDescription: String {
86+
"\(Self.self)"
87+
}
8088
}

Sources/CoreDataRepository/IdentifiedUnmanagedModel.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public protocol IdentifiedUnmanagedModel: ReadableUnmanagedModel {
1010
associatedtype UnmanagedId: Equatable
1111
var unmanagedId: UnmanagedId { get }
1212
static var unmanagedIdExpression: NSExpression { get }
13+
/// Enables including ``UnmanagedId`` in ``errorDescription``
14+
static func errorDescription(for unmanagedId: UnmanagedId) -> String
1315
}
1416

1517
extension IdentifiedUnmanagedModel {
@@ -29,10 +31,12 @@ extension IdentifiedUnmanagedModel {
2931
)
3032
let fetchResult = try context.fetch(request)
3133
guard let managed = fetchResult.first, fetchResult.count == 1 else {
32-
throw CoreDataError.noMatchFoundWhenReadingItem
34+
throw CoreDataError
35+
.noMatchFoundWhenReadingItem(description: "\(Self.self) -- id: \(errorDescription(for: id))")
3336
}
3437
guard !managed.isDeleted else {
35-
throw CoreDataError.fetchedObjectIsFlaggedAsDeleted
38+
throw CoreDataError
39+
.fetchedObjectIsFlaggedAsDeleted(description: "\(Self.self) -- id: \(errorDescription(for: id))")
3640
}
3741
return managed
3842
}

Sources/CoreDataRepository/Internal/AggregateSubscription.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@ final class AggregateSubscription<Value: Numeric & Sendable>: Subscription<Value
3232
}
3333

3434
guard let value: Value = result.asAggregateValue() else {
35-
self?.fail(.fetchedObjectFailedToCastToExpectedType)
35+
self?.fail(.fetchedObjectFailedToCastToExpectedType(description: nil))
3636
return
3737
}
3838
self?.send(value)
3939
}
4040
}
4141

4242
@usableFromInline
43+
// swiftlint:disable:next function_body_length
4344
convenience init(
4445
function: CoreDataRepository.AggregateFunction,
4546
context: NSManagedObjectContext,
@@ -93,7 +94,20 @@ final class AggregateSubscription<Value: Numeric & Sendable>: Subscription<Value
9394
context: context,
9495
continuation: continuation
9596
)
96-
fail(.propertyDoesNotMatchEntity)
97+
guard let entityName = entityDesc.name ?? entityDesc.managedObjectClassName else {
98+
fail(.propertyDoesNotMatchEntity(description: nil))
99+
return
100+
}
101+
guard let attributeEntityName = attributeDesc.entity.name ?? attributeDesc.entity.managedObjectClassName
102+
else {
103+
fail(.propertyDoesNotMatchEntity(description: entityName))
104+
return
105+
}
106+
fail(
107+
.propertyDoesNotMatchEntity(
108+
description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)"
109+
)
110+
)
97111
return
98112
}
99113
self.init(request: request, context: context, continuation: continuation)

Sources/CoreDataRepository/Internal/AggregateThrowingSubscription.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,15 @@ final class AggregateThrowingSubscription<Value: Numeric & Sendable>: ThrowingSu
3636
}
3737

3838
guard let value: Value = result.asAggregateValue() else {
39-
self?.fail(.fetchedObjectFailedToCastToExpectedType)
39+
self?.fail(.fetchedObjectFailedToCastToExpectedType(description: nil))
4040
return
4141
}
4242
self?.send(value)
4343
}
4444
}
4545

4646
@usableFromInline
47+
// swiftlint:disable:next function_body_length
4748
convenience init(
4849
function: CoreDataRepository.AggregateFunction,
4950
context: NSManagedObjectContext,
@@ -97,7 +98,20 @@ final class AggregateThrowingSubscription<Value: Numeric & Sendable>: ThrowingSu
9798
context: context,
9899
continuation: continuation
99100
)
100-
fail(.propertyDoesNotMatchEntity)
101+
guard let entityName = entityDesc.name ?? entityDesc.managedObjectClassName else {
102+
fail(.propertyDoesNotMatchEntity(description: nil))
103+
return
104+
}
105+
guard let attributeEntityName = attributeDesc.entity.name ?? attributeDesc.entity.managedObjectClassName
106+
else {
107+
fail(.propertyDoesNotMatchEntity(description: entityName))
108+
return
109+
}
110+
fail(
111+
.propertyDoesNotMatchEntity(
112+
description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)"
113+
)
114+
)
101115
return
102116
}
103117
self.init(request: request, context: context, continuation: continuation)

0 commit comments

Comments
 (0)