|
// |
|
// ObjectGraphCloner.swift |
|
// |
|
// |
|
// Created by beader on 2022/12/29. |
|
// |
|
|
|
import Foundation |
|
import CoreData |
|
|
|
class ObjectGraphCloner { |
|
typealias OriginObject = NSManagedObject |
|
typealias ClonedObject = NSManagedObject |
|
|
|
enum CloneError: Error { |
|
case contextError |
|
case entityNameError |
|
case castError |
|
case missingObjectError |
|
} |
|
|
|
// Mapping from origin objects to new cloned objects |
|
var clonedObjects: [OriginObject: ClonedObject] = [:] |
|
|
|
func cloneObjectGraph(_ object: OriginObject) throws -> ClonedObject { |
|
try traverseAndCloneObjects(from: object) |
|
try traverseAndCloneRelationships() |
|
return clonedObjects[object]! |
|
} |
|
|
|
/// Traverse from source node and clone objects in graph without relationships |
|
private func traverseAndCloneObjects(from object: OriginObject) throws { |
|
// if object is already cloned, stop traversing from this node |
|
guard !clonedObjects.keys.contains(object) else { |
|
return |
|
} |
|
clonedObjects[object] = try cloneObjectAttributes(object) |
|
|
|
let relationships = object.entity.relationshipsByName |
|
for (relationshipName, relationshipDescription) in relationships { |
|
for destination in extractDestinationSet(from: object, relationshipName: relationshipName, relationshipDescription: relationshipDescription) { |
|
if let destination = destination as? NSManagedObject { |
|
try traverseAndCloneObjects(from: destination) |
|
} |
|
} |
|
} |
|
} |
|
|
|
/// Clone relationships from origin objects to cloned objects |
|
private func traverseAndCloneRelationships() throws { |
|
for (originObject, clonedObject) in clonedObjects { |
|
try cloneRelationships(originObject: originObject, clonedObject: clonedObject) |
|
} |
|
} |
|
|
|
private func cloneObjectAttributes(_ object: OriginObject) throws -> ClonedObject { |
|
guard let context = object.managedObjectContext else { |
|
throw CloneError.contextError |
|
} |
|
|
|
guard let entityName = object.entity.name else { |
|
throw CloneError.entityNameError |
|
} |
|
|
|
let clonedObject = NSEntityDescription.insertNewObject( |
|
forEntityName: entityName, |
|
into: context |
|
) |
|
|
|
let attributes = object.entity.attributesByName |
|
for (attributeName, _) in attributes { |
|
let attributeValue = object.primitiveValue(forKey: attributeName) |
|
clonedObject.setValue(attributeValue, forKey: attributeName) |
|
} |
|
return clonedObject |
|
} |
|
|
|
private func cloneRelationships(originObject: OriginObject, clonedObject: ClonedObject) throws { |
|
let relationships = originObject.entity.relationshipsByName |
|
for (relationshipName, relationshipDescription) in relationships { |
|
let isToMany = relationshipDescription.isToMany |
|
let isOrdered = relationshipDescription.isOrdered |
|
if isToMany { |
|
if isOrdered { |
|
let destinations = destinationObjectOrderedSet(for: originObject, relationshipName: relationshipName) |
|
let clonedDestinations = try getClonedObjects(for: destinations) |
|
clonedObject.setValue(clonedDestinations, forKey: relationshipName) |
|
} else { |
|
let destinations = destinationObjectSet(for: originObject, relationshipName: relationshipName) |
|
let clonedDestinations = try getClonedObjects(for: destinations) |
|
clonedObject.setValue(clonedDestinations, forKey: relationshipName) |
|
} |
|
} else { |
|
if let destination = destinationObject(for: originObject, relationshipName: relationshipName) { |
|
if let clonedDestination = clonedObjects[destination] { |
|
clonedObject.setValue(clonedDestination, forKey: relationshipName) |
|
} else { |
|
throw CloneError.missingObjectError |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
private func destinationObject(for object: OriginObject, relationshipName: String) -> NSManagedObject? { |
|
object.primitiveValue(forKey: relationshipName) as? NSManagedObject |
|
} |
|
|
|
private func destinationObjectSet(for object: OriginObject, relationshipName: String) -> NSSet? { |
|
object.primitiveValue(forKey: relationshipName) as? NSSet |
|
} |
|
|
|
private func destinationObjectOrderedSet(for object: OriginObject, relationshipName: String) -> NSOrderedSet? { |
|
object.primitiveValue(forKey: relationshipName) as? NSOrderedSet |
|
} |
|
|
|
private func getClonedObjects(for objects: NSSet?) throws -> NSSet? { |
|
guard let objects else { return nil } |
|
var clonedObjects = [NSManagedObject]() |
|
for object in objects { |
|
if let object = object as? NSManagedObject { |
|
if let clonedObject = self.clonedObjects[object] { |
|
clonedObjects.append(clonedObject) |
|
} else { |
|
throw CloneError.missingObjectError |
|
} |
|
} else { |
|
throw CloneError.castError |
|
} |
|
} |
|
return NSSet(array: clonedObjects) |
|
} |
|
|
|
private func getClonedObjects(for objects: NSOrderedSet?) throws -> NSOrderedSet? { |
|
guard let objects else { return nil } |
|
var clonedObjects = [NSManagedObject]() |
|
for object in objects { |
|
if let object = object as? NSManagedObject { |
|
clonedObjects.append(object) |
|
} else { |
|
throw CloneError.castError |
|
} |
|
} |
|
return NSOrderedSet(array: clonedObjects) |
|
} |
|
|
|
private func extractDestinationSet(from object: OriginObject, relationshipName: String, relationshipDescription: NSRelationshipDescription) -> NSSet { |
|
let destinations: NSSet |
|
if relationshipDescription.isToMany { |
|
destinations = object.primitiveValue(forKey: relationshipName) as? NSSet ?? [] |
|
} else if let destination = object.primitiveValue(forKey: relationshipName) { |
|
destinations = NSSet(object: destination) |
|
} else { |
|
destinations = [] |
|
} |
|
return destinations |
|
} |
|
} |