// // Relationship.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: Operators infix operator .= : AssignmentPrecedence // MARK: - DynamicObject public extension DynamicObject where Self: CoreStoreObject { public typealias Relationship = RelationshipContainer } // MARK: - RelationshipContainer public enum RelationshipContainer { // MARK: - ToOne public final class ToOne: RelationshipProtocol { // MARK: - public static func .= (_ relationship: RelationshipContainer.ToOne, _ value: D?) { relationship.value = value } public static func .= (_ relationship: RelationshipContainer.ToOne, _ relationship2: RelationshipContainer.ToOne) { relationship.value = relationship2.value } public convenience init(_ keyPath: KeyPath, deleteRule: DeleteRule = .nullify) { self.init(keyPath: keyPath, inverseKeyPath: { nil }, deleteRule: deleteRule) } public convenience init(_ keyPath: KeyPath, inverse: @escaping (D) -> RelationshipContainer.ToOne, deleteRule: DeleteRule = .nullify) { self.init(keyPath: keyPath, inverseKeyPath: { inverse(D.meta).keyPath }, deleteRule: deleteRule) } public convenience init(_ keyPath: KeyPath, inverse: @escaping (D) -> RelationshipContainer.ToManyOrdered, deleteRule: DeleteRule = .nullify) { self.init(keyPath: keyPath, inverseKeyPath: { inverse(D.meta).keyPath }, deleteRule: deleteRule) } public convenience init(_ keyPath: KeyPath, inverse: @escaping (D) -> RelationshipContainer.ToManyUnordered, deleteRule: DeleteRule = .nullify) { self.init(keyPath: keyPath, inverseKeyPath: { inverse(D.meta).keyPath }, deleteRule: deleteRule) } public var value: D? { get { let object = self.parentObject() as! O CoreStore.assert( object.rawObject!.isRunningInAllowedQueue() == true, "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." ) return object.rawObject!.getValue( forKvcKey: self.keyPath, didGetValue: { $0.flatMap({ D.cs_fromRaw(object: $0 as! NSManagedObject) }) } ) } set { let object = self.parentObject() as! O CoreStore.assert( object.rawObject!.isRunningInAllowedQueue() == true, "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." ) CoreStore.assert( object.rawObject!.isEditableInContext() == true, "Attempted to update a \(cs_typeName(O.self))'s value from outside a transaction." ) object.rawObject!.setValue( newValue, forKvcKey: self.keyPath, willSetValue: { $0?.rawObject } ) } } // MARK: RelationshipProtocol public let keyPath: KeyPath internal let isToMany = false internal let isOrdered = false internal let deleteRule: NSDeleteRule internal let minCount: Int = 0 internal let maxCount: Int = 1 internal let inverse: (type: CoreStoreObject.Type, keyPath: () -> KeyPath?) internal var parentObject: () -> CoreStoreObject = { CoreStore.abort("Attempted to access values from a \(cs_typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types.") } // MARK: Private private init(keyPath: KeyPath, inverseKeyPath: @escaping () -> KeyPath?, deleteRule: DeleteRule) { self.keyPath = keyPath self.deleteRule = deleteRule.nativeValue self.inverse = (D.self, inverseKeyPath) } } // MARK: - ToManyOrdered public final class ToManyOrdered: RelationshipProtocol { // MARK: - public static func .= (_ relationship: RelationshipContainer.ToManyOrdered, _ value: [D]) { relationship.value = value } public static func .= (_ relationship: RelationshipContainer.ToManyOrdered, _ value: C) where C.Iterator.Element == D { relationship.value = Array(value) } public static func .= (_ relationship: RelationshipContainer.ToManyOrdered, _ relationship2: RelationshipContainer.ToManyOrdered) { relationship.value = relationship2.value } public convenience init(_ keyPath: KeyPath, deleteRule: DeleteRule = .nullify, minCount: Int = 0, maxCount: Int = 0) { self.init(keyPath: keyPath, inverseKeyPath: { nil }, deleteRule: deleteRule, minCount: minCount, maxCount: maxCount) } public convenience init(_ keyPath: KeyPath, inverse: @escaping (D) -> RelationshipContainer.ToOne, deleteRule: DeleteRule = .nullify, minCount: Int = 0, maxCount: Int = 0) { self.init(keyPath: keyPath, inverseKeyPath: { inverse(D.meta).keyPath }, deleteRule: deleteRule, minCount: minCount, maxCount: maxCount) } public convenience init(_ keyPath: KeyPath, inverse: @escaping (D) -> RelationshipContainer.ToManyOrdered, deleteRule: DeleteRule = .nullify, minCount: Int = 0, maxCount: Int = 0) { self.init(keyPath: keyPath, inverseKeyPath: { inverse(D.meta).keyPath }, deleteRule: deleteRule, minCount: minCount, maxCount: maxCount) } public convenience init(_ keyPath: KeyPath, inverse: @escaping (D) -> RelationshipContainer.ToManyUnordered, deleteRule: DeleteRule = .nullify, minCount: Int = 0, maxCount: Int = 0) { self.init(keyPath: keyPath, inverseKeyPath: { inverse(D.meta).keyPath }, deleteRule: deleteRule, minCount: minCount, maxCount: maxCount) } // TODO: add subscripts, indexed operations for more performant single updates public var value: [D] { get { let object = self.parentObject() as! O CoreStore.assert( object.rawObject!.isRunningInAllowedQueue() == true, "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." ) return object.rawObject!.getValue( forKvcKey: self.keyPath, didGetValue: { guard let orderedSet = $0 as! NSOrderedSet? else { return [] } return orderedSet.map({ D.cs_fromRaw(object: $0 as! NSManagedObject) }) } ) } set { let object = self.parentObject() as! O CoreStore.assert( object.rawObject!.isRunningInAllowedQueue() == true, "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." ) CoreStore.assert( object.rawObject!.isEditableInContext() == true, "Attempted to update a \(cs_typeName(O.self))'s value from outside a transaction." ) object.rawObject!.setValue( newValue, forKvcKey: self.keyPath, willSetValue: { NSOrderedSet(array: $0.map({ $0.rawObject! })) } ) } } // MARK: RelationshipProtocol public let keyPath: KeyPath internal let isToMany = true internal let isOptional = true internal let isOrdered = true internal let deleteRule: NSDeleteRule internal let minCount: Int internal let maxCount: Int internal let inverse: (type: CoreStoreObject.Type, keyPath: () -> KeyPath?) internal var parentObject: () -> CoreStoreObject = { CoreStore.abort("Attempted to access values from a \(cs_typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types.") } // MARK: Private private init(keyPath: String, inverseKeyPath: @escaping () -> String?, deleteRule: DeleteRule, minCount: Int, maxCount: Int) { self.keyPath = keyPath self.deleteRule = deleteRule.nativeValue self.inverse = (D.self, inverseKeyPath) let range = (max(0, minCount) ... maxCount) self.minCount = range.lowerBound self.maxCount = range.upperBound } } // MARK: - ToManyUnordered public final class ToManyUnordered: RelationshipProtocol { // MARK: - public static func .= (_ relationship: RelationshipContainer.ToManyUnordered, _ value: Set) { relationship.value = value } public static func .= (_ relationship: RelationshipContainer.ToManyUnordered, _ value: C) where C.Iterator.Element == D { relationship.value = Set(value) } public static func .= (_ relationship: RelationshipContainer.ToManyUnordered, _ relationship2: RelationshipContainer.ToManyUnordered) { relationship.value = relationship2.value } public static func .= (_ relationship: RelationshipContainer.ToManyUnordered, _ relationship2: RelationshipContainer.ToManyOrdered) { relationship.value = Set(relationship2.value) } public convenience init(_ keyPath: KeyPath, deleteRule: DeleteRule = .nullify, minCount: Int = 0, maxCount: Int = 0) { self.init(keyPath: keyPath, inverseKeyPath: { nil }, deleteRule: deleteRule, minCount: minCount, maxCount: maxCount) } public convenience init(_ keyPath: KeyPath, inverse: @escaping (D) -> RelationshipContainer.ToOne, deleteRule: DeleteRule = .nullify, minCount: Int = 0, maxCount: Int = 0) { self.init(keyPath: keyPath, inverseKeyPath: { inverse(D.meta).keyPath }, deleteRule: deleteRule, minCount: minCount, maxCount: maxCount) } public convenience init(_ keyPath: KeyPath, inverse: @escaping (D) -> RelationshipContainer.ToManyOrdered, deleteRule: DeleteRule = .nullify, minCount: Int = 0, maxCount: Int = 0) { self.init(keyPath: keyPath, inverseKeyPath: { inverse(D.meta).keyPath }, deleteRule: deleteRule, minCount: minCount, maxCount: maxCount) } public convenience init(_ keyPath: KeyPath, inverse: @escaping (D) -> RelationshipContainer.ToManyUnordered, deleteRule: DeleteRule = .nullify, minCount: Int = 0, maxCount: Int = 0) { self.init(keyPath: keyPath, inverseKeyPath: { inverse(D.meta).keyPath }, deleteRule: deleteRule, minCount: minCount, maxCount: maxCount) } // TODO: add subscripts, indexed operations for more performant single updates public var value: Set { get { let object = self.parentObject() as! O CoreStore.assert( object.rawObject!.isRunningInAllowedQueue() == true, "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." ) return object.rawObject!.getValue( forKvcKey: self.keyPath, didGetValue: { guard let set = $0 as! NSSet? else { return [] } return Set(set.map({ D.cs_fromRaw(object: $0 as! NSManagedObject) })) } ) } set { let object = self.parentObject() as! O CoreStore.assert( object.rawObject!.isRunningInAllowedQueue() == true, "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." ) CoreStore.assert( object.rawObject!.isEditableInContext() == true, "Attempted to update a \(cs_typeName(O.self))'s value from outside a transaction." ) object.rawObject!.setValue( newValue, forKvcKey: self.keyPath, willSetValue: { NSSet(array: $0.map({ $0.rawObject! })) } ) } } // MARK: RelationshipProtocol public let keyPath: KeyPath internal let isToMany = true internal let isOptional = true internal let isOrdered = true internal let deleteRule: NSDeleteRule internal let minCount: Int internal let maxCount: Int internal let inverse: (type: CoreStoreObject.Type, keyPath: () -> KeyPath?) internal var parentObject: () -> CoreStoreObject = { CoreStore.abort("Attempted to access values from a \(cs_typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types.") } // MARK: Private private init(keyPath: KeyPath, inverseKeyPath: @escaping () -> KeyPath?, deleteRule: DeleteRule, minCount: Int, maxCount: Int) { self.keyPath = keyPath self.deleteRule = deleteRule.nativeValue self.inverse = (D.self, inverseKeyPath) let range = (max(0, minCount) ... maxCount) self.minCount = range.lowerBound self.maxCount = range.upperBound } } // MARK: - DeleteRule public enum DeleteRule { case nullify case cascade case deny fileprivate var nativeValue: NSDeleteRule { switch self { case .nullify: return .nullifyDeleteRule case .cascade: return .cascadeDeleteRule case .deny: return .denyDeleteRule } } } } // MARK: - RelationshipProtocol internal protocol RelationshipProtocol: class { var keyPath: KeyPath { get } var isToMany: Bool { get } var isOrdered: Bool { get } var deleteRule: NSDeleteRule { get } var inverse: (type: CoreStoreObject.Type, keyPath: () -> KeyPath?) { get } var parentObject: () -> CoreStoreObject { get set } var minCount: Int { get } var maxCount: Int { get } }