Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 44 additions & 39 deletions Sources/CoreDataRepository/CoreDataError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -48,40 +48,46 @@ 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)

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."
)
Expand All @@ -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<T>(block: () async throws -> T) async throws(Self) -> T {
do {
Expand Down
15 changes: 13 additions & 2 deletions Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -426,7 +426,18 @@ extension CoreDataRepository {
groupBy: NSAttributeDescription? = nil
) async -> Result<Value, CoreDataError> {
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<NSDictionary>.request(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Sources/CoreDataRepository/CoreDataRepository+Delete.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 6 additions & 4 deletions Sources/CoreDataRepository/CoreDataRepository+Read_Batch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/CoreDataRepository/FetchableUnmanagedModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public protocol FetchableUnmanagedModel: Sendable {

/// ``NSFetchRequest`` for ``ManagedModel`` with a strongly typed ``NSFetchRequest.ResultType``
static func managedFetchRequest() -> NSFetchRequest<ManagedModel>

/// A description of the context from where an error is thrown
var errorDescription: String { get }
}

extension FetchableUnmanagedModel {
Expand All @@ -77,4 +80,9 @@ extension FetchableUnmanagedModel {
.managedObjectClassName
)
}

@inlinable
public var errorDescription: String {
"\(Self.self)"
}
}
8 changes: 6 additions & 2 deletions Sources/CoreDataRepository/IdentifiedUnmanagedModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
18 changes: 16 additions & 2 deletions Sources/CoreDataRepository/Internal/AggregateSubscription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ final class AggregateSubscription<Value: Numeric & Sendable>: Subscription<Value
}

guard let value: Value = result.asAggregateValue() else {
self?.fail(.fetchedObjectFailedToCastToExpectedType)
self?.fail(.fetchedObjectFailedToCastToExpectedType(description: nil))
return
}
self?.send(value)
}
}

@usableFromInline
// swiftlint:disable:next function_body_length
convenience init(
function: CoreDataRepository.AggregateFunction,
context: NSManagedObjectContext,
Expand Down Expand Up @@ -93,7 +94,20 @@ final class AggregateSubscription<Value: Numeric & Sendable>: Subscription<Value
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ final class AggregateThrowingSubscription<Value: Numeric & Sendable>: ThrowingSu
}

guard let value: Value = result.asAggregateValue() else {
self?.fail(.fetchedObjectFailedToCastToExpectedType)
self?.fail(.fetchedObjectFailedToCastToExpectedType(description: nil))
return
}
self?.send(value)
}
}

@usableFromInline
// swiftlint:disable:next function_body_length
convenience init(
function: CoreDataRepository.AggregateFunction,
context: NSManagedObjectContext,
Expand Down Expand Up @@ -97,7 +98,20 @@ final class AggregateThrowingSubscription<Value: Numeric & Sendable>: 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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/CoreDataRepository/NSManagedObject+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ extension NSManagedObject {
@inlinable
public func asManagedModel<T: NSManagedObject>() throws -> T {
guard let repoManaged = self as? T else {
throw CoreDataError.fetchedObjectFailedToCastToExpectedType
throw CoreDataError.fetchedObjectFailedToCastToExpectedType(description: "\(Self.self) -> \(T.self)")
}
return repoManaged
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading