diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index a1a3ed0..4c2be98 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -535,6 +535,10 @@ B5E41EBC1EA8C3B7006240F0 /* MigrationMappingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E41EBA1EA8C3B7006240F0 /* MigrationMappingProvider.swift */; }; B5E41EBD1EA8C3B7006240F0 /* MigrationMappingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E41EBA1EA8C3B7006240F0 /* MigrationMappingProvider.swift */; }; B5E41EBE1EA8C3B7006240F0 /* MigrationMappingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E41EBA1EA8C3B7006240F0 /* MigrationMappingProvider.swift */; }; + B5E41EC01EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E41EBF1EA9BB37006240F0 /* DynamicSchema+Convenience.swift */; }; + B5E41EC11EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E41EBF1EA9BB37006240F0 /* DynamicSchema+Convenience.swift */; }; + B5E41EC21EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E41EBF1EA9BB37006240F0 /* DynamicSchema+Convenience.swift */; }; + B5E41EC31EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E41EBF1EA9BB37006240F0 /* DynamicSchema+Convenience.swift */; }; B5E834B91B76311F001D3D50 /* BaseDataTransaction+Importing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E834B81B76311F001D3D50 /* BaseDataTransaction+Importing.swift */; }; B5E834BB1B7691F3001D3D50 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E834BA1B7691F3001D3D50 /* Functions.swift */; }; B5E84EDF1AFF84500064E85B /* DataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84EDB1AFF84500064E85B /* DataStack.swift */; }; @@ -801,6 +805,7 @@ B5E222221CA4E12600BA2E95 /* CSSynchronousDataTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSSynchronousDataTransaction.swift; sourceTree = ""; }; B5E222291CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSUnsafeDataTransaction.swift; sourceTree = ""; }; B5E41EBA1EA8C3B7006240F0 /* MigrationMappingProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationMappingProvider.swift; sourceTree = ""; }; + B5E41EBF1EA9BB37006240F0 /* DynamicSchema+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DynamicSchema+Convenience.swift"; sourceTree = ""; }; B5E834B81B76311F001D3D50 /* BaseDataTransaction+Importing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BaseDataTransaction+Importing.swift"; sourceTree = ""; }; B5E834BA1B7691F3001D3D50 /* Functions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; B5E84ED81AFF82360064E85B /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; @@ -1343,6 +1348,7 @@ B5FAD6A81B50A4B300714891 /* Progress+Convenience.swift */, B5202CF91C04688100DED140 /* NSFetchedResultsController+Convenience.swift */, B5A9921E1EA898710091A2E3 /* UserInfo.swift */, + B5E41EBF1EA9BB37006240F0 /* DynamicSchema+Convenience.swift */, ); path = Convenience; sourceTree = ""; @@ -1759,6 +1765,7 @@ B56965241B356B820075EE4A /* MigrationResult.swift in Sources */, B5FE4DAC1C85D44E00FA6A91 /* SQLiteStore.swift in Sources */, B501FDE71CA8D20500BE22EF /* CSListObserver.swift in Sources */, + B5E41EC01EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */, B501FDE21CA8D1F500BE22EF /* CSListMonitor.swift in Sources */, 2F291E2719C6D3CF007AF63F /* CoreStore.swift in Sources */, B5ECDC111CA816E500C7F112 /* CSTweak.swift in Sources */, @@ -1933,6 +1940,7 @@ 82BA18D41C4BBD7100A0916E /* NSManagedObjectContext+Querying.swift in Sources */, 82BA18D51C4BBD7100A0916E /* NSManagedObjectContext+Setup.swift in Sources */, B501FDE91CA8D20500BE22EF /* CSListObserver.swift in Sources */, + B5E41EC11EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */, B501FDE41CA8D1F500BE22EF /* CSListMonitor.swift in Sources */, B5FE4DA31C8481E100FA6A91 /* StorageInterface.swift in Sources */, B5ECDC131CA816E500C7F112 /* CSTweak.swift in Sources */, @@ -2107,6 +2115,7 @@ B52DD19D1BE1F92C00949AFE /* BaseDataTransaction.swift in Sources */, B5220E131D1305ED009BC71E /* SectionBy.swift in Sources */, B559CD4D1CAA8C6D00E4D58B /* CSStorageInterface.swift in Sources */, + B5E41EC31EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */, B5ECDBE91CA6BEA300C7F112 /* CSClauseTypes.swift in Sources */, B52DD1B81BE1F94000949AFE /* DataStack+Migration.swift in Sources */, B5ECDC091CA8138100C7F112 /* CSOrderBy.swift in Sources */, @@ -2281,6 +2290,7 @@ B56321B31BD6521C006C9394 /* NSManagedObjectContext+Setup.swift in Sources */, B501FDEA1CA8D20500BE22EF /* CSListObserver.swift in Sources */, B501FDE51CA8D1F500BE22EF /* CSListMonitor.swift in Sources */, + B5E41EC21EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */, B5ECDC141CA816E500C7F112 /* CSTweak.swift in Sources */, B56321AE1BD6521C006C9394 /* NotificationObserver.swift in Sources */, B56321931BD65216006C9394 /* DataStack+Querying.swift in Sources */, diff --git a/CoreStoreDemo/CoreStoreDemo/AppDelegate.swift b/CoreStoreDemo/CoreStoreDemo/AppDelegate.swift index 2e0af90..59d7571 100644 --- a/CoreStoreDemo/CoreStoreDemo/AppDelegate.swift +++ b/CoreStoreDemo/CoreStoreDemo/AppDelegate.swift @@ -8,6 +8,7 @@ import UIKit +import CoreStore // MARK: - AppDelegate @@ -22,6 +23,47 @@ class AppDelegate: UIResponder, UIApplicationDelegate { application.statusBarStyle = .lightContent + /// Generated by CoreStore on 4/21/17, 2:41 PM + class Place: CoreStoreObject { + + let latitude = Value.Optional("latitude", default: 0.0) + let title = Value.Optional("title") + let longitude = Value.Optional("longitude", default: 0.0) + let subtitle = Value.Optional("subtitle") + } + class Palette: CoreStoreObject { + + let saturation = Value.Optional("saturation", default: 0.0) + let hue = Value.Optional("hue", default: 0) + let brightness = Value.Optional("brightness", default: 0.0) + let colorName = Value.Optional("colorName", isTransient: true) + } + class TimeZone: CoreStoreObject { + + let secondsFromGMT = Value.Optional("secondsFromGMT", default: 0) + let name = Value.Optional("name") + let daylightSavingTimeOffset = Value.Optional("daylightSavingTimeOffset", default: 0.0) + let abbreviation = Value.Optional("abbreviation") + let hasDaylightSavingTime = Value.Optional("hasDaylightSavingTime") + } + + + + let schema = CoreStoreSchema( + modelVersion: "CoreStoreDemo", + entities: [ + Entity("Place"), + Entity("Palette"), + Entity("TimeZone"), + ], + versionLock: [ + "Place": [0x25cb5bd001887b92, 0xfe86dd433a5e0430, 0xcca50ac3f3659b68, 0xfe4e494ff66439b0], + "Palette": [0xa306515d026d3c43, 0x1b299716733e56f6, 0x53bff8954221a1b6, 0xa74d6b1e613923ab], + "TimeZone": [0x92e08db969e46163, 0xae9cf1ab738868c5, 0xb6a269249771a562, 0x58a357eab4c99ed5] + ] + ) + + print(schema.printCoreStoreSchema()) return true } } diff --git a/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/Palette.swift b/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/Palette.swift index 74e4888..fffc643 100644 --- a/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/Palette.swift +++ b/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/Palette.swift @@ -49,7 +49,7 @@ class Palette: NSManagedObject { } set { - self.setValue(newValue, forKvcKey: #keyPath(Palette.colorName)) + self.setValue(newValue.cs_toImportableNativeType(), forKvcKey: #keyPath(Palette.colorName)) } } diff --git a/Sources/Convenience/DynamicSchema+Convenience.swift b/Sources/Convenience/DynamicSchema+Convenience.swift new file mode 100644 index 0000000..610e02c --- /dev/null +++ b/Sources/Convenience/DynamicSchema+Convenience.swift @@ -0,0 +1,249 @@ +// +// DynamicSchema+Convenience.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: - DynamicSchema + +public extension DynamicSchema { + + public func printCoreStoreSchema() -> String { + + let model = self.rawModel() + let entitiesByName = model.entitiesByName + + var output = "/// Generated by CoreStore on \(DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .short))\n" + var addedInverse: Set = [] + for (entityName, entity) in entitiesByName { + + let superName: String + if let superEntity = entity.superentity { + + superName = superEntity.name! + } + else { + + superName = String(describing: CoreStoreObject.self) + } + output.append("class \(entityName): \(superName) {\n") + defer { + + output.append("}\n") + } + + let attributesByName = entity.attributesByName + if !attributesByName.isEmpty { + + output.append(" \n") + for (attributeName, attribute) in attributesByName { + + let containerType: String + if attribute.isOptional { + + containerType = "Value.Optional" + } + else { + + containerType = "Value.Required" + } + let valueType: Any.Type + var defaultString = "" + switch attribute.attributeType { + + case .integer16AttributeType: + valueType = Int16.self + if let defaultValue = (attribute.defaultValue as! Int16.ImportableNativeType?).flatMap(Int16.cs_fromImportableNativeType), + defaultValue != Int16.cs_emptyValue() { + + defaultString = ", default: \(defaultValue)" + } + case .integer32AttributeType: + valueType = Int32.self + if let defaultValue = (attribute.defaultValue as! Int32.ImportableNativeType?).flatMap(Int32.cs_fromImportableNativeType), + defaultValue != Int32.cs_emptyValue() { + + defaultString = ", default: \(defaultValue)" + } + case .integer64AttributeType: + valueType = Int64.self + if let defaultValue = (attribute.defaultValue as! Int64.ImportableNativeType?).flatMap(Int64.cs_fromImportableNativeType), + defaultValue != Int54.cs_emptyValue() { + + defaultString = ", default: \(defaultValue)" + } + case .decimalAttributeType: + valueType = NSDecimalNumber.self + if let defaultValue = (attribute.defaultValue as! NSDecimalNumber.ImportableNativeType?).flatMap(NSDecimalNumber.cs_fromImportableNativeType), + defaultValue != NSDecimalNumber.cs_emptyValue() { + + defaultString = ", default: NSDecimalNumber(string: \"\(defaultValue.description(withLocale: nil))\")" + } + case .doubleAttributeType: + valueType = Double.self + if let defaultValue = (attribute.defaultValue as! Double.ImportableNativeType?).flatMap(Double.cs_fromImportableNativeType), + defaultValue != Double.cs_emptyValue() { + + defaultString = ", default: \(defaultValue)" + } + case .floatAttributeType: + valueType = Float.self + if let defaultValue = (attribute.defaultValue as! Float.ImportableNativeType?).flatMap(Float.cs_fromImportableNativeType), + defaultValue != Float.cs_emptyValue() { + + defaultString = ", default: \(defaultValue)" + } + case .stringAttributeType: + valueType = String.self + if let defaultValue = (attribute.defaultValue as! String.ImportableNativeType?).flatMap(String.cs_fromImportableNativeType), + defaultValue != String.cs_emptyValue() { + + // TODO: escape strings + defaultString = ", default: \"\(defaultValue)\"" + } + case .booleanAttributeType: + valueType = Bool.self + if let defaultValue = (attribute.defaultValue as! Bool.ImportableNativeType?).flatMap(Bool.cs_fromImportableNativeType), + defaultValue != Bool.cs_emptyValue() { + + defaultString = ", default: \(defaultValue ? "true" : "false")" + } + case .dateAttributeType: + valueType = Date.self + if let defaultValue = (attribute.defaultValue as! Date.ImportableNativeType?).flatMap(Date.cs_fromImportableNativeType), + defaultValue != Date.cs_emptyValue() { + + defaultString = ", default: Date(timeIntervalSinceReferenceDate: \(defaultValue.timeIntervalSinceReferenceDate))" + } + case .binaryDataAttributeType: + valueType = Data.self + if let defaultValue = (attribute.defaultValue as! Data.ImportableNativeType?).flatMap(Data.cs_fromImportableNativeType), + defaultValue != Data.cs_emptyValue() { + + let count = defaultValue.count + let bytes = defaultValue.withUnsafeBytes { (pointer: UnsafePointer) in + + return (0 ..< (count / MemoryLayout.size)) + .map({ "\("0x\(String(pointer[$0], radix: 16, uppercase: false))")" }) + } + defaultString = ", default: Data(bytes: [\(bytes.joined(separator: ", "))])" + } + default: + fatalError("Unsupported attribute type: \(attribute.attributeType)") + } + let transientString = attribute.isTransient ? ", isTransient: true" : "" + output.append(" let \(attributeName) = \(containerType)<\(String(describing: valueType))>(\"\(attributeName)\"\(defaultString)\(transientString))\n") + } + } + + let relationshipsByName = entity.relationshipsByName + if !relationshipsByName.isEmpty { + + output.append(" \n") + for (relationshipName, relationship) in relationshipsByName { + + let containerType: String + var minCountString = "" + var maxCountString = "" + if relationship.isToMany { + + let minCount = relationship.minCount + let maxCount = relationship.maxCount + if relationship.isOrdered { + + containerType = "Relationship.ToManyOrdered" + } + else { + + containerType = "Relationship.ToManyUnordered" + } + if minCount > 0 { + + minCountString = ", minCount: \(minCount)" + } + if maxCount > 0 { + + maxCountString = ", maxCount: \(maxCount)" + } + } + else { + + containerType = "Relationship.ToOne" + } + var inverseString = "" + let relationshipQualifier = "\(entityName).\(relationshipName)" + if !addedInverse.contains(relationshipQualifier), + let inverseRelationship = relationship.inverseRelationship { + + inverseString = ", inverse: { $0.\(inverseRelationship.name) }" + addedInverse.insert("\(relationship.destinationEntity!.name!).\(inverseRelationship.name)") + } + var deleteRuleString = "" + if relationship.deleteRule != .nullifyDeleteRule { + + switch relationship.deleteRule { + + case .cascadeDeleteRule: + deleteRuleString = ", deleteRule: .cascade" + + case .denyDeleteRule: + deleteRuleString = ", deleteRule: .deny" + + case .nullifyDeleteRule: + deleteRuleString = ", deleteRule: .nullify" + + default: + fatalError("Unsupported delete rule \((relationship.deleteRule)) for relationship \"\(relationshipQualifier)\"") + } + } + output.append(" let \(relationshipName) = \(containerType)<\(relationship.destinationEntity!.name!)>(\"\(relationshipName)\"\(inverseString)\(deleteRuleString)\(minCountString)\(maxCountString))\n") + } + } + } + output.append("\n\n\n") + output.append("CoreStoreSchema(\n") + output.append(" modelVersion: \"\(self.modelVersion)\",\n") + output.append(" entities: [\n") + for (entityName, entity) in entitiesByName { + + var abstractString = "" + if entity.isAbstract { + + abstractString = ", isAbstract: true" + } + var versionHashModifierString = "" + if let versionHashModifier = entity.versionHashModifier { + + versionHashModifierString = ", versionHashModifier: \"\(versionHashModifier)\"" + } + output.append(" Entity<\(entityName)>(\"\(entityName)\"\(abstractString)\(versionHashModifierString)),\n") + } + output.append(" ],\n") + output.append(" versionLock: \(VersionLock(entityVersionHashesByName: model.entityVersionHashesByName).description.components(separatedBy: "\n").joined(separator: "\n "))\n") + output.append(")\n\n") + return output + } +} diff --git a/Sources/Convenience/NSManagedObject+Convenience.swift b/Sources/Convenience/NSManagedObject+Convenience.swift index fa60a9e..daf88b5 100644 --- a/Sources/Convenience/NSManagedObject+Convenience.swift +++ b/Sources/Convenience/NSManagedObject+Convenience.swift @@ -78,29 +78,29 @@ public extension NSManagedObject { } @nonobjc @inline(__always) - public func getValue(forKvcKey kvcKey: KeyPath) -> Any? { + public func getValue(forKvcKey kvcKey: KeyPath) -> CoreDataNativeType? { self.willAccessValue(forKey: kvcKey) defer { self.didAccessValue(forKey: kvcKey) } - return self.primitiveValue(forKey: kvcKey) + return self.primitiveValue(forKey: kvcKey) as! CoreDataNativeType? } @nonobjc @inline(__always) - public func getValue(forKvcKey kvcKey: KeyPath, didGetValue: (Any?) throws -> T) rethrows -> T { + public func getValue(forKvcKey kvcKey: KeyPath, didGetValue: (CoreDataNativeType?) throws -> T) rethrows -> T { self.willAccessValue(forKey: kvcKey) defer { self.didAccessValue(forKey: kvcKey) } - return try didGetValue(self.primitiveValue(forKey: kvcKey)) + return try didGetValue(self.primitiveValue(forKey: kvcKey) as! CoreDataNativeType?) } @nonobjc @inline(__always) - public func getValue(forKvcKey kvcKey: KeyPath, willGetValue: () throws -> Void, didGetValue: (Any?) throws -> T) rethrows -> T { + public func getValue(forKvcKey kvcKey: KeyPath, willGetValue: () throws -> Void, didGetValue: (CoreDataNativeType?) throws -> T) rethrows -> T { self.willAccessValue(forKey: kvcKey) defer { @@ -108,12 +108,11 @@ public extension NSManagedObject { self.didAccessValue(forKey: kvcKey) } try willGetValue() - return try didGetValue(self.primitiveValue(forKey: kvcKey)) + return try didGetValue(self.primitiveValue(forKey: kvcKey) as! CoreDataNativeType?) } @nonobjc @inline(__always) - @discardableResult - public func setValue(_ value: Any?, forKvcKey KVCKey: KeyPath) -> Any? { + public func setValue(_ value: CoreDataNativeType?, forKvcKey KVCKey: KeyPath) { self.willChangeValue(forKey: KVCKey) defer { @@ -121,12 +120,10 @@ public extension NSManagedObject { self.didChangeValue(forKey: KVCKey) } self.setPrimitiveValue(value, forKey: KVCKey) - return value } @nonobjc @inline(__always) - @discardableResult - public func setValue(_ value: T, forKvcKey KVCKey: KeyPath, willSetValue: (T) throws -> Any?) rethrows -> T { + public func setValue(_ value: T, forKvcKey KVCKey: KeyPath, willSetValue: (T) throws -> CoreDataNativeType?) rethrows { self.willChangeValue(forKey: KVCKey) defer { @@ -134,20 +131,6 @@ public extension NSManagedObject { self.didChangeValue(forKey: KVCKey) } self.setPrimitiveValue(try willSetValue(value), forKey: KVCKey) - return value - } - - @nonobjc @inline(__always) - @discardableResult - public func setValue(_ value: T, forKvcKey KVCKey: KeyPath, willSetValue: (T) throws -> Any?, didSetValue: (T) -> T = { $0 }) rethrows -> T { - - self.willChangeValue(forKey: KVCKey) - defer { - - self.didChangeValue(forKey: KVCKey) - } - self.setPrimitiveValue(try willSetValue(value), forKey: KVCKey) - return didSetValue(value) } /** diff --git a/Sources/CoreStoreStrings.swift b/Sources/CoreStoreStrings.swift index 617ac54..5fc467b 100644 --- a/Sources/CoreStoreStrings.swift +++ b/Sources/CoreStoreStrings.swift @@ -45,7 +45,7 @@ public typealias ModelConfiguration = String? // MARK: - ModelVersion /** - An `String` that pertains to the name of a versioned *.xcdatamodeld file (without the file extension). + An `String` that pertains to the name of a versioned *.xcdatamodeld file (without the file extension). Model version strings don't necessarily have to be numeric or ordered in any way. The migration sequence will always be decided by (or the lack of) the `MigrationChain`. */ public typealias ModelVersion = String diff --git a/Sources/Importing/ImportableUniqueObject.swift b/Sources/Importing/ImportableUniqueObject.swift index 97f59fa..97e77d1 100644 --- a/Sources/Importing/ImportableUniqueObject.swift +++ b/Sources/Importing/ImportableUniqueObject.swift @@ -190,7 +190,7 @@ public extension ImportableUniqueObject where Self: DynamicObject { .setValue( newValue, forKvcKey: type(of: self).uniqueIDKeyPath, - willSetValue: { $0.cs_toImportableNativeType() } + willSetValue: { ($0.cs_toImportableNativeType() as! CoreDataNativeType) } ) } } diff --git a/Sources/Internal/NSEntityDescription+DynamicModel.swift b/Sources/Internal/NSEntityDescription+DynamicModel.swift index b5d91d8..bfce9af 100644 --- a/Sources/Internal/NSEntityDescription+DynamicModel.swift +++ b/Sources/Internal/NSEntityDescription+DynamicModel.swift @@ -38,23 +38,29 @@ internal extension NSEntityDescription { guard let userInfo = self.userInfo, let typeName = userInfo[UserInfoKey.CoreStoreManagedObjectTypeName] as! String?, - let entityName = userInfo[UserInfoKey.CoreStoreManagedObjectEntityName] as! String? else { + let entityName = userInfo[UserInfoKey.CoreStoreManagedObjectEntityName] as! String?, + let isAbstract = userInfo[UserInfoKey.CoreStoreManagedObjectIsAbstract] as! Bool? else { return nil } return CoreStoreSchema.AnyEntity( type: NSClassFromString(typeName) as! CoreStoreObject.Type, - entityName: entityName + entityName: entityName, + isAbstract: isAbstract, + versionHashModifier: userInfo[UserInfoKey.CoreStoreManagedObjectVersionHashModifier] as! String? ) } set { if let newValue = newValue { - self.userInfo = [ + var userInfo: [AnyHashable : Any] = [ UserInfoKey.CoreStoreManagedObjectTypeName: NSStringFromClass(newValue.type), - UserInfoKey.CoreStoreManagedObjectEntityName: newValue.entityName + UserInfoKey.CoreStoreManagedObjectEntityName: newValue.entityName, + UserInfoKey.CoreStoreManagedObjectIsAbstract: newValue.isAbstract ] + userInfo[UserInfoKey.CoreStoreManagedObjectVersionHashModifier] = newValue.versionHashModifier + self.userInfo = userInfo } else { @@ -72,5 +78,7 @@ internal extension NSEntityDescription { fileprivate static let CoreStoreManagedObjectTypeName = "CoreStoreManagedObjectTypeName" fileprivate static let CoreStoreManagedObjectEntityName = "CoreStoreManagedObjectEntityName" + fileprivate static let CoreStoreManagedObjectIsAbstract = "CoreStoreManagedObjectIsAbstract" + fileprivate static let CoreStoreManagedObjectVersionHashModifier = "CoreStoreManagedObjectVersionHashModifier" } } diff --git a/Sources/Internal/NSManagedObject+Logging.swift b/Sources/Internal/NSManagedObject+Logging.swift index 91dc4ba..2db850f 100644 --- a/Sources/Internal/NSManagedObject+Logging.swift +++ b/Sources/Internal/NSManagedObject+Logging.swift @@ -48,6 +48,25 @@ internal extension NSManagedObject { } return nil } + + @nonobjc + internal func isEditableInContext() -> Bool? { + + guard let context = self.managedObjectContext else { + + return nil + } + if context.isTransactionContext { + + return true + } + if context.isDataStackContext { + + return false + } + return nil + } + // TODO: test before release (rolled back) // @nonobjc // internal static func cs_swizzleMethodsForLogging() { diff --git a/Sources/Migrating/Migration Mapping Providers/MigrationMappingProvider.swift b/Sources/Migrating/Migration Mapping Providers/MigrationMappingProvider.swift index 61fcdda..52e1402 100644 --- a/Sources/Migrating/Migration Mapping Providers/MigrationMappingProvider.swift +++ b/Sources/Migrating/Migration Mapping Providers/MigrationMappingProvider.swift @@ -31,6 +31,78 @@ import Foundation public protocol MigrationMappingProvider { - associatedtype SourceType: DynamicObject - associatedtype DestinationType: DynamicObject + associatedtype SourceSchema: DynamicSchema + associatedtype DestinationSchema: DynamicSchema + + var sourceSchema: SourceSchema { get } + var destinationSchema: DestinationSchema { get } + + init(source: SourceSchema, destination: DestinationSchema) + +// func migrate( +// from oldObject: SourceType, +// to newObject: DestinationType, +// transaction: UnsafeDataTransaction +// ) +// +// func forEachPropertyMapping( +// from oldObject: SourceType, +// to newObject: DestinationType, +// removed: (_ keyPath: KeyPath) -> Void, +// added: (_ keyPath: KeyPath) -> Void, +// transformed: (_ keyPath: KeyPath) -> Void, +// copied: (_ keyPath: KeyPath) -> Void +// ) +} + +public extension MigrationMappingProvider { + +// func migrate(from oldObject: SourceType, to newObject: DestinationType, transaction: UnsafeDataTransaction) { +// +// +// } +// +// func forEachPropertyMapping(from oldObject: SourceType, to newObject: DestinationType, removed: (_ keyPath: KeyPath) -> Void, added: (_ keyPath: KeyPath) -> Void, transformed: (_ keyPath: KeyPath) -> Void) { +// +// let oldAttributes = oldObject.cs_toRaw().entity.attributesByName +// let newAttributes = newObject.cs_toRaw().entity.attributesByName +// let oldAttributeKeys = Set(oldAttributes.keys) +// let newAttributeKeys = Set(newAttributes.keys) +// for keyPath in +// } +} + +public extension MigrationMappingProvider { + +} + + +// MARK: - UnsafeMigrationProxyObject + +public final class UnsafeMigrationProxyObject { + + public subscript(kvcKey: KeyPath) -> CoreDataNativeType? { + + get { + + return self.rawObject.cs_accessValueForKVCKey(kvcKey) + } + set { + + self.rawObject.cs_setValue(newValue, forKVCKey: kvcKey) + } + } + + + // MARK: Internal + + internal init(_ rawObject: NSManagedObject) { + + self.rawObject = rawObject + } + + + // MARK: Private + + private let rawObject: NSManagedObject } diff --git a/Sources/ObjectiveC/NSManagedObject+ObjectiveC.swift b/Sources/ObjectiveC/NSManagedObject+ObjectiveC.swift index d695e65..73f7866 100644 --- a/Sources/ObjectiveC/NSManagedObject+ObjectiveC.swift +++ b/Sources/ObjectiveC/NSManagedObject+ObjectiveC.swift @@ -38,7 +38,7 @@ public extension NSManagedObject { - returns: the primitive value for the KVC key */ @objc - public func cs_accessValueForKVCKey(_ kvcKey: KeyPath) -> Any? { + public func cs_accessValueForKVCKey(_ kvcKey: KeyPath) -> CoreDataNativeType? { return self.getValue(forKvcKey: kvcKey) } @@ -50,7 +50,7 @@ public extension NSManagedObject { - parameter KVCKey: the KVC key */ @objc - public func cs_setValue(_ value: Any?, forKVCKey KVCKey: KeyPath) { + public func cs_setValue(_ value: CoreDataNativeType?, forKVCKey KVCKey: KeyPath) { self.setValue(value, forKvcKey: KVCKey) } diff --git a/Sources/Setup/DataStack.swift b/Sources/Setup/DataStack.swift index 715caf4..30d07b6 100644 --- a/Sources/Setup/DataStack.swift +++ b/Sources/Setup/DataStack.swift @@ -96,6 +96,14 @@ public final class DataStack: Equatable { return self.schemaHistory.currentModelVersion } + /** + Returns the `DataStack`'s model schema. + */ + public var modelSchema: DynamicSchema { + + return self.schemaHistory.schemaByVersion[self.schemaHistory.currentModelVersion]! + } + /** Returns the entity name-to-class type mapping from the `DataStack`'s model. */ diff --git a/Sources/Setup/Dynamic Models/Dynamic Schema/CoreStoreSchema.swift b/Sources/Setup/Dynamic Models/Dynamic Schema/CoreStoreSchema.swift index 6a8cc12..64cfc63 100644 --- a/Sources/Setup/Dynamic Models/Dynamic Schema/CoreStoreSchema.swift +++ b/Sources/Setup/Dynamic Models/Dynamic Schema/CoreStoreSchema.swift @@ -31,7 +31,7 @@ import Foundation public final class CoreStoreSchema: DynamicSchema { - public convenience init(modelVersion: String, entities: [DynamicEntity], versionLock: VersionLock? = nil) { + public convenience init(modelVersion: ModelVersion, entities: [DynamicEntity], versionLock: VersionLock? = nil) { self.init( modelVersion: modelVersion, @@ -40,7 +40,7 @@ public final class CoreStoreSchema: DynamicSchema { ) } - public required init(modelVersion: String, entitiesByConfiguration: [String: [DynamicEntity]], versionLock: VersionLock? = nil) { + public required init(modelVersion: ModelVersion, entitiesByConfiguration: [String: [DynamicEntity]], versionLock: VersionLock? = nil) { var actualEntitiesByConfiguration: [String: Set] = [:] for (configuration, entities) in entitiesByConfiguration { @@ -129,14 +129,20 @@ public final class CoreStoreSchema: DynamicSchema { internal init(_ entity: DynamicEntity) { - self.type = entity.type - self.entityName = entity.entityName + self.init( + type: entity.type, + entityName: entity.entityName, + isAbstract: entity.isAbstract, + versionHashModifier: entity.versionHashModifier + ) } - internal init(type: CoreStoreObject.Type, entityName: String) { + internal init(type: CoreStoreObject.Type, entityName: String, isAbstract: Bool, versionHashModifier: String?) { self.type = type self.entityName = entityName + self.isAbstract = isAbstract + self.versionHashModifier = versionHashModifier } @@ -146,6 +152,8 @@ public final class CoreStoreSchema: DynamicSchema { return lhs.type == rhs.type && lhs.entityName == rhs.entityName + && lhs.isAbstract == rhs.isAbstract + && lhs.versionHashModifier == rhs.versionHashModifier } // MARK: Hashable @@ -154,12 +162,16 @@ public final class CoreStoreSchema: DynamicSchema { return ObjectIdentifier(self.type).hashValue ^ self.entityName.hashValue + ^ self.isAbstract.hashValue + ^ (self.versionHashModifier ?? "").hashValue } // MARK: DynamicEntity internal let type: CoreStoreObject.Type internal let entityName: EntityName + internal let isAbstract: Bool + internal let versionHashModifier: String? } @@ -193,6 +205,7 @@ public final class CoreStoreSchema: DynamicSchema { let entityDescription = NSEntityDescription() entityDescription.anyEntity = entity entityDescription.name = entity.entityName + entityDescription.isAbstract = entity.isAbstract entityDescription.managedObjectClassName = NSStringFromClass(NSManagedObject.self) func createProperties(for type: CoreStoreObject.Type) -> [NSPropertyDescription] { @@ -216,8 +229,8 @@ public final class CoreStoreSchema: DynamicSchema { case let relationship as RelationshipProtocol: let description = NSRelationshipDescription() description.name = relationship.keyPath - description.minCount = 0 - description.maxCount = relationship.isToMany ? 0 : 1 + description.minCount = relationship.minCount + description.maxCount = relationship.maxCount description.isOrdered = relationship.isOrdered description.deleteRule = relationship.deleteRule // TODO: versionHash, renamingIdentifier, etc diff --git a/Sources/Setup/Dynamic Models/Entity.swift b/Sources/Setup/Dynamic Models/Entity.swift index 91508fd..1dce650 100644 --- a/Sources/Setup/Dynamic Models/Entity.swift +++ b/Sources/Setup/Dynamic Models/Entity.swift @@ -34,6 +34,8 @@ public protocol DynamicEntity { var type: CoreStoreObject.Type { get } var entityName: EntityName { get } + var isAbstract: Bool { get } + var versionHashModifier: String? { get } } @@ -41,15 +43,17 @@ public protocol DynamicEntity { public struct Entity: DynamicEntity, Hashable { - public init(_ entityName: String) { + public init(_ entityName: String, isAbstract: Bool = false, versionHashModifier: String? = nil) { - self.init(O.self, entityName) + self.init(O.self, entityName, isAbstract: isAbstract, versionHashModifier: versionHashModifier) } - public init(_ type: O.Type, _ entityName: String) { + public init(_ type: O.Type, _ entityName: String, isAbstract: Bool = false, versionHashModifier: String? = nil) { self.type = type self.entityName = entityName + self.isAbstract = isAbstract + self.versionHashModifier = versionHashModifier } @@ -57,6 +61,8 @@ public struct Entity: DynamicEntity, Hashable { public let type: CoreStoreObject.Type public let entityName: EntityName + public let isAbstract: Bool + public let versionHashModifier: String? // MARK: Equatable @@ -65,6 +71,8 @@ public struct Entity: DynamicEntity, Hashable { return lhs.type == rhs.type && lhs.entityName == rhs.entityName + && lhs.isAbstract == rhs.isAbstract + && lhs.versionHashModifier == rhs.versionHashModifier } // MARK: Hashable @@ -72,5 +80,8 @@ public struct Entity: DynamicEntity, Hashable { public var hashValue: Int { return ObjectIdentifier(self.type).hashValue + ^ self.entityName.hashValue + ^ self.isAbstract.hashValue + ^ (self.versionHashModifier ?? "").hashValue } } diff --git a/Sources/Setup/Dynamic Models/Relationship.swift b/Sources/Setup/Dynamic Models/Relationship.swift index 7cba56f..9ed34b4 100644 --- a/Sources/Setup/Dynamic Models/Relationship.swift +++ b/Sources/Setup/Dynamic Models/Relationship.swift @@ -100,6 +100,10 @@ public enum RelationshipContainer { self.accessRawObject().isRunningInAllowedQueue() == true, "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." ) + CoreStore.assert( + self.accessRawObject().isEditableInContext() == true, + "Attempted to update a \(cs_typeName(O.self))'s value from outside a transaction." + ) self.accessRawObject() .setValue( newValue, @@ -117,6 +121,8 @@ public enum RelationshipContainer { 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 accessRawObject: () -> NSManagedObject = { @@ -157,24 +163,24 @@ public enum RelationshipContainer { relationship.value = relationship2.value } - public convenience init(_ keyPath: KeyPath, deleteRule: DeleteRule = .nullify) { + public convenience init(_ keyPath: KeyPath, deleteRule: DeleteRule = .nullify, minCount: Int = 0, maxCount: Int = 0) { - self.init(keyPath: keyPath, inverseKeyPath: { nil }, deleteRule: deleteRule) + 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) { + 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) + 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) { + 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) + 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) { + 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) + 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 @@ -206,6 +212,10 @@ public enum RelationshipContainer { self.accessRawObject().isRunningInAllowedQueue() == true, "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." ) + CoreStore.assert( + self.accessRawObject().isEditableInContext() == true, + "Attempted to update a \(cs_typeName(O.self))'s value from outside a transaction." + ) self.accessRawObject() .setValue( newValue, @@ -224,6 +234,8 @@ public enum RelationshipContainer { 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 accessRawObject: () -> NSManagedObject = { @@ -234,11 +246,15 @@ public enum RelationshipContainer { // MARK: Private - private init(keyPath: String, inverseKeyPath: @escaping () -> String?, deleteRule: DeleteRule) { + 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 } } @@ -269,24 +285,24 @@ public enum RelationshipContainer { relationship.value = Set(relationship2.value) } - public convenience init(_ keyPath: KeyPath, deleteRule: DeleteRule = .nullify) { + public convenience init(_ keyPath: KeyPath, deleteRule: DeleteRule = .nullify, minCount: Int = 0, maxCount: Int = 0) { - self.init(keyPath: keyPath, inverseKeyPath: { nil }, deleteRule: deleteRule) + 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) { + 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) + 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) { + 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) + 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) { + 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) + 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 @@ -318,6 +334,10 @@ public enum RelationshipContainer { self.accessRawObject().isRunningInAllowedQueue() == true, "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." ) + CoreStore.assert( + self.accessRawObject().isEditableInContext() == true, + "Attempted to update a \(cs_typeName(O.self))'s value from outside a transaction." + ) self.accessRawObject() .setValue( newValue, @@ -336,6 +356,8 @@ public enum RelationshipContainer { 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 accessRawObject: () -> NSManagedObject = { @@ -346,11 +368,15 @@ public enum RelationshipContainer { // MARK: Private - private init(keyPath: KeyPath, inverseKeyPath: @escaping () -> KeyPath?, deleteRule: DeleteRule) { + 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 } } @@ -386,4 +412,6 @@ internal protocol RelationshipProtocol: class { var deleteRule: NSDeleteRule { get } var inverse: (type: CoreStoreObject.Type, keyPath: () -> KeyPath?) { get } var accessRawObject: () -> NSManagedObject { get set } + var minCount: Int { get } + var maxCount: Int { get } } diff --git a/Sources/Setup/Dynamic Models/Value.swift b/Sources/Setup/Dynamic Models/Value.swift index 6c9b9d4..616427c 100644 --- a/Sources/Setup/Dynamic Models/Value.swift +++ b/Sources/Setup/Dynamic Models/Value.swift @@ -86,11 +86,15 @@ public enum ValueContainer { self.accessRawObject().isRunningInAllowedQueue() == true, "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." ) + CoreStore.assert( + self.accessRawObject().isEditableInContext() == true, + "Attempted to update a \(cs_typeName(O.self))'s value from outside a transaction." + ) self.accessRawObject() .setValue( newValue, forKvcKey: self.keyPath, - willSetValue: { $0.cs_toImportableNativeType() } + willSetValue: { ($0.cs_toImportableNativeType() as! CoreDataNativeType) } ) } } @@ -163,11 +167,15 @@ public enum ValueContainer { self.accessRawObject().isRunningInAllowedQueue() == true, "Attempted to access \(cs_typeName(O.self))'s value outside it's designated queue." ) + CoreStore.assert( + self.accessRawObject().isEditableInContext() == true, + "Attempted to update a \(cs_typeName(O.self))'s value from outside a transaction." + ) self.accessRawObject() .setValue( newValue, forKvcKey: self.keyPath, - willSetValue: { $0?.cs_toImportableNativeType() } + willSetValue: { ($0?.cs_toImportableNativeType() as! CoreDataNativeType?) } ) } }