From 4fc10afe1efdaa38c5eaedce5a60fec42981764d Mon Sep 17 00:00:00 2001 From: John Estropia Date: Thu, 6 Apr 2017 20:28:17 +0900 Subject: [PATCH] relationships done! --- CoreStore.xcodeproj/project.pbxproj | 10 + .../xcschemes/CoreStore iOS.xcscheme | 5 + CoreStoreTests/DynamicModelTests.swift | 29 +- Sources/Setup/Dynamic Models/Attribute.swift | 37 ++- Sources/Setup/Dynamic Models/Entity.swift | 122 +++---- .../Setup/Dynamic Models/ManagedObject.swift | 37 ++- .../ManagedObjectProtocol.swift | 10 +- .../Setup/Dynamic Models/ObjectModel.swift | 309 ++++++++++++++++-- .../Setup/Dynamic Models/Relationship.swift | 200 ++++++++++++ .../Transactions/BaseDataTransaction.swift | 2 +- 10 files changed, 648 insertions(+), 113 deletions(-) create mode 100644 Sources/Setup/Dynamic Models/Relationship.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 1228450..2795cb4 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -444,6 +444,10 @@ B5D339F21E94AF5800C880DE /* CoreStoreStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D339F01E94AF5800C880DE /* CoreStoreStrings.swift */; }; B5D339F31E94AF5800C880DE /* CoreStoreStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D339F01E94AF5800C880DE /* CoreStoreStrings.swift */; }; B5D339F41E94AF5800C880DE /* CoreStoreStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D339F01E94AF5800C880DE /* CoreStoreStrings.swift */; }; + B5D33A011E96012400C880DE /* Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D33A001E96012400C880DE /* Relationship.swift */; }; + B5D33A021E96012400C880DE /* Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D33A001E96012400C880DE /* Relationship.swift */; }; + B5D33A031E96012400C880DE /* Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D33A001E96012400C880DE /* Relationship.swift */; }; + B5D33A041E96012400C880DE /* Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D33A001E96012400C880DE /* Relationship.swift */; }; B5D372841A39CD6900F583D9 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B5D372821A39CD6900F583D9 /* Model.xcdatamodeld */; }; B5D39A0219FD00C9000E91BB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5D39A0119FD00C9000E91BB /* Foundation.framework */; }; B5D3F6451C887C0A00C7492A /* LegacySQLiteStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D3F6441C887C0A00C7492A /* LegacySQLiteStore.swift */; }; @@ -732,6 +736,7 @@ B5D339E61E9493A500C880DE /* Entity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Entity.swift; sourceTree = ""; }; B5D339EB1E9495E500C880DE /* Attribute+Querying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Attribute+Querying.swift"; sourceTree = ""; }; B5D339F01E94AF5800C880DE /* CoreStoreStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreStoreStrings.swift; sourceTree = ""; }; + B5D33A001E96012400C880DE /* Relationship.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Relationship.swift; sourceTree = ""; }; B5D372831A39CD6900F583D9 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; B5D39A0119FD00C9000E91BB /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; B5D3F6441C887C0A00C7492A /* LegacySQLiteStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacySQLiteStore.swift; sourceTree = ""; }; @@ -1082,6 +1087,7 @@ isa = PBXGroup; children = ( B5D339E11E948C3600C880DE /* Attribute.swift */, + B5D33A001E96012400C880DE /* Relationship.swift */, B5D339D71E9489AB00C880DE /* ManagedObject.swift */, B5D339E61E9493A500C880DE /* Entity.swift */, B5D339DC1E9489C700C880DE /* ManagedObjectProtocol.swift */, @@ -1712,6 +1718,7 @@ B5AEFAB51C9962AE00AD137F /* CoreStoreBridge.swift in Sources */, B5E2222A1CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift in Sources */, B5E84F391AFF85470064E85B /* NSManagedObjectContext+Querying.swift in Sources */, + B5D33A011E96012400C880DE /* Relationship.swift in Sources */, B5E84EE81AFF84610064E85B /* CoreStoreLogger.swift in Sources */, B533C4DB1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */, B559CD491CAA8C6D00E4D58B /* CSStorageInterface.swift in Sources */, @@ -1876,6 +1883,7 @@ B5E2222C1CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift in Sources */, 82BA18A71C4BBD2900A0916E /* CoreStore+Logging.swift in Sources */, 82BA18D81C4BBD7100A0916E /* WeakObject.swift in Sources */, + B5D33A021E96012400C880DE /* Relationship.swift in Sources */, B559CD4B1CAA8C6D00E4D58B /* CSStorageInterface.swift in Sources */, B533C4DC1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */, B5ECDC311CA81CDC00C7F112 /* CSCoreStore+Transaction.swift in Sources */, @@ -2040,6 +2048,7 @@ B52DD19A1BE1F92800949AFE /* CoreStore+Logging.swift in Sources */, B52DD1A71BE1F93200949AFE /* BaseDataTransaction+Querying.swift in Sources */, B546F96C1C9AF26D00D5AC55 /* CSInMemoryStore.swift in Sources */, + B5D33A041E96012400C880DE /* Relationship.swift in Sources */, B52DD1C61BE1F94600949AFE /* NSManagedObjectContext+CoreStore.swift in Sources */, B533C4DE1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */, B5220E141D130614009BC71E /* DataStack+Observing.swift in Sources */, @@ -2204,6 +2213,7 @@ B5E2222D1CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift in Sources */, B563219F1BD65216006C9394 /* ObjectMonitor.swift in Sources */, B56321B61BD6521C006C9394 /* WeakObject.swift in Sources */, + B5D33A031E96012400C880DE /* Relationship.swift in Sources */, B559CD4C1CAA8C6D00E4D58B /* CSStorageInterface.swift in Sources */, B533C4DD1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */, B5ECDC321CA81CDC00C7F112 /* CSCoreStore+Transaction.swift in Sources */, diff --git a/CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore iOS.xcscheme b/CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore iOS.xcscheme index b6a4ac8..281d0ed 100644 --- a/CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore iOS.xcscheme +++ b/CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore iOS.xcscheme @@ -63,6 +63,11 @@ + + ("species", default: "Swift") + let enemy = Relationship.ToOne("enemy", inverse: { $0.toy }) } class Mascot: Bird { let nickname = Attribute.Optional("nickname") let year = Attribute.Required("year", default: 2016) } +class Cat: ManagedObject { + + let name = Attribute.Required("name") + let toy = Relationship.ToOne("toy") +} class DynamicModelTests: BaseTestDataTestCase { @@ -33,7 +39,8 @@ class DynamicModelTests: BaseTestDataTestCase { version: "V1", entities: [ Entity("Bird"), - Entity("Mascot") + Entity("Mascot"), + Entity("Cat") ] ) ) @@ -63,8 +70,19 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(mascot.species*, "Swift") XCTAssertEqual(mascot.nickname*, nil) + mascot.species .= "Swift3" + XCTAssertEqual(mascot.species*, "Swift3") + mascot.nickname .= "Riko" XCTAssertEqual(mascot.nickname*, "Riko") + + let cat = transaction.create(Into()) + XCTAssertNil(cat.toy.value) + + cat.toy .= mascot + XCTAssertEqual(cat.toy.value, mascot) + XCTAssertEqual(cat.toy.value?.enemy.value, cat) + XCTAssertEqual(mascot.enemy.value, cat) }, success: { @@ -81,7 +99,10 @@ class DynamicModelTests: BaseTestDataTestCase { let p1 = Bird.where({ $0.species == "Sparrow" }) XCTAssertEqual(p1.predicate, Where("%K == %@", "species", "Sparrow").predicate) - let bird = transaction.fetchOne(From()) + let bird = transaction.fetchOne( + From(), + Tweak({ $0.includesSubentities = false }) + ) XCTAssertNotNil(bird) XCTAssertEqual(bird!.species*, "Sparrow") @@ -91,6 +112,10 @@ class DynamicModelTests: BaseTestDataTestCase { let mascot = transaction.fetchOne(From()) XCTAssertNotNil(mascot) XCTAssertEqual(mascot!.nickname*, "Riko") + XCTAssertEqual(mascot!.species*, "Swift3") + + let cat = transaction.fetchOne(From()) + XCTAssertEqual(cat?.toy.value, mascot) let p3 = Mascot.where({ $0.year == 2016 }) XCTAssertEqual(p3.predicate, Where("%K == %@", "year", 2016).predicate) diff --git a/Sources/Setup/Dynamic Models/Attribute.swift b/Sources/Setup/Dynamic Models/Attribute.swift index 46f9a11..3ebdea1 100644 --- a/Sources/Setup/Dynamic Models/Attribute.swift +++ b/Sources/Setup/Dynamic Models/Attribute.swift @@ -33,9 +33,17 @@ infix operator .= : AssignmentPrecedence postfix operator * +// MARK: - ManagedObjectProtocol + +public extension ManagedObjectProtocol where Self: ManagedObject { + + public typealias Attribute = AttributeContainer +} + + // MARK: - AttributeContainer -public enum AttributeContainer { +public enum AttributeContainer { // MARK: - Required @@ -51,12 +59,11 @@ public enum AttributeContainer { return attribute.value } - public let keyPath: String - - public init(_ keyPath: String, `default`: V = V.cs_emptyValue()) { + public init(_ keyPath: String, `default`: V = V.cs_emptyValue(), isIndexed: Bool = false) { self.keyPath = keyPath self.defaultValue = `default`.cs_toImportableNativeType() + self.isIndexed = isIndexed } public var value: V { @@ -76,19 +83,23 @@ public enum AttributeContainer { } } - // MARK: Internal + + // MARK: AttributeProtocol internal static var attributeType: NSAttributeType { return V.cs_rawAttributeType } - internal let defaultValue: Any? + public let keyPath: String + internal let isOptional = false + internal let isIndexed: Bool + internal let defaultValue: Any? internal var accessRawObject: () -> NSManagedObject = { - fatalError("\(O.self) property values should not be accessed") + fatalError("\(O.self) attribute values should not be accessed") } } @@ -107,8 +118,6 @@ public enum AttributeContainer { return attribute.value } - public let keyPath: String - public init(_ keyPath: String, `default`: V? = nil) { self.keyPath = keyPath @@ -136,18 +145,21 @@ public enum AttributeContainer { } - // MARK: Internal + // MARK: AttributeProtocol internal static var attributeType: NSAttributeType { return V.cs_rawAttributeType } - internal let defaultValue: Any? + public let keyPath: String internal let isOptional = true + internal let isIndexed = false + internal let defaultValue: Any? + internal var accessRawObject: () -> NSManagedObject = { - fatalError("\(O.self) property values should not be accessed") + fatalError("\(O.self) attribute values should not be accessed") } } } @@ -161,6 +173,7 @@ internal protocol AttributeProtocol: class { var keyPath: String { get } var isOptional: Bool { get } + var isIndexed: Bool { get } var defaultValue: Any? { get } var accessRawObject: () -> NSManagedObject { get set } } diff --git a/Sources/Setup/Dynamic Models/Entity.swift b/Sources/Setup/Dynamic Models/Entity.swift index d9e45d7..9219a1b 100644 --- a/Sources/Setup/Dynamic Models/Entity.swift +++ b/Sources/Setup/Dynamic Models/Entity.swift @@ -32,7 +32,8 @@ import ObjectiveC public protocol EntityProtocol { - var entityDescription: NSEntityDescription { get } + var type: ManagedObject.Type { get } + var entityName: EntityName { get } } @@ -40,74 +41,16 @@ public protocol EntityProtocol { public struct Entity: EntityProtocol { - public let entityDescription: NSEntityDescription - internal var dynamicClass: AnyClass { - - return NSClassFromString(self.entityDescription.managedObjectClassName!)! - } - public init(_ entityName: String) { - let dynamicClassName = String(reflecting: O.self) - .appending("__\(entityName)") - .replacingOccurrences(of: ".", with: "_") - .replacingOccurrences(of: "<", with: "_") - .replacingOccurrences(of: ">", with: "_") - // TODO: assign entityName through ModelVersion and - // TODO: set NSEntityDescription.userInfo AnyEntity - let newClass: AnyClass? - - if NSClassFromString(dynamicClassName) == nil { - - newClass = objc_allocateClassPair(NSManagedObject.self, dynamicClassName, 0) - } - else { - - newClass = nil - } - - defer { - - if let newClass = newClass { - - objc_registerClassPair(newClass) - } - } - - let entityDescription = NSEntityDescription() - entityDescription.userInfo = [ - EntityIdentifier.UserInfoKey.CoreStoreManagedObjectName: String(reflecting: O.self) - ] - entityDescription.name = entityName - entityDescription.managedObjectClassName = NSStringFromClass(NSManagedObject.self) -// entityDescription.managedObjectClassName = dynamicClassName // TODO: return to NSManagedObject - entityDescription.properties = type(of: self).initializeAttributes(Mirror(reflecting: O.meta)) - - self.entityDescription = entityDescription + self.type = O.self + self.entityName = entityName } - private static func initializeAttributes(_ mirror: Mirror) -> [NSAttributeDescription] { - - var attributeDescriptions: [NSAttributeDescription] = [] - for child in mirror.children { - - guard case let property as AttributeProtocol = child.value else { - - continue - } - let attributeDescription = NSAttributeDescription() - attributeDescription.name = property.keyPath - attributeDescription.attributeType = type(of: property).attributeType - attributeDescription.isOptional = property.isOptional - attributeDescription.defaultValue = property.defaultValue - attributeDescriptions.append(attributeDescription) - } - if let baseEntityAttributeDescriptions = mirror.superclassMirror.flatMap(self.initializeAttributes) { - - attributeDescriptions.append(contentsOf: baseEntityAttributeDescriptions) - } - return attributeDescriptions - } + // MARK: EntityProtocol + + public let type: ManagedObject.Type + public let entityName: EntityName } @@ -158,10 +101,10 @@ internal struct EntityIdentifier: Hashable { internal init(_ entityDescription: NSEntityDescription) { - if let coreStoreManagedObjectName = entityDescription.userInfo?[EntityIdentifier.UserInfoKey.CoreStoreManagedObjectName] as! String? { + if let entity = entityDescription.anyEntity { self.category = .coreStore - self.interfacedClassName = coreStoreManagedObjectName + self.interfacedClassName = NSStringFromClass(entity.type) } else { @@ -187,12 +130,53 @@ internal struct EntityIdentifier: Hashable { return self.category.hashValue ^ self.interfacedClassName.hashValue } +} + + +// MARK: - NSEntityDescription + +internal extension NSEntityDescription { + + @nonobjc + internal var anyEntity: ObjectModel.AnyEntity? { + + get { + + guard let userInfo = self.userInfo, + let typeName = userInfo[UserInfoKey.CoreStoreManagedObjectTypeName] as! String?, + let entityName = userInfo[UserInfoKey.CoreStoreManagedObjectEntityName] as! String? else { + + return nil + } + return ObjectModel.AnyEntity( + type: NSClassFromString(typeName) as! ManagedObject.Type, + entityName: entityName + ) + } + set { + + if let newValue = newValue { + + self.userInfo = [ + UserInfoKey.CoreStoreManagedObjectTypeName: NSStringFromClass(newValue.type), + UserInfoKey.CoreStoreManagedObjectEntityName: newValue.entityName + ] + } + else { + + self.userInfo = [:] + } + } + } - // MARK: FilePrivate + // MARK: Private + + // MARK: - UserInfoKey fileprivate enum UserInfoKey { - fileprivate static let CoreStoreManagedObjectName = "CoreStoreManagedObjectName" + fileprivate static let CoreStoreManagedObjectTypeName = "CoreStoreManagedObjectTypeName" + fileprivate static let CoreStoreManagedObjectEntityName = "CoreStoreManagedObjectEntityName" } } diff --git a/Sources/Setup/Dynamic Models/ManagedObject.swift b/Sources/Setup/Dynamic Models/ManagedObject.swift index 700aced..246d478 100644 --- a/Sources/Setup/Dynamic Models/ManagedObject.swift +++ b/Sources/Setup/Dynamic Models/ManagedObject.swift @@ -29,7 +29,7 @@ import Foundation // MARK: - ManagedObject -open class ManagedObject: ManagedObjectProtocol { +open class ManagedObject: ManagedObjectProtocol, Hashable { public required init(_ object: NSManagedObject) { @@ -45,6 +45,31 @@ open class ManagedObject: ManagedObjectProtocol { } + // MARK: Equatable + + public static func == (lhs: ManagedObject, rhs: ManagedObject) -> Bool { + + guard lhs.isMeta == rhs.isMeta else { + + return false + } + if lhs.isMeta { + + return type(of: lhs) == type(of: rhs) + } + return lhs.rawObject!.isEqual(rhs.rawObject!) + } + + + // MARK: Hashable + + public var hashValue: Int { + + return ObjectIdentifier(self).hashValue + ^ (self.isMeta ? 0 : self.rawObject!.hashValue) + } + + // MARK: Internal internal let rawObject: NSManagedObject? @@ -58,11 +83,17 @@ open class ManagedObject: ManagedObjectProtocol { _ = mirror.superclassMirror.flatMap({ self.initializeAttributes($0, accessRawObject) }) for child in mirror.children { - guard case let property as AttributeProtocol = child.value else { + switch child.value { + case let property as AttributeProtocol: + property.accessRawObject = accessRawObject + + case let property as RelationshipProtocol: + property.accessRawObject = accessRawObject + + default: continue } - property.accessRawObject = accessRawObject } } } diff --git a/Sources/Setup/Dynamic Models/ManagedObjectProtocol.swift b/Sources/Setup/Dynamic Models/ManagedObjectProtocol.swift index 29bc548..f933f07 100644 --- a/Sources/Setup/Dynamic Models/ManagedObjectProtocol.swift +++ b/Sources/Setup/Dynamic Models/ManagedObjectProtocol.swift @@ -37,8 +37,6 @@ public protocol ManagedObjectProtocol: class { public extension ManagedObjectProtocol where Self: ManagedObject { - public typealias Attribute = AttributeContainer - @inline(__always) public static func keyPath(_ attribute: (Self) -> AttributeContainer.Required) -> String { @@ -73,7 +71,7 @@ extension NSManagedObject: ManagedObjectProtocol { // MARK: ManagedObjectProtocol - public static func cs_from(object: NSManagedObject) -> Self { + public class func cs_from(object: NSManagedObject) -> Self { @inline(__always) func forceCast(_ value: Any) -> T { @@ -83,7 +81,7 @@ extension NSManagedObject: ManagedObjectProtocol { return forceCast(object) } - public static func cs_forceCreate(entityDescription: NSEntityDescription, into context: NSManagedObjectContext, assignTo store: NSPersistentStore) -> Self { + public class func cs_forceCreate(entityDescription: NSEntityDescription, into context: NSManagedObjectContext, assignTo store: NSPersistentStore) -> Self { let object = self.init(entity: entityDescription, insertInto: context) defer { @@ -101,12 +99,12 @@ extension ManagedObject { // MARK: ManagedObjectProtocol - public static func cs_from(object: NSManagedObject) -> Self { + public class func cs_from(object: NSManagedObject) -> Self { return self.init(object) } - public static func cs_forceCreate(entityDescription: NSEntityDescription, into context: NSManagedObjectContext, assignTo store: NSPersistentStore) -> Self { + public class func cs_forceCreate(entityDescription: NSEntityDescription, into context: NSManagedObjectContext, assignTo store: NSPersistentStore) -> Self { let type = NSClassFromString(entityDescription.managedObjectClassName!)! as! NSManagedObject.Type let object = type.init(entity: entityDescription, insertInto: context) diff --git a/Sources/Setup/Dynamic Models/ObjectModel.swift b/Sources/Setup/Dynamic Models/ObjectModel.swift index 692f276..ad94134 100644 --- a/Sources/Setup/Dynamic Models/ObjectModel.swift +++ b/Sources/Setup/Dynamic Models/ObjectModel.swift @@ -31,46 +31,315 @@ import Foundation public final class ObjectModel { - public let version: String - public convenience init(version: String, entities: [EntityProtocol]) { - self.init(version: version, configurationEntities: [DataStack.defaultConfigurationName: entities]) + self.init(version: version, entitiesByConfiguration: [DataStack.defaultConfigurationName: entities]) } - public required init(version: String, configurationEntities: [String: [EntityProtocol]]) { + public required init(version: String, entitiesByConfiguration: [String: [EntityProtocol]]) { + + var actualEntitiesByConfiguration: [String: Set] = [:] + for (configuration, entities) in entitiesByConfiguration { + + actualEntitiesByConfiguration[configuration] = Set(entities.map(AnyEntity.init)) + } + let allEntities = Set(actualEntitiesByConfiguration.values.joined()) + actualEntitiesByConfiguration[DataStack.defaultConfigurationName] = allEntities + + CoreStore.assert( + autoreleasepool { + + let expectedCount = allEntities.count + return Set(allEntities.map({ ObjectIdentifier($0.type) })).count == expectedCount + && Set(allEntities.map({ $0.entityName })).count == expectedCount + }, + "Ambiguous entity types or entity names were found in the model. Ensure that the entity types and entity names are unique to each other. Entities: \(allEntities)" + ) self.version = version - - var entityConfigurations: [String: Set] = [:] - for (configuration, entities) in configurationEntities { - - entityConfigurations[configuration] = Set(entities.map({ $0.entityDescription })) - } - let allEntities = Set(entityConfigurations.map({ $0.value }).joined()) - entityConfigurations[DataStack.defaultConfigurationName] = allEntities - - self.entityConfigurations = entityConfigurations - self.entities = allEntities + self.entitiesByConfiguration = actualEntitiesByConfiguration + self.allEntities = allEntities } // MARK: Internal - internal let entities: Set - internal let entityConfigurations: [String: Set] + // MARK: - AnyEntity + + internal struct AnyEntity: EntityProtocol, Hashable { + + internal init(_ entity: EntityProtocol) { + + self.type = entity.type + self.entityName = entity.entityName + } + + internal init(type: ManagedObject.Type, entityName: String) { + + self.type = type + self.entityName = entityName + } + + + // MARK: Equatable + + static func == (lhs: AnyEntity, rhs: AnyEntity) -> Bool { + + return lhs.type == rhs.type + && lhs.entityName == rhs.entityName + } + + // MARK: Hashable + + var hashValue: Int { + + return ObjectIdentifier(self.type).hashValue + ^ self.entityName.hashValue + } + + // MARK: EntityProtocol + + internal let type: ManagedObject.Type + internal let entityName: EntityName + } + + + // MARK: - internal func createModel() -> NSManagedObjectModel { let model = NSManagedObjectModel() - model.entities = self.entities.sorted(by: { $0.name! < $1.name! }) - for (configuration, entities) in self.entityConfigurations { + let entityDescriptionsByEntity: [AnyEntity: NSEntityDescription] = ModelCache.performUpdate { + + var entityDescriptionsByEntity: [AnyEntity: NSEntityDescription] = [:] + for entity in self.allEntities { + + let entityDescription = ModelCache.entityDescription( + for: entity, + initializer: ObjectModel.firstPassCreateEntityDescription + ) + entityDescriptionsByEntity[entity] = entityDescription + } + ObjectModel.secondPassConnectRelationshipAttributes(for: entityDescriptionsByEntity) + ObjectModel.thirdPassConnectInheritanceTree(for: entityDescriptionsByEntity) + return entityDescriptionsByEntity + } + model.entities = entityDescriptionsByEntity.values.sorted(by: { $0.name! < $1.name! }) + for (configuration, entities) in self.entitiesByConfiguration { model.setEntities( - entities.sorted(by: { $0.name! < $1.name! }), + entities + .map({ entityDescriptionsByEntity[$0]! }) + .sorted(by: { $0.name! < $1.name! }), forConfigurationName: configuration ) } return model } + + + // MARK: FilePrivate + + fileprivate static func firstPassCreateEntityDescription(from entity: AnyEntity) -> NSEntityDescription { + + let entityDescription = NSEntityDescription() + entityDescription.anyEntity = entity + entityDescription.name = entity.entityName + entityDescription.managedObjectClassName = NSStringFromClass(NSManagedObject.self) + + func createProperties(for type: ManagedObject.Type) -> [NSPropertyDescription] { + + var propertyDescriptions: [NSPropertyDescription] = [] + for child in Mirror(reflecting: type.meta).children { + + switch child.value { + + case let attribute as AttributeProtocol: + let description = NSAttributeDescription() + description.name = attribute.keyPath + description.attributeType = type(of: attribute).attributeType + description.isOptional = attribute.isOptional + description.isIndexed = attribute.isIndexed + description.defaultValue = attribute.defaultValue + description.isTransient = false + // TODO: versionHash, renamingIdentifier, etc + // TODO: Separate attributes for Value, Transient, Relationship + propertyDescriptions.append(description) + + case let relationship as RelationshipProtocol: + let description = NSRelationshipDescription() + description.name = relationship.keyPath + description.minCount = 0 + description.maxCount = relationship.isToMany ? 0 : 1 + description.isOrdered = relationship.isOrdered + description.deleteRule = relationship.deleteRule + // TODO: versionHash, renamingIdentifier, etc + // TODO: Separate attributes for Value, Transient, Relationship + propertyDescriptions.append(description) + + default: + continue + } + } + return propertyDescriptions + } + + entityDescription.properties = createProperties(for: entity.type) + return entityDescription + } + + fileprivate static func secondPassConnectRelationshipAttributes(for entityDescriptionsByEntity: [AnyEntity: NSEntityDescription]) { + + var relationshipsByNameByEntity: [AnyEntity: [String: NSRelationshipDescription]] = [:] + for (entity, entityDescription) in entityDescriptionsByEntity { + + relationshipsByNameByEntity[entity] = entityDescription.relationshipsByName + } + func findEntity(for type: ManagedObject.Type) -> AnyEntity { + + var matchedEntities: Set = [] + for (entity, _) in entityDescriptionsByEntity where entity.type == type { + + matchedEntities.insert(entity) + } + if matchedEntities.count == 1 { + + return matchedEntities.first! + } + if matchedEntities.isEmpty { + + CoreStore.abort( + "No \(cs_typeName("Entity<\(type)>")) instance found in the \(cs_typeName(ObjectModel.self))." + ) + } + else { + + CoreStore.abort( + "Ambiguous entity types or entity names were found in the model. Ensure that the entity types and entity names are unique to each other. Entities: \(matchedEntities)" + ) + } + } + + func findInverseRelationshipMatching(destinationEntity: AnyEntity, destinationKeyPath: String) -> NSRelationshipDescription { + + for case (destinationKeyPath, let relationshipDescription) in relationshipsByNameByEntity[destinationEntity]! { + + return relationshipDescription + } + CoreStore.abort( + "The inverse relationship for \"\(destinationEntity.type).\(destinationKeyPath)\" could not be found. Make sure to set the `inverse:` initializer argument for one of the paired \(cs_typeName("Relationship.ToOne")), \(cs_typeName("Relationship.ToManyOrdered")), or \(cs_typeName("Relationship.ToManyUnozrdered"))" + ) + } + + for (entity, entityDescription) in entityDescriptionsByEntity { + + let relationshipsByName = relationshipsByNameByEntity[entity]! + for child in Mirror(reflecting: entity.type.meta).children { + + switch child.value { + + case let relationship as RelationshipProtocol: + let (destinationType, destinationKeyPath) = relationship.inverse + let destinationEntity = findEntity(for: destinationType) + let description = relationshipsByName[relationship.keyPath]! + description.destinationEntity = entityDescriptionsByEntity[destinationEntity]! + + if let destinationKeyPath = destinationKeyPath { + + let inverseRelationshipDescription = findInverseRelationshipMatching( + destinationEntity: destinationEntity, + destinationKeyPath: destinationKeyPath + ) + description.inverseRelationship = inverseRelationshipDescription + + inverseRelationshipDescription.inverseRelationship = description + inverseRelationshipDescription.destinationEntity = entityDescription + + description.destinationEntity!.properties = description.destinationEntity!.properties + } + + default: + continue + } + } + } + for (entity, entityDescription) in entityDescriptionsByEntity { + + for (name, relationshipDescription) in entityDescription.relationshipsByName { + + CoreStore.assert( + relationshipDescription.destinationEntity != nil, + "The destination entity for relationship \"\(entity.type).\(name)\" could not be resolved." + ) + CoreStore.assert( + relationshipDescription.inverseRelationship != nil, + "The inverse relationship for \"\(entity.type).\(name)\" could not be found. Make sure to set the `inverse:` argument of the initializer for one of the paired \(cs_typeName("Relationship.ToOne")), \(cs_typeName("Relationship.ToManyOrdered")), or \(cs_typeName("Relationship.ToManyUnozrdered"))" + ) + } + } + } + + fileprivate static func thirdPassConnectInheritanceTree(for entityDescriptionsByEntity: [AnyEntity: NSEntityDescription]) { + + func connectBaseEntity(mirror: Mirror, entityDescription: NSEntityDescription) { + + guard let superclassMirror = mirror.superclassMirror, + let superType = superclassMirror.subjectType as? ManagedObject.Type, + superType != ManagedObject.self else { + + return + } + for (superEntity, superEntityDescription) in entityDescriptionsByEntity where superEntity.type == superType { + + if !superEntityDescription.subentities.contains(entityDescription) { + + superEntityDescription.subentities.append(entityDescription) + } + connectBaseEntity(mirror: superclassMirror, entityDescription: superEntityDescription) + } + } + for (entity, entityDescription) in entityDescriptionsByEntity { + + connectBaseEntity( + mirror: Mirror(reflecting: entity.type.meta), + entityDescription: entityDescription + ) + } + } + + + // MARK: Private + + private let version: String + private let allEntities: Set + private let entitiesByConfiguration: [String: Set] +} + + +// MARK: - ModelCache + +fileprivate enum ModelCache { + + fileprivate static func performUpdate(_ closure: () -> T) -> T { + + return self.barrierQueue.cs_barrierSync(closure) + } + + fileprivate static func entityDescription(for entity: ObjectModel.AnyEntity, initializer: (ObjectModel.AnyEntity) -> NSEntityDescription) -> NSEntityDescription { + + if let cachedEntityDescription = self.entityDescriptionsByEntity[entity] { + + return cachedEntityDescription + } + let entityDescription = withoutActuallyEscaping(initializer, do: { $0(entity) }) + self.entityDescriptionsByEntity[entity] = entityDescription + return entityDescription + } + + + // MARK: Private + + private static let barrierQueue = DispatchQueue.concurrent("com.coreStore.modelCacheBarrierQueue") + + private static var entityDescriptionsByEntity: [ObjectModel.AnyEntity: NSEntityDescription] = [:] } diff --git a/Sources/Setup/Dynamic Models/Relationship.swift b/Sources/Setup/Dynamic Models/Relationship.swift new file mode 100644 index 0000000..ea7d593 --- /dev/null +++ b/Sources/Setup/Dynamic Models/Relationship.swift @@ -0,0 +1,200 @@ +// +// 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: - ManagedObjectProtocol + +public extension ManagedObjectProtocol where Self: ManagedObject { + + 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 postfix func * (_ relationship: RelationshipContainer.ToOne) -> D? { + + return relationship.value + } + + public init(_ keyPath: String, deleteRule: DeleteRule = .nullify) { + + self.keyPath = keyPath + self.deleteRule = deleteRule.nativeValue + self.inverse = (D.self, nil) + } + + public init(_ keyPath: String, inverse: (D) -> RelationshipContainer.ToOne, deleteRule: DeleteRule = .nullify) { + + self.keyPath = keyPath + self.deleteRule = deleteRule.nativeValue + + let inverseRelationship = inverse(D.meta) + self.inverse = (D.self, inverseRelationship.keyPath) + } + + public var value: D? { + + get { + + let object = self.accessRawObject() + let key = self.keyPath + return object.value(forKey: key) + .flatMap({ D.cs_from(object: $0 as! NSManagedObject) }) + } + set { + + let object = self.accessRawObject() + let key = self.keyPath + object.setValue(newValue?.rawObject, forKey: key) + } + } + + + // MARK: RelationshipProtocol + + public let keyPath: String + + internal let isToMany = false + internal let isOrdered = false + internal let deleteRule: NSDeleteRule + internal let inverse: (type: ManagedObject.Type, keyPath: String?) + + internal var accessRawObject: () -> NSManagedObject = { + + fatalError("\(O.self) relationship values should not be accessed") + } + } + + + // MARK: - ToManyOrdered + + public final class ToManyOrdered: RelationshipProtocol { + + // MARK: - + + public static func .= (_ relationship: RelationshipContainer.ToManyOrdered, _ value: [D]) { + + relationship.value = value + } + + public static postfix func * (_ relationship: RelationshipContainer.ToManyOrdered) -> [D] { + + return relationship.value + } + + public init(_ keyPath: String, deleteRule: DeleteRule = .nullify) { + + self.keyPath = keyPath + self.deleteRule = deleteRule.nativeValue + self.inverse = (D.self, nil) + } + + public var value: [D] { + + get { + + let object = self.accessRawObject() + let key = self.keyPath + guard let orderedSet = object.value(forKey: key) as! NSOrderedSet? else { + + return [] + } + return orderedSet.array as! [D] + } + set { + + let object = self.accessRawObject() + let key = self.keyPath + object.setValue(NSOrderedSet(array: newValue), forKey: key) + } + } + + + // MARK: RelationshipProtocol + + public let keyPath: String + + internal let isToMany = true + internal let isOptional = true + internal let isOrdered = true + internal let deleteRule: NSDeleteRule + internal let inverse: (type: ManagedObject.Type, keyPath: String?) + + internal var accessRawObject: () -> NSManagedObject = { + + fatalError("\(O.self) relationship values should not be accessed") + } + } + + + // 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: String { get } + var isToMany: Bool { get } + var isOrdered: Bool { get } + var deleteRule: NSDeleteRule { get } + var inverse: (type: ManagedObject.Type, keyPath: String?) { get } + var accessRawObject: () -> NSManagedObject { get set } +} diff --git a/Sources/Transactions/BaseDataTransaction.swift b/Sources/Transactions/BaseDataTransaction.swift index b640833..0578419 100644 --- a/Sources/Transactions/BaseDataTransaction.swift +++ b/Sources/Transactions/BaseDataTransaction.swift @@ -456,7 +456,7 @@ public /*abstract*/ class BaseDataTransaction { internal let context: NSManagedObjectContext internal let transactionQueue: DispatchQueue - internal let childTransactionQueue = DispatchQueue.serial("com.corestore.datastack.childtransactionqueue") + internal let childTransactionQueue = DispatchQueue.serial("com.corestore.datastack.childTransactionQueue") internal let supportsUndo: Bool internal let bypassesQueueing: Bool internal var isCommitted = false