// // CustomSchemaMappingProvider.swift // CoreStore // // Copyright © 2017 John Rommel Estropia // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import CoreData import Foundation // MARK: - CustomSchemaMappingProvider open class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { public let sourceVersion: ModelVersion public let destinationVersion: ModelVersion public required init(from sourceVersion: ModelVersion, to destinationVersion: ModelVersion, entityMappings: Set = []) { CoreStore.assert( cs_lazy { let sources = entityMappings.flatMap({ $0.entityMappingSourceEntity }) let destinations = entityMappings.flatMap({ $0.entityMappingDestinationEntity }) return sources.count == Set(sources).count && destinations.count == Set(destinations).count }, "Duplicate source/destination entities found in provided \"entityMappings\" argument." ) self.sourceVersion = sourceVersion self.destinationVersion = destinationVersion self.entityMappings = entityMappings } // MARK: - CustomMapping public enum CustomMapping: Hashable { public typealias Transformer = (_ sourceObject: UnsafeSourceObject, _ createDestinationObject: () -> UnsafeDestinationObject) throws -> Void case deleteEntity(sourceEntity: EntityName) case insertEntity(destinationEntity: EntityName) case copyEntity(sourceEntity: EntityName, destinationEntity: EntityName) case transformEntity(sourceEntity: EntityName, destinationEntity: EntityName, transformer: Transformer) static func inferredTransformation(_ sourceObject: UnsafeSourceObject, _ createDestinationObject: () -> UnsafeDestinationObject) throws -> Void { let destinationObject = createDestinationObject() destinationObject.enumerateAttributes { (attribute, sourceAttribute) in if let sourceAttribute = sourceAttribute { destinationObject[attribute] = sourceObject[sourceAttribute] } } } var entityMappingSourceEntity: EntityName? { switch self { case .deleteEntity(let sourceEntity), .copyEntity(let sourceEntity, _), .transformEntity(let sourceEntity, _, _): return sourceEntity case .insertEntity: return nil } } var entityMappingDestinationEntity: EntityName? { switch self { case .insertEntity(let destinationEntity), .copyEntity(_, let destinationEntity), .transformEntity(_, let destinationEntity, _): return destinationEntity case .deleteEntity: return nil } } // MARK: Equatable public static func == (lhs: CustomMapping, rhs: CustomMapping) -> Bool { switch (lhs, rhs) { case (.deleteEntity(let sourceEntity1), .deleteEntity(let sourceEntity2)): return sourceEntity1 == sourceEntity2 case (.insertEntity(let destinationEntity1), .insertEntity(let destinationEntity2)): return destinationEntity1 == destinationEntity2 case (.copyEntity(let sourceEntity1, let destinationEntity1), .copyEntity(let sourceEntity2, let destinationEntity2)): return sourceEntity1 == sourceEntity2 && destinationEntity1 == destinationEntity2 case (.transformEntity(let sourceEntity1, let destinationEntity1, _), .transformEntity(let sourceEntity2, let destinationEntity2, _)): return sourceEntity1 == sourceEntity2 && destinationEntity1 == destinationEntity2 default: return false } } // MARK: Hashable public var hashValue: Int { switch self { case .deleteEntity(let sourceEntity): return sourceEntity.hashValue case .insertEntity(let destinationEntity): return destinationEntity.hashValue case .copyEntity(let sourceEntity, let destinationEntity): return sourceEntity.hashValue ^ destinationEntity.hashValue case .transformEntity(let sourceEntity, let destinationEntity, _): return sourceEntity.hashValue ^ destinationEntity.hashValue } } } // MARK: - UnsafeSourceObject public final class UnsafeSourceObject { public subscript(attribute: KeyPath) -> Any? { return self.rawObject.cs_accessValueForKVCKey(attribute) } public subscript(attribute: NSAttributeDescription) -> Any? { return self.rawObject.cs_accessValueForKVCKey(attribute.name) } public func enumerateAttributes(_ closure: (_ attribute: NSAttributeDescription) -> Void) { for case let attribute as NSAttributeDescription in self.rawObject.entity.properties { closure(attribute) } } // MARK: Internal internal init(_ rawObject: NSManagedObject) { self.rawObject = rawObject } // MARK: Private private let rawObject: NSManagedObject } // MARK: - UnsafeDestinationObject public final class UnsafeDestinationObject { public subscript(attribute: KeyPath) -> Any? { get { return self.rawObject.cs_accessValueForKVCKey(attribute) } set { self.rawObject.cs_setValue(newValue, forKVCKey: attribute) } } public subscript(attribute: NSAttributeDescription) -> Any? { get { return self.rawObject.cs_accessValueForKVCKey(attribute.name) } set { self.rawObject.cs_setValue(newValue, forKVCKey: attribute.name) } } public func enumerateAttributes(_ closure: (_ attribute: NSAttributeDescription, _ sourceAttribute: NSAttributeDescription?) -> Void) { for case let attribute as NSAttributeDescription in self.rawObject.entity.properties { closure(attribute, self.sourceAttributesByDestinationKey[attribute.name]) } } // MARK: Internal internal init(_ rawObject: NSManagedObject, _ sourceAttributesByDestinationKey: [KeyPath: NSAttributeDescription]) { self.rawObject = rawObject self.sourceAttributesByDestinationKey = sourceAttributesByDestinationKey } // MARK: Private private let rawObject: NSManagedObject private let sourceAttributesByDestinationKey: [KeyPath: NSAttributeDescription] } // MARK: Equatable public static func == (lhs: CustomSchemaMappingProvider, rhs: CustomSchemaMappingProvider) -> Bool { return lhs.sourceVersion == rhs.sourceVersion && lhs.destinationVersion == rhs.destinationVersion && type(of: lhs) == type(of: rhs) } // MARK: Hashable public var hashValue: Int { return self.sourceVersion.hashValue ^ self.destinationVersion.hashValue } // MARK: SchemaMappingProvider public func createMappingModel(from sourceSchema: DynamicSchema, to destinationSchema: DynamicSchema, storage: LocalStorage) throws -> (mappingModel: NSMappingModel, migrationType: MigrationType) { let sourceModel = sourceSchema.rawModel() let destinationModel = destinationSchema.rawModel() let mappingModel = NSMappingModel() let (deleteMappings, insertMappings, copyMappings, transformMappings) = self.resolveEntityMappings( sourceModel: sourceModel, destinationModel: destinationModel ) func expression(forSource sourceEntity: NSEntityDescription) -> NSExpression { return NSExpression(format: "FETCH(FUNCTION($\(NSMigrationManagerKey), \"fetchRequestForSourceEntityNamed:predicateString:\" , \"\(sourceEntity.name!)\", \"\(NSPredicate(value: true))\"), $\(NSMigrationManagerKey).\(#keyPath(NSMigrationManager.sourceContext)), \(false))") } let sourceEntitiesByName = sourceModel.entitiesByName let destinationEntitiesByName = destinationModel.entitiesByName var entityMappings: [NSEntityMapping] = [] for case .deleteEntity(let sourceEntityName) in deleteMappings { let sourceEntity = sourceEntitiesByName[sourceEntityName]! let entityMapping = NSEntityMapping() entityMapping.sourceEntityName = sourceEntity.name entityMapping.sourceEntityVersionHash = sourceEntity.versionHash entityMapping.mappingType = .removeEntityMappingType entityMapping.sourceExpression = expression(forSource: sourceEntity) entityMappings.append(entityMapping) } for case .insertEntity(let destinationEntityName) in insertMappings { let destinationEntity = destinationEntitiesByName[destinationEntityName]! let entityMapping = NSEntityMapping() entityMapping.destinationEntityName = destinationEntity.name entityMapping.destinationEntityVersionHash = destinationEntity.versionHash entityMapping.mappingType = .addEntityMappingType entityMapping.attributeMappings = autoreleasepool { () -> [NSPropertyMapping] in var attributeMappings: [NSPropertyMapping] = [] for (_, destinationAttribute) in destinationEntity.attributesByName { let propertyMapping = NSPropertyMapping() propertyMapping.name = destinationAttribute.name attributeMappings.append(propertyMapping) } return attributeMappings } entityMapping.relationshipMappings = autoreleasepool { () -> [NSPropertyMapping] in var relationshipMappings: [NSPropertyMapping] = [] for (_, destinationRelationship) in destinationEntity.relationshipsByName { let propertyMapping = NSPropertyMapping() propertyMapping.name = destinationRelationship.name relationshipMappings.append(propertyMapping) } return relationshipMappings } entityMappings.append(entityMapping) } for case .copyEntity(let sourceEntityName, let destinationEntityName) in copyMappings { let sourceEntity = sourceEntitiesByName[sourceEntityName]! let destinationEntity = destinationEntitiesByName[destinationEntityName]! let entityMapping = NSEntityMapping() entityMapping.sourceEntityName = sourceEntity.name entityMapping.sourceEntityVersionHash = sourceEntity.versionHash entityMapping.destinationEntityName = destinationEntity.name entityMapping.destinationEntityVersionHash = destinationEntity.versionHash entityMapping.mappingType = .copyEntityMappingType entityMapping.sourceExpression = expression(forSource: sourceEntity) entityMapping.attributeMappings = autoreleasepool { () -> [NSPropertyMapping] in let sourceAttributes = sourceEntity.cs_resolvedAttributeRenamingIdentities() let destinationAttributes = destinationEntity.cs_resolvedAttributeRenamingIdentities() var attributeMappings: [NSPropertyMapping] = [] for (renamingIdentifier, destination) in destinationAttributes { let sourceAttribute = sourceAttributes[renamingIdentifier]!.attribute let destinationAttribute = destination.attribute let propertyMapping = NSPropertyMapping() propertyMapping.name = destinationAttribute.name propertyMapping.valueExpression = NSExpression(format: "$\(NSMigrationSourceObjectKey).\(sourceAttribute.name)") attributeMappings.append(propertyMapping) } return attributeMappings } let entityMappingName = entityMapping.name! entityMapping.relationshipMappings = autoreleasepool { () -> [NSPropertyMapping] in let destinationRelationships = destinationEntity.cs_resolvedRelationshipRenamingIdentities() var relationshipMappings: [NSPropertyMapping] = [] for (_, destination) in destinationRelationships { let destinationRelationship = destination.relationship let propertyMapping = NSPropertyMapping() propertyMapping.name = destinationRelationship.name propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationManagerKey), \"\(#selector(NSMigrationManager.destinationInstances(forEntityMappingName:sourceInstances:)))\" , \"\(entityMappingName)\", $\(NSMigrationSourceObjectKey))[0]") relationshipMappings.append(propertyMapping) } return relationshipMappings } entityMappings.append(entityMapping) } for case .transformEntity(let sourceEntityName, let destinationEntityName, let transformEntity) in transformMappings { let sourceEntity = sourceEntitiesByName[sourceEntityName]! let destinationEntity = destinationEntitiesByName[destinationEntityName]! let entityMapping = NSEntityMapping() entityMapping.sourceEntityName = sourceEntity.name entityMapping.sourceEntityVersionHash = sourceEntity.versionHash entityMapping.destinationEntityName = destinationEntity.name entityMapping.destinationEntityVersionHash = destinationEntity.versionHash entityMapping.mappingType = .customEntityMappingType entityMapping.sourceExpression = expression(forSource: sourceEntity) entityMapping.entityMigrationPolicyClassName = NSStringFromClass(CustomEntityMigrationPolicy.self) var userInfo: [AnyHashable: Any] = [ CustomEntityMigrationPolicy.UserInfoKey.transformer: transformEntity ] autoreleasepool { let sourceAttributes = sourceEntity.cs_resolvedAttributeRenamingIdentities() let destinationAttributes = destinationEntity.cs_resolvedAttributeRenamingIdentities() let transformedRenamingIdentifiers = Set(destinationAttributes.keys) .intersection(sourceAttributes.keys) var sourceAttributesByDestinationKey: [KeyPath: NSAttributeDescription] = [:] for renamingIdentifier in transformedRenamingIdentifiers { let sourceAttribute = sourceAttributes[renamingIdentifier]!.attribute let destinationAttribute = destinationAttributes[renamingIdentifier]!.attribute sourceAttributesByDestinationKey[destinationAttribute.name] = sourceAttribute } userInfo[CustomEntityMigrationPolicy.UserInfoKey.sourceAttributesByDestinationKey] = sourceAttributesByDestinationKey } let entityMappingName = entityMapping.name! entityMapping.relationshipMappings = autoreleasepool { () -> [NSPropertyMapping] in let destinationRelationships = destinationEntity.cs_resolvedRelationshipRenamingIdentities() var relationshipMappings: [NSPropertyMapping] = [] for (_, destination) in destinationRelationships { let destinationRelationship = destination.relationship let propertyMapping = NSPropertyMapping() propertyMapping.name = destinationRelationship.name propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationManagerKey), \"\(#selector(NSMigrationManager.destinationInstances(forEntityMappingName:sourceInstances:)))\" , \"\(entityMappingName)\", $\(NSMigrationSourceObjectKey))[0]") relationshipMappings.append(propertyMapping) } return relationshipMappings } entityMapping.userInfo = userInfo entityMappings.append(entityMapping) } mappingModel.entityMappings = entityMappings return ( mappingModel, .heavyweight( sourceVersion: self.sourceVersion, destinationVersion: self.destinationVersion ) ) } // MARK: Private // MARK: - CustomEntityMigrationPolicy private final class CustomEntityMigrationPolicy: NSEntityMigrationPolicy { // MARK: NSEntityMigrationPolicy override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws { let userInfo = mapping.userInfo! let transformer = userInfo[CustomEntityMigrationPolicy.UserInfoKey.transformer]! as! CustomMapping.Transformer let sourceAttributesByDestinationKey = userInfo[CustomEntityMigrationPolicy.UserInfoKey.sourceAttributesByDestinationKey] as! [KeyPath: NSAttributeDescription] var dInstance: NSManagedObject? try transformer( UnsafeSourceObject(sInstance), { let rawObject = NSEntityDescription.insertNewObject( forEntityName: mapping.destinationEntityName!, into: manager.destinationContext ) dInstance = rawObject return UnsafeDestinationObject(rawObject, sourceAttributesByDestinationKey) } ) if let dInstance = dInstance { manager.associate( sourceInstance: sInstance, withDestinationInstance: dInstance, for: mapping ) } } override func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws { try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager) } // MARK: FilePrivate fileprivate enum UserInfoKey { fileprivate static let transformer = "CoreStore.CustomEntityMigrationPolicy.transformer" fileprivate static let sourceAttributesByDestinationKey = "CoreStore.CustomEntityMigrationPolicy.sourceAttributesByDestinationKey" } } // MARK: - private let entityMappings: Set private func resolveEntityMappings(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel) -> (delete: Set, insert: Set, copy: Set, transform: Set) { var deleteMappings: Set = [] var insertMappings: Set = [] var copyMappings: Set = [] var transformMappings: Set = [] var allMappedSourceKeys: [KeyPath: KeyPath] = [:] var allMappedDestinationKeys: [KeyPath: KeyPath] = [:] let sourceRenamingIdentifiers = sourceModel.cs_resolvedRenamingIdentities() let sourceEntityNames = sourceModel.entitiesByName let destinationRenamingIdentifiers = destinationModel.cs_resolvedRenamingIdentities() let destinationEntityNames = destinationModel.entitiesByName let removedRenamingIdentifiers = Set(sourceRenamingIdentifiers.keys) .subtracting(destinationRenamingIdentifiers.keys) let addedRenamingIdentifiers = Set(destinationRenamingIdentifiers.keys) .subtracting(sourceRenamingIdentifiers.keys) let transformedRenamingIdentifiers = Set(destinationRenamingIdentifiers.keys) .subtracting(addedRenamingIdentifiers) .subtracting(removedRenamingIdentifiers) // First pass: resolve source-destination entities for mapping in self.entityMappings { switch mapping { case .deleteEntity(let sourceEntity): CoreStore.assert( sourceEntityNames[sourceEntity] != nil, "A \(cs_typeName(CustomMapping.self)) with value '\(mapping)' passed to \(cs_typeName(CustomSchemaMappingProvider.self)) could not be mapped to any \(cs_typeName(NSEntityDescription.self)) from the source \(cs_typeName(NSManagedObjectModel.self))." ) CoreStore.assert( allMappedSourceKeys[sourceEntity] == nil, "Duplicate \(cs_typeName(CustomMapping.self))s found for source entity name \"\(sourceEntity)\" in \(cs_typeName(CustomSchemaMappingProvider.self))." ) deleteMappings.insert(mapping) allMappedSourceKeys[sourceEntity] = "" case .insertEntity(let destinationEntity): CoreStore.assert( destinationEntityNames[destinationEntity] != nil, "A \(cs_typeName(CustomMapping.self)) with value '\(mapping)' passed to \(cs_typeName(CustomSchemaMappingProvider.self)) could not be mapped to any \(cs_typeName(NSEntityDescription.self)) from the destination \(cs_typeName(NSManagedObjectModel.self))." ) CoreStore.assert( allMappedDestinationKeys[destinationEntity] == nil, "Duplicate \(cs_typeName(CustomMapping.self))s found for destination entity name \"\(destinationEntity)\" in \(cs_typeName(CustomSchemaMappingProvider.self))." ) insertMappings.insert(mapping) allMappedDestinationKeys[destinationEntity] = "" case .transformEntity(let sourceEntity, let destinationEntity, _): CoreStore.assert( sourceEntityNames[sourceEntity] != nil, "A \(cs_typeName(CustomMapping.self)) with value '\(mapping)' passed to \(cs_typeName(CustomSchemaMappingProvider.self)) could not be mapped to any \(cs_typeName(NSEntityDescription.self)) from the source \(cs_typeName(NSManagedObjectModel.self))." ) CoreStore.assert( destinationEntityNames[destinationEntity] != nil, "A \(cs_typeName(CustomMapping.self)) with value '\(mapping)' passed to \(cs_typeName(CustomSchemaMappingProvider.self)) could not be mapped to any \(cs_typeName(NSEntityDescription.self)) from the destination \(cs_typeName(NSManagedObjectModel.self))." ) CoreStore.assert( allMappedSourceKeys[sourceEntity] == nil, "Duplicate \(cs_typeName(CustomMapping.self))s found for source entity name \"\(sourceEntity)\" in \(cs_typeName(CustomSchemaMappingProvider.self))." ) CoreStore.assert( allMappedDestinationKeys[destinationEntity] == nil, "Duplicate \(cs_typeName(CustomMapping.self))s found for destination entity name \"\(destinationEntity)\" in \(cs_typeName(CustomSchemaMappingProvider.self))." ) transformMappings.insert(mapping) allMappedSourceKeys[sourceEntity] = destinationEntity allMappedDestinationKeys[destinationEntity] = sourceEntity case .copyEntity(let sourceEntity, let destinationEntity): CoreStore.assert( sourceEntityNames[sourceEntity] != nil, "A \(cs_typeName(CustomMapping.self)) with value '\(mapping)' passed to \(cs_typeName(CustomSchemaMappingProvider.self)) could not be mapped to any \(cs_typeName(NSEntityDescription.self)) from the source \(cs_typeName(NSManagedObjectModel.self))." ) CoreStore.assert( destinationEntityNames[destinationEntity] != nil, "A \(cs_typeName(CustomMapping.self)) with value '\(mapping)' passed to \(cs_typeName(CustomSchemaMappingProvider.self)) could not be mapped to any \(cs_typeName(NSEntityDescription.self)) from the destination \(cs_typeName(NSManagedObjectModel.self))." ) CoreStore.assert( sourceEntityNames[sourceEntity]!.versionHash == destinationEntityNames[destinationEntity]!.versionHash, "A \(cs_typeName(CustomMapping.self)) with value '\(mapping)' was passed to \(cs_typeName(CustomSchemaMappingProvider.self)) but the \(cs_typeName(NSEntityDescription.self))'s \"versionHash\" of the source and destination entities do not match." ) CoreStore.assert( allMappedSourceKeys[sourceEntity] == nil, "Duplicate \(cs_typeName(CustomMapping.self))s found for source entity name \"\(sourceEntity)\" in \(cs_typeName(CustomSchemaMappingProvider.self))." ) CoreStore.assert( allMappedDestinationKeys[destinationEntity] == nil, "Duplicate \(cs_typeName(CustomMapping.self))s found for destination entity name \"\(destinationEntity)\" in \(cs_typeName(CustomSchemaMappingProvider.self))." ) copyMappings.insert(mapping) allMappedSourceKeys[sourceEntity] = destinationEntity allMappedDestinationKeys[destinationEntity] = sourceEntity } for renamingIdentifier in transformedRenamingIdentifiers { let sourceEntity = sourceRenamingIdentifiers[renamingIdentifier]!.entity let destinationEntity = destinationRenamingIdentifiers[renamingIdentifier]!.entity let sourceEntityName = sourceEntity.name! let destinationEntityName = destinationEntity.name! switch (allMappedSourceKeys[sourceEntityName], allMappedDestinationKeys[destinationEntityName]) { case (nil, nil): if sourceEntity.versionHash == destinationEntity.versionHash { copyMappings.insert( .copyEntity( sourceEntity: sourceEntityName, destinationEntity: destinationEntityName ) ) } else { transformMappings.insert( .transformEntity( sourceEntity: sourceEntityName, destinationEntity: destinationEntityName, transformer: CustomMapping.inferredTransformation ) ) } allMappedSourceKeys[sourceEntityName] = destinationEntityName allMappedDestinationKeys[destinationEntityName] = sourceEntityName case (""?, nil): insertMappings.insert(.insertEntity(destinationEntity: destinationEntityName)) allMappedDestinationKeys[destinationEntityName] = "" case (nil, ""?): deleteMappings.insert(.deleteEntity(sourceEntity: sourceEntityName)) allMappedSourceKeys[sourceEntityName] = "" default: continue } } for renamingIdentifier in removedRenamingIdentifiers { let sourceEntity = sourceRenamingIdentifiers[renamingIdentifier]!.entity let sourceEntityName = sourceEntity.name! switch allMappedSourceKeys[sourceEntityName] { case nil: deleteMappings.insert(.deleteEntity(sourceEntity: sourceEntityName)) allMappedSourceKeys[sourceEntityName] = "" default: continue } } for renamingIdentifier in addedRenamingIdentifiers { let destinationEntity = destinationRenamingIdentifiers[renamingIdentifier]!.entity let destinationEntityName = destinationEntity.name! switch allMappedDestinationKeys[destinationEntityName] { case nil: insertMappings.insert(.insertEntity(destinationEntity: destinationEntityName)) allMappedDestinationKeys[destinationEntityName] = "" default: continue } } } return (deleteMappings, insertMappings, copyMappings, transformMappings) } }