From 43f61359dabcc6ec5f72112f5aadd9dcf8be3557 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 15 Jan 2020 18:29:58 +0900 Subject: [PATCH 01/20] prototype new Fields as propertyWrappers (Swift 5.2 above only) --- CoreStore.xcodeproj/project.pbxproj | 96 ++++- CoreStoreTests/DynamicModelTests.swift | 174 +++++---- CoreStoreTests/ListObserverTests.swift | 28 +- CoreStoreTests/ObjectObserverTests.swift | 6 +- CoreStoreTests/TransactionTests.swift | 6 +- CoreStoreTests/WhereTests.swift | 26 +- Sources/AttributeProtocol.swift | 2 +- ...reStore+CustomDebugStringConvertible.swift | 3 +- Sources/CoreStoreError.swift | 3 - Sources/CoreStoreObject+DataSources.swift | 23 +- Sources/CoreStoreObject+Observing.swift | 27 ++ Sources/CoreStoreObject+Querying.swift | 82 ++++ Sources/CoreStoreObject.swift | 36 +- Sources/CoreStoreSchema.swift | 19 + Sources/DynamicObject.swift | 73 ++-- Sources/DynamicObjectMeta.swift | 143 ------- Sources/Field.Computed.swift | 305 +++++++++++++++ Sources/Field.PlistCoded.swift | 9 + Sources/Field.Stored.swift | 362 ++++++++++++++++++ Sources/Field.swift | 71 ++++ Sources/FieldOptionalType.swift | 56 +++ Sources/FieldProtocol.swift | 57 +++ Sources/FieldStorableType.swift | 259 +++++++++++++ Sources/KeyPath+Querying.swift | 14 + Sources/KeyPathGenericBindings.swift | 30 +- Sources/ObjectPublisher.swift | 30 ++ Sources/ObjectSnapshot.swift | 34 ++ Sources/PartialObject.swift | 83 ++++ Sources/PropertyProtocol.swift | 2 +- Sources/RelationshipProtocol.swift | 2 +- Sources/StorageInterface.swift | 8 +- Sources/VersionLock.swift | 9 +- Sources/Where.swift | 55 ++- 33 files changed, 1796 insertions(+), 337 deletions(-) delete mode 100644 Sources/DynamicObjectMeta.swift create mode 100644 Sources/Field.Computed.swift create mode 100644 Sources/Field.PlistCoded.swift create mode 100644 Sources/Field.Stored.swift create mode 100644 Sources/Field.swift create mode 100644 Sources/FieldOptionalType.swift create mode 100644 Sources/FieldProtocol.swift create mode 100644 Sources/FieldStorableType.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 3fac3b2..1815a05 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -322,7 +322,6 @@ B52FD3AB1E3B3EF10001D919 /* NSManagedObject+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52FD3A91E3B3EF10001D919 /* NSManagedObject+Logging.swift */; }; B52FD3AC1E3B3EF10001D919 /* NSManagedObject+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52FD3A91E3B3EF10001D919 /* NSManagedObject+Logging.swift */; }; B52FD3AD1E3B3EF10001D919 /* NSManagedObject+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52FD3A91E3B3EF10001D919 /* NSManagedObject+Logging.swift */; }; - B53304AA230BA4F7007C2BD8 /* DynamicObjectMeta.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53304A9230BA4F7007C2BD8 /* DynamicObjectMeta.swift */; }; B533C4DB1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533C4DA1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift */; }; B533C4DC1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533C4DA1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift */; }; B533C4DD1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533C4DA1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift */; }; @@ -548,6 +547,34 @@ B56924021EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56923FE1EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift */; }; B56964D41B22FFAD0075EE4A /* DataStack+Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56964D31B22FFAD0075EE4A /* DataStack+Migration.swift */; }; B56965241B356B820075EE4A /* MigrationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56965231B356B820075EE4A /* MigrationResult.swift */; }; + B56E4ECA23CD9B4800E1708C /* Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EC923CD9B4800E1708C /* Field.swift */; }; + B56E4ECB23CD9B4800E1708C /* Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EC923CD9B4800E1708C /* Field.swift */; }; + B56E4ECC23CD9B4800E1708C /* Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EC923CD9B4800E1708C /* Field.swift */; }; + B56E4ECD23CD9B4800E1708C /* Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EC923CD9B4800E1708C /* Field.swift */; }; + B56E4ECF23CD9E4200E1708C /* Field.Stored.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4ECE23CD9E4200E1708C /* Field.Stored.swift */; }; + B56E4ED023CD9E4200E1708C /* Field.Stored.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4ECE23CD9E4200E1708C /* Field.Stored.swift */; }; + B56E4ED123CD9E4200E1708C /* Field.Stored.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4ECE23CD9E4200E1708C /* Field.Stored.swift */; }; + B56E4ED223CD9E4200E1708C /* Field.Stored.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4ECE23CD9E4200E1708C /* Field.Stored.swift */; }; + B56E4ED423CDB54A00E1708C /* FieldProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4ED323CDB54A00E1708C /* FieldProtocol.swift */; }; + B56E4ED523CDB54A00E1708C /* FieldProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4ED323CDB54A00E1708C /* FieldProtocol.swift */; }; + B56E4ED623CDB54A00E1708C /* FieldProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4ED323CDB54A00E1708C /* FieldProtocol.swift */; }; + B56E4ED723CDB54A00E1708C /* FieldProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4ED323CDB54A00E1708C /* FieldProtocol.swift */; }; + B56E4ED923CEB8E700E1708C /* FieldStorableType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4ED823CEB8E700E1708C /* FieldStorableType.swift */; }; + B56E4EDA23CEB8E700E1708C /* FieldStorableType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4ED823CEB8E700E1708C /* FieldStorableType.swift */; }; + B56E4EDB23CEB8E700E1708C /* FieldStorableType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4ED823CEB8E700E1708C /* FieldStorableType.swift */; }; + B56E4EDC23CEB8E700E1708C /* FieldStorableType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4ED823CEB8E700E1708C /* FieldStorableType.swift */; }; + B56E4EDF23CEBCF000E1708C /* FieldOptionalType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */; }; + B56E4EE023CEBCF000E1708C /* FieldOptionalType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */; }; + B56E4EE123CEBCF000E1708C /* FieldOptionalType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */; }; + B56E4EE223CEBCF000E1708C /* FieldOptionalType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */; }; + B56E4EE423CEDF0900E1708C /* Field.Computed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Computed.swift */; }; + B56E4EE523CEDF0900E1708C /* Field.Computed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Computed.swift */; }; + B56E4EE623CEDF0900E1708C /* Field.Computed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Computed.swift */; }; + B56E4EE723CEDF0900E1708C /* Field.Computed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Computed.swift */; }; + B56E4EE923CF0C4500E1708C /* Field.PlistCoded.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE823CF0C4500E1708C /* Field.PlistCoded.swift */; }; + B56E4EEA23CF0C4500E1708C /* Field.PlistCoded.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE823CF0C4500E1708C /* Field.PlistCoded.swift */; }; + B56E4EEB23CF0C4500E1708C /* Field.PlistCoded.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE823CF0C4500E1708C /* Field.PlistCoded.swift */; }; + B56E4EEC23CF0C4500E1708C /* Field.PlistCoded.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE823CF0C4500E1708C /* Field.PlistCoded.swift */; }; B57D27BE1D0BBE8200539C58 /* BaseTestDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */; }; B57D27BF1D0BBE8200539C58 /* BaseTestDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */; }; B57D27C01D0BBE8200539C58 /* BaseTestDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */; }; @@ -979,7 +1006,6 @@ B52F743C1E9B8724005F3DAC /* XcodeDataModelSchema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XcodeDataModelSchema.swift; sourceTree = ""; }; B52F74491E9B8740005F3DAC /* CoreStoreSchema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreStoreSchema.swift; sourceTree = ""; }; B52FD3A91E3B3EF10001D919 /* NSManagedObject+Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Logging.swift"; sourceTree = ""; }; - B53304A9230BA4F7007C2BD8 /* DynamicObjectMeta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicObjectMeta.swift; sourceTree = ""; }; B533C4DA1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+CoreStore.swift"; sourceTree = ""; }; B538BA701D15B3E30003A766 /* CoreStoreBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CoreStoreBridge.m; sourceTree = ""; }; B53B275E1EE3B92E00E9B352 /* CoreStoreManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreStoreManagedObject.swift; sourceTree = ""; }; @@ -1038,6 +1064,13 @@ B56923FE1EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSUnsafeDataModelSchema.swift; sourceTree = ""; }; B56964D31B22FFAD0075EE4A /* DataStack+Migration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "DataStack+Migration.swift"; sourceTree = ""; }; B56965231B356B820075EE4A /* MigrationResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationResult.swift; sourceTree = ""; }; + B56E4EC923CD9B4800E1708C /* Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.swift; sourceTree = ""; }; + B56E4ECE23CD9E4200E1708C /* Field.Stored.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.Stored.swift; sourceTree = ""; }; + B56E4ED323CDB54A00E1708C /* FieldProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldProtocol.swift; sourceTree = ""; }; + B56E4ED823CEB8E700E1708C /* FieldStorableType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldStorableType.swift; sourceTree = ""; }; + B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldOptionalType.swift; sourceTree = ""; }; + B56E4EE323CEDF0900E1708C /* Field.Computed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.Computed.swift; sourceTree = ""; }; + B56E4EE823CF0C4500E1708C /* Field.PlistCoded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.PlistCoded.swift; sourceTree = ""; }; B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTestDataTestCase.swift; sourceTree = ""; }; B57D27C11D0BC20100539C58 /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; B58085741CDF7F00004C2EEB /* SetupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupTests.swift; sourceTree = ""; }; @@ -1386,6 +1419,7 @@ children = ( B5831B6F1F34AC3400A9F647 /* AttributeProtocol.swift */, B5831B741F34AC7A00A9F647 /* RelationshipProtocol.swift */, + B56E4ED323CDB54A00E1708C /* FieldProtocol.swift */, B50564D22350CC3100482308 /* PropertyProtocol.swift */, B53D9E5823513712000F48FB /* DiffableDataSourceSnapshotProtocol.swift */, B50E175623517DE4004F033C /* Differentiable.swift */, @@ -1513,13 +1547,34 @@ name = Migrating; sourceTree = ""; }; + B56E4EC823CD9B2E00E1708C /* Field Properties */ = { + isa = PBXGroup; + children = ( + B56E4EC923CD9B4800E1708C /* Field.swift */, + B56E4ECE23CD9E4200E1708C /* Field.Stored.swift */, + B56E4EE323CEDF0900E1708C /* Field.Computed.swift */, + B56E4EE823CF0C4500E1708C /* Field.PlistCoded.swift */, + B56E4EDD23CEBB0400E1708C /* Supported Values */, + ); + name = "Field Properties"; + sourceTree = ""; + }; + B56E4EDD23CEBB0400E1708C /* Supported Values */ = { + isa = PBXGroup; + children = ( + B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */, + B56E4ED823CEB8E700E1708C /* FieldStorableType.swift */, + ); + name = "Supported Values"; + sourceTree = ""; + }; B57358D71E5A7F9B0094B50A /* Dynamic Models */ = { isa = PBXGroup; children = ( B5D339D71E9489AB00C880DE /* CoreStoreObject.swift */, - B53304A9230BA4F7007C2BD8 /* DynamicObjectMeta.swift */, B53CA9A11EF1EF1600E0F440 /* PartialObject.swift */, - B5831B6E1F3355C300A9F647 /* Properties */, + B56E4EC823CD9B2E00E1708C /* Field Properties */, + B5831B6E1F3355C300A9F647 /* Legacy Properties */, B52F74391E9B8724005F3DAC /* Dynamic Schema */, B5D339DC1E9489C700C880DE /* DynamicObject.swift */, B52F742E1E9B50D0005F3DAC /* SchemaHistory.swift */, @@ -1529,7 +1584,7 @@ name = "Dynamic Models"; sourceTree = ""; }; - B5831B6E1F3355C300A9F647 /* Properties */ = { + B5831B6E1F3355C300A9F647 /* Legacy Properties */ = { isa = PBXGroup; children = ( B5D33A001E96012400C880DE /* Relationship.swift */, @@ -1543,7 +1598,7 @@ B509D7D223C84E1900F42824 /* Transformable.Required.swift */, B509D7D723C84E2600F42824 /* Transformable.Optional.swift */, ); - name = Properties; + name = "Legacy Properties"; sourceTree = ""; }; B5A1DAC61F111BBE003CF369 /* KeyPath Utilities */ = { @@ -2145,6 +2200,7 @@ files = ( B5BF7FB7234C97CE0070E741 /* DiffableDataSource.TableViewAdapter-UIKit.swift in Sources */, B5DE5230230BDA1300A22534 /* CoreStoreDefaults.swift in Sources */, + B56E4ED423CDB54A00E1708C /* FieldProtocol.swift in Sources */, B509D7C423C848DA00F42824 /* Relationship.ToOne.swift in Sources */, B5E84F221AFF84860064E85B /* ObjectMonitor.swift in Sources */, B5ECDBF91CA804FD00C7F112 /* NSManagedObjectContext+ObjectiveC.swift in Sources */, @@ -2194,6 +2250,7 @@ B5E1B5A81CAA49E2007FD580 /* CSDataStack+Migrating.swift in Sources */, B50132302346B76E00FC238B /* Internals.FetchedDiffableDataSourceSnapshotDelegate.swift in Sources */, B5D339F11E94AF5800C880DE /* CoreStoreStrings.swift in Sources */, + B56E4EE923CF0C4500E1708C /* Field.PlistCoded.swift in Sources */, B56007161B4018AB00A9A8F9 /* MigrationChain.swift in Sources */, B5E1B59D1CAA2568007FD580 /* CSDataStack+Observing.swift in Sources */, B5ECDC231CA81A3900C7F112 /* CSCoreStore+Querying.swift in Sources */, @@ -2213,10 +2270,12 @@ B50E175223517C6B004F033C /* Internals.DiffableDataUIDispatcher.Changeset.swift in Sources */, B5E84EE71AFF84610064E85B /* CoreStore+Logging.swift in Sources */, B546F9731C9C553300D5AC55 /* SetupResult.swift in Sources */, + B56E4EDF23CEBCF000E1708C /* FieldOptionalType.swift in Sources */, B53CA9A21EF1EF1600E0F440 /* PartialObject.swift in Sources */, B56007111B3F6BD500A9A8F9 /* Into.swift in Sources */, B5E84F111AFF847B0064E85B /* Select.swift in Sources */, B51260931E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */, + B56E4ECA23CD9B4800E1708C /* Field.swift in Sources */, B5DAFB482203D9F8003FCCD0 /* Where.Expression.swift in Sources */, B509D7D823C84E2600F42824 /* Transformable.Optional.swift in Sources */, B5FE4DA21C8481E100FA6A91 /* StorageInterface.swift in Sources */, @@ -2254,7 +2313,6 @@ B5E1B5A21CAA4365007FD580 /* CSCoreStore+Observing.swift in Sources */, B5E84EDF1AFF84500064E85B /* DataStack.swift in Sources */, B50E175723517DE4004F033C /* Differentiable.swift in Sources */, - B53304AA230BA4F7007C2BD8 /* DynamicObjectMeta.swift in Sources */, B59AFF411C6593E400C0ABE2 /* NSPersistentStoreCoordinator+Setup.swift in Sources */, B5E84F231AFF84860064E85B /* ListMonitor.swift in Sources */, B5BF7FC6234D7E460070E741 /* ObjectSnapshot.swift in Sources */, @@ -2271,9 +2329,11 @@ B5FAD6A91B50A4B400714891 /* Progress+Convenience.swift in Sources */, B5E84EFC1AFF846E0064E85B /* SynchronousDataTransaction.swift in Sources */, B5215CA91FA4810300139E3A /* QueryChainBuilder.swift in Sources */, + B56E4EE423CEDF0900E1708C /* Field.Computed.swift in Sources */, B50E174D23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */, B5E222231CA4E12600BA2E95 /* CSSynchronousDataTransaction.swift in Sources */, B5E84F281AFF84920064E85B /* NSManagedObject+Convenience.swift in Sources */, + B56E4ECF23CD9E4200E1708C /* Field.Stored.swift in Sources */, B52F744A1E9B8740005F3DAC /* CoreStoreSchema.swift in Sources */, B5AEFAB51C9962AE00AD137F /* CoreStoreBridge.swift in Sources */, B5E2222A1CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift in Sources */, @@ -2314,6 +2374,7 @@ B52F743D1E9B8724005F3DAC /* DynamicSchema.swift in Sources */, B5E8A72021C1015300EF006A /* CoreStoreObject+Observing.swift in Sources */, B53D9E5923513712000F48FB /* DiffableDataSourceSnapshotProtocol.swift in Sources */, + B56E4ED923CEB8E700E1708C /* FieldStorableType.swift in Sources */, B5474D152227C08700B21FEC /* Internals.CoreStoreFetchRequest.swift in Sources */, B56923FF1EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift in Sources */, B5215CAE1FA4812500139E3A /* SectionMonitorBuilder.swift in Sources */, @@ -2429,6 +2490,7 @@ B5ECDC251CA81A3900C7F112 /* CSCoreStore+Querying.swift in Sources */, B549F6741E56A92800FBAB2D /* CoreDataNativeType.swift in Sources */, B509C7F51E54511B0061C547 /* ImportableAttributeType.swift in Sources */, + B56E4EEA23CF0C4500E1708C /* Field.PlistCoded.swift in Sources */, 82BA18B31C4BBD3900A0916E /* ImportableUniqueObject.swift in Sources */, B50EE14323473C96009B8C47 /* CoreStoreObject+DataSources.swift in Sources */, B55BB4DA23503B9600C33E34 /* EnvironmentValues+DataSources.swift in Sources */, @@ -2448,6 +2510,7 @@ B5831F432212700400D8604C /* Where.Expression.swift in Sources */, B51260941E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */, B5FE4DA81C84FB4400FA6A91 /* InMemoryStore.swift in Sources */, + B56E4EE023CEBCF000E1708C /* FieldOptionalType.swift in Sources */, B5F8496D234898240029D57B /* ListSnapshot.swift in Sources */, B53FBA001CAB2D2F00F0D40A /* CSMigrationResult.swift in Sources */, B5DBE2D31C991B3E00B5CEFA /* CSDataStack.swift in Sources */, @@ -2475,6 +2538,7 @@ B5D8CA772346EAEE0055D7D1 /* DataStack+DataSources.swift in Sources */, 82BA18D01C4BBD7100A0916E /* Internals.MigrationManager.swift in Sources */, B5DE5231230BDA1300A22534 /* CoreStoreDefaults.swift in Sources */, + B56E4ED523CDB54A00E1708C /* FieldProtocol.swift in Sources */, B52F74461E9B8724005F3DAC /* XcodeDataModelSchema.swift in Sources */, 82BA18C61C4BBD5900A0916E /* DataStack+Migration.swift in Sources */, B59851491C90289D00C99590 /* NSPersistentStoreCoordinator+Setup.swift in Sources */, @@ -2505,6 +2569,7 @@ B514EF0F23A8DB180093DBA4 /* DiffableDataSource.Target.swift in Sources */, B52F744B1E9B8740005F3DAC /* CoreStoreSchema.swift in Sources */, B5AEFAB61C9962AE00AD137F /* CoreStoreBridge.swift in Sources */, + B56E4EE523CEDF0900E1708C /* Field.Computed.swift in Sources */, B5E2222C1CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift in Sources */, 82BA18A71C4BBD2900A0916E /* CoreStore+Logging.swift in Sources */, B50E174E23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */, @@ -2549,6 +2614,7 @@ B5474D162227C08700B21FEC /* Internals.CoreStoreFetchRequest.swift in Sources */, B501322B2346A9AE00FC238B /* ListPublisher.swift in Sources */, B56924001EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift in Sources */, + B56E4EDA23CEB8E700E1708C /* FieldStorableType.swift in Sources */, B5215CAF1FA4812500139E3A /* SectionMonitorBuilder.swift in Sources */, 82BA18D61C4BBD7100A0916E /* NSManagedObjectContext+Transaction.swift in Sources */, B56923ED1EB827F6007C4DC9 /* SchemaMappingProvider.swift in Sources */, @@ -2564,8 +2630,10 @@ B5BF7FC7234D7E460070E741 /* ObjectSnapshot.swift in Sources */, 82BA18CC1C4BBD6400A0916E /* Progress+Convenience.swift in Sources */, B5BF7FB8234C97CE0070E741 /* DiffableDataSource.TableViewAdapter-UIKit.swift in Sources */, + B56E4ED023CD9E4200E1708C /* Field.Stored.swift in Sources */, 82BA18C01C4BBD5300A0916E /* DataStack+Observing.swift in Sources */, 82BA18A61C4BBD2900A0916E /* DefaultLogger.swift in Sources */, + B56E4ECB23CD9B4800E1708C /* Field.swift in Sources */, B509D7BE23C8480A00F42824 /* Value.Optional.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2662,6 +2730,7 @@ B559CD471CAA8B6300E4D58B /* CSSetupResult.swift in Sources */, B50E17642351FA66004F033C /* Internals.Closure.swift in Sources */, B5ECDBF01CA6BF2000C7F112 /* CSFrom.swift in Sources */, + B56E4EEC23CF0C4500E1708C /* Field.PlistCoded.swift in Sources */, B549F6761E56A92800FBAB2D /* CoreDataNativeType.swift in Sources */, B509C7F71E54511B0061C547 /* ImportableAttributeType.swift in Sources */, B5220E1F1D130810009BC71E /* CSListObserver.swift in Sources */, @@ -2681,6 +2750,7 @@ B52DD1BD1BE1F94300949AFE /* NSManagedObject+Convenience.swift in Sources */, B5831F4222126FED00D8604C /* KeyPathGenericBindings.swift in Sources */, B53CA9A51EF1EF1600E0F440 /* PartialObject.swift in Sources */, + B56E4EE223CEBCF000E1708C /* FieldOptionalType.swift in Sources */, B52DD1AD1BE1F93900949AFE /* Where.swift in Sources */, B53FBA1C1CAB63E200F0D40A /* NSManagedObject+ObjectiveC.swift in Sources */, B5F8496F234898240029D57B /* ListSnapshot.swift in Sources */, @@ -2708,6 +2778,7 @@ B52DD1B81BE1F94000949AFE /* DataStack+Migration.swift in Sources */, B5ECDC091CA8138100C7F112 /* CSOrderBy.swift in Sources */, B5D8CA792346EAEF0055D7D1 /* DataStack+DataSources.swift in Sources */, + B56E4ED723CDB54A00E1708C /* FieldProtocol.swift in Sources */, B56923C71EB823B4007C4DC9 /* NSEntityDescription+Migration.swift in Sources */, B5DE5233230BDA1300A22534 /* CoreStoreDefaults.swift in Sources */, B52DD1A51BE1F92F00949AFE /* ImportableUniqueObject.swift in Sources */, @@ -2738,6 +2809,7 @@ B514EF1123A8DB190093DBA4 /* DiffableDataSource.Target.swift in Sources */, B50E175A23517DE4004F033C /* Differentiable.swift in Sources */, B52F744D1E9B8740005F3DAC /* CoreStoreSchema.swift in Sources */, + B56E4EE723CEDF0900E1708C /* Field.Computed.swift in Sources */, B52DD19A1BE1F92800949AFE /* CoreStore+Logging.swift in Sources */, B52DD1A71BE1F93200949AFE /* BaseDataTransaction+Querying.swift in Sources */, B546F96C1C9AF26D00D5AC55 /* CSInMemoryStore.swift in Sources */, @@ -2782,6 +2854,7 @@ B50E175F2351848E004F033C /* Internals.DiffableDataUIDispatcher.DiffResult.swift in Sources */, B5474D182227C08700B21FEC /* Internals.CoreStoreFetchRequest.swift in Sources */, B501322E2346A9B100FC238B /* ListPublisher.swift in Sources */, + B56E4EDC23CEB8E700E1708C /* FieldStorableType.swift in Sources */, B56924021EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift in Sources */, B5215CB11FA4812500139E3A /* SectionMonitorBuilder.swift in Sources */, B5220E171D1306DF009BC71E /* UnsafeDataTransaction+Observing.swift in Sources */, @@ -2797,8 +2870,10 @@ B5D339EA1E9493A500C880DE /* Entity.swift in Sources */, B5BF7FC9234D7E460070E741 /* ObjectSnapshot.swift in Sources */, B52DD1AA1BE1F93500949AFE /* TypeErasedClauses.swift in Sources */, + B56E4ED223CD9E4200E1708C /* Field.Stored.swift in Sources */, B53FBA021CAB2D2F00F0D40A /* CSMigrationResult.swift in Sources */, B51FE5AF1CD4D00300E54258 /* CoreStore+CustomDebugStringConvertible.swift in Sources */, + B56E4ECD23CD9B4800E1708C /* Field.swift in Sources */, B509D7C223C8480B00F42824 /* Value.Optional.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2895,6 +2970,7 @@ B563217F1BD65216006C9394 /* CoreStore.swift in Sources */, B549F6751E56A92800FBAB2D /* CoreDataNativeType.swift in Sources */, B509C7F61E54511B0061C547 /* ImportableAttributeType.swift in Sources */, + B56E4EEB23CF0C4500E1708C /* Field.PlistCoded.swift in Sources */, B5E1B5961CAA0C15007FD580 /* CSObjectMonitor.swift in Sources */, B50EE14423473C97009B8C47 /* CoreStoreObject+DataSources.swift in Sources */, B55BB4D923503B9600C33E34 /* EnvironmentValues+DataSources.swift in Sources */, @@ -2914,6 +2990,7 @@ B5202CFD1C046E8400DED140 /* NSFetchedResultsController+Convenience.swift in Sources */, B5FE4DA91C84FB4400FA6A91 /* InMemoryStore.swift in Sources */, B5831F442212700500D8604C /* Where.Expression.swift in Sources */, + B56E4EE123CEBCF000E1708C /* FieldOptionalType.swift in Sources */, B5F8496E234898240029D57B /* ListSnapshot.swift in Sources */, B51260951E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */, B53FBA011CAB2D2F00F0D40A /* CSMigrationResult.swift in Sources */, @@ -2941,6 +3018,7 @@ B5D8CA782346EAEF0055D7D1 /* DataStack+DataSources.swift in Sources */, B56923C61EB823B4007C4DC9 /* NSEntityDescription+Migration.swift in Sources */, B56321A71BD65216006C9394 /* MigrationResult.swift in Sources */, + B56E4ED623CDB54A00E1708C /* FieldProtocol.swift in Sources */, B5DE5232230BDA1300A22534 /* CoreStoreDefaults.swift in Sources */, B598514A1C90289E00C99590 /* NSPersistentStoreCoordinator+Setup.swift in Sources */, B52F74471E9B8724005F3DAC /* XcodeDataModelSchema.swift in Sources */, @@ -2971,6 +3049,7 @@ B514EF1023A8DB190093DBA4 /* DiffableDataSource.Target.swift in Sources */, B563218A1BD65216006C9394 /* SynchronousDataTransaction.swift in Sources */, B52F744C1E9B8740005F3DAC /* CoreStoreSchema.swift in Sources */, + B56E4EE623CEDF0900E1708C /* Field.Computed.swift in Sources */, B5AEFAB71C9962AE00AD137F /* CoreStoreBridge.swift in Sources */, B5E2222D1CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift in Sources */, B50E174F23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */, @@ -3015,6 +3094,7 @@ B5474D172227C08700B21FEC /* Internals.CoreStoreFetchRequest.swift in Sources */, B501322D2346A9B000FC238B /* ListPublisher.swift in Sources */, B56924011EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift in Sources */, + B56E4EDB23CEB8E700E1708C /* FieldStorableType.swift in Sources */, B5215CB01FA4812500139E3A /* SectionMonitorBuilder.swift in Sources */, B56321B41BD6521C006C9394 /* NSManagedObjectContext+Transaction.swift in Sources */, B56923EE1EB827F6007C4DC9 /* SchemaMappingProvider.swift in Sources */, @@ -3030,8 +3110,10 @@ B5BF7FC8234D7E460070E741 /* ObjectSnapshot.swift in Sources */, B56321A41BD65216006C9394 /* CoreStore+Migration.swift in Sources */, B5BF7FB9234C97CE0070E741 /* DiffableDataSource.TableViewAdapter-UIKit.swift in Sources */, + B56E4ED123CD9E4200E1708C /* Field.Stored.swift in Sources */, B56321A01BD65216006C9394 /* ObjectObserver.swift in Sources */, B56321951BD65216006C9394 /* TypeErasedClauses.swift in Sources */, + B56E4ECC23CD9B4800E1708C /* Field.swift in Sources */, B509D7C023C8480B00F42824 /* Value.Optional.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index 2b40a97..99f4e3a 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -37,40 +37,38 @@ import CoreStore #endif class Animal: CoreStoreObject { - - let species = Value.Required("species", initial: "Swift") + + @Field.Stored("species") + var species: String = "Swift" + let master = Relationship.ToOne("master") let color = Transformable.Optional("color") } class Dog: Animal { - - let nickname = Value.Optional("nickname") + + @Field.Stored("nickname") + var nickname: String? + let age = Value.Required("age", initial: 1) let friends = Relationship.ToManyOrdered("friends") let friendedBy = Relationship.ToManyUnordered("friendedBy", inverse: { $0.friends }) } class Person: CoreStoreObject { - - let title = Value.Required( - "title", - initial: "Mr.", - customSetter: Person.setTitle - ) - - let name = Value.Required( - "name", - initial: "", - customSetter: Person.setName - ) - - let displayName = Value.Optional( + + @Field.Stored("title", customSetter: Person.setTitle(_:_:)) + var title: String = "Mr." + + @Field.Stored("name", customSetter: Person.setName(_:_:)) + var name: String = "" + + @Field.Computed( "displayName", - isTransient: true, customGetter: Person.getDisplayName(_:), affectedByKeyPaths: Person.keyPathsAffectingDisplayName() ) + var displayName: String? let spouse = Relationship.ToOne("spouse") @@ -81,34 +79,34 @@ class Person: CoreStoreObject { private static func setTitle(_ partialObject: PartialObject, _ newValue: String) { - partialObject.setPrimitiveValue(newValue, for: { $0.title }) - partialObject.setPrimitiveValue(nil, for: { $0.displayName }) + partialObject.setPrimitiveValue(newValue, for: \.$title) + partialObject.setPrimitiveValue(nil, for: { $0.$displayName }) } private static func setName(_ partialObject: PartialObject, _ newValue: String) { - partialObject.setPrimitiveValue(newValue, for: { $0.name }) - partialObject.setPrimitiveValue(nil, for: { $0.displayName }) + partialObject.setPrimitiveValue(newValue, for: \.$name) + partialObject.setPrimitiveValue(nil, for: \.$displayName) } static func getDisplayName(_ partialObject: PartialObject) -> String? { - if let displayName = partialObject.primitiveValue(for: { $0.displayName }) { + if let displayName = partialObject.primitiveValue(for: \.$displayName) { return displayName } - let title = partialObject.value(for: { $0.title }) - let name = partialObject.value(for: { $0.name }) + let title = partialObject.value(for: \.$title) + let name = partialObject.value(for: \.$name) let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + partialObject.setPrimitiveValue(displayName, for: \.$displayName) return displayName } static func keyPathsAffectingDisplayName() -> Set { return [ - String(keyPath: \Person.title), - String(keyPath: \Person.name) + String(keyPath: \Person.$title), + String(keyPath: \Person.$name) ] } } @@ -138,13 +136,13 @@ class DynamicModelTests: BaseTestDataTestCase { ) self.prepareStack(dataStack, configurations: [nil]) { (stack) in - let k1 = String(keyPath: \Animal.species) + let k1 = String(keyPath: \Animal.$species) XCTAssertEqual(k1, "species") - let k2 = String(keyPath: \Dog.species) + let k2 = String(keyPath: \Dog.$species) XCTAssertEqual(k2, "species") - let k3 = String(keyPath: \Dog.nickname) + let k3 = String(keyPath: \Dog.$nickname) XCTAssertEqual(k3, "nickname") let updateDone = self.expectation(description: "update-done") @@ -156,11 +154,11 @@ class DynamicModelTests: BaseTestDataTestCase { asynchronous: { (transaction) in let animal = transaction.create(Into()) - XCTAssertEqual(animal.species.value, "Swift") - XCTAssertTrue(type(of: animal.species.value) == String.self) + XCTAssertEqual(animal.species, "Swift") + XCTAssertTrue(type(of: animal.species) == String.self) - animal.species .= "Sparrow" - XCTAssertEqual(animal.species.value, "Sparrow") + animal.species = "Sparrow" + XCTAssertEqual(animal.species, "Sparrow") animal.color .= .yellow XCTAssertEqual(animal.color.value, Color.yellow) @@ -169,8 +167,8 @@ class DynamicModelTests: BaseTestDataTestCase { switch property.keyPath { - case String(keyPath: \Animal.species): - XCTAssertTrue(property is ValueContainer.Required) + case String(keyPath: \Animal.$species): + XCTAssertTrue(property is FieldContainer.Stored) case String(keyPath: \Animal.master): XCTAssertTrue(property is RelationshipContainer.ToOne) @@ -184,16 +182,16 @@ class DynamicModelTests: BaseTestDataTestCase { } let dog = transaction.create(Into()) - XCTAssertEqual(dog.species.value, "Swift") - XCTAssertEqual(dog.nickname.value, nil) + XCTAssertEqual(dog.species, "Swift") + XCTAssertEqual(dog.nickname, nil) XCTAssertEqual(dog.age.value, 1) for property in Dog.metaProperties(includeSuperclasses: true) { switch property.keyPath { - case String(keyPath: \Dog.species): - XCTAssertTrue(property is ValueContainer.Required) + case String(keyPath: \Dog.$species): + XCTAssertTrue(property is FieldContainer.Stored) case String(keyPath: \Dog.master): XCTAssertTrue(property is RelationshipContainer.ToOne) @@ -201,8 +199,8 @@ class DynamicModelTests: BaseTestDataTestCase { case String(keyPath: \Dog.color): XCTAssertTrue(property is TransformableContainer.Optional) - case String(keyPath: \Dog.nickname): - XCTAssertTrue(property is ValueContainer.Optional) + case String(keyPath: \Dog.$nickname): + XCTAssertTrue(property is FieldContainer.Stored) case String(keyPath: \Dog.age): XCTAssertTrue(property is ValueContainer.Required) @@ -231,17 +229,17 @@ class DynamicModelTests: BaseTestDataTestCase { // // #endif - let didSetObserver = dog.species.observe(options: [.new, .old]) { (object, change) in + let didSetObserver = dog.observe(\.$species, options: [.new, .old]) { (object, change) in XCTAssertEqual(object, dog) XCTAssertEqual(change.kind, .setting) XCTAssertEqual(change.newValue, "Dog") XCTAssertEqual(change.oldValue, "Swift") XCTAssertFalse(change.isPrior) - XCTAssertEqual(object.species.value, "Dog") + XCTAssertEqual(object.species, "Dog") didSetObserverDone.fulfill() } - let willSetObserver = dog.species.observe(options: [.new, .old, .prior]) { (object, change) in + let willSetObserver = dog.observe(\.$species, options: [.new, .old, .prior]) { (object, change) in XCTAssertEqual(object, dog) XCTAssertEqual(change.kind, .setting) @@ -250,25 +248,25 @@ class DynamicModelTests: BaseTestDataTestCase { if change.isPrior { XCTAssertNil(change.newValue) - XCTAssertEqual(object.species.value, "Swift") + XCTAssertEqual(object.species, "Swift") willSetPriorObserverDone.fulfill() } else { XCTAssertEqual(change.newValue, "Dog") - XCTAssertEqual(object.species.value, "Dog") + XCTAssertEqual(object.species, "Dog") willSetNotPriorObserverDone.fulfill() } } - dog.species .= "Dog" - XCTAssertEqual(dog.species.value, "Dog") + dog.species = "Dog" + XCTAssertEqual(dog.species, "Dog") didSetObserver.invalidate() willSetObserver.invalidate() - dog.nickname .= "Spot" - XCTAssertEqual(dog.nickname.value, "Spot") + dog.nickname = "Spot" + XCTAssertEqual(dog.nickname, "Spot") let person = transaction.create(Into()) XCTAssertTrue(person.pets.value.isEmpty) @@ -280,7 +278,7 @@ class DynamicModelTests: BaseTestDataTestCase { ["title", "name"] ) - person.name .= "Joe" + person.name = "Joe" XCTAssertEqual(person.rawObject!.value(forKey: "name") as! String?, "Joe") XCTAssertEqual(person.rawObject!.value(forKey: "displayName") as! String?, "Mr. Joe") @@ -288,35 +286,35 @@ class DynamicModelTests: BaseTestDataTestCase { person.rawObject!.setValue("AAAA", forKey: "displayName") XCTAssertEqual(person.rawObject!.value(forKey: "displayName") as! String?, "AAAA") - person.name .= "John" - XCTAssertEqual(person.name.value, "John") - XCTAssertEqual(person.displayName.value, "Mr. John") // Custom getter + person.name = "John" + XCTAssertEqual(person.name, "John") + XCTAssertEqual(person.displayName, "Mr. John") // Custom getter let personSnapshot1 = person.asSnapshot(in: transaction)! - XCTAssertEqual(person.name.value, personSnapshot1.name) - XCTAssertEqual(person.title.value, personSnapshot1.title) - XCTAssertEqual(person.displayName.value, personSnapshot1.displayName) + XCTAssertEqual(person.name, personSnapshot1.$name) + XCTAssertEqual(person.title, personSnapshot1.$title) + XCTAssertEqual(person.displayName, personSnapshot1.$displayName) - person.title .= "Sir" - XCTAssertEqual(person.displayName.value, "Sir John") + person.title = "Sir" + XCTAssertEqual(person.displayName, "Sir John") - XCTAssertEqual(personSnapshot1.name, "John") - XCTAssertEqual(personSnapshot1.title, "Mr.") - XCTAssertEqual(personSnapshot1.displayName, "Mr. John") + XCTAssertEqual(personSnapshot1.$name, "John") + XCTAssertEqual(personSnapshot1.$title, "Mr.") + XCTAssertEqual(personSnapshot1.$displayName, "Mr. John") let personSnapshot2 = person.asSnapshot(in: transaction)! - XCTAssertEqual(person.name.value, personSnapshot2.name) - XCTAssertEqual(person.title.value, personSnapshot2.title) - XCTAssertEqual(person.displayName.value, personSnapshot2.displayName) + XCTAssertEqual(person.name, personSnapshot2.$name) + XCTAssertEqual(person.title, personSnapshot2.$title) + XCTAssertEqual(person.displayName, personSnapshot2.$displayName) var personSnapshot3 = personSnapshot2 - personSnapshot3.name = "James" - XCTAssertEqual(personSnapshot1.name, "John") - XCTAssertEqual(personSnapshot1.displayName, "Mr. John") - XCTAssertEqual(personSnapshot2.name, "John") - XCTAssertEqual(personSnapshot2.displayName, "Sir John") - XCTAssertEqual(personSnapshot3.name, "James") - XCTAssertEqual(personSnapshot3.displayName, "Sir John") + personSnapshot3.$name = "James" + XCTAssertEqual(personSnapshot1.$name, "John") + XCTAssertEqual(personSnapshot1.$displayName, "Mr. John") + XCTAssertEqual(personSnapshot2.$name, "John") + XCTAssertEqual(personSnapshot2.$displayName, "Sir John") + XCTAssertEqual(personSnapshot3.$name, "James") + XCTAssertEqual(personSnapshot3.$displayName, "Sir John") person.pets.value.insert(dog) @@ -338,20 +336,20 @@ class DynamicModelTests: BaseTestDataTestCase { stack.perform( asynchronous: { (transaction) in - let p1 = Where({ $0.species == "Sparrow" }) + let p1 = Where({ $0.$species == "Sparrow" }) XCTAssertEqual(p1.predicate, NSPredicate(format: "%K == %@", "species", "Sparrow")) let bird = try transaction.fetchOne(From(), p1) XCTAssertNotNil(bird) - XCTAssertEqual(bird!.species.value, "Sparrow") + XCTAssertEqual(bird!.species, "Sparrow") - let p2 = Where({ $0.nickname == "Spot" }) + let p2 = Where({ $0.$nickname == "Spot" }) XCTAssertEqual(p2.predicate, NSPredicate(format: "%K == %@", "nickname", "Spot")) - let dog = try transaction.fetchOne(From().where(\.nickname == "Spot")) + let dog = try transaction.fetchOne(From().where(\.$nickname == "Spot")) XCTAssertNotNil(dog) - XCTAssertEqual(dog!.nickname.value, "Spot") - XCTAssertEqual(dog!.species.value, "Dog") + XCTAssertEqual(dog!.nickname, "Spot") + XCTAssertEqual(dog!.species, "Dog") let person = try transaction.fetchOne(From()) XCTAssertNotNil(person) @@ -365,12 +363,12 @@ class DynamicModelTests: BaseTestDataTestCase { _ = try transaction.fetchAll( From() - .where(\Animal.species == "Dog" && \Dog.age == 10) + .where(\Animal.$species == "Dog" && \Dog.age == 10) ) _ = try transaction.fetchAll( From() - .where(\Dog.age == 10 && \Animal.species == "Dog") - .orderBy(.ascending({ $0.species })) + .where(\Dog.age == 10 && \Animal.$species == "Dog") + .orderBy(.ascending({ $0.$species })) ) _ = try transaction.fetchAll( From(), @@ -378,11 +376,11 @@ class DynamicModelTests: BaseTestDataTestCase { ) _ = try transaction.fetchAll( From(), - Where({ $0.species == "Dog" && $0.age == 10 }) + Where({ $0.$species == "Dog" && $0.age == 10 }) ) _ = try transaction.fetchAll( From(), - Where({ $0.age == 10 && $0.species == "Dog" }) + Where({ $0.age == 10 && $0.$species == "Dog" }) ) _ = try transaction.fetchAll( From(), @@ -409,8 +407,8 @@ class DynamicModelTests: BaseTestDataTestCase { @objc dynamic func test_ThatDynamicModelKeyPaths_CanBeCreated() { - XCTAssertEqual(String(keyPath: \Animal.species), "species") - XCTAssertEqual(String(keyPath: \Dog.species), "species") + XCTAssertEqual(String(keyPath: \Animal.$species), "species") + XCTAssertEqual(String(keyPath: \Dog.$species), "species") } @nonobjc diff --git a/CoreStoreTests/ListObserverTests.swift b/CoreStoreTests/ListObserverTests.swift index 7257df9..ac1bb5a 100644 --- a/CoreStoreTests/ListObserverTests.swift +++ b/CoreStoreTests/ListObserverTests.swift @@ -53,7 +53,7 @@ class ListObserverTests: BaseTestDataTestCase { var events = 0 - let willChangeExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"), object: observer, handler: { (note) -> Bool in @@ -67,7 +67,7 @@ class ListObserverTests: BaseTestDataTestCase { return events == 0 } ) - let didInsertSectionExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitor:didInsertSection:toSectionIndex:"), object: observer, handler: { (note) -> Bool in @@ -87,7 +87,7 @@ class ListObserverTests: BaseTestDataTestCase { return events == 1 } ) - let didInsertObjectExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitor:didInsertObject:toIndexPath:"), object: observer, handler: { (note) -> Bool in @@ -119,7 +119,7 @@ class ListObserverTests: BaseTestDataTestCase { return events == 2 } ) - let didChangeExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"), object: observer, handler: { (note) -> Bool in @@ -184,7 +184,7 @@ class ListObserverTests: BaseTestDataTestCase { var events = 0 - let willChangeExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"), object: observer, handler: { (note) -> Bool in @@ -199,7 +199,7 @@ class ListObserverTests: BaseTestDataTestCase { } ) - let didUpdateObjectExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitor:didUpdateObject:atIndexPath:"), object: observer, handler: { (note) -> Bool in @@ -250,7 +250,7 @@ class ListObserverTests: BaseTestDataTestCase { return events == 1 || events == 2 } ) - let didChangeExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"), object: observer, handler: { (note) -> Bool in @@ -329,7 +329,7 @@ class ListObserverTests: BaseTestDataTestCase { var events = 0 - let willChangeExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"), object: observer, handler: { (note) -> Bool in @@ -343,7 +343,7 @@ class ListObserverTests: BaseTestDataTestCase { return events == 0 } ) - let didMoveObjectExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitor:didMoveObject:fromIndexPath:toIndexPath:"), object: observer, handler: { (note) -> Bool in @@ -376,7 +376,7 @@ class ListObserverTests: BaseTestDataTestCase { return events == 1 } ) - let didChangeExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"), object: observer, handler: { (note) -> Bool in @@ -437,7 +437,7 @@ class ListObserverTests: BaseTestDataTestCase { var events = 0 - let willChangeExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"), object: observer, handler: { (note) -> Bool in @@ -451,7 +451,7 @@ class ListObserverTests: BaseTestDataTestCase { return events == 0 } ) - let didUpdateObjectExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitor:didDeleteObject:fromIndexPath:"), object: observer, handler: { (note) -> Bool in @@ -480,7 +480,7 @@ class ListObserverTests: BaseTestDataTestCase { return events == 1 || events == 2 } ) - let didDeleteSectionExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitor:didDeleteSection:fromSectionIndex:"), object: observer, handler: { (note) -> Bool in @@ -508,7 +508,7 @@ class ListObserverTests: BaseTestDataTestCase { return events == 3 } ) - let didChangeExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"), object: observer, handler: { (note) -> Bool in diff --git a/CoreStoreTests/ObjectObserverTests.swift b/CoreStoreTests/ObjectObserverTests.swift index df92521..4072916 100644 --- a/CoreStoreTests/ObjectObserverTests.swift +++ b/CoreStoreTests/ObjectObserverTests.swift @@ -57,7 +57,7 @@ class ObjectObserverTests: BaseTestDataTestCase { var events = 0 - let willUpdateExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "objectMonitor:willUpdateObject:"), object: observer, handler: { (note) -> Bool in @@ -74,7 +74,7 @@ class ObjectObserverTests: BaseTestDataTestCase { return events == 0 } ) - let didUpdateExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "objectMonitor:didUpdateObject:changedPersistentKeys:"), object: observer, handler: { (note) -> Bool in @@ -154,7 +154,7 @@ class ObjectObserverTests: BaseTestDataTestCase { var events = 0 - let didDeleteExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "objectMonitor:didDeleteObject:"), object: observer, handler: { (note) -> Bool in diff --git a/CoreStoreTests/TransactionTests.swift b/CoreStoreTests/TransactionTests.swift index a26e15b..e91bfb7 100644 --- a/CoreStoreTests/TransactionTests.swift +++ b/CoreStoreTests/TransactionTests.swift @@ -400,7 +400,7 @@ final class TransactionTests: BaseTestCase { XCTAssertFalse(monitor.hasObjects()) var events = 0 - let willChangeExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"), object: observer, handler: { (note) -> Bool in @@ -414,7 +414,7 @@ final class TransactionTests: BaseTestCase { return events == 0 } ) - let didInsertObjectExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitor:didInsertObject:toIndexPath:"), object: observer, handler: { (note) -> Bool in @@ -444,7 +444,7 @@ final class TransactionTests: BaseTestCase { return events == 1 } ) - let didChangeExpectation = self.expectation( + _ = self.expectation( forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"), object: observer, handler: { (note) -> Bool in diff --git a/CoreStoreTests/WhereTests.swift b/CoreStoreTests/WhereTests.swift index d9b2ed5..9ceec1f 100644 --- a/CoreStoreTests/WhereTests.swift +++ b/CoreStoreTests/WhereTests.swift @@ -109,8 +109,8 @@ final class WhereTests: XCTestCase { ) XCTAssertAllEqual( "master.pets.species", - (\Animal.master ~ \.pets ~ \.species).description, - String(keyPath: \Animal.master ~ \.pets ~ \.species) + (\Animal.master ~ \.pets ~ \.$species).description, + String(keyPath: \Animal.master ~ \.pets ~ \.$species) ) XCTAssertAllEqual( "master.pets.master", @@ -167,8 +167,8 @@ final class WhereTests: XCTestCase { ) XCTAssertAllEqual( "ANY master.pets.species", - (\Animal.master ~ \.pets ~ \.species).any().description, - String(keyPath: (\Animal.master ~ \.pets ~ \.species).any()) + (\Animal.master ~ \.pets ~ \.$species).any().description, + String(keyPath: (\Animal.master ~ \.pets ~ \.$species).any()) ) } } @@ -196,8 +196,8 @@ final class WhereTests: XCTestCase { ) XCTAssertAllEqual( "ALL master.pets.species", - (\Animal.master ~ \.pets ~ \.species).all().description, - String(keyPath: (\Animal.master ~ \.pets ~ \.species).all()) + (\Animal.master ~ \.pets ~ \.$species).all().description, + String(keyPath: (\Animal.master ~ \.pets ~ \.$species).all()) ) } } @@ -225,8 +225,8 @@ final class WhereTests: XCTestCase { ) XCTAssertAllEqual( "NONE master.pets.species", - (\Animal.master ~ \.pets ~ \.species).none().description, - String(keyPath: (\Animal.master ~ \.pets ~ \.species).none()) + (\Animal.master ~ \.pets ~ \.$species).none().description, + String(keyPath: (\Animal.master ~ \.pets ~ \.$species).none()) ) } } @@ -247,7 +247,7 @@ final class WhereTests: XCTestCase { } do { - let whereClause: Where = (\.master ~ \.name) == dummy + let whereClause: Where = (\.master ~ \.$name) == dummy let predicate = NSPredicate(format: "master.name == %@", dummy) XCTAssertAllEqual(whereClause, Where(predicate)) XCTAssertAllEqual(whereClause.predicate, predicate) @@ -265,7 +265,7 @@ final class WhereTests: XCTestCase { } do { - let whereClause: Where = (\.master ~ \.spouse ~ \.name) == dummy + let whereClause: Where = (\.master ~ \.spouse ~ \.$name) == dummy let predicate = NSPredicate(format: "master.spouse.name == %@", dummy) XCTAssertAllEqual(whereClause, Where(predicate)) XCTAssertAllEqual(whereClause.predicate, predicate) @@ -301,7 +301,7 @@ final class WhereTests: XCTestCase { } do { - let whereClause: Where = (\.master ~ \.pets ~ \.species).any() == dummy + let whereClause: Where = (\.master ~ \.pets ~ \.$species).any() == dummy let predicate = NSPredicate(format: "ANY master.pets.species == %@", dummy) XCTAssertAllEqual(whereClause, Where(predicate)) XCTAssertAllEqual(whereClause.predicate, predicate) @@ -319,7 +319,7 @@ final class WhereTests: XCTestCase { } do { - let whereClause: Where = (\.master ~ \.pets ~ \.species).all() == dummy + let whereClause: Where = (\.master ~ \.pets ~ \.$species).all() == dummy let predicate = NSPredicate(format: "ALL master.pets.species == %@", dummy) XCTAssertAllEqual(whereClause, Where(predicate)) XCTAssertAllEqual(whereClause.predicate, predicate) @@ -337,7 +337,7 @@ final class WhereTests: XCTestCase { } do { - let whereClause: Where = (\.master ~ \.pets ~ \.species).none() == dummy + let whereClause: Where = (\.master ~ \.pets ~ \.$species).none() == dummy let predicate = NSPredicate(format: "NONE master.pets.species == %@", dummy) XCTAssertAllEqual(whereClause, Where(predicate)) XCTAssertAllEqual(whereClause.predicate, predicate) diff --git a/Sources/AttributeProtocol.swift b/Sources/AttributeProtocol.swift index 5447d9a..01f5fb3 100644 --- a/Sources/AttributeProtocol.swift +++ b/Sources/AttributeProtocol.swift @@ -29,7 +29,7 @@ import CoreData // MARK: - AttributeProtocol -internal protocol AttributeProtocol: PropertyProtocol { +internal protocol AttributeProtocol: AnyObject, PropertyProtocol { typealias EntityDescriptionValues = ( attributeType: NSAttributeType, diff --git a/Sources/CoreStore+CustomDebugStringConvertible.swift b/Sources/CoreStore+CustomDebugStringConvertible.swift index 4eb4144..1825acd 100644 --- a/Sources/CoreStore+CustomDebugStringConvertible.swift +++ b/Sources/CoreStore+CustomDebugStringConvertible.swift @@ -94,9 +94,10 @@ extension CoreStoreError: CustomDebugStringConvertible, CoreStoreDebugStringConv firstLine = ".progressiveMigrationRequired" info.append(("localStoreURL", localStoreURL)) - case .asynchronousMigrationRequired(let localStoreURL): + case .asynchronousMigrationRequired(let localStoreURL, let NSError): firstLine = ".asynchronousMigrationRequired" info.append(("localStoreURL", localStoreURL)) + info.append(("NSError", NSError)) case .internalError(let NSError): firstLine = ".internalError" diff --git a/Sources/CoreStoreError.swift b/Sources/CoreStoreError.swift index ad19859..e2a5dd9 100644 --- a/Sources/CoreStoreError.swift +++ b/Sources/CoreStoreError.swift @@ -198,9 +198,6 @@ public enum CoreStoreError: Error, CustomNSError, Hashable { case (.userError(let error1), .userError(let error2)): switch (error1, error2) { - - case (let error1 as AnyHashable, let error2 as AnyHashable): - return error1 == error2 case (let error1 as NSError, let error2 as NSError): return error1.isEqual(error2) diff --git a/Sources/CoreStoreObject+DataSources.swift b/Sources/CoreStoreObject+DataSources.swift index 52155f4..2a18563 100644 --- a/Sources/CoreStoreObject+DataSources.swift +++ b/Sources/CoreStoreObject+DataSources.swift @@ -1,9 +1,26 @@ // // CoreStoreObject+DataSources.swift -// CoreStore iOS +// CoreStore // -// Created by John Estropia on 2019/10/04. -// Copyright © 2019 John Rommel Estropia. All rights reserved. +// Copyright © 2020 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. // #if canImport(UIKit) || canImport(AppKit) diff --git a/Sources/CoreStoreObject+Observing.swift b/Sources/CoreStoreObject+Observing.swift index b9f4996..a04f839 100644 --- a/Sources/CoreStoreObject+Observing.swift +++ b/Sources/CoreStoreObject+Observing.swift @@ -27,6 +27,33 @@ import Foundation import CoreData +extension DynamicObject where Self: CoreStoreObject { + + public func observe(_ keyPath: KeyPath.Stored>, options: NSKeyValueObservingOptions = [], changeHandler: @escaping (Self, CoreStoreObjectValueDiff) -> Void) -> CoreStoreObjectKeyValueObservation { + + let result = _CoreStoreObjectKeyValueObservation( + object: self.rawObject!, + keyPath: self[keyPath: keyPath].keyPath, + callback: { (object, kind, newValue, oldValue, _, isPrior) in + + let notification = CoreStoreObjectValueDiff( + kind: kind, + newNativeValue: newValue as? V.QueryableNativeType, + oldNativeValue: oldValue as? V.QueryableNativeType, + isPrior: isPrior + ) + changeHandler( + Self.cs_fromRaw(object: object), + notification + ) + } + ) + result.start(options) + return result + } +} + + // MARK: CoreStoreObjectKeyValueObservation /** diff --git a/Sources/CoreStoreObject+Querying.swift b/Sources/CoreStoreObject+Querying.swift index 8d21e91..c63b937 100644 --- a/Sources/CoreStoreObject+Querying.swift +++ b/Sources/CoreStoreObject+Querying.swift @@ -26,6 +26,88 @@ import CoreData import Foundation +// MARK: - FieldContainer.Value + +extension FieldContainer.Stored { + + /** + Creates a `Where` clause by comparing if a property is equal to a value + ``` + let person = dataStack.fetchOne(From().where({ $0.nickname == "John" })) + ``` + */ + public static func == (_ attribute: Self, _ value: V) -> Where { + + return Where(attribute.keyPath, isEqualTo: value) + } + + /** + Creates a `Where` clause by comparing if a property is not equal to a value + ``` + let person = dataStack.fetchOne(From().where({ $0.nickname != "John" })) + ``` + */ + public static func != (_ attribute: Self, _ value: V) -> Where { + + return !Where(attribute.keyPath, isEqualTo: value) + } + + /** + Creates a `Where` clause by comparing if a property is less than a value + ``` + let person = dataStack.fetchOne(From().where({ $0.age < 20 })) + ``` + */ + public static func < (_ attribute: Self, _ value: V) -> Where { + + return Where("%K < %@", attribute.keyPath, value.cs_toFieldStoredNativeType() as Any) + } + + /** + Creates a `Where` clause by comparing if a property is greater than a value + ``` + let person = dataStack.fetchOne(From().where({ $0.age > 20 })) + ``` + */ + public static func > (_ attribute: Self, _ value: V) -> Where { + + return Where("%K > %@", attribute.keyPath, value.cs_toFieldStoredNativeType() as Any) + } + + /** + Creates a `Where` clause by comparing if a property is less than or equal to a value + ``` + let person = dataStack.fetchOne(From().where({ $0.age <= 20 })) + ``` + */ + public static func <= (_ attribute: Self, _ value: V) -> Where { + + return Where("%K <= %@", attribute.keyPath, value.cs_toFieldStoredNativeType() as Any) + } + + /** + Creates a `Where` clause by comparing if a property is greater than or equal to a value + ``` + let person = dataStack.fetchOne(From().where({ $0.age >= 20 })) + ``` + */ + public static func >= (_ attribute: Self, _ value: V) -> Where { + + return Where("%K >= %@", attribute.keyPath, value.cs_toFieldStoredNativeType() as Any) + } + + /** + Creates a `Where` clause by checking if a sequence contains the value of a property + ``` + let dog = dataStack.fetchOne(From().where({ ["Pluto", "Snoopy", "Scooby"] ~= $0.nickname })) + ``` + */ + public static func ~= (_ sequence: S, _ attribute: Self) -> Where where S.Iterator.Element == V { + + return Where(attribute.keyPath, isMemberOf: sequence) + } +} + // MARK: - ValueContainer.Required extension ValueContainer.Required { diff --git a/Sources/CoreStoreObject.swift b/Sources/CoreStoreObject.swift index 6040d85..001a646 100644 --- a/Sources/CoreStoreObject.swift +++ b/Sources/CoreStoreObject.swift @@ -69,6 +69,11 @@ open /*abstract*/ class CoreStoreObject: DynamicObject, Hashable { self.isMeta = false self.rawObject = (rawObject as! CoreStoreManagedObject) + + guard Self.meta.needsReflection else { + + return + } self.registerReceiver( mirror: Mirror(reflecting: self), object: self @@ -117,6 +122,11 @@ open /*abstract*/ class CoreStoreObject: DynamicObject, Hashable { internal let rawObject: CoreStoreManagedObject? internal let isMeta: Bool + internal lazy var needsReflection: Bool = self.containsLegacyAttributes( + mirror: Mirror(reflecting: self), + object: self + ) + internal class func metaProperties(includeSuperclasses: Bool) -> [PropertyProtocol] { func keyPaths(_ allKeyPaths: inout [PropertyProtocol], for dynamicType: CoreStoreObject.Type) { @@ -137,9 +147,33 @@ open /*abstract*/ class CoreStoreObject: DynamicObject, Hashable { keyPaths(&allKeyPaths, for: self) return allKeyPaths } - + // MARK: Private + + private func containsLegacyAttributes(mirror: Mirror, object: CoreStoreObject) -> Bool { + + if let superclassMirror = mirror.superclassMirror, + self.containsLegacyAttributes(mirror: superclassMirror, object: object) { + + return true + } + for child in mirror.children { + + switch child.value { + + case is AttributeProtocol: + return true + + case is RelationshipProtocol: + return true + + default: + continue + } + } + return false + } private func registerReceiver(mirror: Mirror, object: CoreStoreObject) { diff --git a/Sources/CoreStoreSchema.swift b/Sources/CoreStoreSchema.swift index e6ff4e1..39252ad 100644 --- a/Sources/CoreStoreSchema.swift +++ b/Sources/CoreStoreSchema.swift @@ -286,6 +286,25 @@ public final class CoreStoreSchema: DynamicSchema { for property in type.metaProperties(includeSuperclasses: false) { switch property { + + case let attribute as FieldAttributeProtocol: + Internals.assert( + !NSManagedObject.instancesRespond(to: Selector(attribute.keyPath)), + "Attribute Property name \"\(String(reflecting: entity.type)).\(attribute.keyPath)\" is not allowed because it collides with \"\(String(reflecting: NSManagedObject.self)).\(attribute.keyPath)\"" + ) + let entityDescriptionValues = attribute.entityDescriptionValues() + let description = NSAttributeDescription() + description.name = attribute.keyPath + description.attributeType = entityDescriptionValues.attributeType + description.isOptional = entityDescriptionValues.isOptional + description.defaultValue = entityDescriptionValues.defaultValue + description.isTransient = entityDescriptionValues.isTransient + description.allowsExternalBinaryDataStorage = entityDescriptionValues.allowsExternalBinaryDataStorage + description.versionHashModifier = entityDescriptionValues.versionHashModifier + description.renamingIdentifier = entityDescriptionValues.renamingIdentifier + propertyDescriptions.append(description) + keyPathsByAffectedKeyPaths[attribute.keyPath] = entityDescriptionValues.affectedByKeyPaths + customGetterSetterByKeyPaths[attribute.keyPath] = (attribute.getter, attribute.setter) case let attribute as AttributeProtocol: Internals.assert( diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index f3fb9ef..101d580 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -153,41 +153,68 @@ extension CoreStoreObject { public class func cs_snapshotDictionary(id: ObjectID, context: NSManagedObjectContext) -> [String: Any]? { - func initializeAttributes(mirror: Mirror, object: Self, into attributes: inout [KeyPathString: Any]) { + var values: [KeyPathString: Any] = [:] + if self.meta.needsReflection { - if let superClassMirror = mirror.superclassMirror { + func initializeAttributes(mirror: Mirror, object: Self, into attributes: inout [KeyPathString: Any]) { - initializeAttributes( - mirror: superClassMirror, - object: object, - into: &attributes - ) + if let superClassMirror = mirror.superclassMirror { + + initializeAttributes( + mirror: superClassMirror, + object: object, + into: &attributes + ) + } + for child in mirror.children { + + switch child.value { + + case let property as FieldAttributeProtocol: + attributes[property.keyPath] = type(of: property).read(field: property, for: object.rawObject!) + + case let property as AttributeProtocol: + attributes[property.keyPath] = property.valueForSnapshot + + case let property as RelationshipProtocol: + attributes[property.keyPath] = property.valueForSnapshot + + default: + continue + } + } } - for child in mirror.children { + guard let object = context.fetchExisting(id) as CoreStoreObject? else { - switch child.value { + return nil + } + initializeAttributes( + mirror: Mirror(reflecting: object), + object: object as! Self, + into: &values + ) + } + else { - case let property as AttributeProtocol: - attributes[property.keyPath] = property.valueForSnapshot + guard + let object = context.fetchExisting(id) as CoreStoreObject?, + let rawObject = object.rawObject + else { - case let property as RelationshipProtocol: - attributes[property.keyPath] = property.valueForSnapshot + return nil + } + for property in self.metaProperties(includeSuperclasses: true) { + + switch property { + + case let property as FieldAttributeProtocol: + values[property.keyPath] = type(of: property).read(field: property, for: rawObject) default: continue } } } - guard let object = context.fetchExisting(id) as CoreStoreObject? else { - - return nil - } - var values: [KeyPathString: Any] = [:] - initializeAttributes( - mirror: Mirror(reflecting: object), - object: object as! Self, - into: &values - ) return values } diff --git a/Sources/DynamicObjectMeta.swift b/Sources/DynamicObjectMeta.swift deleted file mode 100644 index dec8602..0000000 --- a/Sources/DynamicObjectMeta.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// DynamicObjectMeta.swift -// CoreStore iOS -// -// Created by John Estropia on 2019/08/20. -// Copyright © 2019 John Rommel Estropia. All rights reserved. -// - -#if swift(>=5.1) - -import CoreData -import Foundation - - -// MARK: - DynamicObjectMeta - -@dynamicMemberLookup -public struct DynamicObjectMeta: CustomDebugStringConvertible { - - // MARK: Public - - public typealias Root = R - public typealias Destination = D - - - // MARK: CustomDebugStringConvertible - - public var debugDescription: String { - - return self.keyPathString - } - - - // MARK: Internal - - internal let keyPathString: KeyPathString - - internal init(keyPathString: KeyPathString) { - - self.keyPathString = keyPathString - } - - internal func appending(keyPathString: KeyPathString) -> DynamicObjectMeta<(R, D), D2> { - - return .init(keyPathString: [self.keyPathString, keyPathString].joined(separator: ".")) - } -} - - -// MARK: - DynamicObjectMeta where Destination: NSManagedObject - -extension DynamicObjectMeta where Destination: NSManagedObject { - - /** - Returns the value for the property identified by a given key. - */ - public subscript(dynamicMember member: KeyPath) -> DynamicObjectMeta<(Root, Destination), V.ReturnValueType> { - - let keyPathString = String(keyPath: member) - return self.appending(keyPathString: keyPathString) - } - - /** - Returns the value for the property identified by a given key. - */ - public subscript(dynamicMember member: KeyPath) -> DynamicObjectMeta<(Root, Destination), V> { - - let keyPathString = String(keyPath: member) - return self.appending(keyPathString: keyPathString) - } - - /** - Returns the value for the property identified by a given key. - */ - public subscript(dynamicMember member: KeyPath) -> DynamicObjectMeta<(Root, Destination), V> { - - // TODO: not working - let keyPathString = String(keyPath: member) - return self.appending(keyPathString: keyPathString) - } - - /** - Returns the value for the property identified by a given key. - */ - public subscript(dynamicMember member: KeyPath) -> DynamicObjectMeta<(Root, Destination), V> { - - let keyPathString = String(keyPath: member) - return self.appending(keyPathString: keyPathString) - } - - /** - Returns the value for the property identified by a given key. - */ - public subscript(dynamicMember member: KeyPath) -> DynamicObjectMeta<(Root, Destination), V> { - - let keyPathString = String(keyPath: member) - return self.appending(keyPathString: keyPathString) - } - - /** - Returns the value for the property identified by a given key. - */ - public subscript(dynamicMember member: KeyPath) -> DynamicObjectMeta<(Root, Destination), V> { - - let keyPathString = String(keyPath: member) - return self.appending(keyPathString: keyPathString) - } - - /** - Returns the value for the property identified by a given key. - */ - public subscript(dynamicMember member: KeyPath) -> DynamicObjectMeta<(Root, Destination), V> { - - let keyPathString = String(keyPath: member) - return self.appending(keyPathString: keyPathString) - } -} - - -// MARK: - DynamicObjectMeta where Destination: CoreStoreObject - -extension DynamicObjectMeta where Destination: CoreStoreObject { - - /** - Returns the value for the property identified by a given key. - */ - public subscript(dynamicMember member: KeyPath) -> DynamicObjectMeta<(Root, Destination), K.ReturnValueType> { - - let keyPathString = String(keyPath: member) - return self.appending(keyPathString: keyPathString) - } - - /** - Returns the value for the property identified by a given key. - */ - public subscript(dynamicMember member: KeyPath) -> DynamicObjectMeta<(Root, Destination), K.DestinationValueType> { - - let keyPathString = String(keyPath: member) - return self.appending(keyPathString: keyPathString) - } -} - -#endif diff --git a/Sources/Field.Computed.swift b/Sources/Field.Computed.swift new file mode 100644 index 0000000..1bd55dd --- /dev/null +++ b/Sources/Field.Computed.swift @@ -0,0 +1,305 @@ +// +// Field.Computed.swift +// CoreStore +// +// Copyright © 2020 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: - FieldContainer + +extension FieldContainer { + + // MARK: - Computed + + /** + The containing type for computed property values. Any type that conforms to `FieldStorableType` are supported. + ``` + class Animal: CoreStoreObject { + @Field.Stored("species") + var species = "" + + @Field.Computed("pluralName", customGetter: Animal.pluralName(_:)) + var pluralName: String = "" + + @Field.PlistCoded("color") + var color: UIColor? + } + ``` + - Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Computed(...) var` syntax will be ignored. + */ + @propertyWrapper + public struct Computed: AttributeKeyPathStringConvertible, FieldAttributeProtocol { + + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + @Field.Stored("title") + var title: String = "Mr." + + @Field.Stored("name") + var name: String = "" + + @Field.Computed("displayName", customGetter: Person.getName(_:)) + var displayName: String = "" + + private static func getName(_ partialObject: PartialObject) -> String { + let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + if !cachedDisplayName.isEmpty { + return cachedDisplayName + } + let title = partialObject.value(for: \.$title) + let name = partialObject.value(for: \.$name) + let displayName = "\(title) \(name)" + partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + return displayName + } + } + ``` + - parameter keyPath: the permanent attribute name for this property. + - parameter renamingIdentifier: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property and a destination entity property that share the same identifier indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's name. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ + public init( + _ keyPath: KeyPathString, + renamingIdentifier: @autoclosure @escaping () -> String? = nil, + customGetter: ((_ partialObject: PartialObject) -> V)? = nil, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { + + self.init( + keyPath: keyPath, + isOptional: false, + renamingIdentifier: renamingIdentifier, + customGetter: customGetter, + customSetter: customSetter, + affectedByKeyPaths: affectedByKeyPaths + ) + } + + /** + Overload for compiler message only + */ + @available(*, unavailable, message: "Field.Computed properties are not allowed to have default values.") + public init( + wrappedValue initial: @autoclosure @escaping () -> V, + _ keyPath: KeyPathString, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + renamingIdentifier: @autoclosure @escaping () -> String? = nil, + customGetter: ((_ partialObject: PartialObject) -> V)? = nil, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { + + fatalError() + } + + + // MARK: @propertyWrapper + + @available(*, unavailable) + public var wrappedValue: V { + + get { fatalError() } + set { fatalError() } + } + + public var projectedValue: Self { + + return self + } + + public static subscript( + _enclosingInstance instance: O, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath + ) -> V { + + get { + + Internals.assert( + instance.rawObject != nil, + "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." + ) + return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V + } + set { + + Internals.assert( + instance.rawObject != nil, + "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." + ) + return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue) + } + } + + + // MARK: AnyKeyPathStringConvertible + + public var cs_keyPathString: String { + + return self.keyPath + } + + + // MARK: KeyPathStringConvertible + + public typealias ObjectType = O + public typealias DestinationValueType = V + + + // MARK: AttributeKeyPathStringConvertible + + public typealias ReturnValueType = DestinationValueType + + + // MARK: PropertyProtocol + + internal let keyPath: KeyPathString + + + // MARK: FieldProtocol + + internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { + + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + let field = field as! Self + if let customGetter = field.customGetter { + + return customGetter(PartialObject(rawObject)) + } + let keyPath = field.keyPath + switch rawObject.value(forKey: keyPath) { + + case let rawValue as V: + return rawValue + + default: + return nil + } + } + + internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) { + + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + Internals.assert( + rawObject.isEditableInContext() == true, + "Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction." + ) + let newValue = newValue as! V + let field = field as! Self + let keyPath = field.keyPath + if let customSetter = field.customSetter { + + return customSetter(PartialObject(rawObject), newValue) + } + return rawObject.setValue(newValue, forKey: keyPath) + } + + + // MARK: FieldAttributeProtocol + + internal let entityDescriptionValues: () -> FieldAttributeProtocol.EntityDescriptionValues + + internal var getter: CoreStoreManagedObject.CustomGetter? { + + guard let customGetter = self.customGetter else { + + return nil + } + let keyPath = self.keyPath + return { (_ id: Any) -> Any? in + + let rawObject = id as! CoreStoreManagedObject + rawObject.willAccessValue(forKey: keyPath) + defer { + + rawObject.didAccessValue(forKey: keyPath) + } + return customGetter(PartialObject(rawObject)) + } + } + + internal var setter: CoreStoreManagedObject.CustomSetter? { + + guard let customSetter = self.customSetter else { + + return nil + } + let keyPath = self.keyPath + return { (_ id: Any, _ newValue: Any?) -> Void in + + let rawObject = id as! CoreStoreManagedObject + rawObject.willChangeValue(forKey: keyPath) + defer { + + rawObject.didChangeValue(forKey: keyPath) + } + return customSetter(PartialObject(rawObject), newValue as! V) + } + } + + + // MARK: FilePrivate + + fileprivate init( + keyPath: KeyPathString, + isOptional: Bool, + renamingIdentifier: @escaping () -> String?, + customGetter: ((_ partialObject: PartialObject) -> V)?, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? , + affectedByKeyPaths: @escaping () -> Set) { + + self.keyPath = keyPath + self.entityDescriptionValues = { + ( + attributeType: .undefinedAttributeType, + isOptional: isOptional, + isTransient: true, + allowsExternalBinaryDataStorage: false, + versionHashModifier: nil, + renamingIdentifier: renamingIdentifier(), + affectedByKeyPaths: affectedByKeyPaths(), + defaultValue: nil + ) + } + self.customGetter = customGetter + self.customSetter = customSetter + } + + + // MARK: Private + + private let customGetter: ((_ partialObject: PartialObject) -> V)? + private let customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? + } +} diff --git a/Sources/Field.PlistCoded.swift b/Sources/Field.PlistCoded.swift new file mode 100644 index 0000000..192ae0d --- /dev/null +++ b/Sources/Field.PlistCoded.swift @@ -0,0 +1,9 @@ +// +// Field.PlistCoded.swift +// CoreStore +// +// Created by John Estropia on 2020/01/15. +// Copyright © 2020 John Rommel Estropia. All rights reserved. +// + +import Foundation diff --git a/Sources/Field.Stored.swift b/Sources/Field.Stored.swift new file mode 100644 index 0000000..d9a7b0a --- /dev/null +++ b/Sources/Field.Stored.swift @@ -0,0 +1,362 @@ +// +// Field.Stored.swift +// CoreStore +// +// Copyright © 2020 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: - FieldContainer + +extension FieldContainer { + + // MARK: - Stored + + /** + The containing type for stored property values. Any type that conforms to `FieldStorableType` are supported. + ``` + class Animal: CoreStoreObject { + @Field.Stored("species") + var species = "" + + @Field.Computed("pluralName", customGetter: Animal.pluralName(_:)) + var pluralName: String = "" + + @Field.PlistCoded("color") + var color: UIColor? + } + ``` + - Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Stored(...) var` syntax will be ignored. + */ + @propertyWrapper + public struct Stored: AttributeKeyPathStringConvertible, FieldAttributeProtocol { + + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + @Field.Stored("title") + var title: String = "Mr." + + @Field.Stored("name") + var name: String = "" + + @Field.Computed("displayName", customGetter: Person.getName(_:)) + var displayName: String = "" + + private static func getName(_ partialObject: PartialObject) -> String { + let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + if !cachedDisplayName.isEmpty { + return cachedDisplayName + } + let title = partialObject.value(for: \.$title) + let name = partialObject.value(for: \.$name) + let displayName = "\(title) \(name)" + partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + return displayName + } + } + ``` + - parameter initial: the initial value for the property when the object is first create + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter renamingIdentifier: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property and a destination entity property that share the same identifier indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's name. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ + public init( + wrappedValue initial: @autoclosure @escaping () -> V, + _ keyPath: KeyPathString, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + renamingIdentifier: @autoclosure @escaping () -> String? = nil, + customGetter: ((_ partialObject: PartialObject) -> V)? = nil, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { + + self.init( + wrappedValue: initial, + keyPath: keyPath, + isOptional: false, + versionHashModifier: versionHashModifier, + renamingIdentifier: renamingIdentifier, + customGetter: customGetter, + customSetter: customSetter, + affectedByKeyPaths: affectedByKeyPaths + ) + } + + + // MARK: @propertyWrapper + + @available(*, unavailable) + public var wrappedValue: V { + + get { fatalError() } + set { fatalError() } + } + + public var projectedValue: Self { + + return self + } + + public static subscript( + _enclosingInstance instance: O, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath + ) -> V { + + get { + + Internals.assert( + instance.rawObject != nil, + "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." + ) + return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V + } + set { + + Internals.assert( + instance.rawObject != nil, + "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." + ) + return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue) + } + } + + + // MARK: AnyKeyPathStringConvertible + + public var cs_keyPathString: String { + + return self.keyPath + } + + + // MARK: KeyPathStringConvertible + + public typealias ObjectType = O + public typealias DestinationValueType = V + + + // MARK: AttributeKeyPathStringConvertible + + public typealias ReturnValueType = DestinationValueType + + + // MARK: PropertyProtocol + + internal let keyPath: KeyPathString + + + // MARK: FieldProtocol + + internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { + + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + let field = field as! Self + if let customGetter = field.customGetter { + + return customGetter(PartialObject(rawObject)) + } + let keyPath = field.keyPath + guard case let rawValue as V.FieldStoredNativeType = rawObject.value(forKey: keyPath) else { + + return nil + } + return V.cs_fromFieldStoredNativeType(rawValue) + } + + internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) { + + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + Internals.assert( + rawObject.isEditableInContext() == true, + "Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction." + ) + let newValue = newValue as! V + let field = field as! Self + let keyPath = field.keyPath + if let customSetter = field.customSetter { + + return customSetter(PartialObject(rawObject), newValue) + } + return rawObject.setValue( + newValue.cs_toFieldStoredNativeType(), + forKey: keyPath + ) + } + + + // MARK: FieldAttributeProtocol + + internal let entityDescriptionValues: () -> FieldAttributeProtocol.EntityDescriptionValues + + internal var getter: CoreStoreManagedObject.CustomGetter? { + + guard let customGetter = self.customGetter else { + + return nil + } + let keyPath = self.keyPath + return { (_ id: Any) -> Any? in + + let rawObject = id as! CoreStoreManagedObject + rawObject.willAccessValue(forKey: keyPath) + defer { + + rawObject.didAccessValue(forKey: keyPath) + } + let value = customGetter(PartialObject(rawObject)) + return value.cs_toFieldStoredNativeType() + } + } + + internal var setter: CoreStoreManagedObject.CustomSetter? { + + guard let customSetter = self.customSetter else { + + return nil + } + let keyPath = self.keyPath + return { (_ id: Any, _ newValue: Any?) -> Void in + + let rawObject = id as! CoreStoreManagedObject + rawObject.willChangeValue(forKey: keyPath) + defer { + + rawObject.didChangeValue(forKey: keyPath) + } + customSetter( + PartialObject(rawObject), + V.cs_fromFieldStoredNativeType(newValue as! V.FieldStoredNativeType) + ) + } + } + + + // MARK: FilePrivate + + fileprivate init( + wrappedValue initial: @escaping () -> V, + keyPath: KeyPathString, + isOptional: Bool, + versionHashModifier: @escaping () -> String?, + renamingIdentifier: @escaping () -> String?, + customGetter: ((_ partialObject: PartialObject) -> V)?, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? , + affectedByKeyPaths: @escaping () -> Set) { + + self.keyPath = keyPath + self.entityDescriptionValues = { + ( + attributeType: V.cs_rawAttributeType, + isOptional: isOptional, + isTransient: false, + allowsExternalBinaryDataStorage: false, + versionHashModifier: versionHashModifier(), + renamingIdentifier: renamingIdentifier(), + affectedByKeyPaths: affectedByKeyPaths(), + defaultValue: initial().cs_toFieldStoredNativeType() + ) + } + self.customGetter = customGetter + self.customSetter = customSetter + } + + + // MARK: Private + + private let customGetter: ((_ partialObject: PartialObject) -> V)? + private let customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? + } +} + + +// MARK: - FieldContainer.Stored where V: FieldOptionalType + +extension FieldContainer.Stored where V: FieldOptionalType { + + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + @Field.Stored("title") + var title: String = "Mr." + + @Field.Stored("name") + var name: String = "" + + @Field.Computed("displayName", customGetter: Person.getName(_:)) + var displayName: String = "" + + private static func getName(_ partialObject: PartialObject) -> String { + let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + if !cachedDisplayName.isEmpty { + return cachedDisplayName + } + let title = partialObject.value(for: \.$title) + let name = partialObject.value(for: \.$name) + let displayName = "\(title) \(name)" + partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + return displayName + } + } + ``` + - parameter initial: the initial value for the property when the object is first create + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter renamingIdentifier: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property and a destination entity property that share the same identifier indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's name. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ + public init( + wrappedValue initial: @autoclosure @escaping () -> V = nil, + _ keyPath: KeyPathString, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + renamingIdentifier: @autoclosure @escaping () -> String? = nil, + customGetter: ((_ partialObject: PartialObject) -> V)? = nil, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { + + self.init( + wrappedValue: initial, + keyPath: keyPath, + isOptional: true, + versionHashModifier: versionHashModifier, + renamingIdentifier: renamingIdentifier, + customGetter: customGetter, + customSetter: customSetter, + affectedByKeyPaths: affectedByKeyPaths + ) + } +} diff --git a/Sources/Field.swift b/Sources/Field.swift new file mode 100644 index 0000000..19f4df6 --- /dev/null +++ b/Sources/Field.swift @@ -0,0 +1,71 @@ +// +// Field.swift +// CoreStore +// +// Copyright © 2020 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: - DynamicObject + +extension DynamicObject where Self: CoreStoreObject { + + /** + The containing type for value propertiess. `Field` properties support any type that conforms to `ImportableAttributeType`. + ``` + class Animal: CoreStoreObject { + @Field.Stored("species") + var species = "" + + @Field.Stored("nickname") + var nickname: String? + + @Field.PlistCoded("color") + var color: UIColor? + } + ``` + - Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.*(...) var` syntax will be ignored. + */ + public typealias Field = FieldContainer +} + + +// MARK: - FieldContainer + +/** + The containing type for value properties. Use the `DynamicObject.Field` typealias instead for shorter syntax. + ``` + class Animal: CoreStoreObject { + @Field.Stored("species") + var species = "" + + @Field.Stored("nickname") + var nickname: String? + + @Field.PlistCoded("color") + var color: UIColor? + } + ``` + */ +public enum FieldContainer {} diff --git a/Sources/FieldOptionalType.swift b/Sources/FieldOptionalType.swift new file mode 100644 index 0000000..ae7b80b --- /dev/null +++ b/Sources/FieldOptionalType.swift @@ -0,0 +1,56 @@ +// +// FieldOptionalType.swift +// CoreStore +// +// Copyright © 2020 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 Foundation + + +// MARK: - FieldOptionalType + +public protocol FieldOptionalType: ExpressibleByNilLiteral { + + /** + The type for the wrapped value + */ + associatedtype Wrapped + + /** + The wrapped value + */ + var cs_wrappedValue: Wrapped? { get } +} + + +// MARK: - Optional: FieldOptionalType + +extension Optional: FieldOptionalType { + + // MARK: Public + + @inlinable + public var cs_wrappedValue: Wrapped? { + + return self + } +} diff --git a/Sources/FieldProtocol.swift b/Sources/FieldProtocol.swift new file mode 100644 index 0000000..e38e642 --- /dev/null +++ b/Sources/FieldProtocol.swift @@ -0,0 +1,57 @@ +// +// FieldProtocol.swift +// CoreStore +// +// Copyright © 2020 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 Foundation +import CoreData + + +// MARK: - FieldProtocol + +internal protocol FieldProtocol: PropertyProtocol { + + static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? + static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) +} + + +// MARK: - FieldAttributeProtocol + +internal protocol FieldAttributeProtocol: FieldProtocol { + + typealias EntityDescriptionValues = ( + attributeType: NSAttributeType, + isOptional: Bool, + isTransient: Bool, + allowsExternalBinaryDataStorage: Bool, + versionHashModifier: String?, + renamingIdentifier: String?, + affectedByKeyPaths: Set, + defaultValue: Any? + ) + + var entityDescriptionValues: () -> EntityDescriptionValues { get } + var getter: CoreStoreManagedObject.CustomGetter? { get } + var setter: CoreStoreManagedObject.CustomSetter? { get } +} diff --git a/Sources/FieldStorableType.swift b/Sources/FieldStorableType.swift new file mode 100644 index 0000000..5428889 --- /dev/null +++ b/Sources/FieldStorableType.swift @@ -0,0 +1,259 @@ +// +// FieldStorableType.swift +// CoreStore +// +// Copyright © 2020 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 +import CoreGraphics + + +// MARK: - FieldStorableType + +public protocol FieldStorableType { + + /** + The `NSAttributeType` for this type + */ + associatedtype FieldStoredNativeType + + /** + The `NSAttributeType` for this type + */ + static var cs_rawAttributeType: NSAttributeType { get } + + /** + Creates an instance of this type from raw native value. + */ + @inline(__always) + static func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self + + /** + Creates `FieldStoredNativeType` value from this instance. + */ + @inline(__always) + func cs_toFieldStoredNativeType() -> Any? +} + + +// MARK: - FieldStorableType where Self: ImportableAttributeType, FieldStoredNativeType == QueryableNativeType + +extension FieldStorableType where Self: ImportableAttributeType, FieldStoredNativeType == QueryableNativeType { + + @inline(__always) + public static func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self { + + return self.cs_fromQueryableNativeType(value)! + } + + @inline(__always) + public func cs_toFieldStoredNativeType() -> Any? { + + return self.cs_toQueryableNativeType() + } +} + + +// MARK: - Bool + +extension Bool: FieldStorableType {} + + +// MARK: - CGFloat + +extension CGFloat: FieldStorableType {} + + +// MARK: - Data + +extension Data: FieldStorableType {} + + +// MARK: - Date + +extension Date: FieldStorableType {} + + +// MARK: - Double + +extension Double: FieldStorableType {} + + +// MARK: - Float + +extension Float: FieldStorableType {} + + +// MARK: - Int + +extension Int: FieldStorableType {} + + +// MARK: - Int8 + +extension Int8: FieldStorableType {} + + +// MARK: - Int16 + +extension Int16: FieldStorableType {} + + +// MARK: - Int32 + +extension Int32: FieldStorableType {} + + +// MARK: - Int64 + +extension Int64: FieldStorableType {} + + +// MARK: - NSData + +extension NSData: FieldStorableType { + + @nonobjc @inline(__always) + public class func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self { + + return self.cs_fromQueryableNativeType(value)! + } +} + + +// MARK: - NSDate + +extension NSDate: FieldStorableType { + + @nonobjc @inline(__always) + public class func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self { + + return self.cs_fromQueryableNativeType(value)! + } +} + + +// MARK: - NSNumber + +extension NSNumber: FieldStorableType { + + @nonobjc @inline(__always) + public class func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self { + + return self.cs_fromQueryableNativeType(value)! + } +} + + +// MARK: - NSString + +extension NSString: FieldStorableType { + + @nonobjc @inline(__always) + public class func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self { + + return self.cs_fromQueryableNativeType(value)! + } +} + + +// MARK: - NSURL + +extension NSURL: FieldStorableType { + + @nonobjc @inline(__always) + public class func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self { + + return self.cs_fromQueryableNativeType(value)! + } +} + + +// MARK: - NSUUID + +extension NSUUID: FieldStorableType { + + @nonobjc @inline(__always) + public class func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self { + + return self.cs_fromQueryableNativeType(value)! + } +} + + +// MARK: - String + +extension String: FieldStorableType {} + + +// MARK: - URL + +extension URL: FieldStorableType {} + + +// MARK: - UUID + +extension UUID: FieldStorableType {} + + +// MARK: - Optional + +extension Optional: FieldStorableType where Wrapped: FieldStorableType { + + // MARK: FieldStorableType + + public typealias FieldStoredNativeType = Wrapped.FieldStoredNativeType? + + public static var cs_rawAttributeType: NSAttributeType { + + return Wrapped.cs_rawAttributeType + } + + @inline(__always) + public static func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self { + + switch value { + + case nil, + is NSNull: + return nil + + case let value?: + return Wrapped.cs_fromFieldStoredNativeType(value) + } + } + + @inline(__always) + public func cs_toFieldStoredNativeType() -> Any? { + + switch self { + + case nil, + is NSNull: + return nil + + case let value?: + return value.cs_toFieldStoredNativeType() + } + } +} diff --git a/Sources/KeyPath+Querying.swift b/Sources/KeyPath+Querying.swift index f83a8d5..333ae49 100644 --- a/Sources/KeyPath+Querying.swift +++ b/Sources/KeyPath+Querying.swift @@ -359,6 +359,20 @@ public func ~= (_ sequence: } +// MARK: - KeyPath where Root: CoreStoreObject, Value: FieldContainer.Stored + +/** + Creates a `Where` clause by comparing if a property is equal to a value + ``` + let person = dataStack.fetchOne(From().where(\.nickname == "John")) + ``` + */ +public func == (_ keyPath: KeyPath.Stored>, _ value: V) -> Where { + + return Where(keyPath, isEqualTo: value) +} + + // MARK: - KeyPath where Root: CoreStoreObject, Value: ValueContainer.Required /** diff --git a/Sources/KeyPathGenericBindings.swift b/Sources/KeyPathGenericBindings.swift index 0439a34..a7cd483 100644 --- a/Sources/KeyPathGenericBindings.swift +++ b/Sources/KeyPathGenericBindings.swift @@ -106,47 +106,47 @@ extension Int64: AllowedObjectiveCKeyPathValue { extension NSData: AllowedOptionalObjectiveCKeyPathValue { - public typealias DestinationValueType = Self + public typealias DestinationValueType = NSData } extension NSDate: AllowedOptionalObjectiveCKeyPathValue { - public typealias DestinationValueType = Self + public typealias DestinationValueType = NSDate } extension NSManagedObject: AllowedOptionalObjectiveCKeyPathValue { - public typealias DestinationValueType = Self + public typealias DestinationValueType = NSManagedObject } extension NSNumber: AllowedOptionalObjectiveCKeyPathValue { - public typealias DestinationValueType = Self + public typealias DestinationValueType = NSNumber } extension NSString: AllowedOptionalObjectiveCKeyPathValue { - public typealias DestinationValueType = Self + public typealias DestinationValueType = NSString } extension NSSet: AllowedOptionalObjectiveCKeyPathValue { - public typealias DestinationValueType = Self + public typealias DestinationValueType = NSSet } extension NSOrderedSet: AllowedOptionalObjectiveCKeyPathValue { - public typealias DestinationValueType = Self + public typealias DestinationValueType = NSOrderedSet } extension NSURL: AllowedOptionalObjectiveCKeyPathValue { - public typealias DestinationValueType = Self + public typealias DestinationValueType = NSURL } extension NSUUID: AllowedOptionalObjectiveCKeyPathValue { - public typealias DestinationValueType = Self + public typealias DestinationValueType = NSUUID } extension String: AllowedOptionalObjectiveCKeyPathValue { @@ -240,32 +240,32 @@ extension Int64: AllowedObjectiveCAttributeKeyPathValue { extension NSData: AllowedObjectiveCAttributeKeyPathValue { - public typealias ReturnValueType = Self + public typealias ReturnValueType = NSData } extension NSDate: AllowedObjectiveCAttributeKeyPathValue { - public typealias ReturnValueType = Self + public typealias ReturnValueType = NSDate } extension NSNumber: AllowedObjectiveCAttributeKeyPathValue { - public typealias ReturnValueType = Self + public typealias ReturnValueType = NSNumber } extension NSString: AllowedObjectiveCAttributeKeyPathValue { - public typealias ReturnValueType = Self + public typealias ReturnValueType = NSString } extension NSURL: AllowedObjectiveCAttributeKeyPathValue { - public typealias ReturnValueType = Self + public typealias ReturnValueType = NSURL } extension NSUUID: AllowedObjectiveCAttributeKeyPathValue { - public typealias ReturnValueType = Self + public typealias ReturnValueType = NSUUID } extension String: AllowedObjectiveCAttributeKeyPathValue { diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index d2ff520..5426925 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -367,6 +367,36 @@ extension ObjectPublisher where O: CoreStoreObject { // MARK: Public + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Stored>) -> V? { + + guard + let object = self.object, + let rawObject = object.rawObject + else { + + return nil + } + return FieldContainer.Stored.read(field: object[keyPath: member], for: rawObject) as! V? + } + + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Computed>) -> V? { + + guard + let object = self.object, + let rawObject = object.rawObject + else { + + return nil + } + return FieldContainer.Computed.read(field: object[keyPath: member], for: rawObject) as! V? + } + /** Returns the value for the property identified by a given key. */ diff --git a/Sources/ObjectSnapshot.swift b/Sources/ObjectSnapshot.swift index 9eb5a9f..0845636 100644 --- a/Sources/ObjectSnapshot.swift +++ b/Sources/ObjectSnapshot.swift @@ -175,6 +175,40 @@ extension ObjectSnapshot where O: NSManagedObject { extension ObjectSnapshot where O: CoreStoreObject { + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Stored>) -> V { + + get { + + let key = String(keyPath: member) + return self.values[key] as! V + } + set { + + let key = String(keyPath: member) + self.values[key] = newValue + } + } + + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Computed>) -> V { + + get { + + let key = String(keyPath: member) + return self.values[key] as! V + } + set { + + let key = String(keyPath: member) + self.values[key] = newValue + } + } + /** Returns the value for the property identified by a given key. */ diff --git a/Sources/PartialObject.swift b/Sources/PartialObject.swift index 9d42664..be8c345 100644 --- a/Sources/PartialObject.swift +++ b/Sources/PartialObject.swift @@ -41,6 +41,89 @@ public struct PartialObject { return O.cs_fromRaw(object: self.rawObject) } + + + // MARK: Field.Stored accessors/mutators + + /** + Returns the value for the property identified by a given key. + */ + public func value(for property: (O) -> FieldContainer.Stored) -> V { + + return V.cs_fromFieldStoredNativeType( + self.rawObject.value(forKey: property(O.meta).keyPath) as! V.FieldStoredNativeType + ) + } + + /** + Returns the value for the property identified by a given key. + */ + public func value(for property: (O) -> FieldContainer.Computed) -> V { + + switch self.rawObject.value(forKey: property(O.meta).keyPath) { + + case let value as V: + return value + + default: + return nil as Any? as! V // filter NSNull + } + } + + /** + Returns the value for the specified property from the managed object’s private internal storage. + + This method does not invoke the access notification methods (`willAccessValue(forKey:)` and `didAccessValue(forKey:)`). This method is used primarily by subclasses that implement custom accessor methods that need direct access to the receiver’s private storage. + */ + public func primitiveValue(for property: (O) -> FieldContainer.Stored) -> V { + + return V.cs_fromFieldStoredNativeType( + self.rawObject.primitiveValue(forKey: property(O.meta).keyPath) as! V.FieldStoredNativeType + ) + } + + /** + Returns the value for the specified property from the managed object’s private internal storage. + + This method does not invoke the access notification methods (`willAccessValue(forKey:)` and `didAccessValue(forKey:)`). This method is used primarily by subclasses that implement custom accessor methods that need direct access to the receiver’s private storage. + */ + public func primitiveValue(for property: (O) -> FieldContainer.Computed) -> V { + + switch self.rawObject.primitiveValue(forKey: property(O.meta).keyPath) { + + case let value as V: + return value + + default: + return nil as Any? as! V // filter NSNull + } + } + + /** + Sets in the object's private internal storage the value of a given property. + + Sets in the receiver’s private internal storage the value of the property specified by key to value. + */ + public func setPrimitiveValue(_ value: V, for property: (O) -> FieldContainer.Stored) { + + self.rawObject.setPrimitiveValue( + value.cs_toFieldStoredNativeType(), + forKey: property(O.meta).keyPath + ) + } + + /** + Sets in the object's private internal storage the value of a given property. + + Sets in the receiver’s private internal storage the value of the property specified by key to value. + */ + public func setPrimitiveValue(_ value: V, for property: (O) -> FieldContainer.Computed) { + + self.rawObject.setPrimitiveValue( + value, + forKey: property(O.meta).keyPath + ) + } // MARK: Value.Required accessors/mutators diff --git a/Sources/PropertyProtocol.swift b/Sources/PropertyProtocol.swift index 1c6292a..70c2551 100644 --- a/Sources/PropertyProtocol.swift +++ b/Sources/PropertyProtocol.swift @@ -29,7 +29,7 @@ import CoreData // MARK: - PropertyProtocol -internal protocol PropertyProtocol: AnyObject { +internal protocol PropertyProtocol { var keyPath: KeyPathString { get } } diff --git a/Sources/RelationshipProtocol.swift b/Sources/RelationshipProtocol.swift index cd0b4c6..78a637f 100644 --- a/Sources/RelationshipProtocol.swift +++ b/Sources/RelationshipProtocol.swift @@ -29,7 +29,7 @@ import CoreData // MARK: - RelationshipProtocol -internal protocol RelationshipProtocol: PropertyProtocol { +internal protocol RelationshipProtocol: AnyObject, PropertyProtocol { typealias EntityDescriptionValues = ( isToMany: Bool, diff --git a/Sources/StorageInterface.swift b/Sources/StorageInterface.swift index 3eb8581..a94a49f 100644 --- a/Sources/StorageInterface.swift +++ b/Sources/StorageInterface.swift @@ -73,22 +73,22 @@ public struct LocalStorageOptions: OptionSet, ExpressibleByNilLiteral { /** Tells the `DataStack` that the store should not be migrated or recreated, and should simply fail on model mismatch */ - public static let none = LocalStorageOptions(rawValue: 0) + public static let none: LocalStorageOptions = [] /** Tells the `DataStack` to delete and recreate the store on model mismatch, otherwise exceptions will be thrown on failure instead */ - public static let recreateStoreOnModelMismatch = LocalStorageOptions(rawValue: 1 << 0) + public static let recreateStoreOnModelMismatch: LocalStorageOptions = .init(rawValue: 1 << 0) /** Tells the `DataStack` to prevent progressive migrations for the store */ - public static let preventProgressiveMigration = LocalStorageOptions(rawValue: 1 << 1) + public static let preventProgressiveMigration: LocalStorageOptions = .init(rawValue: 1 << 1) /** Tells the `DataStack` to allow lightweight migration for the store when added synchronously */ - public static let allowSynchronousLightweightMigration = LocalStorageOptions(rawValue: 1 << 2) + public static let allowSynchronousLightweightMigration: LocalStorageOptions = .init(rawValue: 1 << 2) diff --git a/Sources/VersionLock.swift b/Sources/VersionLock.swift index 5e5d705..fa4fd3d 100644 --- a/Sources/VersionLock.swift +++ b/Sources/VersionLock.swift @@ -110,13 +110,8 @@ public struct VersionLock: ExpressibleByDictionaryLiteral, Equatable { var hashesByEntityName: [EntityName: Data] = [:] for (entityName, intArray) in keyValues { - - hashesByEntityName[entityName] = Data( - buffer: UnsafeBufferPointer( - start: intArray, - count: intArray.count - ) - ) + + hashesByEntityName[entityName] = intArray.withUnsafeBufferPointer(Data.init(buffer:)) } self.hashesByEntityName = hashesByEntityName } diff --git a/Sources/Where.swift b/Sources/Where.swift index 9c01106..f6b3af7 100644 --- a/Sources/Where.swift +++ b/Sources/Where.swift @@ -174,21 +174,41 @@ public struct Where: WhereClauseType, FetchClause, QueryClause self.init(NSPredicate(format: "\(keyPath) == nil")) } - + /** Initializes a `Where` clause that compares equality - + - parameter keyPath: the keyPath to compare with - parameter value: the arguments for the `==` operator */ - public init(_ keyPath: KeyPathString, isEqualTo value: U?) { - + public init(_ keyPath: KeyPathString, isEqualTo value: V) { + switch value { - + case nil, is NSNull: self.init(NSPredicate(format: "\(keyPath) == nil")) - + + case let value: + self.init(NSPredicate(format: "\(keyPath) == %@", argumentArray: [value.cs_toFieldStoredNativeType() as Any])) + } + } + + /** + Initializes a `Where` clause that compares equality + + - parameter keyPath: the keyPath to compare with + - parameter value: the arguments for the `==` operator + */ + @_disfavoredOverload + public init(_ keyPath: KeyPathString, isEqualTo value: U?) { + + switch value { + + case nil, + is NSNull: + self.init(NSPredicate(format: "\(keyPath) == nil")) + case let value?: self.init(NSPredicate(format: "\(keyPath) == %@", argumentArray: [value.cs_toQueryableNativeType()])) } @@ -222,6 +242,17 @@ public struct Where: WhereClauseType, FetchClause, QueryClause self.init(NSPredicate(format: "\(keyPath) == %@", argumentArray: [objectID])) } + + /** + Initializes a `Where` clause that compares membership + + - parameter keyPath: the keyPath to compare with + - parameter list: the sequence to check membership of + */ + public init(_ keyPath: KeyPathString, isMemberOf list: S) where S.Iterator.Element: FieldStorableType { + + self.init(NSPredicate(format: "\(keyPath) IN %@", list.map({ $0.cs_toFieldStoredNativeType() }) as NSArray)) + } /** Initializes a `Where` clause that compares membership @@ -229,6 +260,7 @@ public struct Where: WhereClauseType, FetchClause, QueryClause - parameter keyPath: the keyPath to compare with - parameter list: the sequence to check membership of */ + @_disfavoredOverload public init(_ keyPath: KeyPathString, isMemberOf list: S) where S.Iterator.Element: QueryableAttributeType { self.init(NSPredicate(format: "\(keyPath) IN %@", list.map({ $0.cs_toQueryableNativeType() }) as NSArray)) @@ -408,6 +440,17 @@ extension Where where O: NSManagedObject { // MARK: - Where where O: CoreStoreObject extension Where where O: CoreStoreObject { + + /** + Initializes a `Where` clause that compares equality + + - parameter keyPath: the keyPath to compare with + - parameter value: the arguments for the `==` operator + */ + public init(_ keyPath: KeyPath.Stored>, isEqualTo value: V) { + + self.init(O.meta[keyPath: keyPath].keyPath, isEqualTo: value) + } /** Initializes a `Where` clause that compares equality to `nil` From bcc2d9def37df3049c2ec623857ede9a2ef768ce Mon Sep 17 00:00:00 2001 From: John Estropia Date: Sat, 18 Jan 2020 16:22:06 +0900 Subject: [PATCH 02/20] Field.Coded implementations for transformable attributes --- CoreStore.xcodeproj/project.pbxproj | 108 +++- .../CoreStoreDemo.xcdatamodel/contents | 4 +- CoreStoreTests/DynamicModelTests.swift | 87 ++- CoreStoreTests/WhereTests.swift | 2 +- Sources/CoreStoreSchema.swift | 66 ++- Sources/DynamicSchema+Convenience.swift | 3 + Sources/Field.Coded.swift | 532 ++++++++++++++++++ Sources/Field.Computed.swift | 12 +- Sources/Field.PlistCoded.swift | 9 - Sources/Field.Stored.swift | 23 +- Sources/FieldAttributeProtocol.swift | 49 ++ Sources/FieldCoderType.swift | 38 ++ .../FieldCoders.DefaultNSSecureCoding.swift | 87 +++ Sources/FieldCoders.Json.swift | 59 ++ Sources/FieldCoders.NSCoding.swift | 91 +++ Sources/FieldCoders.Plist.swift | 59 ++ Sources/FieldCoders.swift | 30 + Sources/FieldProtocol.swift | 21 - Sources/Internals.AnyFieldCoder.swift | 222 ++++++++ Sources/NSEntityDescription+Migration.swift | 44 +- Sources/NSManagedObjectModel+Migration.swift | 22 +- Sources/PartialObject.swift | 38 +- 22 files changed, 1505 insertions(+), 101 deletions(-) create mode 100644 Sources/Field.Coded.swift delete mode 100644 Sources/Field.PlistCoded.swift create mode 100644 Sources/FieldAttributeProtocol.swift create mode 100644 Sources/FieldCoderType.swift create mode 100644 Sources/FieldCoders.DefaultNSSecureCoding.swift create mode 100644 Sources/FieldCoders.Json.swift create mode 100644 Sources/FieldCoders.NSCoding.swift create mode 100644 Sources/FieldCoders.Plist.swift create mode 100644 Sources/FieldCoders.swift create mode 100644 Sources/Internals.AnyFieldCoder.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 1815a05..a1267e9 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -133,6 +133,42 @@ B509D7D923C84E2600F42824 /* Transformable.Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = B509D7D723C84E2600F42824 /* Transformable.Optional.swift */; }; B509D7DA23C84E2600F42824 /* Transformable.Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = B509D7D723C84E2600F42824 /* Transformable.Optional.swift */; }; B509D7DB23C84E2600F42824 /* Transformable.Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = B509D7D723C84E2600F42824 /* Transformable.Optional.swift */; }; + B50C3EDA23D0545800B29880 /* FieldAttributeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3ED923D0545700B29880 /* FieldAttributeProtocol.swift */; }; + B50C3EDB23D0545800B29880 /* FieldAttributeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3ED923D0545700B29880 /* FieldAttributeProtocol.swift */; }; + B50C3EDC23D0545800B29880 /* FieldAttributeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3ED923D0545700B29880 /* FieldAttributeProtocol.swift */; }; + B50C3EDD23D0545800B29880 /* FieldAttributeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3ED923D0545700B29880 /* FieldAttributeProtocol.swift */; }; + B50C3EE023D062C300B29880 /* FieldCoderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EDF23D062C300B29880 /* FieldCoderType.swift */; }; + B50C3EE123D062C300B29880 /* FieldCoderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EDF23D062C300B29880 /* FieldCoderType.swift */; }; + B50C3EE223D062C300B29880 /* FieldCoderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EDF23D062C300B29880 /* FieldCoderType.swift */; }; + B50C3EE323D062C300B29880 /* FieldCoderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EDF23D062C300B29880 /* FieldCoderType.swift */; }; + B50C3EE523D153EA00B29880 /* Field.Coded.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EE423D153EA00B29880 /* Field.Coded.swift */; }; + B50C3EE623D153EA00B29880 /* Field.Coded.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EE423D153EA00B29880 /* Field.Coded.swift */; }; + B50C3EE723D153EA00B29880 /* Field.Coded.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EE423D153EA00B29880 /* Field.Coded.swift */; }; + B50C3EE823D153EA00B29880 /* Field.Coded.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EE423D153EA00B29880 /* Field.Coded.swift */; }; + B50C3EEA23D1601400B29880 /* FieldCoders.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EE923D1601400B29880 /* FieldCoders.swift */; }; + B50C3EEB23D1601400B29880 /* FieldCoders.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EE923D1601400B29880 /* FieldCoders.swift */; }; + B50C3EEC23D1601400B29880 /* FieldCoders.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EE923D1601400B29880 /* FieldCoders.swift */; }; + B50C3EED23D1601400B29880 /* FieldCoders.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EE923D1601400B29880 /* FieldCoders.swift */; }; + B50C3EEF23D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EEE23D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift */; }; + B50C3EF023D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EEE23D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift */; }; + B50C3EF123D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EEE23D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift */; }; + B50C3EF223D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EEE23D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift */; }; + B50C3EF423D1623A00B29880 /* FieldCoders.NSCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EF323D1623A00B29880 /* FieldCoders.NSCoding.swift */; }; + B50C3EF523D1623A00B29880 /* FieldCoders.NSCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EF323D1623A00B29880 /* FieldCoders.NSCoding.swift */; }; + B50C3EF623D1623A00B29880 /* FieldCoders.NSCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EF323D1623A00B29880 /* FieldCoders.NSCoding.swift */; }; + B50C3EF723D1623A00B29880 /* FieldCoders.NSCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EF323D1623A00B29880 /* FieldCoders.NSCoding.swift */; }; + B50C3EF923D1987D00B29880 /* FieldCoders.Json.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EF823D1987D00B29880 /* FieldCoders.Json.swift */; }; + B50C3EFA23D1987D00B29880 /* FieldCoders.Json.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EF823D1987D00B29880 /* FieldCoders.Json.swift */; }; + B50C3EFB23D1987D00B29880 /* FieldCoders.Json.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EF823D1987D00B29880 /* FieldCoders.Json.swift */; }; + B50C3EFC23D1987D00B29880 /* FieldCoders.Json.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EF823D1987D00B29880 /* FieldCoders.Json.swift */; }; + B50C3EFE23D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EFD23D1AB1400B29880 /* FieldCoders.Plist.swift */; }; + B50C3EFF23D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EFD23D1AB1400B29880 /* FieldCoders.Plist.swift */; }; + B50C3F0023D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EFD23D1AB1400B29880 /* FieldCoders.Plist.swift */; }; + B50C3F0123D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3EFD23D1AB1400B29880 /* FieldCoders.Plist.swift */; }; + B50C3F0323D1B01C00B29880 /* Internals.AnyFieldCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3F0223D1B01C00B29880 /* Internals.AnyFieldCoder.swift */; }; + B50C3F0423D1B01C00B29880 /* Internals.AnyFieldCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3F0223D1B01C00B29880 /* Internals.AnyFieldCoder.swift */; }; + B50C3F0523D1B01C00B29880 /* Internals.AnyFieldCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3F0223D1B01C00B29880 /* Internals.AnyFieldCoder.swift */; }; + B50C3F0623D1B01C00B29880 /* Internals.AnyFieldCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C3F0223D1B01C00B29880 /* Internals.AnyFieldCoder.swift */; }; B50E174D23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E174C23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift */; }; B50E174E23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E174C23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift */; }; B50E174F23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E174C23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift */; }; @@ -571,10 +607,6 @@ B56E4EE523CEDF0900E1708C /* Field.Computed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Computed.swift */; }; B56E4EE623CEDF0900E1708C /* Field.Computed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Computed.swift */; }; B56E4EE723CEDF0900E1708C /* Field.Computed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Computed.swift */; }; - B56E4EE923CF0C4500E1708C /* Field.PlistCoded.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE823CF0C4500E1708C /* Field.PlistCoded.swift */; }; - B56E4EEA23CF0C4500E1708C /* Field.PlistCoded.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE823CF0C4500E1708C /* Field.PlistCoded.swift */; }; - B56E4EEB23CF0C4500E1708C /* Field.PlistCoded.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE823CF0C4500E1708C /* Field.PlistCoded.swift */; }; - B56E4EEC23CF0C4500E1708C /* Field.PlistCoded.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE823CF0C4500E1708C /* Field.PlistCoded.swift */; }; B57D27BE1D0BBE8200539C58 /* BaseTestDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */; }; B57D27BF1D0BBE8200539C58 /* BaseTestDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */; }; B57D27C01D0BBE8200539C58 /* BaseTestDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */; }; @@ -967,6 +999,15 @@ B509D7CD23C8492800F42824 /* Relationship.ToManyUnordered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Relationship.ToManyUnordered.swift; sourceTree = ""; }; B509D7D223C84E1900F42824 /* Transformable.Required.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transformable.Required.swift; sourceTree = ""; }; B509D7D723C84E2600F42824 /* Transformable.Optional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transformable.Optional.swift; sourceTree = ""; }; + B50C3ED923D0545700B29880 /* FieldAttributeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldAttributeProtocol.swift; sourceTree = ""; }; + B50C3EDF23D062C300B29880 /* FieldCoderType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCoderType.swift; sourceTree = ""; }; + B50C3EE423D153EA00B29880 /* Field.Coded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.Coded.swift; sourceTree = ""; }; + B50C3EE923D1601400B29880 /* FieldCoders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCoders.swift; sourceTree = ""; }; + B50C3EEE23D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCoders.DefaultNSSecureCoding.swift; sourceTree = ""; }; + B50C3EF323D1623A00B29880 /* FieldCoders.NSCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCoders.NSCoding.swift; sourceTree = ""; }; + B50C3EF823D1987D00B29880 /* FieldCoders.Json.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCoders.Json.swift; sourceTree = ""; }; + B50C3EFD23D1AB1400B29880 /* FieldCoders.Plist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCoders.Plist.swift; sourceTree = ""; }; + B50C3F0223D1B01C00B29880 /* Internals.AnyFieldCoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.AnyFieldCoder.swift; sourceTree = ""; }; B50E174C23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.DiffableDataUIDispatcher.StagedChangeset.swift; sourceTree = ""; }; B50E175123517C6B004F033C /* Internals.DiffableDataUIDispatcher.Changeset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.DiffableDataUIDispatcher.Changeset.swift; sourceTree = ""; }; B50E175623517DE4004F033C /* Differentiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Differentiable.swift; sourceTree = ""; }; @@ -1070,7 +1111,6 @@ B56E4ED823CEB8E700E1708C /* FieldStorableType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldStorableType.swift; sourceTree = ""; }; B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldOptionalType.swift; sourceTree = ""; }; B56E4EE323CEDF0900E1708C /* Field.Computed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.Computed.swift; sourceTree = ""; }; - B56E4EE823CF0C4500E1708C /* Field.PlistCoded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.PlistCoded.swift; sourceTree = ""; }; B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTestDataTestCase.swift; sourceTree = ""; }; B57D27C11D0BC20100539C58 /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; B58085741CDF7F00004C2EEB /* SetupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupTests.swift; sourceTree = ""; }; @@ -1420,6 +1460,7 @@ B5831B6F1F34AC3400A9F647 /* AttributeProtocol.swift */, B5831B741F34AC7A00A9F647 /* RelationshipProtocol.swift */, B56E4ED323CDB54A00E1708C /* FieldProtocol.swift */, + B50C3ED923D0545700B29880 /* FieldAttributeProtocol.swift */, B50564D22350CC3100482308 /* PropertyProtocol.swift */, B53D9E5823513712000F48FB /* DiffableDataSourceSnapshotProtocol.swift */, B50E175623517DE4004F033C /* Differentiable.swift */, @@ -1427,6 +1468,19 @@ name = Protocols; sourceTree = ""; }; + B50C3EDE23D05BB200B29880 /* Coders */ = { + isa = PBXGroup; + children = ( + B50C3EDF23D062C300B29880 /* FieldCoderType.swift */, + B50C3EE923D1601400B29880 /* FieldCoders.swift */, + B50C3EEE23D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift */, + B50C3EF323D1623A00B29880 /* FieldCoders.NSCoding.swift */, + B50C3EF823D1987D00B29880 /* FieldCoders.Json.swift */, + B50C3EFD23D1AB1400B29880 /* FieldCoders.Plist.swift */, + ); + name = Coders; + sourceTree = ""; + }; B51B5C2922D43854009FA3BA /* KeyPaths */ = { isa = PBXGroup; children = ( @@ -1553,7 +1607,8 @@ B56E4EC923CD9B4800E1708C /* Field.swift */, B56E4ECE23CD9E4200E1708C /* Field.Stored.swift */, B56E4EE323CEDF0900E1708C /* Field.Computed.swift */, - B56E4EE823CF0C4500E1708C /* Field.PlistCoded.swift */, + B50C3EE423D153EA00B29880 /* Field.Coded.swift */, + B50C3EDE23D05BB200B29880 /* Coders */, B56E4EDD23CEBB0400E1708C /* Supported Values */, ); name = "Field Properties"; @@ -1846,6 +1901,7 @@ B50564CC2350699700482308 /* Protocols */, B53B275E1EE3B92E00E9B352 /* CoreStoreManagedObject.swift */, B533C4DA1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift */, + B50C3F0223D1B01C00B29880 /* Internals.AnyFieldCoder.swift */, B5C976E61C6E3A5900B1AF90 /* Internals.CoreStoreFetchedResultsController.swift */, B5474D142227C08700B21FEC /* Internals.CoreStoreFetchRequest.swift */, B5BF7FAC234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift */, @@ -2206,6 +2262,7 @@ B5ECDBF91CA804FD00C7F112 /* NSManagedObjectContext+ObjectiveC.swift in Sources */, B5CA2B081F7E5ACA004B1936 /* WhereClauseType.swift in Sources */, B50E17612351FA66004F033C /* Internals.Closure.swift in Sources */, + B50C3EEA23D1601400B29880 /* FieldCoders.swift in Sources */, B5C976E71C6E3A5A00B1AF90 /* Internals.CoreStoreFetchedResultsController.swift in Sources */, B56923F51EB828BF007C4DC9 /* CSDynamicSchema.swift in Sources */, B5F1DA901B9AA991007C5CBB /* ImportableUniqueObject.swift in Sources */, @@ -2229,6 +2286,7 @@ B55BB4DB23503B9700C33E34 /* EnvironmentValues+DataSources.swift in Sources */, B5A9921F1EA898710091A2E3 /* UserInfo.swift in Sources */, B54A6A551BA15F2A007870FD /* Internals.FetchedResultsControllerDelegate.swift in Sources */, + B50C3F0323D1B01C00B29880 /* Internals.AnyFieldCoder.swift in Sources */, B5D339E21E948C3600C880DE /* Value.swift in Sources */, B5A261211B64BFDB006EB6D3 /* MigrationType.swift in Sources */, B53FBA0B1CAB5E6500F0D40A /* CSCoreStore+Migrating.swift in Sources */, @@ -2250,7 +2308,6 @@ B5E1B5A81CAA49E2007FD580 /* CSDataStack+Migrating.swift in Sources */, B50132302346B76E00FC238B /* Internals.FetchedDiffableDataSourceSnapshotDelegate.swift in Sources */, B5D339F11E94AF5800C880DE /* CoreStoreStrings.swift in Sources */, - B56E4EE923CF0C4500E1708C /* Field.PlistCoded.swift in Sources */, B56007161B4018AB00A9A8F9 /* MigrationChain.swift in Sources */, B5E1B59D1CAA2568007FD580 /* CSDataStack+Observing.swift in Sources */, B5ECDC231CA81A3900C7F112 /* CSCoreStore+Querying.swift in Sources */, @@ -2270,6 +2327,7 @@ B50E175223517C6B004F033C /* Internals.DiffableDataUIDispatcher.Changeset.swift in Sources */, B5E84EE71AFF84610064E85B /* CoreStore+Logging.swift in Sources */, B546F9731C9C553300D5AC55 /* SetupResult.swift in Sources */, + B50C3EFE23D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */, B56E4EDF23CEBCF000E1708C /* FieldOptionalType.swift in Sources */, B53CA9A21EF1EF1600E0F440 /* PartialObject.swift in Sources */, B56007111B3F6BD500A9A8F9 /* Into.swift in Sources */, @@ -2306,6 +2364,7 @@ B52F74451E9B8724005F3DAC /* XcodeDataModelSchema.swift in Sources */, B5FAD6AC1B51285300714891 /* Internals.MigrationManager.swift in Sources */, B50EE14223473C92009B8C47 /* CoreStoreObject+DataSources.swift in Sources */, + B50C3EE023D062C300B29880 /* FieldCoderType.swift in Sources */, B5E84EF61AFF846E0064E85B /* DataStack+Transaction.swift in Sources */, B5FEC18E1C9166E200532541 /* NSPersistentStore+Setup.swift in Sources */, B5BF7FCB234D80910070E741 /* Internals.LazyNonmutating.swift in Sources */, @@ -2320,6 +2379,7 @@ B56964D41B22FFAD0075EE4A /* DataStack+Migration.swift in Sources */, B5D339DD1E9489C700C880DE /* DynamicObject.swift in Sources */, B5A5F2661CAEC50F004AB9AF /* CSSelect.swift in Sources */, + B50C3EE523D153EA00B29880 /* Field.Coded.swift in Sources */, B5ECDBE51CA6BEA300C7F112 /* CSClauseTypes.swift in Sources */, B5519A4A1CA1F4FB002BEF78 /* CSError.swift in Sources */, B52F742F1E9B50D0005F3DAC /* SchemaHistory.swift in Sources */, @@ -2333,6 +2393,7 @@ B50E174D23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */, B5E222231CA4E12600BA2E95 /* CSSynchronousDataTransaction.swift in Sources */, B5E84F281AFF84920064E85B /* NSManagedObject+Convenience.swift in Sources */, + B50C3EF423D1623A00B29880 /* FieldCoders.NSCoding.swift in Sources */, B56E4ECF23CD9E4200E1708C /* Field.Stored.swift in Sources */, B52F744A1E9B8740005F3DAC /* CoreStoreSchema.swift in Sources */, B5AEFAB51C9962AE00AD137F /* CoreStoreBridge.swift in Sources */, @@ -2345,7 +2406,9 @@ B5D33A011E96012400C880DE /* Relationship.swift in Sources */, B514EF0E23A8BEEA0093DBA4 /* DiffableDataSource.Target.swift in Sources */, B5E84EE81AFF84610064E85B /* CoreStoreLogger.swift in Sources */, + B50C3EF923D1987D00B29880 /* FieldCoders.Json.swift in Sources */, B56923C91EB82410007C4DC9 /* NSManagedObjectModel+Migration.swift in Sources */, + B50C3EDA23D0545800B29880 /* FieldAttributeProtocol.swift in Sources */, B56923E41EB827F5007C4DC9 /* CustomSchemaMappingProvider.swift in Sources */, B58D0C631EAA0C7E003EDD87 /* NSManagedObject+DynamicModel.swift in Sources */, B533C4DB1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */, @@ -2385,6 +2448,7 @@ B53FBA041CAB300C00F0D40A /* CSMigrationType.swift in Sources */, B509D7C923C8491C00F42824 /* Relationship.ToManyOrdered.swift in Sources */, B5E84EF41AFF846E0064E85B /* AsynchronousDataTransaction.swift in Sources */, + B50C3EEF23D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift in Sources */, B5831B701F34AC3400A9F647 /* AttributeProtocol.swift in Sources */, B5277672234F1AEB0056BE9F /* NSManagedObjectContext+Logging.swift in Sources */, B5DBE2CD1C9914A900B5CEFA /* CSCoreStore.swift in Sources */, @@ -2446,6 +2510,7 @@ B56923F61EB828BF007C4DC9 /* CSDynamicSchema.swift in Sources */, 82BA18A21C4BBD1D00A0916E /* CoreStoreError.swift in Sources */, B512608A1E9B252B00402229 /* NSEntityDescription+DynamicModel.swift in Sources */, + B50C3EEB23D1601400B29880 /* FieldCoders.swift in Sources */, 82BA18B21C4BBD3900A0916E /* ImportableObject.swift in Sources */, 82BA18AE1C4BBD3100A0916E /* DataStack+Transaction.swift in Sources */, 82BA18AB1C4BBD3100A0916E /* AsynchronousDataTransaction.swift in Sources */, @@ -2469,6 +2534,7 @@ B5A992201EA898720091A2E3 /* UserInfo.swift in Sources */, B5FE4DAD1C85D44E00FA6A91 /* SQLiteStore.swift in Sources */, B5D339E31E948C3600C880DE /* Value.swift in Sources */, + B50C3F0423D1B01C00B29880 /* Internals.AnyFieldCoder.swift in Sources */, B50E175323517C6B004F033C /* Internals.DiffableDataUIDispatcher.Changeset.swift in Sources */, 82BA18C51C4BBD5300A0916E /* ListObserver.swift in Sources */, B53FBA0D1CAB5E6500F0D40A /* CSCoreStore+Migrating.swift in Sources */, @@ -2490,7 +2556,6 @@ B5ECDC251CA81A3900C7F112 /* CSCoreStore+Querying.swift in Sources */, B549F6741E56A92800FBAB2D /* CoreDataNativeType.swift in Sources */, B509C7F51E54511B0061C547 /* ImportableAttributeType.swift in Sources */, - B56E4EEA23CF0C4500E1708C /* Field.PlistCoded.swift in Sources */, 82BA18B31C4BBD3900A0916E /* ImportableUniqueObject.swift in Sources */, B50EE14323473C96009B8C47 /* CoreStoreObject+DataSources.swift in Sources */, B55BB4DA23503B9600C33E34 /* EnvironmentValues+DataSources.swift in Sources */, @@ -2510,6 +2575,7 @@ B5831F432212700400D8604C /* Where.Expression.swift in Sources */, B51260941E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */, B5FE4DA81C84FB4400FA6A91 /* InMemoryStore.swift in Sources */, + B50C3EFF23D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */, B56E4EE023CEBCF000E1708C /* FieldOptionalType.swift in Sources */, B5F8496D234898240029D57B /* ListSnapshot.swift in Sources */, B53FBA001CAB2D2F00F0D40A /* CSMigrationResult.swift in Sources */, @@ -2546,6 +2612,7 @@ B596BBB71DD5BC67001DCDD9 /* FetchableSource.swift in Sources */, B5FEC18F1C9166E600532541 /* NSPersistentStore+Setup.swift in Sources */, 82BA18B71C4BBD3F00A0916E /* CoreStore+Querying.swift in Sources */, + B50C3EE123D062C300B29880 /* FieldCoderType.swift in Sources */, 82BA18AA1C4BBD3100A0916E /* BaseDataTransaction.swift in Sources */, 82BA18A91C4BBD3100A0916E /* Into.swift in Sources */, 82BA18D11C4BBD7100A0916E /* Internals.NotificationObserver.swift in Sources */, @@ -2560,6 +2627,7 @@ B5D339ED1E9495E500C880DE /* CoreStoreObject+Querying.swift in Sources */, B509D7D423C84E1900F42824 /* Transformable.Required.swift in Sources */, 82BA18AC1C4BBD3100A0916E /* SynchronousDataTransaction.swift in Sources */, + B50C3EE623D153EA00B29880 /* Field.Coded.swift in Sources */, 82BA18C71C4BBD5900A0916E /* CoreStore+Migration.swift in Sources */, B5E222251CA4E12600BA2E95 /* CSSynchronousDataTransaction.swift in Sources */, 82BA18C41C4BBD5300A0916E /* ListMonitor.swift in Sources */, @@ -2573,6 +2641,7 @@ B5E2222C1CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift in Sources */, 82BA18A71C4BBD2900A0916E /* CoreStore+Logging.swift in Sources */, B50E174E23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */, + B50C3EF523D1623A00B29880 /* FieldCoders.NSCoding.swift in Sources */, 82BA18D81C4BBD7100A0916E /* Internals.WeakObject.swift in Sources */, B56923E91EB827F5007C4DC9 /* InferredSchemaMappingProvider.swift in Sources */, B53B27601EE3B92E00E9B352 /* CoreStoreManagedObject.swift in Sources */, @@ -2585,7 +2654,9 @@ B58D0C641EAA0C7E003EDD87 /* NSManagedObject+DynamicModel.swift in Sources */, B533C4DC1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */, B5ECDC311CA81CDC00C7F112 /* CSCoreStore+Transaction.swift in Sources */, + B50C3EFA23D1987D00B29880 /* FieldCoders.Json.swift in Sources */, B5DE522C230BD7D500A22534 /* Internals.swift in Sources */, + B50C3EDB23D0545800B29880 /* FieldAttributeProtocol.swift in Sources */, 82BA18AF1C4BBD3100A0916E /* CoreStore+Transaction.swift in Sources */, 82BA18CB1C4BBD6400A0916E /* NSManagedObject+Convenience.swift in Sources */, 82BA18B51C4BBD3F00A0916E /* BaseDataTransaction+Querying.swift in Sources */, @@ -2625,6 +2696,7 @@ B5DBE2CE1C9914A900B5CEFA /* CSCoreStore.swift in Sources */, B5831B711F34AC3400A9F647 /* AttributeProtocol.swift in Sources */, B546F95E1C9A12B800D5AC55 /* CSSQliteStore.swift in Sources */, + B50C3EF023D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift in Sources */, B5ECDC0D1CA8161B00C7F112 /* CSGroupBy.swift in Sources */, B5D339E81E9493A500C880DE /* Entity.swift in Sources */, B5BF7FC7234D7E460070E741 /* ObjectSnapshot.swift in Sources */, @@ -2686,6 +2758,7 @@ B5DBE2D01C9914A900B5CEFA /* CSCoreStore.swift in Sources */, B5CA2B0B1F7E5ACA004B1936 /* WhereClauseType.swift in Sources */, B56923F81EB828BF007C4DC9 /* CSDynamicSchema.swift in Sources */, + B50C3EED23D1601400B29880 /* FieldCoders.swift in Sources */, B52DD1BE1BE1F94300949AFE /* Progress+Convenience.swift in Sources */, B512608C1E9B252B00402229 /* NSEntityDescription+DynamicModel.swift in Sources */, B5ECDC151CA816E500C7F112 /* CSTweak.swift in Sources */, @@ -2709,6 +2782,7 @@ B52FD3AD1E3B3EF10001D919 /* NSManagedObject+Logging.swift in Sources */, B52F74441E9B8724005F3DAC /* UnsafeDataModelSchema.swift in Sources */, B5ECDC2D1CA81CC700C7F112 /* CSDataStack+Transaction.swift in Sources */, + B50C3F0623D1B01C00B29880 /* Internals.AnyFieldCoder.swift in Sources */, B5A992221EA898720091A2E3 /* UserInfo.swift in Sources */, B5D7A5BA1CA3BF8F005C752B /* CSInto.swift in Sources */, B5D339E51E948C3600C880DE /* Value.swift in Sources */, @@ -2730,7 +2804,6 @@ B559CD471CAA8B6300E4D58B /* CSSetupResult.swift in Sources */, B50E17642351FA66004F033C /* Internals.Closure.swift in Sources */, B5ECDBF01CA6BF2000C7F112 /* CSFrom.swift in Sources */, - B56E4EEC23CF0C4500E1708C /* Field.PlistCoded.swift in Sources */, B549F6761E56A92800FBAB2D /* CoreDataNativeType.swift in Sources */, B509C7F71E54511B0061C547 /* ImportableAttributeType.swift in Sources */, B5220E1F1D130810009BC71E /* CSListObserver.swift in Sources */, @@ -2750,6 +2823,7 @@ B52DD1BD1BE1F94300949AFE /* NSManagedObject+Convenience.swift in Sources */, B5831F4222126FED00D8604C /* KeyPathGenericBindings.swift in Sources */, B53CA9A51EF1EF1600E0F440 /* PartialObject.swift in Sources */, + B50C3F0123D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */, B56E4EE223CEBCF000E1708C /* FieldOptionalType.swift in Sources */, B52DD1AD1BE1F93900949AFE /* Where.swift in Sources */, B53FBA1C1CAB63E200F0D40A /* NSManagedObject+ObjectiveC.swift in Sources */, @@ -2786,6 +2860,7 @@ B52F74481E9B8724005F3DAC /* XcodeDataModelSchema.swift in Sources */, B5519A621CA21954002BEF78 /* CSAsynchronousDataTransaction.swift in Sources */, B52DD19C1BE1F92C00949AFE /* Into.swift in Sources */, + B50C3EE323D062C300B29880 /* FieldCoderType.swift in Sources */, B5FE4DA51C8481E100FA6A91 /* StorageInterface.swift in Sources */, B596BBB91DD5BC67001DCDD9 /* FetchableSource.swift in Sources */, B5FE4DAA1C84FB4400FA6A91 /* InMemoryStore.swift in Sources */, @@ -2800,6 +2875,7 @@ B52F74321E9B50D0005F3DAC /* SchemaHistory.swift in Sources */, B509D7D623C84E1900F42824 /* Transformable.Required.swift in Sources */, B5220E211D130816009BC71E /* CSObjectObserver.swift in Sources */, + B50C3EE823D153EA00B29880 /* Field.Coded.swift in Sources */, B5D339EF1E9495E500C880DE /* CoreStoreObject+Querying.swift in Sources */, B52DD19F1BE1F92C00949AFE /* SynchronousDataTransaction.swift in Sources */, B52DD1CB1BE1F94600949AFE /* Internals.WeakObject.swift in Sources */, @@ -2813,6 +2889,7 @@ B52DD19A1BE1F92800949AFE /* CoreStore+Logging.swift in Sources */, B52DD1A71BE1F93200949AFE /* BaseDataTransaction+Querying.swift in Sources */, B546F96C1C9AF26D00D5AC55 /* CSInMemoryStore.swift in Sources */, + B50C3EF723D1623A00B29880 /* FieldCoders.NSCoding.swift in Sources */, B56923EB1EB827F5007C4DC9 /* InferredSchemaMappingProvider.swift in Sources */, B50E175023517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */, B53B27621EE3B92E00E9B352 /* CoreStoreManagedObject.swift in Sources */, @@ -2825,7 +2902,9 @@ B58D0C661EAA0C7E003EDD87 /* NSManagedObject+DynamicModel.swift in Sources */, B533C4DE1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */, B5220E141D130614009BC71E /* DataStack+Observing.swift in Sources */, + B50C3EFC23D1987D00B29880 /* FieldCoders.Json.swift in Sources */, B52DD1A21BE1F92C00949AFE /* CoreStore+Transaction.swift in Sources */, + B50C3EDD23D0545800B29880 /* FieldAttributeProtocol.swift in Sources */, B5DE522E230BD7D600A22534 /* Internals.swift in Sources */, B5E2222E1CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift in Sources */, B5220E191D130761009BC71E /* ListMonitor.swift in Sources */, @@ -2865,6 +2944,7 @@ B5519A5C1CA2008C002BEF78 /* CSBaseDataTransaction.swift in Sources */, B5DBE2D51C991B3E00B5CEFA /* CSDataStack.swift in Sources */, B5831B731F34AC3400A9F647 /* AttributeProtocol.swift in Sources */, + B50C3EF223D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift in Sources */, B5AEFAB81C9962AE00AD137F /* CoreStoreBridge.swift in Sources */, B598514B1C90289F00C99590 /* NSPersistentStoreCoordinator+Setup.swift in Sources */, B5D339EA1E9493A500C880DE /* Entity.swift in Sources */, @@ -2926,6 +3006,7 @@ B56923F71EB828BF007C4DC9 /* CSDynamicSchema.swift in Sources */, B56321801BD65216006C9394 /* CoreStoreError.swift in Sources */, B512608B1E9B252B00402229 /* NSEntityDescription+DynamicModel.swift in Sources */, + B50C3EEC23D1601400B29880 /* FieldCoders.swift in Sources */, B56321AD1BD6521C006C9394 /* Internals.MigrationManager.swift in Sources */, B563219D1BD65216006C9394 /* DataStack+Observing.swift in Sources */, B56321961BD65216006C9394 /* From.swift in Sources */, @@ -2949,6 +3030,7 @@ B5A992211EA898720091A2E3 /* UserInfo.swift in Sources */, B563218C1BD65216006C9394 /* DataStack+Transaction.swift in Sources */, B5D339E41E948C3600C880DE /* Value.swift in Sources */, + B50C3F0523D1B01C00B29880 /* Internals.AnyFieldCoder.swift in Sources */, B50E175423517C6B004F033C /* Internals.DiffableDataUIDispatcher.Changeset.swift in Sources */, B53FBA0E1CAB5E6500F0D40A /* CSCoreStore+Migrating.swift in Sources */, B563219E1BD65216006C9394 /* CoreStore+Observing.swift in Sources */, @@ -2970,7 +3052,6 @@ B563217F1BD65216006C9394 /* CoreStore.swift in Sources */, B549F6751E56A92800FBAB2D /* CoreDataNativeType.swift in Sources */, B509C7F61E54511B0061C547 /* ImportableAttributeType.swift in Sources */, - B56E4EEB23CF0C4500E1708C /* Field.PlistCoded.swift in Sources */, B5E1B5961CAA0C15007FD580 /* CSObjectMonitor.swift in Sources */, B50EE14423473C97009B8C47 /* CoreStoreObject+DataSources.swift in Sources */, B55BB4D923503B9600C33E34 /* EnvironmentValues+DataSources.swift in Sources */, @@ -2990,6 +3071,7 @@ B5202CFD1C046E8400DED140 /* NSFetchedResultsController+Convenience.swift in Sources */, B5FE4DA91C84FB4400FA6A91 /* InMemoryStore.swift in Sources */, B5831F442212700500D8604C /* Where.Expression.swift in Sources */, + B50C3F0023D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */, B56E4EE123CEBCF000E1708C /* FieldOptionalType.swift in Sources */, B5F8496E234898240029D57B /* ListSnapshot.swift in Sources */, B51260951E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */, @@ -3026,6 +3108,7 @@ B56321A11BD65216006C9394 /* ListMonitor.swift in Sources */, B5E1B5A51CAA4365007FD580 /* CSCoreStore+Observing.swift in Sources */, B596BBB81DD5BC67001DCDD9 /* FetchableSource.swift in Sources */, + B50C3EE223D062C300B29880 /* FieldCoderType.swift in Sources */, B56321881BD65216006C9394 /* BaseDataTransaction.swift in Sources */, B56321A31BD65216006C9394 /* DataStack+Migration.swift in Sources */, B56321901BD65216006C9394 /* ImportableUniqueObject.swift in Sources */, @@ -3040,6 +3123,7 @@ B563218F1BD65216006C9394 /* ImportableObject.swift in Sources */, B509D7D523C84E1900F42824 /* Transformable.Required.swift in Sources */, B56321991BD65216006C9394 /* OrderBy.swift in Sources */, + B50C3EE723D153EA00B29880 /* Field.Coded.swift in Sources */, B5D339EE1E9495E500C880DE /* CoreStoreObject+Querying.swift in Sources */, B56321A51BD65216006C9394 /* MigrationChain.swift in Sources */, B5E222261CA4E12600BA2E95 /* CSSynchronousDataTransaction.swift in Sources */, @@ -3053,6 +3137,7 @@ B5AEFAB71C9962AE00AD137F /* CoreStoreBridge.swift in Sources */, B5E2222D1CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift in Sources */, B50E174F23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */, + B50C3EF623D1623A00B29880 /* FieldCoders.NSCoding.swift in Sources */, B563219F1BD65216006C9394 /* ObjectMonitor.swift in Sources */, B56321B61BD6521C006C9394 /* Internals.WeakObject.swift in Sources */, B56923EA1EB827F5007C4DC9 /* InferredSchemaMappingProvider.swift in Sources */, @@ -3065,7 +3150,9 @@ B56923E61EB827F5007C4DC9 /* CustomSchemaMappingProvider.swift in Sources */, B58D0C651EAA0C7E003EDD87 /* NSManagedObject+DynamicModel.swift in Sources */, B533C4DD1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */, + B50C3EFB23D1987D00B29880 /* FieldCoders.Json.swift in Sources */, B5ECDC321CA81CDC00C7F112 /* CSCoreStore+Transaction.swift in Sources */, + B50C3EDC23D0545800B29880 /* FieldAttributeProtocol.swift in Sources */, B5DE522D230BD7D600A22534 /* Internals.swift in Sources */, B56321851BD65216006C9394 /* CoreStore+Logging.swift in Sources */, B56321921BD65216006C9394 /* BaseDataTransaction+Querying.swift in Sources */, @@ -3105,6 +3192,7 @@ B5DBE2CF1C9914A900B5CEFA /* CSCoreStore.swift in Sources */, B5831B721F34AC3400A9F647 /* AttributeProtocol.swift in Sources */, B546F95F1C9A12B800D5AC55 /* CSSQliteStore.swift in Sources */, + B50C3EF123D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift in Sources */, B5ECDC0E1CA8161B00C7F112 /* CSGroupBy.swift in Sources */, B5D339E91E9493A500C880DE /* Entity.swift in Sources */, B5BF7FC8234D7E460070E741 /* ObjectSnapshot.swift in Sources */, diff --git a/CoreStoreDemo/CoreStoreDemo/CoreStoreDemo.xcdatamodeld/CoreStoreDemo.xcdatamodel/contents b/CoreStoreDemo/CoreStoreDemo/CoreStoreDemo.xcdatamodeld/CoreStoreDemo.xcdatamodel/contents index 7df0666..f334ebd 100644 --- a/CoreStoreDemo/CoreStoreDemo/CoreStoreDemo.xcdatamodeld/CoreStoreDemo.xcdatamodel/contents +++ b/CoreStoreDemo/CoreStoreDemo/CoreStoreDemo.xcdatamodeld/CoreStoreDemo.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -20,7 +20,7 @@ - + \ No newline at end of file diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index 99f4e3a..7cd1362 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -41,8 +41,10 @@ class Animal: CoreStoreObject { @Field.Stored("species") var species: String = "Swift" + @Field.Coded("color", coder: FieldCoders.NSCoding.self) + var color: Color? = .blue + let master = Relationship.ToOne("master") - let color = Transformable.Optional("color") } class Dog: Animal { @@ -55,6 +57,10 @@ class Dog: Animal { let friendedBy = Relationship.ToManyUnordered("friendedBy", inverse: { $0.friends }) } +struct CustomType { + var string = "customString" +} + class Person: CoreStoreObject { @Field.Stored("title", customSetter: Person.setTitle(_:_:)) @@ -70,12 +76,50 @@ class Person: CoreStoreObject { ) var displayName: String? + @Field.Computed( + "customType", + customGetter: Person.getCustomField(_:) + ) + var customField: CustomType + + @Field.Coded( + "job", coder: ( + encode: { $0.data }, + decode: { $0.flatMap(Job.init(data:)) ?? .unemployed } + ) + ) + var job: Job = .unemployed + let spouse = Relationship.ToOne("spouse") let pets = Relationship.ToManyUnordered("pets", inverse: { $0.master }) private let _spouse = Relationship.ToOne("_spouseInverse", inverse: { $0.spouse }) + enum Job: String { + + case unemployed + case engineer + case doctor + case lawyer + + init?(data: Data) { + + guard + let rawValue = String(data: data, encoding: .utf8), + let value = Self.init(rawValue: rawValue) + else { + + return nil + } + self = value + } + + var data: Data { + + return Data(self.rawValue.utf8) + } + } private static func setTitle(_ partialObject: PartialObject, _ newValue: String) { @@ -88,6 +132,17 @@ class Person: CoreStoreObject { partialObject.setPrimitiveValue(newValue, for: \.$name) partialObject.setPrimitiveValue(nil, for: \.$displayName) } + + static func getCustomField(_ partialObject: PartialObject) -> CustomType { + + if let customField = partialObject.primitiveValue(for: \.$customField) { + + return customField + } + let customField = CustomType() + partialObject.setPrimitiveValue(customField, for: \.$customField) + return customField + } static func getDisplayName(_ partialObject: PartialObject) -> String? { @@ -130,7 +185,7 @@ class DynamicModelTests: BaseTestDataTestCase { versionLock: [ "Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a], "Dog": [0xe3f0afeb109b283a, 0x29998d292938eb61, 0x6aab788333cfc2a3, 0x492ff1d295910ea7], - "Person": [0x2831cf046084d96d, 0xbe19b13ace54641, 0x635a082728b0f6f0, 0x3d4ef2dd4b74a87c] + "Person": [0xf3e6ba6016bbedc6, 0x50dedf64f0eba490, 0xa32088a0ee83468d, 0xb72d1d0b37bd0992] ] ) ) @@ -156,12 +211,13 @@ class DynamicModelTests: BaseTestDataTestCase { let animal = transaction.create(Into()) XCTAssertEqual(animal.species, "Swift") XCTAssertTrue(type(of: animal.species) == String.self) + XCTAssertEqual(animal.color, Color.blue) animal.species = "Sparrow" XCTAssertEqual(animal.species, "Sparrow") - animal.color .= .yellow - XCTAssertEqual(animal.color.value, Color.yellow) + animal.color = .yellow + XCTAssertEqual(animal.color, Color.yellow) for property in Animal.metaProperties(includeSuperclasses: true) { @@ -173,8 +229,8 @@ class DynamicModelTests: BaseTestDataTestCase { case String(keyPath: \Animal.master): XCTAssertTrue(property is RelationshipContainer.ToOne) - case String(keyPath: \Animal.color): - XCTAssertTrue(property is TransformableContainer.Optional) + case String(keyPath: \Animal.$color): + XCTAssertTrue(property is FieldContainer.Coded) default: XCTFail("Unknown KeyPath: \"\(property.keyPath)\"") @@ -196,8 +252,8 @@ class DynamicModelTests: BaseTestDataTestCase { case String(keyPath: \Dog.master): XCTAssertTrue(property is RelationshipContainer.ToOne) - case String(keyPath: \Dog.color): - XCTAssertTrue(property is TransformableContainer.Optional) + case String(keyPath: \Dog.$color): + XCTAssertTrue(property is FieldContainer.Coded) case String(keyPath: \Dog.$nickname): XCTAssertTrue(property is FieldContainer.Stored) @@ -270,6 +326,8 @@ class DynamicModelTests: BaseTestDataTestCase { let person = transaction.create(Into()) XCTAssertTrue(person.pets.value.isEmpty) + XCTAssertEqual(person.customField.string, "customString") + XCTAssertEqual(person.job, .unemployed) XCTAssertEqual( person.rawObject! @@ -301,6 +359,12 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(personSnapshot1.$name, "John") XCTAssertEqual(personSnapshot1.$title, "Mr.") XCTAssertEqual(personSnapshot1.$displayName, "Mr. John") + + person.customField.string = "newCustomString" + XCTAssertEqual(person.customField.string, "newCustomString") + + person.job = .engineer + XCTAssertEqual(person.job, .engineer) let personSnapshot2 = person.asSnapshot(in: transaction)! XCTAssertEqual(person.name, personSnapshot2.$name) @@ -315,6 +379,7 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(personSnapshot2.$displayName, "Sir John") XCTAssertEqual(personSnapshot3.$name, "James") XCTAssertEqual(personSnapshot3.$displayName, "Sir John") + person.pets.value.insert(dog) @@ -342,6 +407,7 @@ class DynamicModelTests: BaseTestDataTestCase { let bird = try transaction.fetchOne(From(), p1) XCTAssertNotNil(bird) XCTAssertEqual(bird!.species, "Sparrow") + XCTAssertEqual(bird!.color, Color.yellow) let p2 = Where({ $0.$nickname == "Spot" }) XCTAssertEqual(p2.predicate, NSPredicate(format: "%K == %@", "nickname", "Spot")) @@ -353,6 +419,11 @@ class DynamicModelTests: BaseTestDataTestCase { let person = try transaction.fetchOne(From()) XCTAssertNotNil(person) + XCTAssertEqual(person!.name, "John") + XCTAssertEqual(person!.title, "Sir") + XCTAssertEqual(person!.displayName, "Sir John") + XCTAssertEqual(person!.customField.string, "customString") + XCTAssertEqual(person!.job, .engineer) XCTAssertEqual(person!.pets.value.first, dog) let p3 = Where({ $0.age == 10 }) diff --git a/CoreStoreTests/WhereTests.swift b/CoreStoreTests/WhereTests.swift index 9ceec1f..1dbff23 100644 --- a/CoreStoreTests/WhereTests.swift +++ b/CoreStoreTests/WhereTests.swift @@ -67,7 +67,7 @@ final class WhereTests: XCTestCase { dynamic func test_ThatDynamicModelKeyPaths_CanBeCreated() { XCTAssertAllEqual(String(keyPath: \TestEntity1.testEntityID), "testEntityID") - XCTAssertAllEqual(String(keyPath: \Animal.color), "color") + XCTAssertAllEqual(String(keyPath: \Animal.$color), "color") } @objc diff --git a/Sources/CoreStoreSchema.swift b/Sources/CoreStoreSchema.swift index 39252ad..ef6f2c7 100644 --- a/Sources/CoreStoreSchema.swift +++ b/Sources/CoreStoreSchema.swift @@ -209,20 +209,23 @@ public final class CoreStoreSchema: DynamicSchema { let rawModel = NSManagedObjectModel() var entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription] = [:] var allCustomGettersSetters: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]] = [:] + var allFieldCoders: [DynamicEntity: [KeyPathString: Internals.AnyFieldCoder]] = [:] for entity in self.allEntities { - let (entityDescription, customGetterSetterByKeyPaths) = self.entityDescription( + let (entityDescription, customGetterSetterByKeyPaths, fieldCoders) = self.entityDescription( for: entity, initializer: CoreStoreSchema.firstPassCreateEntityDescription(from:in:) ) entityDescriptionsByEntity[entity] = (entityDescription.copy() as! NSEntityDescription) allCustomGettersSetters[entity] = customGetterSetterByKeyPaths + allFieldCoders[entity] = fieldCoders } CoreStoreSchema.secondPassConnectRelationshipAttributes(for: entityDescriptionsByEntity) CoreStoreSchema.thirdPassConnectInheritanceTreeAndIndexes(for: entityDescriptionsByEntity) CoreStoreSchema.fourthPassSynthesizeManagedObjectClasses( for: entityDescriptionsByEntity, - allCustomGettersSetters: allCustomGettersSetters + allCustomGettersSetters: allCustomGettersSetters, + allFieldCoders: allFieldCoders ) rawModel.entities = entityDescriptionsByEntity.values.sorted(by: { $0.name! < $1.name! }) @@ -254,22 +257,46 @@ public final class CoreStoreSchema: DynamicSchema { private var entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription] = [:] private var customGettersSettersByEntity: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]] = [:] + private var fieldCodersByEntity: [DynamicEntity: [KeyPathString: Internals.AnyFieldCoder]] = [:] private weak var cachedRawModel: NSManagedObjectModel? - private func entityDescription(for entity: DynamicEntity, initializer: (DynamicEntity, ModelVersion) -> (entity: NSEntityDescription, customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter])) -> (entity: NSEntityDescription, customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]) { + private func entityDescription( + for entity: DynamicEntity, + initializer: (DynamicEntity, ModelVersion) -> ( + entity: NSEntityDescription, + customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter], + fieldCoders: [KeyPathString: Internals.AnyFieldCoder] + ) + ) -> ( + entity: NSEntityDescription, + customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter], + fieldCoders: [KeyPathString: Internals.AnyFieldCoder] + ) { if let cachedEntityDescription = self.entityDescriptionsByEntity[entity] { - return (cachedEntityDescription, self.customGettersSettersByEntity[entity] ?? [:]) + return ( + cachedEntityDescription, + self.customGettersSettersByEntity[entity] ?? [:], + self.fieldCodersByEntity[entity] ?? [:] + ) } let modelVersion = self.modelVersion - let (entityDescription, customGetterSetterByKeyPaths) = withoutActuallyEscaping(initializer, do: { $0(entity, modelVersion) }) + let (entityDescription, customGetterSetterByKeyPaths, fieldCoders) = withoutActuallyEscaping( + initializer, + do: { $0(entity, modelVersion) } + ) self.entityDescriptionsByEntity[entity] = entityDescription self.customGettersSettersByEntity[entity] = customGetterSetterByKeyPaths - return (entityDescription, customGetterSetterByKeyPaths) + self.fieldCodersByEntity[entity] = fieldCoders + return (entityDescription, customGetterSetterByKeyPaths, fieldCoders) } - - private static func firstPassCreateEntityDescription(from entity: DynamicEntity, in modelVersion: ModelVersion) -> (entity: NSEntityDescription, customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]) { + + private static func firstPassCreateEntityDescription(from entity: DynamicEntity, in modelVersion: ModelVersion) -> ( + entity: NSEntityDescription, + customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter], + fieldCoders: [KeyPathString: Internals.AnyFieldCoder] + ) { let entityDescription = NSEntityDescription() entityDescription.coreStoreEntity = entity @@ -280,6 +307,7 @@ public final class CoreStoreSchema: DynamicSchema { var keyPathsByAffectedKeyPaths: [KeyPathString: Set] = [:] var customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter] = [:] + var fieldCoders: [KeyPathString: Internals.AnyFieldCoder] = [:] func createProperties(for type: CoreStoreObject.Type) -> [NSPropertyDescription] { var propertyDescriptions: [NSPropertyDescription] = [] @@ -302,9 +330,15 @@ public final class CoreStoreSchema: DynamicSchema { description.allowsExternalBinaryDataStorage = entityDescriptionValues.allowsExternalBinaryDataStorage description.versionHashModifier = entityDescriptionValues.versionHashModifier description.renamingIdentifier = entityDescriptionValues.renamingIdentifier + + let valueTransformer = entityDescriptionValues.valueTransformer + description.valueTransformerName = valueTransformer?.transformerName.rawValue + propertyDescriptions.append(description) + keyPathsByAffectedKeyPaths[attribute.keyPath] = entityDescriptionValues.affectedByKeyPaths customGetterSetterByKeyPaths[attribute.keyPath] = (attribute.getter, attribute.setter) + fieldCoders[attribute.keyPath] = valueTransformer case let attribute as AttributeProtocol: Internals.assert( @@ -350,7 +384,7 @@ public final class CoreStoreSchema: DynamicSchema { } entityDescription.properties = createProperties(for: entity.type as! CoreStoreObject.Type) entityDescription.keyPathsByAffectedKeyPaths = keyPathsByAffectedKeyPaths - return (entityDescription, customGetterSetterByKeyPaths) + return (entityDescription, customGetterSetterByKeyPaths, fieldCoders) } private static func secondPassConnectRelationshipAttributes(for entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription]) { @@ -523,7 +557,11 @@ public final class CoreStoreSchema: DynamicSchema { } } - private static func fourthPassSynthesizeManagedObjectClasses(for entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription], allCustomGettersSetters: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]]) { + private static func fourthPassSynthesizeManagedObjectClasses( + for entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription], + allCustomGettersSetters: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]], + allFieldCoders: [DynamicEntity: [KeyPathString: Internals.AnyFieldCoder]] + ) { func createManagedObjectSubclass(for entityDescription: NSEntityDescription, customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]?) { @@ -636,5 +674,13 @@ public final class CoreStoreSchema: DynamicSchema { customGetterSetterByKeyPaths: allCustomGettersSetters[dynamicEntity] ) } + + _ = allFieldCoders + .flatMap({ (_, values) in values }) + .reduce( + into: [:] as [NSValueTransformerName: Internals.AnyFieldCoder], + { (result, element) in result[element.value.transformerName] = element.value } + ) + .forEach({ (_, fieldCoder) in fieldCoder.register() }) } } diff --git a/Sources/DynamicSchema+Convenience.swift b/Sources/DynamicSchema+Convenience.swift index f357116..c70ccc4 100644 --- a/Sources/DynamicSchema+Convenience.swift +++ b/Sources/DynamicSchema+Convenience.swift @@ -177,6 +177,9 @@ extension DynamicSchema { defaultString = ", initial: /* required */" } + case .undefinedAttributeType: + #warning("TODO: Field.Computed") + continue default: fatalError("Unsupported attribute type: \(attribute.attributeType.rawValue)") } diff --git a/Sources/Field.Coded.swift b/Sources/Field.Coded.swift new file mode 100644 index 0000000..94e30fd --- /dev/null +++ b/Sources/Field.Coded.swift @@ -0,0 +1,532 @@ +// +// Field.Coded.swift +// CoreStore +// +// Copyright © 2020 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: - FieldContainer + +extension FieldContainer { + + // MARK: - Coded + + /** + The containing type for stored property values. Any type that conforms to `FieldStorableType` are supported. + ``` + class Animal: CoreStoreObject { + @Field.Stored("species") + var species = "" + + @Field.Computed("pluralName", customGetter: Animal.pluralName(_:)) + var pluralName: String = "" + + @Field.PlistCoded("color") + var color: UIColor? + } + ``` + - Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Stored(...) var` syntax will be ignored. + */ + @propertyWrapper + public struct Coded: AttributeKeyPathStringConvertible, FieldAttributeProtocol { + + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + @Field.Stored("title") + var title: String = "Mr." + + @Field.Stored("name") + var name: String = "" + + @Field.Computed("displayName", customGetter: Person.getName(_:)) + var displayName: String = "" + + private static func getName(_ partialObject: PartialObject) -> String { + let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + if !cachedDisplayName.isEmpty { + return cachedDisplayName + } + let title = partialObject.value(for: \.$title) + let name = partialObject.value(for: \.$name) + let displayName = "\(title) \(name)" + partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + return displayName + } + } + ``` + - parameter initial: the initial value for the property when the object is first create + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ + public init( + wrappedValue initial: @autoclosure @escaping () -> V, + _ keyPath: KeyPathString, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, + coder fieldCoderType: Coder.Type, + customGetter: ((_ partialObject: PartialObject) -> V)? = nil, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) where Coder.FieldStoredValue == V { + + self.init( + defaultValue: initial, + keyPath: keyPath, + isOptional: false, + versionHashModifier: versionHashModifier, + renamingIdentifier: previousVersionKeyPath, + valueTransformer: { Internals.AnyFieldCoder(fieldCoderType) }, + customGetter: customGetter, + customSetter: customSetter, + affectedByKeyPaths: affectedByKeyPaths + ) + } + + public init( + wrappedValue initial: @autoclosure @escaping () -> V, + _ keyPath: KeyPathString, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, + coder: (encode: (V) -> Data?, decode: (Data?) -> V), + customGetter: ((_ partialObject: PartialObject) -> V)? = nil, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) { + + self.init( + defaultValue: initial, + keyPath: keyPath, + isOptional: false, + versionHashModifier: versionHashModifier, + renamingIdentifier: previousVersionKeyPath, + valueTransformer: { Internals.AnyFieldCoder(tag: UUID(), encode: coder.encode, decode: coder.decode) }, + customGetter: customGetter, + customSetter: customSetter, + affectedByKeyPaths: affectedByKeyPaths + ) + } + + + // MARK: @propertyWrapper + + @available(*, unavailable) + public var wrappedValue: V { + + get { fatalError() } + set { fatalError() } + } + + public var projectedValue: Self { + + return self + } + + public static subscript( + _enclosingInstance instance: O, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath + ) -> V { + + get { + + Internals.assert( + instance.rawObject != nil, + "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." + ) + return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V + } + set { + + Internals.assert( + instance.rawObject != nil, + "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." + ) + return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue) + } + } + + + // MARK: AnyKeyPathStringConvertible + + public var cs_keyPathString: String { + + return self.keyPath + } + + + // MARK: KeyPathStringConvertible + + public typealias ObjectType = O + public typealias DestinationValueType = V + + + // MARK: AttributeKeyPathStringConvertible + + public typealias ReturnValueType = DestinationValueType + + + // MARK: PropertyProtocol + + internal let keyPath: KeyPathString + + + // MARK: FieldProtocol + + internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { + + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + let field = field as! Self + if let customGetter = field.customGetter { + + return customGetter(PartialObject(rawObject)) + } + let keyPath = field.keyPath + switch rawObject.value(forKey: keyPath) { + + case let rawValue as V: + return rawValue + + default: + return nil + } + } + + internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) { + + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + Internals.assert( + rawObject.isEditableInContext() == true, + "Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction." + ) + let newValue = newValue as! V + let field = field as! Self + let keyPath = field.keyPath + if let customSetter = field.customSetter { + + return customSetter(PartialObject(rawObject), newValue) + } + return rawObject.setValue(newValue, forKey: keyPath) + } + + + // MARK: FieldAttributeProtocol + + internal let entityDescriptionValues: () -> FieldAttributeProtocol.EntityDescriptionValues + + internal var getter: CoreStoreManagedObject.CustomGetter? { + + let keyPath = self.keyPath + guard let customGetter = self.customGetter else { + + return { (_ id: Any) -> Any? in + + let rawObject = id as! CoreStoreManagedObject + rawObject.willAccessValue(forKey: keyPath) + defer { + + rawObject.didAccessValue(forKey: keyPath) + } + switch rawObject.primitiveValue(forKey: keyPath) { + + case let valueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox: + rawObject.setPrimitiveValue(valueBox.value, forKey: keyPath) + return valueBox.value + + case let value: + return value + } + } + } + return { (_ id: Any) -> Any? in + + let rawObject = id as! CoreStoreManagedObject + rawObject.willAccessValue(forKey: keyPath) + defer { + + rawObject.didAccessValue(forKey: keyPath) + } + let value = customGetter(PartialObject(rawObject)) + return value + } + } + + internal var setter: CoreStoreManagedObject.CustomSetter? { + + guard let customSetter = self.customSetter else { + + return nil + } + let keyPath = self.keyPath + return { (_ id: Any, _ newValue: Any?) -> Void in + + let rawObject = id as! CoreStoreManagedObject + rawObject.willChangeValue(forKey: keyPath) + defer { + + rawObject.didChangeValue(forKey: keyPath) + } + customSetter( + PartialObject(rawObject), + newValue as! V + ) + } + } + + + // MARK: FilePrivate + + fileprivate init( + defaultValue: @escaping () -> Any?, + keyPath: KeyPathString, + isOptional: Bool, + versionHashModifier: @escaping () -> String?, + renamingIdentifier: @escaping () -> String?, + valueTransformer: @escaping () -> Internals.AnyFieldCoder?, + customGetter: ((_ partialObject: PartialObject) -> V)?, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? , + affectedByKeyPaths: @escaping () -> Set) { + + self.keyPath = keyPath + self.entityDescriptionValues = { + + let fieldCoder = valueTransformer() + return ( + attributeType: .transformableAttributeType, + isOptional: isOptional, + isTransient: false, + allowsExternalBinaryDataStorage: false, + versionHashModifier: versionHashModifier(), + renamingIdentifier: renamingIdentifier(), + valueTransformer: fieldCoder, + affectedByKeyPaths: affectedByKeyPaths(), + defaultValue: Internals.AnyFieldCoder.TransformableDefaultValueCodingBox( + defaultValue: defaultValue(), + fieldCoder: fieldCoder + ) + ) + } + self.customGetter = customGetter + self.customSetter = customSetter + } + + + // MARK: Private + + private let customGetter: ((_ partialObject: PartialObject) -> V)? + private let customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? + } +} + + +// MARK: - FieldContainer.Coded where V: FieldOptionalType + +extension FieldContainer.Coded where V: FieldOptionalType { + + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + @Field.Stored("title") + var title: String = "Mr." + + @Field.Stored("name") + var name: String = "" + + @Field.Computed("displayName", customGetter: Person.getName(_:)) + var displayName: String = "" + + private static func getName(_ partialObject: PartialObject) -> String { + let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + if !cachedDisplayName.isEmpty { + return cachedDisplayName + } + let title = partialObject.value(for: \.$title) + let name = partialObject.value(for: \.$name) + let displayName = "\(title) \(name)" + partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + return displayName + } + } + ``` + - parameter initial: the initial value for the property when the object is first create + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ + public init( + wrappedValue initial: @autoclosure @escaping () -> V = nil, + _ keyPath: KeyPathString, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, + coder: Coder.Type, + customGetter: ((_ partialObject: PartialObject) -> V)? = nil, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) where Coder.FieldStoredValue == V.Wrapped { + + self.init( + defaultValue: { initial().cs_wrappedValue }, + keyPath: keyPath, + isOptional: true, + versionHashModifier: versionHashModifier, + renamingIdentifier: previousVersionKeyPath, + valueTransformer: { Internals.AnyFieldCoder(coder) }, + customGetter: customGetter, + customSetter: customSetter, + affectedByKeyPaths: affectedByKeyPaths + ) + } + + public init( + wrappedValue initial: @autoclosure @escaping () -> V = nil, + _ keyPath: KeyPathString, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, + coder: (encode: (V) -> Data?, decode: (Data?) -> V), + customGetter: ((_ partialObject: PartialObject) -> V)? = nil, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) { + + self.init( + defaultValue: { initial().cs_wrappedValue }, + keyPath: keyPath, + isOptional: true, + versionHashModifier: versionHashModifier, + renamingIdentifier: previousVersionKeyPath, + valueTransformer: { Internals.AnyFieldCoder(tag: UUID(), encode: coder.encode, decode: coder.decode) }, + customGetter: customGetter, + customSetter: customSetter, + affectedByKeyPaths: affectedByKeyPaths + ) + } +} + + +// MARK: - FieldContainer.Coded where V: DefaultNSSecureCodable + +extension FieldContainer.Coded where V: DefaultNSSecureCodable { + + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + @Field.Stored("title") + var title: String = "Mr." + + @Field.Stored("name") + var name: String = "" + + @Field.Computed("displayName", customGetter: Person.getName(_:)) + var displayName: String = "" + + private static func getName(_ partialObject: PartialObject) -> String { + let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + if !cachedDisplayName.isEmpty { + return cachedDisplayName + } + let title = partialObject.value(for: \.$title) + let name = partialObject.value(for: \.$name) + let displayName = "\(title) \(name)" + partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + return displayName + } + } + ``` + - parameter initial: the initial value for the property when the object is first create + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ + public init( + wrappedValue initial: @autoclosure @escaping () -> V, + _ keyPath: KeyPathString, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, + customGetter: ((_ partialObject: PartialObject) -> V)? = nil, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) { + + self.init( + defaultValue: initial, + keyPath: keyPath, + isOptional: false, + versionHashModifier: versionHashModifier, + renamingIdentifier: previousVersionKeyPath, + valueTransformer: { Internals.AnyFieldCoder(FieldCoders.DefaultNSSecureCoding.self) }, + customGetter: customGetter, + customSetter: customSetter, + affectedByKeyPaths: affectedByKeyPaths + ) + } +} + + +// MARK: - FieldContainer.Coded where V: FieldOptionalType, V.Wrapped: DefaultNSSecureCodable + +extension FieldContainer.Coded where V: FieldOptionalType, V.Wrapped: DefaultNSSecureCodable { + + public init( + wrappedValue initial: @autoclosure @escaping () -> V = nil, + _ keyPath: KeyPathString, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, + customGetter: ((_ partialObject: PartialObject) -> V)? = nil, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) { + + self.init( + defaultValue: { initial().cs_wrappedValue }, + keyPath: keyPath, + isOptional: true, + versionHashModifier: versionHashModifier, + renamingIdentifier: previousVersionKeyPath, + valueTransformer: { Internals.AnyFieldCoder(FieldCoders.DefaultNSSecureCoding.self) }, + customGetter: customGetter, + customSetter: customSetter, + affectedByKeyPaths: affectedByKeyPaths + ) + } +} diff --git a/Sources/Field.Computed.swift b/Sources/Field.Computed.swift index 1bd55dd..8693032 100644 --- a/Sources/Field.Computed.swift +++ b/Sources/Field.Computed.swift @@ -79,14 +79,12 @@ extension FieldContainer { } ``` - parameter keyPath: the permanent attribute name for this property. - - parameter renamingIdentifier: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property and a destination entity property that share the same identifier indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's name. - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( _ keyPath: KeyPathString, - renamingIdentifier: @autoclosure @escaping () -> String? = nil, customGetter: ((_ partialObject: PartialObject) -> V)? = nil, customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { @@ -94,7 +92,6 @@ extension FieldContainer { self.init( keyPath: keyPath, isOptional: false, - renamingIdentifier: renamingIdentifier, customGetter: customGetter, customSetter: customSetter, affectedByKeyPaths: affectedByKeyPaths @@ -102,14 +99,13 @@ extension FieldContainer { } /** - Overload for compiler message only + Overload for compiler error message only */ - @available(*, unavailable, message: "Field.Computed properties are not allowed to have default values.") + @available(*, unavailable, message: "Field.Computed properties are not allowed to have initial values, including `nil`.") public init( wrappedValue initial: @autoclosure @escaping () -> V, _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, - renamingIdentifier: @autoclosure @escaping () -> String? = nil, customGetter: ((_ partialObject: PartialObject) -> V)? = nil, customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { @@ -274,7 +270,6 @@ extension FieldContainer { fileprivate init( keyPath: KeyPathString, isOptional: Bool, - renamingIdentifier: @escaping () -> String?, customGetter: ((_ partialObject: PartialObject) -> V)?, customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? , affectedByKeyPaths: @escaping () -> Set) { @@ -287,7 +282,8 @@ extension FieldContainer { isTransient: true, allowsExternalBinaryDataStorage: false, versionHashModifier: nil, - renamingIdentifier: renamingIdentifier(), + renamingIdentifier: nil, + valueTransformer: nil, affectedByKeyPaths: affectedByKeyPaths(), defaultValue: nil ) diff --git a/Sources/Field.PlistCoded.swift b/Sources/Field.PlistCoded.swift deleted file mode 100644 index 192ae0d..0000000 --- a/Sources/Field.PlistCoded.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// Field.PlistCoded.swift -// CoreStore -// -// Created by John Estropia on 2020/01/15. -// Copyright © 2020 John Rommel Estropia. All rights reserved. -// - -import Foundation diff --git a/Sources/Field.Stored.swift b/Sources/Field.Stored.swift index d9a7b0a..16babcd 100644 --- a/Sources/Field.Stored.swift +++ b/Sources/Field.Stored.swift @@ -81,7 +81,7 @@ extension FieldContainer { - parameter initial: the initial value for the property when the object is first create - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - - parameter renamingIdentifier: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property and a destination entity property that share the same identifier indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's name. + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. @@ -90,17 +90,18 @@ extension FieldContainer { wrappedValue initial: @autoclosure @escaping () -> V, _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, - renamingIdentifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, customGetter: ((_ partialObject: PartialObject) -> V)? = nil, customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, - affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) { self.init( wrappedValue: initial, keyPath: keyPath, isOptional: false, versionHashModifier: versionHashModifier, - renamingIdentifier: renamingIdentifier, + renamingIdentifier: previousVersionKeyPath, customGetter: customGetter, customSetter: customSetter, affectedByKeyPaths: affectedByKeyPaths @@ -185,11 +186,14 @@ extension FieldContainer { return customGetter(PartialObject(rawObject)) } let keyPath = field.keyPath - guard case let rawValue as V.FieldStoredNativeType = rawObject.value(forKey: keyPath) else { + switch rawObject.value(forKey: keyPath) { + case let rawValue as V.FieldStoredNativeType: + return V.cs_fromFieldStoredNativeType(rawValue) + + default: return nil } - return V.cs_fromFieldStoredNativeType(rawValue) } internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) { @@ -284,6 +288,7 @@ extension FieldContainer { allowsExternalBinaryDataStorage: false, versionHashModifier: versionHashModifier(), renamingIdentifier: renamingIdentifier(), + valueTransformer: nil, affectedByKeyPaths: affectedByKeyPaths(), defaultValue: initial().cs_toFieldStoredNativeType() ) @@ -334,7 +339,7 @@ extension FieldContainer.Stored where V: FieldOptionalType { - parameter initial: the initial value for the property when the object is first create - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - - parameter renamingIdentifier: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property and a destination entity property that share the same identifier indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's name. + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. @@ -343,7 +348,7 @@ extension FieldContainer.Stored where V: FieldOptionalType { wrappedValue initial: @autoclosure @escaping () -> V = nil, _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, - renamingIdentifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, customGetter: ((_ partialObject: PartialObject) -> V)? = nil, customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { @@ -353,7 +358,7 @@ extension FieldContainer.Stored where V: FieldOptionalType { keyPath: keyPath, isOptional: true, versionHashModifier: versionHashModifier, - renamingIdentifier: renamingIdentifier, + renamingIdentifier: previousVersionKeyPath, customGetter: customGetter, customSetter: customSetter, affectedByKeyPaths: affectedByKeyPaths diff --git a/Sources/FieldAttributeProtocol.swift b/Sources/FieldAttributeProtocol.swift new file mode 100644 index 0000000..367434f --- /dev/null +++ b/Sources/FieldAttributeProtocol.swift @@ -0,0 +1,49 @@ +// +// FieldAttributeProtocol.swift +// CoreStore +// +// Copyright © 2020 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 Foundation +import CoreData + + +// MARK: - FieldAttributeProtocol + +internal protocol FieldAttributeProtocol: FieldProtocol { + + typealias EntityDescriptionValues = ( + attributeType: NSAttributeType, + isOptional: Bool, + isTransient: Bool, + allowsExternalBinaryDataStorage: Bool, + versionHashModifier: String?, + renamingIdentifier: String?, + valueTransformer: Internals.AnyFieldCoder?, + affectedByKeyPaths: Set, + defaultValue: Any? + ) + + var entityDescriptionValues: () -> EntityDescriptionValues { get } + var getter: CoreStoreManagedObject.CustomGetter? { get } + var setter: CoreStoreManagedObject.CustomSetter? { get } +} diff --git a/Sources/FieldCoderType.swift b/Sources/FieldCoderType.swift new file mode 100644 index 0000000..1189017 --- /dev/null +++ b/Sources/FieldCoderType.swift @@ -0,0 +1,38 @@ +// +// FieldCoderType.swift +// CoreStore +// +// Copyright © 2020 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 Foundation +import CoreData + + +// MARK: - FieldCoderType + +public protocol FieldCoderType { + + associatedtype FieldStoredValue + + static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? + static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? +} diff --git a/Sources/FieldCoders.DefaultNSSecureCoding.swift b/Sources/FieldCoders.DefaultNSSecureCoding.swift new file mode 100644 index 0000000..989c4ec --- /dev/null +++ b/Sources/FieldCoders.DefaultNSSecureCoding.swift @@ -0,0 +1,87 @@ +// +// FieldCoders.DefaultNSSecureCoding.swift +// CoreStore +// +// Copyright © 2020 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 Foundation + + +// MARK: - FieldCoders + +extension FieldCoders { + + public struct DefaultNSSecureCoding: FieldCoderType { + + // MARK: FieldCoderType + + public typealias FieldStoredValue = T + + public static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? { + + return ValueTransformer(forName: self.transformerName)?.reverseTransformedValue(fieldValue) as? Data + } + + public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? { + + return ValueTransformer(forName: self.transformerName)?.transformedValue(data) as! FieldStoredValue? + } + + + // MARK: Internal + + internal static var transformerName: NSValueTransformerName { + + if #available(iOS 12.0, tvOS 12.0, watchOS 5.0, macOS 10.14, *) { + + return .secureUnarchiveFromDataTransformerName + } + else { + + return .keyedUnarchiveFromDataTransformerName + } + } + } +} + + +public protocol DefaultNSSecureCodable: NSObject, NSSecureCoding {} + +extension NSArray: DefaultNSSecureCodable {} + +extension NSDictionary: DefaultNSSecureCodable {} + +extension NSSet: DefaultNSSecureCodable {} + +extension NSString: DefaultNSSecureCodable {} + +extension NSNumber: DefaultNSSecureCodable {} + +extension NSDate: DefaultNSSecureCodable {} + +extension NSData: DefaultNSSecureCodable {} + +extension NSURL: DefaultNSSecureCodable {} + +extension NSUUID: DefaultNSSecureCodable {} + +extension NSNull: DefaultNSSecureCodable {} diff --git a/Sources/FieldCoders.Json.swift b/Sources/FieldCoders.Json.swift new file mode 100644 index 0000000..02de3a3 --- /dev/null +++ b/Sources/FieldCoders.Json.swift @@ -0,0 +1,59 @@ +// +// FieldCoders.Json.swift +// CoreStore +// +// Copyright © 2020 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 Foundation + + +// MARK: - FieldCoders + +extension FieldCoders { + + // MARK: - Json + + public struct Json: FieldCoderType { + + // MARK: FieldCoderType + + public typealias FieldStoredValue = V + + public static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? { + + guard let fieldValue = fieldValue else { + + return nil + } + return try? JSONEncoder().encode(fieldValue) + } + + public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? { + + guard let data = data else { + + return nil + } + return try? JSONDecoder().decode(FieldStoredValue.self, from: data) + } + } +} diff --git a/Sources/FieldCoders.NSCoding.swift b/Sources/FieldCoders.NSCoding.swift new file mode 100644 index 0000000..672e8d3 --- /dev/null +++ b/Sources/FieldCoders.NSCoding.swift @@ -0,0 +1,91 @@ +// +// FieldCoders.NSCoding.swift +// CoreStore +// +// Copyright © 2020 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 Foundation + + +// MARK: - FieldCoders + +extension FieldCoders { + + // MARK: - NSCoding + + public struct NSCoding: FieldCoderType { + + // MARK: FieldCoderType + + public typealias FieldStoredValue = V + + public static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? { + + guard let fieldValue = fieldValue else { + + return nil + } + if #available(iOS 11.0, macOS 10.13, watchOS 4.0, tvOS 11.0, *) { + + return try! NSKeyedArchiver.archivedData( + withRootObject: fieldValue, + requiringSecureCoding: self.requiresSecureCoding + ) + } + else { + + return NSKeyedArchiver.archivedData(withRootObject: fieldValue) + } + } + + public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? { + + guard let data = data else { + + return nil + } + if #available(iOS 11.0, macOS 10.13, watchOS 4.0, tvOS 11.0, *) { + + return try! NSKeyedUnarchiver.unarchivedObject(ofClass: FieldStoredValue.self, from: data) + } + else { + + return NSKeyedUnarchiver.unarchiveObject(with: data) as! FieldStoredValue? + } + } + + + // MARK: Private + + private static var requiresSecureCoding: Bool { + + switch FieldStoredValue.self { + + case let valueType as NSSecureCoding.Type: + return valueType.supportsSecureCoding + + default: + return false + } + } + } +} diff --git a/Sources/FieldCoders.Plist.swift b/Sources/FieldCoders.Plist.swift new file mode 100644 index 0000000..ffc1908 --- /dev/null +++ b/Sources/FieldCoders.Plist.swift @@ -0,0 +1,59 @@ +// +// FieldCoders.Plist.swift +// CoreStore +// +// Copyright © 2020 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 Foundation + + +// MARK: - FieldCoders + +extension FieldCoders { + + // MARK: - Plist + + public struct Plist: FieldCoderType { + + // MARK: FieldCoderType + + public typealias FieldStoredValue = V + + public static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? { + + guard let fieldValue = fieldValue else { + + return nil + } + return try? PropertyListEncoder().encode(fieldValue) + } + + public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? { + + guard let data = data else { + + return nil + } + return try? PropertyListDecoder().decode(FieldStoredValue.self, from: data) + } + } +} diff --git a/Sources/FieldCoders.swift b/Sources/FieldCoders.swift new file mode 100644 index 0000000..219cb36 --- /dev/null +++ b/Sources/FieldCoders.swift @@ -0,0 +1,30 @@ +// +// FieldCoders.swift +// CoreStore +// +// Copyright © 2020 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. +// + + +/** + Namespace for Built-in Field Coders + */ +public enum FieldCoders {} diff --git a/Sources/FieldProtocol.swift b/Sources/FieldProtocol.swift index e38e642..9ae3014 100644 --- a/Sources/FieldProtocol.swift +++ b/Sources/FieldProtocol.swift @@ -34,24 +34,3 @@ internal protocol FieldProtocol: PropertyProtocol { static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) } - - -// MARK: - FieldAttributeProtocol - -internal protocol FieldAttributeProtocol: FieldProtocol { - - typealias EntityDescriptionValues = ( - attributeType: NSAttributeType, - isOptional: Bool, - isTransient: Bool, - allowsExternalBinaryDataStorage: Bool, - versionHashModifier: String?, - renamingIdentifier: String?, - affectedByKeyPaths: Set, - defaultValue: Any? - ) - - var entityDescriptionValues: () -> EntityDescriptionValues { get } - var getter: CoreStoreManagedObject.CustomGetter? { get } - var setter: CoreStoreManagedObject.CustomSetter? { get } -} diff --git a/Sources/Internals.AnyFieldCoder.swift b/Sources/Internals.AnyFieldCoder.swift new file mode 100644 index 0000000..a59482e --- /dev/null +++ b/Sources/Internals.AnyFieldCoder.swift @@ -0,0 +1,222 @@ +// +// Internals.AnyFieldCoder.swift +// CoreStore +// +// Copyright © 2020 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 Foundation + + +// MARK: - Internals + +extension Internals { + + // MARK: - AnyFieldCoder + + internal struct AnyFieldCoder { + + // MARK: Internal + + internal let transformerName: NSValueTransformerName + internal let transformer: Foundation.ValueTransformer + internal let encodeToStoredData: (_ fieldValue: Any?) -> Data? + internal let decodeFromStoredData: (_ data: Data?) -> Any? + + internal init(_ fieldCoder: Coder.Type) { + + let transformer = CustomValueTransformer(fieldCoder: fieldCoder) + self.transformerName = transformer.id + self.transformer = transformer + self.encodeToStoredData = { + + switch $0 { + + case let value as Coder.FieldStoredValue: + return fieldCoder.encodeToStoredData(value) + + case let valueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox: + return fieldCoder.encodeToStoredData(valueBox.value as! Coder.FieldStoredValue?) + + default: + return fieldCoder.encodeToStoredData(nil) + } + } + self.decodeFromStoredData = { fieldCoder.decodeFromStoredData($0) } + } + + internal init(tag: UUID, encode: @escaping (V) -> Data?, decode: @escaping (Data?) -> V) { + + let transformer = CustomValueTransformer(tag: tag) + self.transformerName = transformer.id + self.transformer = transformer + self.encodeToStoredData = { encode($0 as! V) } + self.decodeFromStoredData = { decode($0) } + } + + internal func register() { + + let transformerName = self.transformerName + if #available(iOS 12.0, tvOS 12.0, watchOS 5.0, macOS 10.14, *) { + + if transformerName == .secureUnarchiveFromDataTransformerName { + + return + } + } + switch transformerName { + + case .keyedUnarchiveFromDataTransformerName, + .unarchiveFromDataTransformerName, + .isNotNilTransformerName, + .isNilTransformerName, + .negateBooleanTransformerName: + return + + case let transformerName: + Self.cachedCoders[transformerName] = self + + Foundation.ValueTransformer.setValueTransformer( + self.transformer, + forName: transformerName + ) + } + } + + + // MARK: FilePrivate + + fileprivate static var cachedCoders: [NSValueTransformerName: AnyFieldCoder] = [:] + + + // MARK: - TransformableDefaultValueCodingBox + + @objc(_CoreStore_Internals_TransformableDefaultValueCodingBox) + internal final class TransformableDefaultValueCodingBox: NSObject, NSSecureCoding { + + // MARK: Internal + + @objc + internal dynamic let transformerName: String + + @objc + internal dynamic let data: Data + + @nonobjc + internal let value: Any? + + internal init?(defaultValue: Any?, fieldCoder: Internals.AnyFieldCoder?) { + + guard + let fieldCoder = fieldCoder, + let defaultValue = defaultValue, + !(defaultValue is NSNull), + let data = fieldCoder.encodeToStoredData(defaultValue) + else { + + return nil + } + self.transformerName = fieldCoder.transformerName.rawValue + self.value = defaultValue + self.data = data + } + + + // MARK: NSSecureCoding + + @objc + dynamic class var supportsSecureCoding: Bool { + + return true + } + + + // MARK: NSCoding + + @objc + dynamic required init?(coder aDecoder: NSCoder) { + + guard + case let transformerName as String = aDecoder.decodeObject(forKey: #keyPath(TransformableDefaultValueCodingBox.transformerName)), + let transformer = ValueTransformer(forName: .init(transformerName)), + case let data as Data = aDecoder.decodeObject(forKey: #keyPath(TransformableDefaultValueCodingBox.data)), + let value = transformer.reverseTransformedValue(data) + else { + + return nil + } + self.transformerName = transformerName + self.data = data + self.value = value + } + + @objc + dynamic func encode(with coder: NSCoder) { + + coder.encode(self.data, forKey: #keyPath(TransformableDefaultValueCodingBox.data)) + coder.encode(self.transformerName, forKey: #keyPath(TransformableDefaultValueCodingBox.transformerName)) + } + } + } + + + // MARK: - CustomValueTransformer + + fileprivate final class CustomValueTransformer: ValueTransformer { + + // MARK: FilePrivate + + fileprivate let id: NSValueTransformerName + + fileprivate init(fieldCoder: Coder.Type) { + + self.id = .init(rawValue: "CoreStore.FieldCoders.CustomValueTransformer<\(String(reflecting: fieldCoder))>.transformerName") + } + + fileprivate init(tag: UUID) { + + self.id = .init(rawValue: "CoreStore.FieldCoders.CustomValueTransformer<\(tag.uuidString)>.transformerName") + } + + + // MARK: ValueTransformer + + override class func transformedValueClass() -> AnyClass { + + return NSData.self + } + + override class func allowsReverseTransformation() -> Bool { + + return true + } + + override func transformedValue(_ value: Any?) -> Any? { + + return AnyFieldCoder.cachedCoders[self.id]?.encodeToStoredData(value) as Data? + } + + override func reverseTransformedValue(_ value: Any?) -> Any? { + + return AnyFieldCoder.cachedCoders[self.id]?.decodeFromStoredData(value as! Data?) + } + } +} diff --git a/Sources/NSEntityDescription+Migration.swift b/Sources/NSEntityDescription+Migration.swift index 801ce25..4ba9c26 100644 --- a/Sources/NSEntityDescription+Migration.swift +++ b/Sources/NSEntityDescription+Migration.swift @@ -33,29 +33,49 @@ extension NSEntityDescription { @nonobjc internal func cs_resolveAttributeNames() -> [String: (attribute: NSAttributeDescription, versionHash: Data)] { - return self.attributesByName.reduce(into: [:], { (result, attribute: (name: String, description: NSAttributeDescription)) in - result[attribute.name] = (attribute.description, attribute.description.versionHash) - }) + + return self.attributesByName.reduce( + into: [:], + { (result, attribute: (name: String, description: NSAttributeDescription)) in + + result[attribute.name] = (attribute.description, attribute.description.versionHash) + } + ) } @nonobjc internal func cs_resolveAttributeRenamingIdentities() -> [String: (attribute: NSAttributeDescription, versionHash: Data)] { - return self.attributesByName.reduce(into: [:], { (result, attribute: (name: String, description: NSAttributeDescription)) in - result[attribute.description.renamingIdentifier ?? attribute.name] = (attribute.description, attribute.description.versionHash) - }) + + return self.attributesByName.reduce( + into: [:], + { (result, attribute: (name: String, description: NSAttributeDescription)) in + + result[attribute.description.renamingIdentifier ?? attribute.name] = (attribute.description, attribute.description.versionHash) + } + ) } @nonobjc internal func cs_resolveRelationshipNames() -> [String: (relationship: NSRelationshipDescription, versionHash: Data)] { - return self.relationshipsByName.reduce(into: [:], { (result, relationship: (name: String, description: NSRelationshipDescription)) in - result[relationship.name] = (relationship.description, relationship.description.versionHash) - }) + + return self.relationshipsByName.reduce( + into: [:], + { (result, relationship: (name: String, description: NSRelationshipDescription)) in + + result[relationship.name] = (relationship.description, relationship.description.versionHash) + } + ) } @nonobjc internal func cs_resolveRelationshipRenamingIdentities() -> [String: (relationship: NSRelationshipDescription, versionHash: Data)] { - return self.relationshipsByName.reduce(into: [:], { (result, relationship: (name: String, description: NSRelationshipDescription)) in - result[relationship.description.renamingIdentifier ?? relationship.name] = (relationship.description, relationship.description.versionHash) - }) + + return self.relationshipsByName.reduce( + into: [:], + { (result, relationship: (name: String, description: NSRelationshipDescription)) in + + result[relationship.description.renamingIdentifier ?? relationship.name] = (relationship.description, relationship.description.versionHash) + } + ) } } diff --git a/Sources/NSManagedObjectModel+Migration.swift b/Sources/NSManagedObjectModel+Migration.swift index 013fd2b..d602320 100644 --- a/Sources/NSManagedObjectModel+Migration.swift +++ b/Sources/NSManagedObjectModel+Migration.swift @@ -33,15 +33,25 @@ extension NSManagedObjectModel { @nonobjc internal func cs_resolveNames() -> [String: (entity: NSEntityDescription, versionHash: Data)] { - return self.entitiesByName.reduce(into: [:], { (result, entity: (name: String, description: NSEntityDescription)) in - result[entity.name] = (entity.description, entity.description.versionHash) - }) + + return self.entitiesByName.reduce( + into: [:], + { (result, entity: (name: String, description: NSEntityDescription)) in + + result[entity.name] = (entity.description, entity.description.versionHash) + } + ) } @nonobjc internal func cs_resolveRenamingIdentities() -> [String: (entity: NSEntityDescription, versionHash: Data)] { - return self.entitiesByName.reduce(into: [:], { (result, entity: (name: String, description: NSEntityDescription)) in - result[entity.description.renamingIdentifier ?? entity.name] = (entity.description, entity.description.versionHash) - }) + + return self.entitiesByName.reduce( + into: [:], + { (result, entity: (name: String, description: NSEntityDescription)) in + + result[entity.description.renamingIdentifier ?? entity.name] = (entity.description, entity.description.versionHash) + } + ) } } diff --git a/Sources/PartialObject.swift b/Sources/PartialObject.swift index be8c345..035b817 100644 --- a/Sources/PartialObject.swift +++ b/Sources/PartialObject.swift @@ -65,8 +65,10 @@ public struct PartialObject { case let value as V: return value - default: - return nil as Any? as! V // filter NSNull + case nil, + is NSNull, + _? /* any other unrelated type */ : + return nil as Any? as! V // There should be no uninitialized state at this point } } @@ -87,15 +89,41 @@ public struct PartialObject { This method does not invoke the access notification methods (`willAccessValue(forKey:)` and `didAccessValue(forKey:)`). This method is used primarily by subclasses that implement custom accessor methods that need direct access to the receiver’s private storage. */ - public func primitiveValue(for property: (O) -> FieldContainer.Computed) -> V { + public func primitiveValue(for property: (O) -> FieldContainer.Computed) -> V? { switch self.rawObject.primitiveValue(forKey: property(O.meta).keyPath) { case let value as V: return value - default: - return nil as Any? as! V // filter NSNull + case nil, + is NSNull, + _? /* any other unrelated type */ : + return nil + } + } + + /** + Returns the value for the specified property from the managed object’s private internal storage. + + This method does not invoke the access notification methods (`willAccessValue(forKey:)` and `didAccessValue(forKey:)`). This method is used primarily by subclasses that implement custom accessor methods that need direct access to the receiver’s private storage. + */ + public func primitiveValue(for property: (O) -> FieldContainer.Coded) -> V? { + + let keyPath = property(O.meta).keyPath + switch self.rawObject.primitiveValue(forKey: keyPath) { + + case let valueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox: + rawObject.setPrimitiveValue(valueBox.value, forKey: keyPath) + return valueBox.value as? V + + case let value as V: + return value + + case nil, + is NSNull, + _? /* any other unrelated type */ : + return nil } } From 92ad89504494d758c8a86c656b3ec5e23525d052 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Mon, 20 Jan 2020 17:13:01 +0900 Subject: [PATCH 03/20] Field.Relationship propertyWrapper --- CoreStore.xcodeproj/project.pbxproj | 30 ++ CoreStoreTests/DynamicModelTests.swift | 133 ++++----- CoreStoreTests/WhereTests.swift | 52 ++-- Sources/CoreStoreSchema.swift | 37 +++ Sources/DynamicObject.swift | 3 + Sources/FIeldRelationshipType.swift | 142 ++++++++++ Sources/Field.Relationship.swift | 346 ++++++++++++++++++++++++ Sources/FieldRelationshipProtocol.swift | 49 ++++ Sources/KeyPath+Querying.swift | 121 +++++++++ Sources/Select.swift | 233 ++-------------- Sources/Where.Expression.swift | 28 ++ Sources/Where.swift | 11 + 12 files changed, 881 insertions(+), 304 deletions(-) create mode 100644 Sources/FIeldRelationshipType.swift create mode 100644 Sources/Field.Relationship.swift create mode 100644 Sources/FieldRelationshipProtocol.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index a1267e9..9d27086 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -613,6 +613,18 @@ B57D27C21D0BC20100539C58 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27C11D0BC20100539C58 /* QueryTests.swift */; }; B57D27C31D0BC20100539C58 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27C11D0BC20100539C58 /* QueryTests.swift */; }; B57D27C41D0BC20100539C58 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27C11D0BC20100539C58 /* QueryTests.swift */; }; + B57E6FA223D302FA000FD031 /* Field.Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57E6FA123D302FA000FD031 /* Field.Relationship.swift */; }; + B57E6FA323D302FA000FD031 /* Field.Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57E6FA123D302FA000FD031 /* Field.Relationship.swift */; }; + B57E6FA423D302FA000FD031 /* Field.Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57E6FA123D302FA000FD031 /* Field.Relationship.swift */; }; + B57E6FA523D302FA000FD031 /* Field.Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57E6FA123D302FA000FD031 /* Field.Relationship.swift */; }; + B57E6FA723D305D6000FD031 /* FIeldRelationshipType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57E6FA623D305D6000FD031 /* FIeldRelationshipType.swift */; }; + B57E6FA823D305D6000FD031 /* FIeldRelationshipType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57E6FA623D305D6000FD031 /* FIeldRelationshipType.swift */; }; + B57E6FA923D305D6000FD031 /* FIeldRelationshipType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57E6FA623D305D6000FD031 /* FIeldRelationshipType.swift */; }; + B57E6FAA23D305D6000FD031 /* FIeldRelationshipType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57E6FA623D305D6000FD031 /* FIeldRelationshipType.swift */; }; + B57E6FAC23D30A5B000FD031 /* FieldRelationshipProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57E6FAB23D30A5B000FD031 /* FieldRelationshipProtocol.swift */; }; + B57E6FAD23D30A5B000FD031 /* FieldRelationshipProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57E6FAB23D30A5B000FD031 /* FieldRelationshipProtocol.swift */; }; + B57E6FAE23D30A5B000FD031 /* FieldRelationshipProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57E6FAB23D30A5B000FD031 /* FieldRelationshipProtocol.swift */; }; + B57E6FAF23D30A5B000FD031 /* FieldRelationshipProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57E6FAB23D30A5B000FD031 /* FieldRelationshipProtocol.swift */; }; B580857A1CDF808C004C2EEB /* SetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58085741CDF7F00004C2EEB /* SetupTests.swift */; }; B580857B1CDF808D004C2EEB /* SetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58085741CDF7F00004C2EEB /* SetupTests.swift */; }; B580857C1CDF808F004C2EEB /* SetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58085741CDF7F00004C2EEB /* SetupTests.swift */; }; @@ -1113,6 +1125,9 @@ B56E4EE323CEDF0900E1708C /* Field.Computed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.Computed.swift; sourceTree = ""; }; B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTestDataTestCase.swift; sourceTree = ""; }; B57D27C11D0BC20100539C58 /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; + B57E6FA123D302FA000FD031 /* Field.Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.Relationship.swift; sourceTree = ""; }; + B57E6FA623D305D6000FD031 /* FIeldRelationshipType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FIeldRelationshipType.swift; sourceTree = ""; }; + B57E6FAB23D30A5B000FD031 /* FieldRelationshipProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldRelationshipProtocol.swift; sourceTree = ""; }; B58085741CDF7F00004C2EEB /* SetupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupTests.swift; sourceTree = ""; }; B581B9312362BB8C002BDB2B /* ObjectPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectPublisherTests.swift; sourceTree = ""; }; B5831B6F1F34AC3400A9F647 /* AttributeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributeProtocol.swift; sourceTree = ""; }; @@ -1461,6 +1476,7 @@ B5831B741F34AC7A00A9F647 /* RelationshipProtocol.swift */, B56E4ED323CDB54A00E1708C /* FieldProtocol.swift */, B50C3ED923D0545700B29880 /* FieldAttributeProtocol.swift */, + B57E6FAB23D30A5B000FD031 /* FieldRelationshipProtocol.swift */, B50564D22350CC3100482308 /* PropertyProtocol.swift */, B53D9E5823513712000F48FB /* DiffableDataSourceSnapshotProtocol.swift */, B50E175623517DE4004F033C /* Differentiable.swift */, @@ -1608,6 +1624,7 @@ B56E4ECE23CD9E4200E1708C /* Field.Stored.swift */, B56E4EE323CEDF0900E1708C /* Field.Computed.swift */, B50C3EE423D153EA00B29880 /* Field.Coded.swift */, + B57E6FA123D302FA000FD031 /* Field.Relationship.swift */, B50C3EDE23D05BB200B29880 /* Coders */, B56E4EDD23CEBB0400E1708C /* Supported Values */, ); @@ -1619,6 +1636,7 @@ children = ( B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */, B56E4ED823CEB8E700E1708C /* FieldStorableType.swift */, + B57E6FA623D305D6000FD031 /* FIeldRelationshipType.swift */, ); name = "Supported Values"; sourceTree = ""; @@ -2301,6 +2319,7 @@ B5C976E31C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift in Sources */, B53FBA121CAB63CB00F0D40A /* Progress+ObjectiveC.swift in Sources */, B5831B751F34AC7A00A9F647 /* RelationshipProtocol.swift in Sources */, + B57E6FA723D305D6000FD031 /* FIeldRelationshipType.swift in Sources */, B51B5C2D22D43E38009FA3BA /* KeyPath+KeyPaths.swift in Sources */, B50564D32350CC3100482308 /* PropertyProtocol.swift in Sources */, B5D8CA762346E7590055D7D1 /* DataStack+DataSources.swift in Sources */, @@ -2435,6 +2454,7 @@ B5A991EC1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */, B5FE4DA71C84FB4400FA6A91 /* InMemoryStore.swift in Sources */, B52F743D1E9B8724005F3DAC /* DynamicSchema.swift in Sources */, + B57E6FA223D302FA000FD031 /* Field.Relationship.swift in Sources */, B5E8A72021C1015300EF006A /* CoreStoreObject+Observing.swift in Sources */, B53D9E5923513712000F48FB /* DiffableDataSourceSnapshotProtocol.swift in Sources */, B56E4ED923CEB8E700E1708C /* FieldStorableType.swift in Sources */, @@ -2446,6 +2466,7 @@ B5E834B91B76311F001D3D50 /* BaseDataTransaction+Importing.swift in Sources */, B5E84EE61AFF84610064E85B /* DefaultLogger.swift in Sources */, B53FBA041CAB300C00F0D40A /* CSMigrationType.swift in Sources */, + B57E6FAC23D30A5B000FD031 /* FieldRelationshipProtocol.swift in Sources */, B509D7C923C8491C00F42824 /* Relationship.ToManyOrdered.swift in Sources */, B5E84EF41AFF846E0064E85B /* AsynchronousDataTransaction.swift in Sources */, B50C3EEF23D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift in Sources */, @@ -2549,6 +2570,7 @@ B53FBA141CAB63CB00F0D40A /* Progress+ObjectiveC.swift in Sources */, B5831B761F34AC7A00A9F647 /* RelationshipProtocol.swift in Sources */, B5BF7FB3234C97910070E741 /* DiffableDataSource.swift in Sources */, + B57E6FA823D305D6000FD031 /* FIeldRelationshipType.swift in Sources */, B5E1B5AA1CAA49E2007FD580 /* CSDataStack+Migrating.swift in Sources */, B5D339F21E94AF5800C880DE /* CoreStoreStrings.swift in Sources */, B5E1B59F1CAA2568007FD580 /* CSDataStack+Observing.swift in Sources */, @@ -2683,6 +2705,7 @@ B5AA37F2235C28EE00FFD4B9 /* DiffableDataSource.CollectionViewAdapter-AppKit.swift in Sources */, B50E175D2351848E004F033C /* Internals.DiffableDataUIDispatcher.DiffResult.swift in Sources */, B5474D162227C08700B21FEC /* Internals.CoreStoreFetchRequest.swift in Sources */, + B57E6FA323D302FA000FD031 /* Field.Relationship.swift in Sources */, B501322B2346A9AE00FC238B /* ListPublisher.swift in Sources */, B56924001EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift in Sources */, B56E4EDA23CEB8E700E1708C /* FieldStorableType.swift in Sources */, @@ -2694,6 +2717,7 @@ 82BA18BE1C4BBD4A00A0916E /* Tweak.swift in Sources */, B509D7C523C848DA00F42824 /* Relationship.ToOne.swift in Sources */, B5DBE2CE1C9914A900B5CEFA /* CSCoreStore.swift in Sources */, + B57E6FAD23D30A5B000FD031 /* FieldRelationshipProtocol.swift in Sources */, B5831B711F34AC3400A9F647 /* AttributeProtocol.swift in Sources */, B546F95E1C9A12B800D5AC55 /* CSSQliteStore.swift in Sources */, B50C3EF023D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift in Sources */, @@ -2797,6 +2821,7 @@ B5BF7FCE234D80910070E741 /* Internals.LazyNonmutating.swift in Sources */, B52DD19E1BE1F92C00949AFE /* AsynchronousDataTransaction.swift in Sources */, B50564D62350CC3100482308 /* PropertyProtocol.swift in Sources */, + B57E6FAA23D305D6000FD031 /* FIeldRelationshipType.swift in Sources */, B5831B781F34AC7A00A9F647 /* RelationshipProtocol.swift in Sources */, B52DD1981BE1F92500949AFE /* CoreStore+Setup.swift in Sources */, B5D339F41E94AF5800C880DE /* CoreStoreStrings.swift in Sources */, @@ -2931,6 +2956,7 @@ 18166886232B9ED20097C275 /* KeyPath+KeyPaths.swift in Sources */, B5E8A72321C1015300EF006A /* CoreStoreObject+Observing.swift in Sources */, B50E175F2351848E004F033C /* Internals.DiffableDataUIDispatcher.DiffResult.swift in Sources */, + B57E6FA523D302FA000FD031 /* Field.Relationship.swift in Sources */, B5474D182227C08700B21FEC /* Internals.CoreStoreFetchRequest.swift in Sources */, B501322E2346A9B100FC238B /* ListPublisher.swift in Sources */, B56E4EDC23CEB8E700E1708C /* FieldStorableType.swift in Sources */, @@ -2942,6 +2968,7 @@ B52DD1B91BE1F94000949AFE /* CoreStore+Migration.swift in Sources */, B509D7C723C848DA00F42824 /* Relationship.ToOne.swift in Sources */, B5519A5C1CA2008C002BEF78 /* CSBaseDataTransaction.swift in Sources */, + B57E6FAF23D30A5B000FD031 /* FieldRelationshipProtocol.swift in Sources */, B5DBE2D51C991B3E00B5CEFA /* CSDataStack.swift in Sources */, B5831B731F34AC3400A9F647 /* AttributeProtocol.swift in Sources */, B50C3EF223D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift in Sources */, @@ -3045,6 +3072,7 @@ B5E1B5AB1CAA49E2007FD580 /* CSDataStack+Migrating.swift in Sources */, B5831B771F34AC7A00A9F647 /* RelationshipProtocol.swift in Sources */, B5BF7FB4234C97910070E741 /* DiffableDataSource.swift in Sources */, + B57E6FA923D305D6000FD031 /* FIeldRelationshipType.swift in Sources */, B5D339F31E94AF5800C880DE /* CoreStoreStrings.swift in Sources */, B5E1B5A01CAA2568007FD580 /* CSDataStack+Observing.swift in Sources */, B5ECDC261CA81A3900C7F112 /* CSCoreStore+Querying.swift in Sources */, @@ -3179,6 +3207,7 @@ B5AA37F3235C28EE00FFD4B9 /* DiffableDataSource.CollectionViewAdapter-AppKit.swift in Sources */, B50E175E2351848E004F033C /* Internals.DiffableDataUIDispatcher.DiffResult.swift in Sources */, B5474D172227C08700B21FEC /* Internals.CoreStoreFetchRequest.swift in Sources */, + B57E6FA423D302FA000FD031 /* Field.Relationship.swift in Sources */, B501322D2346A9B000FC238B /* ListPublisher.swift in Sources */, B56924011EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift in Sources */, B56E4EDB23CEB8E700E1708C /* FieldStorableType.swift in Sources */, @@ -3190,6 +3219,7 @@ B56321841BD65216006C9394 /* DefaultLogger.swift in Sources */, B509D7C623C848DA00F42824 /* Relationship.ToOne.swift in Sources */, B5DBE2CF1C9914A900B5CEFA /* CSCoreStore.swift in Sources */, + B57E6FAE23D30A5B000FD031 /* FieldRelationshipProtocol.swift in Sources */, B5831B721F34AC3400A9F647 /* AttributeProtocol.swift in Sources */, B546F95F1C9A12B800D5AC55 /* CSSQliteStore.swift in Sources */, B50C3EF123D1605C00B29880 /* FieldCoders.DefaultNSSecureCoding.swift in Sources */, diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index 7cd1362..eba30a4 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -44,7 +44,8 @@ class Animal: CoreStoreObject { @Field.Coded("color", coder: FieldCoders.NSCoding.self) var color: Color? = .blue - let master = Relationship.ToOne("master") + @Field.Relationship("master") + var master: Person? } class Dog: Animal { @@ -52,15 +53,45 @@ class Dog: Animal { @Field.Stored("nickname") var nickname: String? - let age = Value.Required("age", initial: 1) - let friends = Relationship.ToManyOrdered("friends") - let friendedBy = Relationship.ToManyUnordered("friendedBy", inverse: { $0.friends }) + @Field.Stored("age") + var age: Int = 1 + + @Field.Relationship("friends") + var friends: [Dog] + + @Field.Relationship("friendedBy", inverse: \.$friends) + var friendedBy: Set } struct CustomType { var string = "customString" } +enum Job: String { + + case unemployed + case engineer + case doctor + case lawyer + + init?(data: Data) { + + guard + let rawValue = String(data: data, encoding: .utf8), + let value = Self.init(rawValue: rawValue) + else { + + return nil + } + self = value + } + + func toData() -> Data { + + return Data(self.rawValue.utf8) + } +} + class Person: CoreStoreObject { @Field.Stored("title", customSetter: Person.setTitle(_:_:)) @@ -84,47 +115,25 @@ class Person: CoreStoreObject { @Field.Coded( "job", coder: ( - encode: { $0.data }, + encode: { $0.toData() }, decode: { $0.flatMap(Job.init(data:)) ?? .unemployed } ) ) var job: Job = .unemployed - let spouse = Relationship.ToOne("spouse") - - let pets = Relationship.ToManyUnordered("pets", inverse: { $0.master }) + @Field.Relationship("spouse") + var spouse: Person? - private let _spouse = Relationship.ToOne("_spouseInverse", inverse: { $0.spouse }) + @Field.Relationship("pets", inverse: \.$master) + var pets: Set - enum Job: String { - - case unemployed - case engineer - case doctor - case lawyer - - init?(data: Data) { - - guard - let rawValue = String(data: data, encoding: .utf8), - let value = Self.init(rawValue: rawValue) - else { - - return nil - } - self = value - } - - var data: Data { - - return Data(self.rawValue.utf8) - } - } + @Field.Relationship("_spouseInverse", inverse: \.$spouse) + private var spouseInverse: Person? private static func setTitle(_ partialObject: PartialObject, _ newValue: String) { partialObject.setPrimitiveValue(newValue, for: \.$title) - partialObject.setPrimitiveValue(nil, for: { $0.$displayName }) + partialObject.setPrimitiveValue(nil, for: \.$displayName) } private static func setName(_ partialObject: PartialObject, _ newValue: String) { @@ -226,8 +235,8 @@ class DynamicModelTests: BaseTestDataTestCase { case String(keyPath: \Animal.$species): XCTAssertTrue(property is FieldContainer.Stored) - case String(keyPath: \Animal.master): - XCTAssertTrue(property is RelationshipContainer.ToOne) + case String(keyPath: \Animal.$master): + XCTAssertTrue(property is FieldContainer.Relationship) case String(keyPath: \Animal.$color): XCTAssertTrue(property is FieldContainer.Coded) @@ -240,7 +249,7 @@ class DynamicModelTests: BaseTestDataTestCase { let dog = transaction.create(Into()) XCTAssertEqual(dog.species, "Swift") XCTAssertEqual(dog.nickname, nil) - XCTAssertEqual(dog.age.value, 1) + XCTAssertEqual(dog.age, 1) for property in Dog.metaProperties(includeSuperclasses: true) { @@ -249,8 +258,8 @@ class DynamicModelTests: BaseTestDataTestCase { case String(keyPath: \Dog.$species): XCTAssertTrue(property is FieldContainer.Stored) - case String(keyPath: \Dog.master): - XCTAssertTrue(property is RelationshipContainer.ToOne) + case String(keyPath: \Dog.$master): + XCTAssertTrue(property is FieldContainer.Relationship) case String(keyPath: \Dog.$color): XCTAssertTrue(property is FieldContainer.Coded) @@ -258,14 +267,14 @@ class DynamicModelTests: BaseTestDataTestCase { case String(keyPath: \Dog.$nickname): XCTAssertTrue(property is FieldContainer.Stored) - case String(keyPath: \Dog.age): - XCTAssertTrue(property is ValueContainer.Required) + case String(keyPath: \Dog.$age): + XCTAssertTrue(property is FieldContainer.Stored) - case String(keyPath: \Dog.friends): - XCTAssertTrue(property is RelationshipContainer.ToManyOrdered) + case String(keyPath: \Dog.$friends): + XCTAssertTrue(property is FieldContainer.Relationship<[Dog]>) - case String(keyPath: \Dog.friendedBy): - XCTAssertTrue(property is RelationshipContainer.ToManyUnordered) + case String(keyPath: \Dog.$friendedBy): + XCTAssertTrue(property is FieldContainer.Relationship>) default: XCTFail("Unknown KeyPath: \"\(property.keyPath)\"") @@ -325,7 +334,7 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(dog.nickname, "Spot") let person = transaction.create(Into()) - XCTAssertTrue(person.pets.value.isEmpty) + XCTAssertTrue(person.pets.isEmpty) XCTAssertEqual(person.customField.string, "customString") XCTAssertEqual(person.job, .unemployed) @@ -382,12 +391,12 @@ class DynamicModelTests: BaseTestDataTestCase { - person.pets.value.insert(dog) + person.pets.insert(dog) XCTAssertEqual(person.pets.count, 1) - XCTAssertEqual(person.pets.value.first, dog) - XCTAssertEqual(person.pets.value.first?.master.value, person) - XCTAssertEqual(dog.master.value, person) - XCTAssertEqual(dog.master.value?.pets.value.first, dog) + XCTAssertEqual(person.pets.first, dog) + XCTAssertEqual(person.pets.first?.master, person) + XCTAssertEqual(dog.master, person) + XCTAssertEqual(dog.master?.pets.first, dog) }, success: { _ in @@ -424,42 +433,44 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(person!.displayName, "Sir John") XCTAssertEqual(person!.customField.string, "customString") XCTAssertEqual(person!.job, .engineer) - XCTAssertEqual(person!.pets.value.first, dog) + XCTAssertEqual(person!.pets.first, dog) - let p3 = Where({ $0.age == 10 }) + let p3 = Where({ $0.$age == 10 }) XCTAssertEqual(p3.predicate, NSPredicate(format: "%K == %d", "age", 10)) - let totalAge = try transaction.queryValue(From().select(Int.self, .sum(\Dog.age))) + let totalAge = try transaction.queryValue( + From().select(Int.self, .sum(\.$age)) + ) XCTAssertEqual(totalAge, 1) _ = try transaction.fetchAll( From() - .where(\Animal.$species == "Dog" && \Dog.age == 10) + .where(\Animal.$species == "Dog" && \Dog.$age == 10) ) _ = try transaction.fetchAll( From() - .where(\Dog.age == 10 && \Animal.$species == "Dog") + .where(\Dog.$age == 10 && \Animal.$species == "Dog") .orderBy(.ascending({ $0.$species })) ) _ = try transaction.fetchAll( From(), - Where({ $0.age > 10 && $0.age <= 15 }) + Where({ $0.$age > 10 && $0.$age <= 15 }) ) _ = try transaction.fetchAll( From(), - Where({ $0.$species == "Dog" && $0.age == 10 }) + Where({ $0.$species == "Dog" && $0.$age == 10 }) ) _ = try transaction.fetchAll( From(), - Where({ $0.age == 10 && $0.$species == "Dog" }) + Where({ $0.$age == 10 && $0.$species == "Dog" }) ) _ = try transaction.fetchAll( From(), - Where({ $0.age > 10 && $0.age <= 15 }) + Where({ $0.$age > 10 && $0.$age <= 15 }) ) _ = try transaction.fetchAll( From(), - (\Dog.age > 10 && \Dog.age <= 15) + (\Dog.$age > 10 && \Dog.$age <= 15) ) }, success: { _ in diff --git a/CoreStoreTests/WhereTests.swift b/CoreStoreTests/WhereTests.swift index 1dbff23..6dcaed7 100644 --- a/CoreStoreTests/WhereTests.swift +++ b/CoreStoreTests/WhereTests.swift @@ -104,18 +104,18 @@ final class WhereTests: XCTestCase { XCTAssertAllEqual( "master.pets", - (\Animal.master ~ \.pets).description, - String(keyPath: \Animal.master ~ \.pets) + (\Animal.$master ~ \.$pets).description, + String(keyPath: \Animal.$master ~ \.$pets) ) XCTAssertAllEqual( "master.pets.species", - (\Animal.master ~ \.pets ~ \.$species).description, - String(keyPath: \Animal.master ~ \.pets ~ \.$species) + (\Animal.$master ~ \.$pets ~ \.$species).description, + String(keyPath: \Animal.$master ~ \.$pets ~ \.$species) ) XCTAssertAllEqual( "master.pets.master", - (\Animal.master ~ \.pets ~ \.master).description, - String(keyPath: \Animal.master ~ \.pets ~ \.master) + (\Animal.$master ~ \.$pets ~ \.$master).description, + String(keyPath: \Animal.$master ~ \.$pets ~ \.$master) ) } } @@ -138,8 +138,8 @@ final class WhereTests: XCTestCase { XCTAssertAllEqual( "master.pets.@count", - (\Animal.master ~ \.pets).count().description, - String(keyPath: (\Animal.master ~ \.pets).count()) + (\Animal.$master ~ \.$pets).count().description, + String(keyPath: (\Animal.$master ~ \.$pets).count()) ) } } @@ -162,13 +162,13 @@ final class WhereTests: XCTestCase { XCTAssertAllEqual( "ANY master.pets", - (\Animal.master ~ \.pets).any().description, - String(keyPath: (\Animal.master ~ \.pets).any()) + (\Animal.$master ~ \.$pets).any().description, + String(keyPath: (\Animal.$master ~ \.$pets).any()) ) XCTAssertAllEqual( "ANY master.pets.species", - (\Animal.master ~ \.pets ~ \.$species).any().description, - String(keyPath: (\Animal.master ~ \.pets ~ \.$species).any()) + (\Animal.$master ~ \.$pets ~ \.$species).any().description, + String(keyPath: (\Animal.$master ~ \.$pets ~ \.$species).any()) ) } } @@ -191,13 +191,13 @@ final class WhereTests: XCTestCase { XCTAssertAllEqual( "ALL master.pets", - (\Animal.master ~ \.pets).all().description, - String(keyPath: (\Animal.master ~ \.pets).all()) + (\Animal.$master ~ \.$pets).all().description, + String(keyPath: (\Animal.$master ~ \.$pets).all()) ) XCTAssertAllEqual( "ALL master.pets.species", - (\Animal.master ~ \.pets ~ \.$species).all().description, - String(keyPath: (\Animal.master ~ \.pets ~ \.$species).all()) + (\Animal.$master ~ \.$pets ~ \.$species).all().description, + String(keyPath: (\Animal.$master ~ \.$pets ~ \.$species).all()) ) } } @@ -220,13 +220,13 @@ final class WhereTests: XCTestCase { XCTAssertAllEqual( "NONE master.pets", - (\Animal.master ~ \.pets).none().description, - String(keyPath: (\Animal.master ~ \.pets).none()) + (\Animal.$master ~ \.$pets).none().description, + String(keyPath: (\Animal.$master ~ \.$pets).none()) ) XCTAssertAllEqual( "NONE master.pets.species", - (\Animal.master ~ \.pets ~ \.$species).none().description, - String(keyPath: (\Animal.master ~ \.pets ~ \.$species).none()) + (\Animal.$master ~ \.$pets ~ \.$species).none().description, + String(keyPath: (\Animal.$master ~ \.$pets ~ \.$species).none()) ) } } @@ -247,7 +247,7 @@ final class WhereTests: XCTestCase { } do { - let whereClause: Where = (\.master ~ \.$name) == dummy + let whereClause: Where = (\.$master ~ \.$name) == dummy let predicate = NSPredicate(format: "master.name == %@", dummy) XCTAssertAllEqual(whereClause, Where(predicate)) XCTAssertAllEqual(whereClause.predicate, predicate) @@ -265,7 +265,7 @@ final class WhereTests: XCTestCase { } do { - let whereClause: Where = (\.master ~ \.spouse ~ \.$name) == dummy + let whereClause: Where = (\.$master ~ \.$spouse ~ \.$name) == dummy let predicate = NSPredicate(format: "master.spouse.name == %@", dummy) XCTAssertAllEqual(whereClause, Where(predicate)) XCTAssertAllEqual(whereClause.predicate, predicate) @@ -283,7 +283,7 @@ final class WhereTests: XCTestCase { } do { - let whereClause: Where = (\.master ~ \.pets).count() == count + let whereClause: Where = (\.$master ~ \.$pets).count() == count let predicate = NSPredicate(format: "master.pets.@count == %d", count) XCTAssertAllEqual(whereClause, Where(predicate)) XCTAssertAllEqual(whereClause.predicate, predicate) @@ -301,7 +301,7 @@ final class WhereTests: XCTestCase { } do { - let whereClause: Where = (\.master ~ \.pets ~ \.$species).any() == dummy + let whereClause: Where = (\.$master ~ \.$pets ~ \.$species).any() == dummy let predicate = NSPredicate(format: "ANY master.pets.species == %@", dummy) XCTAssertAllEqual(whereClause, Where(predicate)) XCTAssertAllEqual(whereClause.predicate, predicate) @@ -319,7 +319,7 @@ final class WhereTests: XCTestCase { } do { - let whereClause: Where = (\.master ~ \.pets ~ \.$species).all() == dummy + let whereClause: Where = (\.$master ~ \.$pets ~ \.$species).all() == dummy let predicate = NSPredicate(format: "ALL master.pets.species == %@", dummy) XCTAssertAllEqual(whereClause, Where(predicate)) XCTAssertAllEqual(whereClause.predicate, predicate) @@ -337,7 +337,7 @@ final class WhereTests: XCTestCase { } do { - let whereClause: Where = (\.master ~ \.pets ~ \.$species).none() == dummy + let whereClause: Where = (\.$master ~ \.$pets ~ \.$species).none() == dummy let predicate = NSPredicate(format: "NONE master.pets.species == %@", dummy) XCTAssertAllEqual(whereClause, Where(predicate)) XCTAssertAllEqual(whereClause.predicate, predicate) diff --git a/Sources/CoreStoreSchema.swift b/Sources/CoreStoreSchema.swift index ef6f2c7..2d61cd9 100644 --- a/Sources/CoreStoreSchema.swift +++ b/Sources/CoreStoreSchema.swift @@ -339,6 +339,23 @@ public final class CoreStoreSchema: DynamicSchema { keyPathsByAffectedKeyPaths[attribute.keyPath] = entityDescriptionValues.affectedByKeyPaths customGetterSetterByKeyPaths[attribute.keyPath] = (attribute.getter, attribute.setter) fieldCoders[attribute.keyPath] = valueTransformer + + case let relationship as FieldRelationshipProtocol: + Internals.assert( + !NSManagedObject.instancesRespond(to: Selector(relationship.keyPath)), + "Relationship Property name \"\(String(reflecting: entity.type)).\(relationship.keyPath)\" is not allowed because it collides with \"\(String(reflecting: NSManagedObject.self)).\(relationship.keyPath)\"" + ) + let entityDescriptionValues = relationship.entityDescriptionValues() + let description = NSRelationshipDescription() + description.name = relationship.keyPath + description.minCount = entityDescriptionValues.minCount + description.maxCount = entityDescriptionValues.maxCount + description.isOrdered = entityDescriptionValues.isOrdered + description.deleteRule = entityDescriptionValues.deleteRule + description.versionHashModifier = entityDescriptionValues.versionHashModifier + description.renamingIdentifier = entityDescriptionValues.renamingIdentifier + propertyDescriptions.append(description) + keyPathsByAffectedKeyPaths[relationship.keyPath] = entityDescriptionValues.affectedByKeyPaths case let attribute as AttributeProtocol: Internals.assert( @@ -437,6 +454,26 @@ public final class CoreStoreSchema: DynamicSchema { for property in entityType.metaProperties(includeSuperclasses: false) { switch property { + + case let relationship as FieldRelationshipProtocol: + let (destinationType, destinationKeyPath) = relationship.entityDescriptionValues().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 + } case let relationship as RelationshipProtocol: let (destinationType, destinationKeyPath) = relationship.entityDescriptionValues().inverse diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index 101d580..32ed92b 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -173,6 +173,9 @@ extension CoreStoreObject { case let property as FieldAttributeProtocol: attributes[property.keyPath] = type(of: property).read(field: property, for: object.rawObject!) + case let property as FieldRelationshipProtocol: + attributes[property.keyPath] = type(of: property).valueForSnapshot(field: property, for: object.rawObject!) + case let property as AttributeProtocol: attributes[property.keyPath] = property.valueForSnapshot diff --git a/Sources/FIeldRelationshipType.swift b/Sources/FIeldRelationshipType.swift new file mode 100644 index 0000000..f054adc --- /dev/null +++ b/Sources/FIeldRelationshipType.swift @@ -0,0 +1,142 @@ +// +// FIeldRelationshipType.swift +// CoreStore +// +// Copyright © 2020 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: - FieldRelationshipType + +public protocol FieldRelationshipType { + + associatedtype DestinationObjectType: CoreStoreObject + + associatedtype NativeValueType: AnyObject + + associatedtype SnapshotValueType + + static func cs_toReturnType(from value: NativeValueType?) -> Self + + static func cs_toNativeType(from value: Self) -> NativeValueType? + + static func cs_valueForSnapshot(from value: NativeValueType?) -> SnapshotValueType +} + +public protocol FieldRelationshipToOneType: FieldRelationshipType {} + + +public protocol FieldRelationshipToManyType: FieldRelationshipType where Self: Sequence {} +public protocol FieldRelationshipToManyOrderedType: FieldRelationshipToManyType {} +public protocol FieldRelationshipToManyUnorderedType: FieldRelationshipToManyType {} + + +extension Optional: FieldRelationshipType, FieldRelationshipToOneType where Wrapped: CoreStoreObject { + + public typealias DestinationObjectType = Wrapped + + public typealias NativeValueType = NSManagedObject + + public typealias SnapshotValueType = NSManagedObjectID? + + public static func cs_toReturnType(from value: NativeValueType?) -> Self { + + return value.map(Wrapped.cs_fromRaw(object:)) + } + + public static func cs_toNativeType(from value: Self) -> NativeValueType? { + + return value?.cs_toRaw() + } + + public static func cs_valueForSnapshot(from value: NativeValueType?) -> SnapshotValueType { + + return value?.objectID + } +} + + +extension Array: FieldRelationshipType, FieldRelationshipToManyType, FieldRelationshipToManyOrderedType where Element: CoreStoreObject { + + public typealias DestinationObjectType = Element + + public typealias NativeValueType = NSOrderedSet + + public typealias SnapshotValueType = [NSManagedObjectID] + + public static func cs_toReturnType(from value: NativeValueType?) -> Self { + + guard let value = value else { + + return [] + } + return value.map({ Element.cs_fromRaw(object: $0 as! NSManagedObject) }) + } + + public static func cs_toNativeType(from value: Self) -> NativeValueType? { + + return NSOrderedSet(array: value.map({ $0.rawObject! })) + } + + public static func cs_valueForSnapshot(from value: NativeValueType?) -> SnapshotValueType { + + guard let value = value else { + + return [] + } + return value.map({ ($0 as! NSManagedObject).objectID }) + } +} + +extension Set: FieldRelationshipType, FieldRelationshipToManyType, FieldRelationshipToManyUnorderedType where Element: CoreStoreObject { + + public typealias DestinationObjectType = Element + + public typealias NativeValueType = NSSet + + public typealias SnapshotValueType = Set + + public static func cs_toReturnType(from value: NativeValueType?) -> Self { + + guard let value = value else { + + return [] + } + return Set(value.map({ Element.cs_fromRaw(object: $0 as! NSManagedObject) })) + } + + public static func cs_toNativeType(from value: Self) -> NativeValueType? { + + return NSSet(array: value.map({ $0.rawObject! })) + } + + public static func cs_valueForSnapshot(from value: NativeValueType?) -> SnapshotValueType { + + guard let value = value else { + + return [] + } + return .init(value.map({ ($0 as! NSManagedObject).objectID })) + } +} diff --git a/Sources/Field.Relationship.swift b/Sources/Field.Relationship.swift new file mode 100644 index 0000000..005dff9 --- /dev/null +++ b/Sources/Field.Relationship.swift @@ -0,0 +1,346 @@ +// +// Field.ToOne.swift +// CoreStore +// +// Copyright © 2020 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: - FieldContainer + +extension FieldContainer { + + // MARK: - Relationship + + @propertyWrapper +// @dynamicMemberLookup + public struct Relationship: RelationshipKeyPathStringConvertible, FieldRelationshipProtocol { + + public typealias DeleteRule = RelationshipContainer.DeleteRule + + + // MARK: @propertyWrapper + + @available(*, unavailable) + public var wrappedValue: V { + + get { fatalError() } + set { fatalError() } + } + + public var projectedValue: Self { + + return self + } + + public static subscript( + _enclosingInstance instance: O, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath + ) -> V { + + get { + + Internals.assert( + instance.rawObject != nil, + "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." + ) + return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V + } + set { + + Internals.assert( + instance.rawObject != nil, + "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." + ) + return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue) + } + } + + + // MARK: AnyKeyPathStringConvertible + + public var cs_keyPathString: String { + + return self.keyPath + } + + + // MARK: KeyPathStringConvertible + + public typealias ObjectType = O + public typealias DestinationValueType = V.DestinationObjectType + + + // MARK: RelationshipKeyPathStringConvertible + + public typealias ReturnValueType = V + + + // MARK: PropertyProtocol + + internal let keyPath: KeyPathString + + + // MARK: FieldProtocol + + internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { + + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + let field = field as! Self + let keyPath = field.keyPath + return V.cs_toReturnType( + from: rawObject.value(forKey: keyPath) as! V.NativeValueType? + ) + } + + internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) { + + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + Internals.assert( + rawObject.isEditableInContext() == true, + "Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction." + ) + let newValue = newValue as! V + let field = field as! Self + let keyPath = field.keyPath + return rawObject.setValue( + V.cs_toNativeType(from: newValue), + forKey: keyPath + ) + } + + + // MARK: FieldRelationshipProtocol + + internal let entityDescriptionValues: () -> FieldRelationshipProtocol.EntityDescriptionValues + + internal static func valueForSnapshot(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { + + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + let field = field as! Self + let keyPath = field.keyPath + return V.cs_valueForSnapshot( + from: rawObject.value(forKey: keyPath) as! V.NativeValueType? + ) + } + + + // MARK: FilePrivate + + fileprivate init( + keyPath: KeyPathString, + isToMany: Bool, + isOrdered: Bool, + deleteRule: DeleteRule, + inverseKeyPath: @escaping () -> KeyPathString?, + versionHashModifier: @escaping () -> String?, + renamingIdentifier: @escaping () -> String?, + affectedByKeyPaths: @escaping () -> Set, + minCount: Int, + maxCount: Int + ) { + + self.keyPath = keyPath + self.entityDescriptionValues = { + + let range = (Swift.max(0, minCount) ... maxCount) + return ( + isToMany: isToMany, + isOrdered: isOrdered, + deleteRule: deleteRule.nativeValue, + inverse: (type: V.DestinationObjectType.self, keyPath: inverseKeyPath()), + versionHashModifier: versionHashModifier(), + renamingIdentifier: renamingIdentifier(), + affectedByKeyPaths: affectedByKeyPaths(), + minCount: range.lowerBound, + maxCount: range.upperBound + ) + } + } + } +} + +extension FieldContainer.Relationship where V: FieldRelationshipToOneType { + + public init( + _ keyPath: KeyPathString, + deleteRule: DeleteRule = .nullify, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) { + + self.init( + keyPath: keyPath, + isToMany: false, + isOrdered: false, + deleteRule: deleteRule, + inverseKeyPath: { nil }, + versionHashModifier: versionHashModifier, + renamingIdentifier: previousVersionKeyPath, + affectedByKeyPaths: affectedByKeyPaths, + minCount: 0, + maxCount: 1 + ) + } + + public init( + _ keyPath: KeyPathString, + inverse: KeyPath.Relationship>, + deleteRule: DeleteRule = .nullify, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) where D: FieldRelationshipType { + + self.init( + keyPath: keyPath, + isToMany: false, + isOrdered: false, + deleteRule: deleteRule, + inverseKeyPath: { V.DestinationObjectType.meta[keyPath: inverse].keyPath }, + versionHashModifier: versionHashModifier, + renamingIdentifier: previousVersionKeyPath, + affectedByKeyPaths: affectedByKeyPaths, + minCount: 0, + maxCount: 1 + ) + } +} + +extension FieldContainer.Relationship: ToManyRelationshipKeyPathStringConvertible where V: FieldRelationshipToManyType {} + +extension FieldContainer.Relationship where V: FieldRelationshipToManyOrderedType { + + public init( + _ keyPath: KeyPathString, + minCount: Int = 0, + maxCount: Int = 0, + deleteRule: DeleteRule = .nullify, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) { + + self.init( + keyPath: keyPath, + isToMany: true, + isOrdered: true, + deleteRule: deleteRule, + inverseKeyPath: { nil }, + versionHashModifier: versionHashModifier, + renamingIdentifier: previousVersionKeyPath, + affectedByKeyPaths: affectedByKeyPaths, + minCount: minCount, + maxCount: maxCount + ) + } + + public init( + _ keyPath: KeyPathString, + minCount: Int = 0, + maxCount: Int = 0, + inverse: @escaping (V.DestinationObjectType) -> FieldContainer.Relationship, + deleteRule: DeleteRule = .nullify, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) where D: FieldRelationshipType { + + self.init( + keyPath: keyPath, + isToMany: true, + isOrdered: true, + deleteRule: deleteRule, + inverseKeyPath: { inverse(V.DestinationObjectType.meta).keyPath }, + versionHashModifier: versionHashModifier, + renamingIdentifier: previousVersionKeyPath, + affectedByKeyPaths: affectedByKeyPaths, + minCount: minCount, + maxCount: maxCount + ) + } +} + +extension FieldContainer.Relationship where V: FieldRelationshipToManyUnorderedType { + + public init( + _ keyPath: KeyPathString, + minCount: Int = 0, + maxCount: Int = 0, + deleteRule: DeleteRule = .nullify, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) { + + self.init( + keyPath: keyPath, + isToMany: true, + isOrdered: false, + deleteRule: deleteRule, + inverseKeyPath: { nil }, + versionHashModifier: versionHashModifier, + renamingIdentifier: previousVersionKeyPath, + affectedByKeyPaths: affectedByKeyPaths, + minCount: minCount, + maxCount: maxCount + ) + } + + public init( + _ keyPath: KeyPathString, + minCount: Int = 0, + maxCount: Int = 0, + inverse: @escaping (V.DestinationObjectType) -> FieldContainer.Relationship, + deleteRule: DeleteRule = .nullify, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = [] + ) where D: FieldRelationshipType { + + self.init( + keyPath: keyPath, + isToMany: true, + isOrdered: false, + deleteRule: deleteRule, + inverseKeyPath: { inverse(V.DestinationObjectType.meta).keyPath }, + versionHashModifier: versionHashModifier, + renamingIdentifier: previousVersionKeyPath, + affectedByKeyPaths: affectedByKeyPaths, + minCount: minCount, + maxCount: maxCount + ) + } +} diff --git a/Sources/FieldRelationshipProtocol.swift b/Sources/FieldRelationshipProtocol.swift new file mode 100644 index 0000000..319ea60 --- /dev/null +++ b/Sources/FieldRelationshipProtocol.swift @@ -0,0 +1,49 @@ +// +// FieldRelationshipProtocol.swift +// CoreStore +// +// Copyright © 2020 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 Foundation +import CoreData + + +// MARK: - FieldRelationshipProtocol + +internal protocol FieldRelationshipProtocol: FieldProtocol { + + typealias EntityDescriptionValues = ( + isToMany: Bool, + isOrdered: Bool, + deleteRule: NSDeleteRule, + inverse: (type: CoreStoreObject.Type, KeyPathString?), + versionHashModifier: String?, + renamingIdentifier: String?, + affectedByKeyPaths: Set, + minCount: Int, + maxCount: Int + ) + + var entityDescriptionValues: () -> EntityDescriptionValues { get } + + static func valueForSnapshot(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? +} diff --git a/Sources/KeyPath+Querying.swift b/Sources/KeyPath+Querying.swift index 333ae49..4cdbb88 100644 --- a/Sources/KeyPath+Querying.swift +++ b/Sources/KeyPath+Querying.swift @@ -447,6 +447,17 @@ public func ~= (_ sequence: S, _ keyPath: KeyPath.Required +/** + Creates a `Where` clause by comparing if a property is less than a value + ``` + let person = dataStack.fetchOne(From().where(\.$age < 20)) + ``` + */ +public func < (_ keyPath: KeyPath.Stored>, _ value: V) -> Where { + + return Where("%K < %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType) +} + /** Creates a `Where` clause by comparing if a property is less than a value ``` @@ -458,6 +469,17 @@ public func < (_ keyPath: KeyPath.Require return Where("%K < %@", O.meta[keyPath: keyPath].keyPath, value.cs_toQueryableNativeType()) } +/** + Creates a `Where` clause by comparing if a property is greater than a value + ``` + let person = dataStack.fetchOne(From().where(\.$age > 20)) + ``` + */ +public func > (_ keyPath: KeyPath.Stored>, _ value: V) -> Where { + + return Where("%K > %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType) +} + /** Creates a `Where` clause by comparing if a property is greater than a value ``` @@ -469,6 +491,17 @@ public func > (_ keyPath: KeyPath.Require return Where("%K > %@", O.meta[keyPath: keyPath].keyPath, value.cs_toQueryableNativeType()) } +/** + Creates a `Where` clause by comparing if a property is less than or equal to a value + ``` + let person = dataStack.fetchOne(From().where(\.$age <= 20)) + ``` + */ +public func <= (_ keyPath: KeyPath.Stored>, _ value: V) -> Where { + + return Where("%K <= %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType) +} + /** Creates a `Where` clause by comparing if a property is less than or equal to a value ``` @@ -480,6 +513,17 @@ public func <= (_ keyPath: KeyPath.Requir return Where("%K <= %@", O.meta[keyPath: keyPath].keyPath, value.cs_toQueryableNativeType()) } +/** + Creates a `Where` clause by comparing if a property is greater than or equal to a value + ``` + let person = dataStack.fetchOne(From().where(\.$age >= 20)) + ``` + */ +public func >= (_ keyPath: KeyPath.Stored>, _ value: V) -> Where { + + return Where("%K >= %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType) +} + /** Creates a `Where` clause by comparing if a property is greater than or equal to a value ``` @@ -494,6 +538,17 @@ public func >= (_ keyPath: KeyPath.Requir // MARK: - KeyPath where Root: CoreStoreObject, Value: ValueContainer.Optional +/** + Creates a `Where` clause by comparing if a property is less than a value + ``` + let person = dataStack.fetchOne(From().where(\.$age < 20)) + ``` + */ +public func < (_ keyPath: KeyPath.Stored>, _ value: V) -> Where where V.Wrapped: Comparable { + + return Where("%K < %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType) +} + /** Creates a `Where` clause by comparing if a property is less than a value ``` @@ -512,6 +567,17 @@ public func < (_ keyPath: KeyPath.Optional>, _ val } } +/** + Creates a `Where` clause by comparing if a property is greater than a value + ``` + let person = dataStack.fetchOne(From().where(\.$age > 20)) + ``` + */ +public func > (_ keyPath: KeyPath.Stored>, _ value: V) -> Where where V.Wrapped: Comparable { + + return Where("%K > %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType) +} + /** Creates a `Where` clause by comparing if a property is greater than a value ``` @@ -530,6 +596,17 @@ public func > (_ keyPath: KeyPath.Optional>, _ val } } +/** + Creates a `Where` clause by comparing if a property is less than or equal to a value + ``` + let person = dataStack.fetchOne(From().where(\.$age <= 20)) + ``` + */ +public func <= (_ keyPath: KeyPath.Stored>, _ value: V) -> Where where V.Wrapped: Comparable { + + return Where("%K <= %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType) +} + /** Creates a `Where` clause by comparing if a property is less than or equal to a value ``` @@ -548,6 +625,17 @@ public func <= (_ keyPath: KeyPath.Optional>, _ va } } +/** + Creates a `Where` clause by comparing if a property is greater than or equal to a value + ``` + let person = dataStack.fetchOne(From().where(\.$age >= 20)) + ``` + */ +public func >= (_ keyPath: KeyPath.Stored>, _ value: V) -> Where where V.Wrapped: Comparable { + + return Where("%K >= %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType) +} + /** Creates a `Where` clause by comparing if a property is greater than or equal to a value ``` @@ -569,6 +657,17 @@ public func >= (_ keyPath: KeyPath.Optional>, _ va // MARK: - KeyPath where Root: CoreStoreObject, Value: RelationshipContainer.ToOne +/** + Creates a `Where` clause by comparing if a property is equal to a value + ``` + let dog = dataStack.fetchOne(From().where(\.$master == john)) + ``` + */ +public func == (_ keyPath: KeyPath.Relationship>, _ object: D.DestinationObjectType?) -> Where { + + return Where(O.meta[keyPath: keyPath].keyPath, isEqualTo: object) +} + /** Creates a `Where` clause by comparing if a property is equal to a value ``` @@ -591,6 +690,17 @@ public func == (_ keyPath: KeyPath.ToOne>, return Where(O.meta[keyPath: keyPath].keyPath, isEqualTo: object) } +/** + Creates a `Where` clause by comparing if a property is not equal to a value + ``` + let dog = dataStack.fetchOne(From().where(\.$master != john)) + ``` + */ +public func != (_ keyPath: KeyPath.Relationship>, _ object: D.DestinationObjectType?) -> Where { + + return !Where(O.meta[keyPath: keyPath].keyPath, isEqualTo: object) +} + /** Creates a `Where` clause by comparing if a property is not equal to a value ``` @@ -613,6 +723,17 @@ public func != (_ keyPath: KeyPath.ToOne>, return !Where(O.meta[keyPath: keyPath].keyPath, isEqualTo: object) } +/** + Creates a `Where` clause by checking if a sequence contains a value of a property + ``` + let dog = dataStack.fetchOne(From().where([john, bob, joe] ~= \.$master)) + ``` + */ +public func ~= (_ sequence: S, _ keyPath: KeyPath.Relationship>) -> Where where S.Iterator.Element == D.DestinationObjectType { + + return Where(O.meta[keyPath: keyPath].keyPath, isMemberOf: sequence) +} + /** Creates a `Where` clause by checking if a sequence contains a value of a property ``` diff --git a/Sources/Select.swift b/Sources/Select.swift index dab14fb..91c43c4 100644 --- a/Sources/Select.swift +++ b/Sources/Select.swift @@ -379,45 +379,15 @@ extension SelectTerm where O: NSManagedObject { // MARK: - SelectTerm where O: CoreStoreObject extension SelectTerm where O: CoreStoreObject { - + /** Provides a `SelectTerm` to a `Select` clause for querying an entity attribute. - parameter keyPath: the attribute name - returns: a `SelectTerm` to a `Select` clause for querying an entity attribute */ - public static func attribute(_ keyPath: KeyPath.Required>) -> SelectTerm { - - return self.attribute(O.meta[keyPath: keyPath].keyPath) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying an entity attribute. - - parameter keyPath: the attribute name - - returns: a `SelectTerm` to a `Select` clause for querying an entity attribute - */ - public static func attribute(_ keyPath: KeyPath.Optional>) -> SelectTerm { - - return self.attribute(O.meta[keyPath: keyPath].keyPath) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying an entity attribute. - - parameter keyPath: the attribute name - - returns: a `SelectTerm` to a `Select` clause for querying an entity attribute - */ - public static func attribute(_ keyPath: KeyPath.Required>) -> SelectTerm { - - return self.attribute(O.meta[keyPath: keyPath].keyPath) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying an entity attribute. - - parameter keyPath: the attribute name - - returns: a `SelectTerm` to a `Select` clause for querying an entity attribute - */ - public static func attribute(_ keyPath: KeyPath.Optional>) -> SelectTerm { - - return self.attribute(O.meta[keyPath: keyPath].keyPath) + public static func attribute(_ keyPath: KeyPath) -> SelectTerm where K.ObjectType == O { + + return self.attribute(O.meta[keyPath: keyPath].cs_keyPathString) } /** @@ -426,42 +396,9 @@ extension SelectTerm where O: CoreStoreObject { - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "average()" is used - returns: a `SelectTerm` to a `Select` clause for querying the average value of an attribute */ - public static func average(_ keyPath: KeyPath.Required>, as alias: KeyPathString? = nil) -> SelectTerm { + public static func average(_ keyPath: KeyPath, as alias: KeyPathString? = nil) -> SelectTerm where K.ObjectType == O{ - return self.average(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying the average value of an attribute. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "average()" is used - - returns: a `SelectTerm` to a `Select` clause for querying the average value of an attribute - */ - public static func average(_ keyPath: KeyPath.Optional>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.average(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying the average value of an attribute. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "average()" is used - - returns: a `SelectTerm` to a `Select` clause for querying the average value of an attribute - */ - public static func average(_ keyPath: KeyPath.Required>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.average(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying the average value of an attribute. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "average()" is used - - returns: a `SelectTerm` to a `Select` clause for querying the average value of an attribute - */ - public static func average(_ keyPath: KeyPath.Optional>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.average(O.meta[keyPath: keyPath].keyPath, as: alias) + return self.average(O.meta[keyPath: keyPath].cs_keyPathString, as: alias) } /** @@ -470,46 +407,10 @@ extension SelectTerm where O: CoreStoreObject { - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "count()" is used - returns: a `SelectTerm` to a `Select` clause for a count query */ - public static func count(_ keyPath: KeyPath.Required>, as alias: KeyPathString? = nil) -> SelectTerm { + public static func count(_ keyPath: KeyPath, as alias: KeyPathString? = nil) -> SelectTerm where K.ObjectType == O { - return self.count(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for a count query. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "count()" is used - - returns: a `SelectTerm` to a `Select` clause for a count query - */ - public static func count(_ keyPath: KeyPath.Optional>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.count(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for a count query. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "count()" is used - - returns: a `SelectTerm` to a `Select` clause for a count query - */ - public static func count(_ keyPath: KeyPath.Required>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.count(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for a count query. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "count()" is used - - returns: a `SelectTerm` to a `Select` clause for a count query - */ - public static func count(_ keyPath: KeyPath.Optional>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.count(O.meta[keyPath: keyPath].keyPath, as: alias) + return self.count(O.meta[keyPath: keyPath].cs_keyPathString, as: alias) } /** @@ -518,46 +419,10 @@ extension SelectTerm where O: CoreStoreObject { - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "max()" is used - returns: a `SelectTerm` to a `Select` clause for querying the maximum value for an attribute */ - public static func maximum(_ keyPath: KeyPath.Required>, as alias: KeyPathString? = nil) -> SelectTerm { + public static func maximum(_ keyPath: KeyPath, as alias: KeyPathString? = nil) -> SelectTerm where K.ObjectType == O { - return self.maximum(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying the maximum value for an attribute. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "max()" is used - - returns: a `SelectTerm` to a `Select` clause for querying the maximum value for an attribute - */ - public static func maximum(_ keyPath: KeyPath.Optional>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.maximum(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying the maximum value for an attribute. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "max()" is used - - returns: a `SelectTerm` to a `Select` clause for querying the maximum value for an attribute - */ - public static func maximum(_ keyPath: KeyPath.Required>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.maximum(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying the maximum value for an attribute. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "max()" is used - - returns: a `SelectTerm` to a `Select` clause for querying the maximum value for an attribute - */ - public static func maximum(_ keyPath: KeyPath.Optional>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.maximum(O.meta[keyPath: keyPath].keyPath, as: alias) + return self.maximum(O.meta[keyPath: keyPath].cs_keyPathString, as: alias) } /** @@ -566,42 +431,9 @@ extension SelectTerm where O: CoreStoreObject { - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "min()" is used - returns: a `SelectTerm` to a `Select` clause for querying the minimum value for an attribute */ - public static func minimum(_ keyPath: KeyPath.Required>, as alias: KeyPathString? = nil) -> SelectTerm { + public static func minimum(_ keyPath: KeyPath, as alias: KeyPathString? = nil) -> SelectTerm where K.ObjectType == O { - return self.minimum(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying the minimum value for an attribute. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "min()" is used - - returns: a `SelectTerm` to a `Select` clause for querying the minimum value for an attribute - */ - public static func minimum(_ keyPath: KeyPath.Optional>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.minimum(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying the minimum value for an attribute. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "min()" is used - - returns: a `SelectTerm` to a `Select` clause for querying the minimum value for an attribute - */ - public static func minimum(_ keyPath: KeyPath.Required>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.minimum(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying the minimum value for an attribute. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "min()" is used - - returns: a `SelectTerm` to a `Select` clause for querying the minimum value for an attribute - */ - public static func minimum(_ keyPath: KeyPath.Optional>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.minimum(O.meta[keyPath: keyPath].keyPath, as: alias) + return self.minimum(O.meta[keyPath: keyPath].cs_keyPathString, as: alias) } /** @@ -610,42 +442,9 @@ extension SelectTerm where O: CoreStoreObject { - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "sum()" is used - returns: a `SelectTerm` to a `Select` clause for querying the sum value for an attribute */ - public static func sum(_ keyPath: KeyPath.Required>, as alias: KeyPathString? = nil) -> SelectTerm { + public static func sum(_ keyPath: KeyPath, as alias: KeyPathString? = nil) -> SelectTerm where K.ObjectType == O { - return self.sum(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying the sum value for an attribute. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "sum()" is used - - returns: a `SelectTerm` to a `Select` clause for querying the sum value for an attribute - */ - public static func sum(_ keyPath: KeyPath.Optional>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.sum(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying the sum value for an attribute. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "sum()" is used - - returns: a `SelectTerm` to a `Select` clause for querying the sum value for an attribute - */ - public static func sum(_ keyPath: KeyPath.Required>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.sum(O.meta[keyPath: keyPath].keyPath, as: alias) - } - - /** - Provides a `SelectTerm` to a `Select` clause for querying the sum value for an attribute. - - parameter keyPath: the attribute name - - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "sum()" is used - - returns: a `SelectTerm` to a `Select` clause for querying the sum value for an attribute - */ - public static func sum(_ keyPath: KeyPath.Optional>, as alias: KeyPathString? = nil) -> SelectTerm { - - return self.sum(O.meta[keyPath: keyPath].keyPath, as: alias) + return self.sum(O.meta[keyPath: keyPath].cs_keyPathString, as: alias) } } diff --git a/Sources/Where.Expression.swift b/Sources/Where.Expression.swift index 8cb7cb8..8c0a0e7 100644 --- a/Sources/Where.Expression.swift +++ b/Sources/Where.Expression.swift @@ -235,6 +235,20 @@ public func ~ ().where((\.$master ~ \.$name) == "John")) + ``` + */ +public func ~ (_ lhs: KeyPath.Relationship>, _ rhs: KeyPath) -> Where.Expression.SingleTarget, K.DestinationValueType> where K.ObjectType == D.DestinationObjectType { + + return .init( + O.meta[keyPath: lhs].cs_keyPathString, + D.DestinationObjectType.meta[keyPath: rhs].cs_keyPathString + ) +} + /** Connects multiple `KeyPathStringConvertible`s to create a type-safe chain usable in query/fetch expressions ``` @@ -277,6 +291,20 @@ public func ~ ().where((\.$master ~ \.$pets).count() > 1)) + ``` + */ +public func ~ (_ lhs: KeyPath.Relationship>, _ rhs: KeyPath) -> Where.Expression.CollectionTarget, K.DestinationValueType> where K.ObjectType == D.DestinationObjectType { + + return .init( + O.meta[keyPath: lhs].cs_keyPathString, + D.DestinationObjectType.meta[keyPath: rhs].cs_keyPathString + ) +} + /** Connects multiple `KeyPathStringConvertible`s to create a type-safe chain usable in query/fetch expressions ``` diff --git a/Sources/Where.swift b/Sources/Where.swift index f6b3af7..f9de0c8 100644 --- a/Sources/Where.swift +++ b/Sources/Where.swift @@ -451,6 +451,17 @@ extension Where where O: CoreStoreObject { self.init(O.meta[keyPath: keyPath].keyPath, isEqualTo: value) } + + /** + Initializes a `Where` clause that compares equality + + - parameter keyPath: the keyPath to compare with + - parameter value: the arguments for the `==` operator + */ + public init(_ keyPath: KeyPath.Relationship>, isEqualTo value: V.DestinationObjectType?) { + + self.init(O.meta[keyPath: keyPath].keyPath, isEqualTo: value) + } /** Initializes a `Where` clause that compares equality to `nil` From e9c331261232a702ed5cecc08f201beb69861cca Mon Sep 17 00:00:00 2001 From: John Estropia Date: Tue, 21 Jan 2020 17:03:12 +0900 Subject: [PATCH 04/20] Fix default encoders for top-level values --- CoreStoreTests/DynamicModelTests.swift | 3 +- Sources/DynamicSchema+Convenience.swift | 239 +++++++++++++++++------- Sources/Field.Relationship.swift | 82 +++++++- Sources/FieldCoders.Json.swift | 4 +- Sources/FieldCoders.NSCoding.swift | 2 +- Sources/FieldCoders.Plist.swift | 4 +- Sources/Relationship.swift | 5 +- 7 files changed, 256 insertions(+), 83 deletions(-) diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index eba30a4..106f7f5 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -114,7 +114,8 @@ class Person: CoreStoreObject { var customField: CustomType @Field.Coded( - "job", coder: ( + "job", + coder: ( encode: { $0.toData() }, decode: { $0.flatMap(Job.init(data:)) ?? .unemployed } ) diff --git a/Sources/DynamicSchema+Convenience.swift b/Sources/DynamicSchema+Convenience.swift index c70ccc4..4f84648 100644 --- a/Sources/DynamicSchema+Convenience.swift +++ b/Sources/DynamicSchema+Convenience.swift @@ -68,89 +68,124 @@ extension DynamicSchema { for (attributeName, attribute) in attributesByName { let containerType: String - if attribute.attributeType == .transformableAttributeType { - - if attribute.isOptional { - - containerType = "Transformable.Optional" - } - else { - - containerType = "Transformable.Required" - } + if attribute.isTransient || attribute.attributeType == .undefinedAttributeType { + + containerType = "Field.Computed" + } + else if attribute.attributeType == .transformableAttributeType { + + containerType = "Field.Coded" } else { - - if attribute.isOptional { - - containerType = "Value.Optional" - } - else { - - containerType = "Value.Required" - } + + containerType = "Field.Stored" } - let valueType: Any.Type + var valueTypeString: String var defaultString = "" + var coderString = "" switch attribute.attributeType { case .integer16AttributeType: - valueType = Int16.self + valueTypeString = String(describing: Int16.self) if let defaultValue = (attribute.defaultValue as! Int16.QueryableNativeType?).flatMap(Int16.cs_fromQueryableNativeType) { - defaultString = ", initial: \(defaultValue)" + defaultString = " = \(defaultValue)" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .integer32AttributeType: - valueType = Int32.self + valueTypeString = String(describing: Int32.self) if let defaultValue = (attribute.defaultValue as! Int32.QueryableNativeType?).flatMap(Int32.cs_fromQueryableNativeType) { - - defaultString = ", initial: \(defaultValue)" + + defaultString = " = \(defaultValue)" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .integer64AttributeType: - valueType = Int64.self + valueTypeString = String(describing: Int64.self) if let defaultValue = (attribute.defaultValue as! Int64.QueryableNativeType?).flatMap(Int64.cs_fromQueryableNativeType) { - defaultString = ", initial: \(defaultValue)" + defaultString = " = \(defaultValue)" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .decimalAttributeType: - valueType = NSDecimalNumber.self + valueTypeString = String(describing: NSDecimalNumber.self) if let defaultValue = (attribute.defaultValue as! NSDecimalNumber?) { - defaultString = ", initial: NSDecimalNumber(string: \"\(defaultValue.description(withLocale: nil))\")" + defaultString = " = NSDecimalNumber(string: \"\(defaultValue.description(withLocale: nil))\")" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .doubleAttributeType: - valueType = Double.self + valueTypeString = String(describing: Double.self) if let defaultValue = (attribute.defaultValue as! Double.QueryableNativeType?).flatMap(Double.cs_fromQueryableNativeType) { - defaultString = ", initial: \(defaultValue)" + defaultString = " = \(defaultValue)" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .floatAttributeType: - valueType = Float.self + valueTypeString = String(describing: Float.self) if let defaultValue = (attribute.defaultValue as! Float.QueryableNativeType?).flatMap(Float.cs_fromQueryableNativeType) { - defaultString = ", initial: \(defaultValue)" + defaultString = " = \(defaultValue)" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .stringAttributeType: - valueType = String.self + valueTypeString = String(describing: String.self) if let defaultValue = (attribute.defaultValue as! String.QueryableNativeType?).flatMap(String.cs_fromQueryableNativeType) { - - // TODO: escape strings - defaultString = ", initial: \"\(defaultValue)\"" + + defaultString = " = \"\(defaultValue.replacingOccurrences(of: "\\", with: "\\\\"))\"" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .booleanAttributeType: - valueType = Bool.self + valueTypeString = String(describing: Bool.self) if let defaultValue = (attribute.defaultValue as! Bool.QueryableNativeType?).flatMap(Bool.cs_fromQueryableNativeType) { - defaultString = ", initial: \(defaultValue ? "true" : "false")" + defaultString = " = \(defaultValue ? "true" : "false")" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .dateAttributeType: - valueType = Date.self + valueTypeString = String(describing: Date.self) if let defaultValue = (attribute.defaultValue as! Date.QueryableNativeType?).flatMap(Date.cs_fromQueryableNativeType) { - defaultString = ", initial: Date(timeIntervalSinceReferenceDate: \(defaultValue.timeIntervalSinceReferenceDate))" + defaultString = " = Date(timeIntervalSinceReferenceDate: \(defaultValue.timeIntervalSinceReferenceDate))" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .binaryDataAttributeType: - valueType = Data.self + valueTypeString = String(describing: Data.self) if let defaultValue = (attribute.defaultValue as! Data.QueryableNativeType?).flatMap(Data.cs_fromQueryableNativeType) { let bytes = defaultValue.withUnsafeBytes { (pointer) in @@ -158,49 +193,106 @@ extension DynamicSchema { .bindMemory(to: UInt64.self) .map({ "\("0x\(String($0, radix: 16, uppercase: false))")" }) } - defaultString = ", initial: Data(bytes: [\(bytes.joined(separator: ", "))])" + defaultString = " = Data(bytes: [\(bytes.joined(separator: ", "))])" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .transformableAttributeType: + if let valueTransformerName = attribute.valueTransformerName { + + coderString = ", coder: /* Required compatible FieldCoderType implementation for ValueTransformer named \"\(valueTransformerName)\" */" + } + else { + + coderString = ", coder: FieldCoders.NSCoding.self" + } if let attributeValueClassName = attribute.attributeValueClassName { - - valueType = NSClassFromString(attributeValueClassName)! + + valueTypeString = String(describing: NSClassFromString(attributeValueClassName)!) + } + else { + + valueTypeString = "/* */" + } + if let defaultValue = attribute.defaultValue { + + switch defaultValue { + + case let defaultValueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox: + if let defaultValue = defaultValueBox.value { + + defaultString = " = /* \"\(defaultValue)\" */" + } + else if attribute.isOptional { + + defaultString = " = nil" + } + else { + + defaultString = " = /* */" + } + + case let defaultValue: + defaultString = " = /* \"\(defaultValue)\" */" + } + } + else if attribute.isOptional { + + defaultString = " = nil" } else { - valueType = (NSCoding & NSCopying).self + defaultString = " = /* */" } - if let defaultValue = attribute.defaultValue { - - defaultString = ", initial: /* \"\(defaultValue)\" */" + if attribute.isOptional { + + valueTypeString += "?" } - else if !attribute.isOptional { - - defaultString = ", initial: /* required */" + + case .undefinedAttributeType where attribute.isTransient: + coderString = ", customGetter: \\* *\\" + if let attributeValueClassName = attribute.attributeValueClassName { + + valueTypeString = String(describing: NSClassFromString(attributeValueClassName)!) } - case .undefinedAttributeType: - #warning("TODO: Field.Computed") - continue + else { + + valueTypeString = " = /* */" + } + if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" + } + default: fatalError("Unsupported attribute type: \(attribute.attributeType.rawValue)") } - let transientString = attribute.isTransient ? ", isTransient: true" : "" - // TODO: escape strings let versionHashModifierString = attribute.versionHashModifier - .flatMap({ ", versionHashModifier: \"\($0)\"" }) ?? "" - // TODO: escape strings + .map({ ", versionHashModifier: \"\($0)\"" }) ?? "" + let renamingIdentifierString = attribute.renamingIdentifier - .flatMap({ ($0 == attributeName ? "" : ", renamingIdentifier: \"\($0)\"") as String }) ?? "" - output.append(" let \(attributeName) = \(containerType)<\(String(describing: valueType))>(\"\(attributeName)\"\(defaultString)\(transientString)\(versionHashModifierString)\(renamingIdentifierString))\n") + .map({ ($0 == attributeName ? "" : ", previousVersionKeyPath: \"\($0)\"") }) ?? "" + if attributeName.hasPrefix("_") { + + output.append(" #warning(\"Field variable names cannot start with underscores)") + } + output.append(" @\(containerType)(\"\(attributeName)\"\(versionHashModifierString)\(renamingIdentifierString)\(coderString))\n") + output.append(" var \(attributeName): \(valueTypeString)\(defaultString)\n\n") } } let relationshipsByName = entity.relationshipsByName if !relationshipsByName.isEmpty { - + output.append(" \n") for (relationshipName, relationship) in relationshipsByName { let containerType: String + let destinationEntityName = relationship.destinationEntity!.name! var minCountString = "" var maxCountString = "" if relationship.isToMany { @@ -209,11 +301,11 @@ extension DynamicSchema { let maxCount = relationship.maxCount if relationship.isOrdered { - containerType = "Relationship.ToManyOrdered" + containerType = "[\(destinationEntityName)]" } else { - containerType = "Relationship.ToManyUnordered" + containerType = "Set<\(destinationEntityName)>" } if minCount > 0 { @@ -225,16 +317,16 @@ extension DynamicSchema { } } else { - - containerType = "Relationship.ToOne" + + containerType = "\(destinationEntityName)?" } 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)") + inverseString = ", inverse: \\.$\(inverseRelationship.name)" + addedInverse.insert("\(destinationEntityName).\(inverseRelationship.name)") } var deleteRuleString = "" if relationship.deleteRule != .nullifyDeleteRule { @@ -255,10 +347,15 @@ extension DynamicSchema { } } let versionHashModifierString = relationship.versionHashModifier - .flatMap({ ", versionHashModifier: \"\($0)\"" }) ?? "" + .map({ ", versionHashModifier: \"\($0)\"" }) ?? "" let renamingIdentifierString = relationship.renamingIdentifier - .flatMap({ ($0 == relationshipName ? "" : ", renamingIdentifier: \"\($0)\"") as String }) ?? "" - output.append(" let \(relationshipName) = \(containerType)<\(relationship.destinationEntity!.name!)>(\"\(relationshipName)\"\(inverseString)\(deleteRuleString)\(minCountString)\(maxCountString)\(versionHashModifierString)\(renamingIdentifierString))\n") + .map({ ($0 == relationshipName ? "" : ", previousVersionKeyPath: \"\($0)\"") }) ?? "" + if relationshipName.hasPrefix("_") { + + output.append(" #error(\"Field variable names cannot start with underscores)\n") + } + output.append(" @Field.Relationship(\"\(relationshipName)\"\(minCountString)\(maxCountString)\(inverseString)\(deleteRuleString)\(versionHashModifierString)\(renamingIdentifierString))\n") + output.append(" var \(relationshipName): \(containerType)\n\n") } } } diff --git a/Sources/Field.Relationship.swift b/Sources/Field.Relationship.swift index 005dff9..630941b 100644 --- a/Sources/Field.Relationship.swift +++ b/Sources/Field.Relationship.swift @@ -37,7 +37,40 @@ extension FieldContainer { // @dynamicMemberLookup public struct Relationship: RelationshipKeyPathStringConvertible, FieldRelationshipProtocol { - public typealias DeleteRule = RelationshipContainer.DeleteRule + /** + Overload for compiler error message only + */ + @available(*, unavailable, message: "Field.Relationship properties are not allowed to have initial values, including `nil`.") + public init( + wrappedValue initial: @autoclosure @escaping () -> V, + _ keyPath: KeyPathString, + minCount: Int = 0, + maxCount: Int = 0, + deleteRule: DeleteRule = .nullify, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil + ) { + + fatalError() + } + + /** + Overload for compiler error message only + */ + @available(*, unavailable, message: "Field.Relationship properties are not allowed to have initial values, including `nil`.") + public init( + wrappedValue initial: @autoclosure @escaping () -> V, + _ keyPath: KeyPathString, + minCount: Int = 0, + maxCount: Int = 0, + inverse: KeyPath.Relationship>, + deleteRule: DeleteRule = .nullify, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil + ) { + + fatalError() + } // MARK: @propertyWrapper @@ -188,6 +221,45 @@ extension FieldContainer { ) } } + + + // MARK: - DeleteRule + + /** + These constants define what happens to relationships when an object is deleted. + */ + public enum DeleteRule { + + // MARK: Public + + /** + If the object is deleted, back pointers from the objects to which it is related are nullified. + */ + case nullify + + /** + If the object is deleted, the destination object or objects of this relationship are also deleted. + */ + case cascade + + /** + If the destination of this relationship is not nil, the delete creates a validation error. + */ + case deny + + + // MARK: Internal + + internal var nativeValue: NSDeleteRule { + + switch self { + + case .nullify: return .nullifyDeleteRule + case .cascade: return .cascadeDeleteRule + case .deny: return .denyDeleteRule + } + } + } } } @@ -271,7 +343,7 @@ extension FieldContainer.Relationship where V: FieldRelationshipToManyOrderedTyp _ keyPath: KeyPathString, minCount: Int = 0, maxCount: Int = 0, - inverse: @escaping (V.DestinationObjectType) -> FieldContainer.Relationship, + inverse: KeyPath.Relationship>, deleteRule: DeleteRule = .nullify, versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, @@ -283,7 +355,7 @@ extension FieldContainer.Relationship where V: FieldRelationshipToManyOrderedTyp isToMany: true, isOrdered: true, deleteRule: deleteRule, - inverseKeyPath: { inverse(V.DestinationObjectType.meta).keyPath }, + inverseKeyPath: { V.DestinationObjectType.meta[keyPath: inverse].keyPath }, versionHashModifier: versionHashModifier, renamingIdentifier: previousVersionKeyPath, affectedByKeyPaths: affectedByKeyPaths, @@ -323,7 +395,7 @@ extension FieldContainer.Relationship where V: FieldRelationshipToManyUnorderedT _ keyPath: KeyPathString, minCount: Int = 0, maxCount: Int = 0, - inverse: @escaping (V.DestinationObjectType) -> FieldContainer.Relationship, + inverse: KeyPath.Relationship>, deleteRule: DeleteRule = .nullify, versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, @@ -335,7 +407,7 @@ extension FieldContainer.Relationship where V: FieldRelationshipToManyUnorderedT isToMany: true, isOrdered: false, deleteRule: deleteRule, - inverseKeyPath: { inverse(V.DestinationObjectType.meta).keyPath }, + inverseKeyPath: { V.DestinationObjectType.meta[keyPath: inverse].keyPath }, versionHashModifier: versionHashModifier, renamingIdentifier: previousVersionKeyPath, affectedByKeyPaths: affectedByKeyPaths, diff --git a/Sources/FieldCoders.Json.swift b/Sources/FieldCoders.Json.swift index 02de3a3..f16a224 100644 --- a/Sources/FieldCoders.Json.swift +++ b/Sources/FieldCoders.Json.swift @@ -44,7 +44,7 @@ extension FieldCoders { return nil } - return try? JSONEncoder().encode(fieldValue) + return try! JSONEncoder().encode([fieldValue]) } public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? { @@ -53,7 +53,7 @@ extension FieldCoders { return nil } - return try? JSONDecoder().decode(FieldStoredValue.self, from: data) + return try! JSONDecoder().decode([FieldStoredValue].self, from: data).first } } } diff --git a/Sources/FieldCoders.NSCoding.swift b/Sources/FieldCoders.NSCoding.swift index 672e8d3..150e081 100644 --- a/Sources/FieldCoders.NSCoding.swift +++ b/Sources/FieldCoders.NSCoding.swift @@ -32,7 +32,7 @@ extension FieldCoders { // MARK: - NSCoding - public struct NSCoding: FieldCoderType { + public struct NSCoding: FieldCoderType { // MARK: FieldCoderType diff --git a/Sources/FieldCoders.Plist.swift b/Sources/FieldCoders.Plist.swift index ffc1908..1055ec5 100644 --- a/Sources/FieldCoders.Plist.swift +++ b/Sources/FieldCoders.Plist.swift @@ -44,7 +44,7 @@ extension FieldCoders { return nil } - return try? PropertyListEncoder().encode(fieldValue) + return try! PropertyListEncoder().encode([fieldValue]) } public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? { @@ -53,7 +53,7 @@ extension FieldCoders { return nil } - return try? PropertyListDecoder().decode(FieldStoredValue.self, from: data) + return try! PropertyListDecoder().decode([FieldStoredValue].self, from: data).first } } } diff --git a/Sources/Relationship.swift b/Sources/Relationship.swift index 212ea09..b61052e 100644 --- a/Sources/Relationship.swift +++ b/Sources/Relationship.swift @@ -63,7 +63,10 @@ extension DynamicObject where Self: CoreStoreObject { public enum RelationshipContainer { // MARK: - DeleteRule - + + /** + These constants define what happens to relationships when an object is deleted. + */ public enum DeleteRule { // MARK: Public From c20fe4ac17e7cbd7f826fbb37fa78911e65d3bc3 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 5 Feb 2020 11:03:57 +0900 Subject: [PATCH 05/20] add Field.Relationship dynamicMemberLookups --- Sources/FIeldRelationshipType.swift | 42 +++++++++++++++++++++++++++++ Sources/ObjectPublisher.swift | 18 +++++++++++++ Sources/ObjectSnapshot.swift | 19 +++++++++++++ 3 files changed, 79 insertions(+) diff --git a/Sources/FIeldRelationshipType.swift b/Sources/FIeldRelationshipType.swift index f054adc..b19e751 100644 --- a/Sources/FIeldRelationshipType.swift +++ b/Sources/FIeldRelationshipType.swift @@ -37,10 +37,16 @@ public protocol FieldRelationshipType { associatedtype SnapshotValueType + associatedtype PublishedType + static func cs_toReturnType(from value: NativeValueType?) -> Self + static func cs_toPublishedType(from value: SnapshotValueType, in context: NSManagedObjectContext) -> PublishedType + static func cs_toNativeType(from value: Self) -> NativeValueType? + static func cs_toSnapshotType(from value: PublishedType) -> SnapshotValueType + static func cs_valueForSnapshot(from value: NativeValueType?) -> SnapshotValueType } @@ -60,16 +66,28 @@ extension Optional: FieldRelationshipType, FieldRelationshipToOneType where Wrap public typealias SnapshotValueType = NSManagedObjectID? + public typealias PublishedType = ObjectPublisher? + public static func cs_toReturnType(from value: NativeValueType?) -> Self { return value.map(Wrapped.cs_fromRaw(object:)) } + public static func cs_toPublishedType(from value: SnapshotValueType, in context: NSManagedObjectContext) -> PublishedType { + + return value.map(context.objectPublisher(objectID:)) + } + public static func cs_toNativeType(from value: Self) -> NativeValueType? { return value?.cs_toRaw() } + public static func cs_toSnapshotType(from value: PublishedType) -> SnapshotValueType { + + return value?.objectID() + } + public static func cs_valueForSnapshot(from value: NativeValueType?) -> SnapshotValueType { return value?.objectID @@ -85,6 +103,8 @@ extension Array: FieldRelationshipType, FieldRelationshipToManyType, FieldRelati public typealias SnapshotValueType = [NSManagedObjectID] + public typealias PublishedType = [ObjectPublisher] + public static func cs_toReturnType(from value: NativeValueType?) -> Self { guard let value = value else { @@ -94,11 +114,21 @@ extension Array: FieldRelationshipType, FieldRelationshipToManyType, FieldRelati return value.map({ Element.cs_fromRaw(object: $0 as! NSManagedObject) }) } + public static func cs_toPublishedType(from value: SnapshotValueType, in context: NSManagedObjectContext) -> PublishedType { + + return value.map(context.objectPublisher(objectID:)) + } + public static func cs_toNativeType(from value: Self) -> NativeValueType? { return NSOrderedSet(array: value.map({ $0.rawObject! })) } + public static func cs_toSnapshotType(from value: PublishedType) -> SnapshotValueType { + + return value.map({ $0.objectID() }) + } + public static func cs_valueForSnapshot(from value: NativeValueType?) -> SnapshotValueType { guard let value = value else { @@ -117,6 +147,8 @@ extension Set: FieldRelationshipType, FieldRelationshipToManyType, FieldRelation public typealias SnapshotValueType = Set + public typealias PublishedType = Set> + public static func cs_toReturnType(from value: NativeValueType?) -> Self { guard let value = value else { @@ -126,11 +158,21 @@ extension Set: FieldRelationshipType, FieldRelationshipToManyType, FieldRelation return Set(value.map({ Element.cs_fromRaw(object: $0 as! NSManagedObject) })) } + public static func cs_toPublishedType(from value: SnapshotValueType, in context: NSManagedObjectContext) -> PublishedType { + + return PublishedType(value.map(context.objectPublisher(objectID:))) + } + public static func cs_toNativeType(from value: Self) -> NativeValueType? { return NSSet(array: value.map({ $0.rawObject! })) } + public static func cs_toSnapshotType(from value: PublishedType) -> SnapshotValueType { + + return SnapshotValueType(value.map({ $0.objectID() })) + } + public static func cs_valueForSnapshot(from value: NativeValueType?) -> SnapshotValueType { guard let value = value else { diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index 5426925..9549851 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -397,6 +397,24 @@ extension ObjectPublisher where O: CoreStoreObject { return FieldContainer.Computed.read(field: object[keyPath: member], for: rawObject) as! V? } + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Relationship>) -> V.PublishedType? { + + guard + let object = self.object, + let rawObject = object.rawObject, + let value = FieldContainer.Relationship.read(field: object[keyPath: member], for: rawObject) as! V? + else { + + return nil + } + let nativeValue = V.cs_toNativeType(from: value) + let snapshotValue = V.cs_valueForSnapshot(from: nativeValue) + return V.cs_toPublishedType(from: snapshotValue, in: self.context) + } + /** Returns the value for the property identified by a given key. */ diff --git a/Sources/ObjectSnapshot.swift b/Sources/ObjectSnapshot.swift index 0845636..cfac82d 100644 --- a/Sources/ObjectSnapshot.swift +++ b/Sources/ObjectSnapshot.swift @@ -209,6 +209,25 @@ extension ObjectSnapshot where O: CoreStoreObject { } } + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Relationship>) -> V.PublishedType { + + get { + + let key = String(keyPath: member) + let context = self.context + let snapshotValue = self.values[key] as! V.SnapshotValueType + return V.cs_toPublishedType(from: snapshotValue, in: context) + } + set { + + let key = String(keyPath: member) + self.values[key] = V.cs_toSnapshotType(from: newValue) + } + } + /** Returns the value for the property identified by a given key. */ From f0f4049798d4070644671fd64d42d7e8564e8070 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Thu, 6 Feb 2020 09:33:08 +0900 Subject: [PATCH 06/20] renamed Field.Computed to Field.Virtual to distinguish from Field.Derived --- CoreStore.xcodeproj/project.pbxproj | 20 +++++------ CoreStoreTests/DynamicModelTests.swift | 4 +-- Sources/DynamicObject.swift | 3 +- Sources/DynamicSchema+Convenience.swift | 2 +- Sources/Field.Coded.swift | 8 ++--- Sources/Field.Stored.swift | 6 ++-- ...eld.Computed.swift => Field.Virtual.swift} | 33 +++++++++++++++---- Sources/ObjectPublisher.swift | 4 +-- Sources/ObjectSnapshot.swift | 2 +- Sources/PartialObject.swift | 6 ++-- 10 files changed, 54 insertions(+), 34 deletions(-) rename Sources/{Field.Computed.swift => Field.Virtual.swift} (92%) diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 9d27086..7e25b1e 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -603,10 +603,10 @@ B56E4EE023CEBCF000E1708C /* FieldOptionalType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */; }; B56E4EE123CEBCF000E1708C /* FieldOptionalType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */; }; B56E4EE223CEBCF000E1708C /* FieldOptionalType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */; }; - B56E4EE423CEDF0900E1708C /* Field.Computed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Computed.swift */; }; - B56E4EE523CEDF0900E1708C /* Field.Computed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Computed.swift */; }; - B56E4EE623CEDF0900E1708C /* Field.Computed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Computed.swift */; }; - B56E4EE723CEDF0900E1708C /* Field.Computed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Computed.swift */; }; + B56E4EE423CEDF0900E1708C /* Field.Virtual.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Virtual.swift */; }; + B56E4EE523CEDF0900E1708C /* Field.Virtual.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Virtual.swift */; }; + B56E4EE623CEDF0900E1708C /* Field.Virtual.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Virtual.swift */; }; + B56E4EE723CEDF0900E1708C /* Field.Virtual.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Virtual.swift */; }; B57D27BE1D0BBE8200539C58 /* BaseTestDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */; }; B57D27BF1D0BBE8200539C58 /* BaseTestDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */; }; B57D27C01D0BBE8200539C58 /* BaseTestDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */; }; @@ -1122,7 +1122,7 @@ B56E4ED323CDB54A00E1708C /* FieldProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldProtocol.swift; sourceTree = ""; }; B56E4ED823CEB8E700E1708C /* FieldStorableType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldStorableType.swift; sourceTree = ""; }; B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldOptionalType.swift; sourceTree = ""; }; - B56E4EE323CEDF0900E1708C /* Field.Computed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.Computed.swift; sourceTree = ""; }; + B56E4EE323CEDF0900E1708C /* Field.Virtual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.Virtual.swift; sourceTree = ""; }; B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTestDataTestCase.swift; sourceTree = ""; }; B57D27C11D0BC20100539C58 /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; B57E6FA123D302FA000FD031 /* Field.Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.Relationship.swift; sourceTree = ""; }; @@ -1622,7 +1622,7 @@ children = ( B56E4EC923CD9B4800E1708C /* Field.swift */, B56E4ECE23CD9E4200E1708C /* Field.Stored.swift */, - B56E4EE323CEDF0900E1708C /* Field.Computed.swift */, + B56E4EE323CEDF0900E1708C /* Field.Virtual.swift */, B50C3EE423D153EA00B29880 /* Field.Coded.swift */, B57E6FA123D302FA000FD031 /* Field.Relationship.swift */, B50C3EDE23D05BB200B29880 /* Coders */, @@ -2408,7 +2408,7 @@ B5FAD6A91B50A4B400714891 /* Progress+Convenience.swift in Sources */, B5E84EFC1AFF846E0064E85B /* SynchronousDataTransaction.swift in Sources */, B5215CA91FA4810300139E3A /* QueryChainBuilder.swift in Sources */, - B56E4EE423CEDF0900E1708C /* Field.Computed.swift in Sources */, + B56E4EE423CEDF0900E1708C /* Field.Virtual.swift in Sources */, B50E174D23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */, B5E222231CA4E12600BA2E95 /* CSSynchronousDataTransaction.swift in Sources */, B5E84F281AFF84920064E85B /* NSManagedObject+Convenience.swift in Sources */, @@ -2659,7 +2659,7 @@ B514EF0F23A8DB180093DBA4 /* DiffableDataSource.Target.swift in Sources */, B52F744B1E9B8740005F3DAC /* CoreStoreSchema.swift in Sources */, B5AEFAB61C9962AE00AD137F /* CoreStoreBridge.swift in Sources */, - B56E4EE523CEDF0900E1708C /* Field.Computed.swift in Sources */, + B56E4EE523CEDF0900E1708C /* Field.Virtual.swift in Sources */, B5E2222C1CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift in Sources */, 82BA18A71C4BBD2900A0916E /* CoreStore+Logging.swift in Sources */, B50E174E23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */, @@ -2910,7 +2910,7 @@ B514EF1123A8DB190093DBA4 /* DiffableDataSource.Target.swift in Sources */, B50E175A23517DE4004F033C /* Differentiable.swift in Sources */, B52F744D1E9B8740005F3DAC /* CoreStoreSchema.swift in Sources */, - B56E4EE723CEDF0900E1708C /* Field.Computed.swift in Sources */, + B56E4EE723CEDF0900E1708C /* Field.Virtual.swift in Sources */, B52DD19A1BE1F92800949AFE /* CoreStore+Logging.swift in Sources */, B52DD1A71BE1F93200949AFE /* BaseDataTransaction+Querying.swift in Sources */, B546F96C1C9AF26D00D5AC55 /* CSInMemoryStore.swift in Sources */, @@ -3161,7 +3161,7 @@ B514EF1023A8DB190093DBA4 /* DiffableDataSource.Target.swift in Sources */, B563218A1BD65216006C9394 /* SynchronousDataTransaction.swift in Sources */, B52F744C1E9B8740005F3DAC /* CoreStoreSchema.swift in Sources */, - B56E4EE623CEDF0900E1708C /* Field.Computed.swift in Sources */, + B56E4EE623CEDF0900E1708C /* Field.Virtual.swift in Sources */, B5AEFAB71C9962AE00AD137F /* CoreStoreBridge.swift in Sources */, B5E2222D1CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift in Sources */, B50E174F23517C03004F033C /* Internals.DiffableDataUIDispatcher.StagedChangeset.swift in Sources */, diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index 106f7f5..d9f817c 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -100,14 +100,14 @@ class Person: CoreStoreObject { @Field.Stored("name", customSetter: Person.setName(_:_:)) var name: String = "" - @Field.Computed( + @Field.Virtual( "displayName", customGetter: Person.getDisplayName(_:), affectedByKeyPaths: Person.keyPathsAffectingDisplayName() ) var displayName: String? - @Field.Computed( + @Field.Virtual( "customType", customGetter: Person.getCustomField(_:) ) diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index 32ed92b..5905216 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -114,7 +114,8 @@ extension NSManagedObject: DynamicObject { public class func cs_fromRaw(object: NSManagedObject) -> Self { - return unsafeDowncast(object, to: self) + // unsafeDowncast fails debug assertion starting Swift 5.2 + return _unsafeUncheckedDowncast(object, to: self) } public static func cs_matches(object: NSManagedObject) -> Bool { diff --git a/Sources/DynamicSchema+Convenience.swift b/Sources/DynamicSchema+Convenience.swift index 4f84648..5254d5f 100644 --- a/Sources/DynamicSchema+Convenience.swift +++ b/Sources/DynamicSchema+Convenience.swift @@ -70,7 +70,7 @@ extension DynamicSchema { let containerType: String if attribute.isTransient || attribute.attributeType == .undefinedAttributeType { - containerType = "Field.Computed" + containerType = "Field.Virtual" } else if attribute.attributeType == .transformableAttributeType { diff --git a/Sources/Field.Coded.swift b/Sources/Field.Coded.swift index 94e30fd..f014a11 100644 --- a/Sources/Field.Coded.swift +++ b/Sources/Field.Coded.swift @@ -40,7 +40,7 @@ extension FieldContainer { @Field.Stored("species") var species = "" - @Field.Computed("pluralName", customGetter: Animal.pluralName(_:)) + @Field.Virtual("pluralName", customGetter: Animal.pluralName(_:)) var pluralName: String = "" @Field.PlistCoded("color") @@ -62,7 +62,7 @@ extension FieldContainer { @Field.Stored("name") var name: String = "" - @Field.Computed("displayName", customGetter: Person.getName(_:)) + @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" private static func getName(_ partialObject: PartialObject) -> String { @@ -366,7 +366,7 @@ extension FieldContainer.Coded where V: FieldOptionalType { @Field.Stored("name") var name: String = "" - @Field.Computed("displayName", customGetter: Person.getName(_:)) + @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" private static func getName(_ partialObject: PartialObject) -> String { @@ -454,7 +454,7 @@ extension FieldContainer.Coded where V: DefaultNSSecureCodable { @Field.Stored("name") var name: String = "" - @Field.Computed("displayName", customGetter: Person.getName(_:)) + @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" private static func getName(_ partialObject: PartialObject) -> String { diff --git a/Sources/Field.Stored.swift b/Sources/Field.Stored.swift index 16babcd..ce7a914 100644 --- a/Sources/Field.Stored.swift +++ b/Sources/Field.Stored.swift @@ -40,7 +40,7 @@ extension FieldContainer { @Field.Stored("species") var species = "" - @Field.Computed("pluralName", customGetter: Animal.pluralName(_:)) + @Field.Virtual("pluralName", customGetter: Animal.pluralName(_:)) var pluralName: String = "" @Field.PlistCoded("color") @@ -62,7 +62,7 @@ extension FieldContainer { @Field.Stored("name") var name: String = "" - @Field.Computed("displayName", customGetter: Person.getName(_:)) + @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" private static func getName(_ partialObject: PartialObject) -> String { @@ -320,7 +320,7 @@ extension FieldContainer.Stored where V: FieldOptionalType { @Field.Stored("name") var name: String = "" - @Field.Computed("displayName", customGetter: Person.getName(_:)) + @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" private static func getName(_ partialObject: PartialObject) -> String { diff --git a/Sources/Field.Computed.swift b/Sources/Field.Virtual.swift similarity index 92% rename from Sources/Field.Computed.swift rename to Sources/Field.Virtual.swift index 8693032..c4fe361 100644 --- a/Sources/Field.Computed.swift +++ b/Sources/Field.Virtual.swift @@ -1,5 +1,5 @@ // -// Field.Computed.swift +// Field.Virtual.swift // CoreStore // // Copyright © 2020 John Rommel Estropia @@ -31,7 +31,7 @@ import Foundation extension FieldContainer { - // MARK: - Computed + // MARK: - Virtual /** The containing type for computed property values. Any type that conforms to `FieldStorableType` are supported. @@ -40,17 +40,17 @@ extension FieldContainer { @Field.Stored("species") var species = "" - @Field.Computed("pluralName", customGetter: Animal.pluralName(_:)) + @Field.Virtual("pluralName", customGetter: Animal.pluralName(_:)) var pluralName: String = "" @Field.PlistCoded("color") var color: UIColor? } ``` - - Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Computed(...) var` syntax will be ignored. + - Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Virtual(...) var` syntax will be ignored. */ @propertyWrapper - public struct Computed: AttributeKeyPathStringConvertible, FieldAttributeProtocol { + public struct Virtual: AttributeKeyPathStringConvertible, FieldAttributeProtocol { /** Initializes the metadata for the property. @@ -62,7 +62,7 @@ extension FieldContainer { @Field.Stored("name") var name: String = "" - @Field.Computed("displayName", customGetter: Person.getName(_:)) + @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" private static func getName(_ partialObject: PartialObject) -> String { @@ -85,7 +85,7 @@ extension FieldContainer { */ public init( _ keyPath: KeyPathString, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, + customGetter: @escaping (_ partialObject: PartialObject) -> V, customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { @@ -299,3 +299,22 @@ extension FieldContainer { private let customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? } } + + +extension FieldContainer.Virtual where V: FieldOptionalType { + + public init( + _ keyPath: KeyPathString, + customGetter: ((_ partialObject: PartialObject) -> V)? = nil, + customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { + + self.init( + keyPath: keyPath, + isOptional: false, + customGetter: customGetter, + customSetter: customSetter, + affectedByKeyPaths: affectedByKeyPaths + ) + } +} diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index 5426925..270ef01 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -385,7 +385,7 @@ extension ObjectPublisher where O: CoreStoreObject { /** Returns the value for the property identified by a given key. */ - public subscript(dynamicMember member: KeyPath.Computed>) -> V? { + public subscript(dynamicMember member: KeyPath.Virtual>) -> V? { guard let object = self.object, @@ -394,7 +394,7 @@ extension ObjectPublisher where O: CoreStoreObject { return nil } - return FieldContainer.Computed.read(field: object[keyPath: member], for: rawObject) as! V? + return FieldContainer.Virtual.read(field: object[keyPath: member], for: rawObject) as! V? } /** diff --git a/Sources/ObjectSnapshot.swift b/Sources/ObjectSnapshot.swift index 0845636..a8698f7 100644 --- a/Sources/ObjectSnapshot.swift +++ b/Sources/ObjectSnapshot.swift @@ -195,7 +195,7 @@ extension ObjectSnapshot where O: CoreStoreObject { /** Returns the value for the property identified by a given key. */ - public subscript(dynamicMember member: KeyPath.Computed>) -> V { + public subscript(dynamicMember member: KeyPath.Virtual>) -> V { get { diff --git a/Sources/PartialObject.swift b/Sources/PartialObject.swift index 035b817..e9ad364 100644 --- a/Sources/PartialObject.swift +++ b/Sources/PartialObject.swift @@ -58,7 +58,7 @@ public struct PartialObject { /** Returns the value for the property identified by a given key. */ - public func value(for property: (O) -> FieldContainer.Computed) -> V { + public func value(for property: (O) -> FieldContainer.Virtual) -> V { switch self.rawObject.value(forKey: property(O.meta).keyPath) { @@ -89,7 +89,7 @@ public struct PartialObject { This method does not invoke the access notification methods (`willAccessValue(forKey:)` and `didAccessValue(forKey:)`). This method is used primarily by subclasses that implement custom accessor methods that need direct access to the receiver’s private storage. */ - public func primitiveValue(for property: (O) -> FieldContainer.Computed) -> V? { + public func primitiveValue(for property: (O) -> FieldContainer.Virtual) -> V? { switch self.rawObject.primitiveValue(forKey: property(O.meta).keyPath) { @@ -145,7 +145,7 @@ public struct PartialObject { Sets in the receiver’s private internal storage the value of the property specified by key to value. */ - public func setPrimitiveValue(_ value: V, for property: (O) -> FieldContainer.Computed) { + public func setPrimitiveValue(_ value: V, for property: (O) -> FieldContainer.Virtual) { self.rawObject.setPrimitiveValue( value, From 2d1b1e059255a68a41d66491a4069bddf4f3d473 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Mon, 17 Feb 2020 18:30:18 +0900 Subject: [PATCH 07/20] add Field.Coded dynamic lookups for ObjectPublisher and ObjectSnapshot --- CoreStoreTests/DynamicModelTests.swift | 15 ++++++++++++++- Sources/ObjectPublisher.swift | 15 +++++++++++++++ Sources/ObjectSnapshot.swift | 17 +++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index d9f817c..5d72143 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -362,6 +362,7 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(person.name, personSnapshot1.$name) XCTAssertEqual(person.title, personSnapshot1.$title) XCTAssertEqual(person.displayName, personSnapshot1.$displayName) + XCTAssertEqual(person.job, personSnapshot1.$job) person.title = "Sir" XCTAssertEqual(person.displayName, "Sir John") @@ -380,15 +381,19 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(person.name, personSnapshot2.$name) XCTAssertEqual(person.title, personSnapshot2.$title) XCTAssertEqual(person.displayName, personSnapshot2.$displayName) + XCTAssertEqual(person.job, personSnapshot2.$job) var personSnapshot3 = personSnapshot2 personSnapshot3.$name = "James" XCTAssertEqual(personSnapshot1.$name, "John") XCTAssertEqual(personSnapshot1.$displayName, "Mr. John") + XCTAssertEqual(personSnapshot1.$job, .unemployed) XCTAssertEqual(personSnapshot2.$name, "John") XCTAssertEqual(personSnapshot2.$displayName, "Sir John") + XCTAssertEqual(personSnapshot2.$job, .engineer) XCTAssertEqual(personSnapshot3.$name, "James") XCTAssertEqual(personSnapshot3.$displayName, "Sir John") + XCTAssertEqual(personSnapshot3.$job, .engineer) @@ -400,7 +405,15 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(dog.master?.pets.first, dog) }, success: { _ in - + + let person = try! stack.fetchOne(From()) + XCTAssertNotNil(person) + + let personPublisher = person!.asPublisher(in: stack) + XCTAssertEqual(personPublisher.$name, "John") + XCTAssertEqual(personPublisher.$displayName, "Sir John") + XCTAssertEqual(personPublisher.$job, .engineer) + updateDone.fulfill() }, failure: { _ in diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index 26855cb..2f65fff 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -397,6 +397,21 @@ extension ObjectPublisher where O: CoreStoreObject { return FieldContainer.Virtual.read(field: object[keyPath: member], for: rawObject) as! V? } + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Coded>) -> V? { + + guard + let object = self.object, + let rawObject = object.rawObject + else { + + return nil + } + return FieldContainer.Coded.read(field: object[keyPath: member], for: rawObject) as! V? + } + /** Returns the value for the property identified by a given key. */ diff --git a/Sources/ObjectSnapshot.swift b/Sources/ObjectSnapshot.swift index 2458e3b..11902ec 100644 --- a/Sources/ObjectSnapshot.swift +++ b/Sources/ObjectSnapshot.swift @@ -209,6 +209,23 @@ extension ObjectSnapshot where O: CoreStoreObject { } } + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Coded>) -> V { + + get { + + let key = String(keyPath: member) + return self.values[key] as! V + } + set { + + let key = String(keyPath: member) + self.values[key] = newValue + } + } + /** Returns the value for the property identified by a given key. */ From 843adf21f7aa7eebe7120499312fe464071d2eda Mon Sep 17 00:00:00 2001 From: John Estropia Date: Tue, 18 Feb 2020 18:17:52 +0900 Subject: [PATCH 08/20] improved API for custom getters and setters in Field properties --- CoreStore.xcodeproj/project.pbxproj | 10 ++ CoreStoreTests/DynamicModelTests.swift | 68 ++++--- Sources/Field.Coded.swift | 93 +++++----- Sources/Field.Stored.swift | 77 ++++---- Sources/Field.Virtual.swift | 56 +++--- Sources/ObjectProxy.swift | 235 +++++++++++++++++++++++++ 6 files changed, 408 insertions(+), 131 deletions(-) create mode 100644 Sources/ObjectProxy.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 3e52e5a..83eff01 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -189,6 +189,10 @@ B50E17622351FA66004F033C /* Internals.Closure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E17602351FA66004F033C /* Internals.Closure.swift */; }; B50E17632351FA66004F033C /* Internals.Closure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E17602351FA66004F033C /* Internals.Closure.swift */; }; B50E17642351FA66004F033C /* Internals.Closure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E17602351FA66004F033C /* Internals.Closure.swift */; }; + B50E42F723FBB91800ED476E /* ObjectProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E42F623FBB91800ED476E /* ObjectProxy.swift */; }; + B50E42F823FBB91800ED476E /* ObjectProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E42F623FBB91800ED476E /* ObjectProxy.swift */; }; + B50E42F923FBB91800ED476E /* ObjectProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E42F623FBB91800ED476E /* ObjectProxy.swift */; }; + B50E42FA23FBB91800ED476E /* ObjectProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E42F623FBB91800ED476E /* ObjectProxy.swift */; }; B50EE14223473C92009B8C47 /* CoreStoreObject+DataSources.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50EE14123473C92009B8C47 /* CoreStoreObject+DataSources.swift */; }; B50EE14323473C96009B8C47 /* CoreStoreObject+DataSources.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50EE14123473C92009B8C47 /* CoreStoreObject+DataSources.swift */; }; B50EE14423473C97009B8C47 /* CoreStoreObject+DataSources.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50EE14123473C92009B8C47 /* CoreStoreObject+DataSources.swift */; }; @@ -1025,6 +1029,7 @@ B50E175623517DE4004F033C /* Differentiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Differentiable.swift; sourceTree = ""; }; B50E175B2351848E004F033C /* Internals.DiffableDataUIDispatcher.DiffResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.DiffableDataUIDispatcher.DiffResult.swift; sourceTree = ""; }; B50E17602351FA66004F033C /* Internals.Closure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.Closure.swift; sourceTree = ""; }; + B50E42F623FBB91800ED476E /* ObjectProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectProxy.swift; sourceTree = ""; }; B50EE14123473C92009B8C47 /* CoreStoreObject+DataSources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreStoreObject+DataSources.swift"; sourceTree = ""; }; B512607E1E97A18000402229 /* CoreStoreObject+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CoreStoreObject+Convenience.swift"; sourceTree = ""; }; B51260881E9B252B00402229 /* NSEntityDescription+DynamicModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSEntityDescription+DynamicModel.swift"; sourceTree = ""; }; @@ -1646,6 +1651,7 @@ children = ( B5D339D71E9489AB00C880DE /* CoreStoreObject.swift */, B53CA9A11EF1EF1600E0F440 /* PartialObject.swift */, + B50E42F623FBB91800ED476E /* ObjectProxy.swift */, B56E4EC823CD9B2E00E1708C /* Field Properties */, B5831B6E1F3355C300A9F647 /* Legacy Properties */, B52F74391E9B8724005F3DAC /* Dynamic Schema */, @@ -2381,6 +2387,7 @@ B5E84F411AFF8CCD0064E85B /* TypeErasedClauses.swift in Sources */, B5E84F0D1AFF847B0064E85B /* BaseDataTransaction+Querying.swift in Sources */, B52F74451E9B8724005F3DAC /* XcodeDataModelSchema.swift in Sources */, + B50E42F723FBB91800ED476E /* ObjectProxy.swift in Sources */, B5FAD6AC1B51285300714891 /* Internals.MigrationManager.swift in Sources */, B50EE14223473C92009B8C47 /* CoreStoreObject+DataSources.swift in Sources */, B50C3EE023D062C300B29880 /* FieldCoderType.swift in Sources */, @@ -2632,6 +2639,7 @@ B59851491C90289D00C99590 /* NSPersistentStoreCoordinator+Setup.swift in Sources */, B5E1B5A41CAA4365007FD580 /* CSCoreStore+Observing.swift in Sources */, B596BBB71DD5BC67001DCDD9 /* FetchableSource.swift in Sources */, + B50E42F823FBB91800ED476E /* ObjectProxy.swift in Sources */, B5FEC18F1C9166E600532541 /* NSPersistentStore+Setup.swift in Sources */, 82BA18B71C4BBD3F00A0916E /* CoreStore+Querying.swift in Sources */, B50C3EE123D062C300B29880 /* FieldCoderType.swift in Sources */, @@ -2883,6 +2891,7 @@ B52DD1A51BE1F92F00949AFE /* ImportableUniqueObject.swift in Sources */, B5E222271CA4E12600BA2E95 /* CSSynchronousDataTransaction.swift in Sources */, B52F74481E9B8724005F3DAC /* XcodeDataModelSchema.swift in Sources */, + B50E42FA23FBB91800ED476E /* ObjectProxy.swift in Sources */, B5519A621CA21954002BEF78 /* CSAsynchronousDataTransaction.swift in Sources */, B52DD19C1BE1F92C00949AFE /* Into.swift in Sources */, B50C3EE323D062C300B29880 /* FieldCoderType.swift in Sources */, @@ -3134,6 +3143,7 @@ B52F74471E9B8724005F3DAC /* XcodeDataModelSchema.swift in Sources */, B5FEC1901C9166E700532541 /* NSPersistentStore+Setup.swift in Sources */, B56321A11BD65216006C9394 /* ListMonitor.swift in Sources */, + B50E42F923FBB91800ED476E /* ObjectProxy.swift in Sources */, B5E1B5A51CAA4365007FD580 /* CSCoreStore+Observing.swift in Sources */, B596BBB81DD5BC67001DCDD9 /* FetchableSource.swift in Sources */, B50C3EE223D062C300B29880 /* FieldCoderType.swift in Sources */, diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index 5d72143..b98dec5 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -94,22 +94,43 @@ enum Job: String { class Person: CoreStoreObject { - @Field.Stored("title", customSetter: Person.setTitle(_:_:)) + @Field.Stored( + "title", + customSetter: { (object, field, newValue) in + field.primitiveValue = newValue + object.$displayName.primitiveValue = nil + } + ) var title: String = "Mr." - @Field.Stored("name", customSetter: Person.setName(_:_:)) + @Field.Stored( + "name", + customSetter: { (object, field, newValue) in + field.primitiveValue = newValue + object.$displayName.primitiveValue = nil + } + ) var name: String = "" @Field.Virtual( "displayName", - customGetter: Person.getDisplayName(_:), + customGetter: Person.getDisplayName(_:_:), affectedByKeyPaths: Person.keyPathsAffectingDisplayName() ) var displayName: String? @Field.Virtual( "customType", - customGetter: Person.getCustomField(_:) + customGetter: { (object, field) in + + if let value = field.primitiveValue { + + return value + } + let value = CustomType() + field.primitiveValue = value + return value + } ) var customField: CustomType @@ -131,40 +152,17 @@ class Person: CoreStoreObject { @Field.Relationship("_spouseInverse", inverse: \.$spouse) private var spouseInverse: Person? - private static func setTitle(_ partialObject: PartialObject, _ newValue: String) { - - partialObject.setPrimitiveValue(newValue, for: \.$title) - partialObject.setPrimitiveValue(nil, for: \.$displayName) - } - - private static func setName(_ partialObject: PartialObject, _ newValue: String) { - - partialObject.setPrimitiveValue(newValue, for: \.$name) - partialObject.setPrimitiveValue(nil, for: \.$displayName) - } + static func getDisplayName(_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> String? { - static func getCustomField(_ partialObject: PartialObject) -> CustomType { + if let value = field.primitiveValue { - if let customField = partialObject.primitiveValue(for: \.$customField) { - - return customField + return value } - let customField = CustomType() - partialObject.setPrimitiveValue(customField, for: \.$customField) - return customField - } - - static func getDisplayName(_ partialObject: PartialObject) -> String? { - - if let displayName = partialObject.primitiveValue(for: \.$displayName) { - - return displayName - } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) - let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: \.$displayName) - return displayName + let title = object.$title.value + let name = object.$name.value + let value = "\(title) \(name)" + field.primitiveValue = value + return value } static func keyPathsAffectingDisplayName() -> Set { diff --git a/Sources/Field.Coded.swift b/Sources/Field.Coded.swift index f014a11..8b9df88 100644 --- a/Sources/Field.Coded.swift +++ b/Sources/Field.Coded.swift @@ -65,15 +65,15 @@ extension FieldContainer { @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" - private static func getName(_ partialObject: PartialObject) -> String { - let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + private static func getName(_ object: ObjectProxy) -> String { + let cachedDisplayName = object.primitiveValue(for: \.$displayName) if !cachedDisplayName.isEmpty { return cachedDisplayName } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) + let title = object.value(for: \.$title) + let name = object.value(for: \.$name) let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + object.setPrimitiveValue(displayName, for: { $0.displayName }) return displayName } } @@ -82,8 +82,8 @@ extension FieldContainer { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -92,8 +92,8 @@ extension FieldContainer { versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, coder fieldCoderType: Coder.Type, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) where Coder.FieldStoredValue == V { @@ -116,8 +116,8 @@ extension FieldContainer { versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, coder: (encode: (V) -> Data?, decode: (Data?) -> V), - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) { @@ -209,7 +209,10 @@ extension FieldContainer { let field = field as! Self if let customGetter = field.customGetter { - return customGetter(PartialObject(rawObject)) + return customGetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: field) + ) } let keyPath = field.keyPath switch rawObject.value(forKey: keyPath) { @@ -237,7 +240,11 @@ extension FieldContainer { let keyPath = field.keyPath if let customSetter = field.customSetter { - return customSetter(PartialObject(rawObject), newValue) + return customSetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: field), + newValue + ) } return rawObject.setValue(newValue, forKey: keyPath) } @@ -279,7 +286,10 @@ extension FieldContainer { rawObject.didAccessValue(forKey: keyPath) } - let value = customGetter(PartialObject(rawObject)) + let value = customGetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: self) + ) return value } } @@ -300,7 +310,8 @@ extension FieldContainer { rawObject.didChangeValue(forKey: keyPath) } customSetter( - PartialObject(rawObject), + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: self), newValue as! V ) } @@ -316,8 +327,8 @@ extension FieldContainer { versionHashModifier: @escaping () -> String?, renamingIdentifier: @escaping () -> String?, valueTransformer: @escaping () -> Internals.AnyFieldCoder?, - customGetter: ((_ partialObject: PartialObject) -> V)?, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? , + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)?, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? , affectedByKeyPaths: @escaping () -> Set) { self.keyPath = keyPath @@ -346,8 +357,8 @@ extension FieldContainer { // MARK: Private - private let customGetter: ((_ partialObject: PartialObject) -> V)? - private let customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? + private let customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? + private let customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? } } @@ -369,15 +380,15 @@ extension FieldContainer.Coded where V: FieldOptionalType { @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" - private static func getName(_ partialObject: PartialObject) -> String { - let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + private static func getName(_ object: ObjectProxy) -> String { + let cachedDisplayName = object.primitiveValue(for: \.$displayName) if !cachedDisplayName.isEmpty { return cachedDisplayName } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) + let title = object.value(for: \.$title) + let name = object.value(for: \.$name) let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + object.setPrimitiveValue(displayName, for: { $0.displayName }) return displayName } } @@ -386,8 +397,8 @@ extension FieldContainer.Coded where V: FieldOptionalType { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -396,8 +407,8 @@ extension FieldContainer.Coded where V: FieldOptionalType { versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, coder: Coder.Type, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) where Coder.FieldStoredValue == V.Wrapped { @@ -420,8 +431,8 @@ extension FieldContainer.Coded where V: FieldOptionalType { versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, coder: (encode: (V) -> Data?, decode: (Data?) -> V), - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) { @@ -457,15 +468,15 @@ extension FieldContainer.Coded where V: DefaultNSSecureCodable { @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" - private static func getName(_ partialObject: PartialObject) -> String { - let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + private static func getName(_ object: ObjectProxy) -> String { + let cachedDisplayName = object.primitiveValue(for: \.$displayName) if !cachedDisplayName.isEmpty { return cachedDisplayName } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) + let title = object.value(for: \.$title) + let name = object.value(for: \.$name) let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + object.setPrimitiveValue(displayName, for: { $0.displayName }) return displayName } } @@ -474,8 +485,8 @@ extension FieldContainer.Coded where V: DefaultNSSecureCodable { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -483,8 +494,8 @@ extension FieldContainer.Coded where V: DefaultNSSecureCodable { _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) { @@ -512,8 +523,8 @@ extension FieldContainer.Coded where V: FieldOptionalType, V.Wrapped: DefaultNSS _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) { diff --git a/Sources/Field.Stored.swift b/Sources/Field.Stored.swift index ce7a914..09bd681 100644 --- a/Sources/Field.Stored.swift +++ b/Sources/Field.Stored.swift @@ -62,28 +62,26 @@ extension FieldContainer { @Field.Stored("name") var name: String = "" - @Field.Virtual("displayName", customGetter: Person.getName(_:)) - var displayName: String = "" - - private static func getName(_ partialObject: PartialObject) -> String { - let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) - if !cachedDisplayName.isEmpty { - return cachedDisplayName + @Field.Virtual( + "displayName", + customGetter: { (object, field) in + if let cached = field.primitiveValue, !cached.isEmpty { + return cached + } + let value = "\(object.$title.value) \(object.$name.value)" + field.primitiveValue = value + return value } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) - let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) - return displayName - } + ) + var displayName: String = "" } ``` - parameter initial: the initial value for the property when the object is first create - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -91,8 +89,8 @@ extension FieldContainer { _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) { @@ -183,7 +181,10 @@ extension FieldContainer { let field = field as! Self if let customGetter = field.customGetter { - return customGetter(PartialObject(rawObject)) + return customGetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: field) + ) } let keyPath = field.keyPath switch rawObject.value(forKey: keyPath) { @@ -211,7 +212,11 @@ extension FieldContainer { let keyPath = field.keyPath if let customSetter = field.customSetter { - return customSetter(PartialObject(rawObject), newValue) + return customSetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: field), + newValue + ) } return rawObject.setValue( newValue.cs_toFieldStoredNativeType(), @@ -239,7 +244,10 @@ extension FieldContainer { rawObject.didAccessValue(forKey: keyPath) } - let value = customGetter(PartialObject(rawObject)) + let value = customGetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: self) + ) return value.cs_toFieldStoredNativeType() } } @@ -260,7 +268,8 @@ extension FieldContainer { rawObject.didChangeValue(forKey: keyPath) } customSetter( - PartialObject(rawObject), + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: self), V.cs_fromFieldStoredNativeType(newValue as! V.FieldStoredNativeType) ) } @@ -275,8 +284,8 @@ extension FieldContainer { isOptional: Bool, versionHashModifier: @escaping () -> String?, renamingIdentifier: @escaping () -> String?, - customGetter: ((_ partialObject: PartialObject) -> V)?, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? , + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)?, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? , affectedByKeyPaths: @escaping () -> Set) { self.keyPath = keyPath @@ -300,8 +309,8 @@ extension FieldContainer { // MARK: Private - private let customGetter: ((_ partialObject: PartialObject) -> V)? - private let customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? + private let customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? + private let customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? } } @@ -323,15 +332,15 @@ extension FieldContainer.Stored where V: FieldOptionalType { @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" - private static func getName(_ partialObject: PartialObject) -> String { - let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + private static func getName(_ object: ObjectProxy) -> String { + let cachedDisplayName = object.primitiveValue(for: \.$displayName) if !cachedDisplayName.isEmpty { return cachedDisplayName } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) + let title = object.value(for: \.$title) + let name = object.value(for: \.$name) let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + object.setPrimitiveValue(displayName, for: { $0.displayName }) return displayName } } @@ -340,8 +349,8 @@ extension FieldContainer.Stored where V: FieldOptionalType { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -349,8 +358,8 @@ extension FieldContainer.Stored where V: FieldOptionalType { _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { self.init( diff --git a/Sources/Field.Virtual.swift b/Sources/Field.Virtual.swift index c4fe361..67f6aa8 100644 --- a/Sources/Field.Virtual.swift +++ b/Sources/Field.Virtual.swift @@ -65,28 +65,28 @@ extension FieldContainer { @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" - private static func getName(_ partialObject: PartialObject) -> String { - let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + private static func getName(_ object: ObjectProxy) -> String { + let cachedDisplayName = object.primitiveValue(for: \.$displayName) if !cachedDisplayName.isEmpty { return cachedDisplayName } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) + let title = object.value(for: \.$title) + let name = object.value(for: \.$name) let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + object.setPrimitiveValue(displayName, for: { $0.displayName }) return displayName } } ``` - parameter keyPath: the permanent attribute name for this property. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( _ keyPath: KeyPathString, - customGetter: @escaping (_ partialObject: PartialObject) -> V, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: @escaping (_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { self.init( @@ -106,8 +106,8 @@ extension FieldContainer { wrappedValue initial: @autoclosure @escaping () -> V, _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { fatalError() @@ -188,7 +188,10 @@ extension FieldContainer { let field = field as! Self if let customGetter = field.customGetter { - return customGetter(PartialObject(rawObject)) + return customGetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: field) + ) } let keyPath = field.keyPath switch rawObject.value(forKey: keyPath) { @@ -216,7 +219,11 @@ extension FieldContainer { let keyPath = field.keyPath if let customSetter = field.customSetter { - return customSetter(PartialObject(rawObject), newValue) + return customSetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: field), + newValue + ) } return rawObject.setValue(newValue, forKey: keyPath) } @@ -241,7 +248,10 @@ extension FieldContainer { rawObject.didAccessValue(forKey: keyPath) } - return customGetter(PartialObject(rawObject)) + return customGetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: self) + ) } } @@ -260,7 +270,11 @@ extension FieldContainer { rawObject.didChangeValue(forKey: keyPath) } - return customSetter(PartialObject(rawObject), newValue as! V) + return customSetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: self), + newValue as! V + ) } } @@ -270,8 +284,8 @@ extension FieldContainer { fileprivate init( keyPath: KeyPathString, isOptional: Bool, - customGetter: ((_ partialObject: PartialObject) -> V)?, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? , + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)?, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? , affectedByKeyPaths: @escaping () -> Set) { self.keyPath = keyPath @@ -295,8 +309,8 @@ extension FieldContainer { // MARK: Private - private let customGetter: ((_ partialObject: PartialObject) -> V)? - private let customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? + private let customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? + private let customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? } } @@ -305,8 +319,8 @@ extension FieldContainer.Virtual where V: FieldOptionalType { public init( _ keyPath: KeyPathString, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { self.init( diff --git a/Sources/ObjectProxy.swift b/Sources/ObjectProxy.swift new file mode 100644 index 0000000..7f54b22 --- /dev/null +++ b/Sources/ObjectProxy.swift @@ -0,0 +1,235 @@ +// +// ObjectProxy.swift +// CoreStore +// +// Copyright © 2020 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: - ObjectProxy + +/** + An `ObjectProxy` is only used when overriding getters and setters for `CoreStoreObject` properties. Custom getters and setters are implemented as a closure that "overrides" the default property getter/setter. The closure receives an `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info every time KVO invokes this accessor method incurs a heavy performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would execute the same accessor again recursively. + */ +@dynamicMemberLookup +public struct ObjectProxy { + + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Stored>) -> FieldProxy { + + return .init(rawObject: self.rawObject, keyPath: member) + } + + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Virtual>) -> FieldProxy { + + return .init(rawObject: self.rawObject, keyPath: member) + } + + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Coded>) -> FieldProxy { + + return .init(rawObject: self.rawObject, keyPath: member) + } + + + // MARK: Internal + + internal let rawObject: CoreStoreManagedObject + + internal init(_ rawObject: CoreStoreManagedObject) { + + self.rawObject = rawObject + } + + + // MARK: - FieldProxy + + public struct FieldProxy { + + // MARK: Public + + /** + Returns the value for the specified property from the object’s private internal storage. + + Accessing this property does not invoke the access notification methods (`willAccessValue(forKey:)` and `didAccessValue(forKey:)`). This method is used primarily to implement custom accessor methods that need direct access to the object's private storage. + */ + public var primitiveValue: V? { + + get { + + return self.getPrimitiveValue() + } + nonmutating set { + + self.setPrimitiveValue(newValue) + } + } + + /** + Returns the value for the property identified by a given key. + + Accessing this property triggers the access notification methods (`willAccessValue(forKey:)` and `didAccessValue(forKey:)`). + */ + public var value: V { + + get { + + return self.getValue() + } + nonmutating set { + + self.setValue(newValue) + } + } + + + // MARK: Internal + + internal init(rawObject: CoreStoreManagedObject, keyPath: KeyPath.Stored>) { + + self.init(rawObject: rawObject, field: O.meta[keyPath: keyPath]) + } + + internal init(rawObject: CoreStoreManagedObject, keyPath: KeyPath.Virtual>) { + + self.init(rawObject: rawObject, field: O.meta[keyPath: keyPath]) + } + + internal init(rawObject: CoreStoreManagedObject, keyPath: KeyPath.Coded>) { + + self.init(rawObject: rawObject, field: O.meta[keyPath: keyPath]) + } + + internal init(rawObject: CoreStoreManagedObject, field: FieldContainer.Stored) { + + let keyPathString = field.keyPath + self.getValue = { + + return FieldContainer.Stored.read(field: field, for: rawObject) as! V + } + self.setValue = { + + FieldContainer.Stored.modify(field: field, for: rawObject, newValue: $0) + } + self.getPrimitiveValue = { + + return V.cs_fromFieldStoredNativeType( + rawObject.primitiveValue(forKey: keyPathString) as! V.FieldStoredNativeType + ) + } + self.setPrimitiveValue = { + + rawObject.setPrimitiveValue( + $0.cs_toFieldStoredNativeType(), + forKey: keyPathString + ) + } + } + + internal init(rawObject: CoreStoreManagedObject, field: FieldContainer.Virtual) { + + let keyPathString = field.keyPath + self.getValue = { + + return FieldContainer.Virtual.read(field: field, for: rawObject) as! V + } + self.setValue = { + + FieldContainer.Virtual.modify(field: field, for: rawObject, newValue: $0) + } + self.getPrimitiveValue = { + + switch rawObject.primitiveValue(forKey: keyPathString) { + + case let value as V: + return value + + case nil, + is NSNull, + _? /* any other unrelated type */ : + return nil + } + } + self.setPrimitiveValue = { + + rawObject.setPrimitiveValue( + $0, + forKey: keyPathString + ) + } + } + + internal init(rawObject: CoreStoreManagedObject, field: FieldContainer.Coded) { + + let keyPathString = field.keyPath + self.getValue = { + + return FieldContainer.Coded.read(field: field, for: rawObject) as! V + } + self.setValue = { + + FieldContainer.Coded.modify(field: field, for: rawObject, newValue: $0) + } + self.getPrimitiveValue = { + + switch rawObject.primitiveValue(forKey: keyPathString) { + + case let valueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox: + rawObject.setPrimitiveValue(valueBox.value, forKey: keyPathString) + return valueBox.value as? V + + case let value as V: + return value + + case nil, + is NSNull, + _? /* any other unrelated type */ : + return nil + } + } + self.setPrimitiveValue = { + + rawObject.setPrimitiveValue( + $0, + forKey: keyPathString + ) + } + } + + + // MARK: Private + + private let getValue: () -> V + private let setValue: (V) -> Void + private let getPrimitiveValue: () -> V? + private let setPrimitiveValue: (V?) -> Void + } +} From 58629bc1dfe87e7c8c3d8b52b81863478e4acb02 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 19 Feb 2020 13:58:14 +0900 Subject: [PATCH 09/20] add missing predicate operator overloads --- Sources/Field.Coded.swift | 2 +- Sources/Field.Stored.swift | 9 ++------- Sources/Field.Virtual.swift | 2 +- Sources/Field.swift | 4 ++-- Sources/KeyPath+Querying.swift | 24 +++++++++++++++++++++++- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/Sources/Field.Coded.swift b/Sources/Field.Coded.swift index 8b9df88..c79699a 100644 --- a/Sources/Field.Coded.swift +++ b/Sources/Field.Coded.swift @@ -43,7 +43,7 @@ extension FieldContainer { @Field.Virtual("pluralName", customGetter: Animal.pluralName(_:)) var pluralName: String = "" - @Field.PlistCoded("color") + @Field.Coded("color", coder: FieldCoders.Plist.self) var color: UIColor? } ``` diff --git a/Sources/Field.Stored.swift b/Sources/Field.Stored.swift index 09bd681..a3bc8dd 100644 --- a/Sources/Field.Stored.swift +++ b/Sources/Field.Stored.swift @@ -43,7 +43,7 @@ extension FieldContainer { @Field.Virtual("pluralName", customGetter: Animal.pluralName(_:)) var pluralName: String = "" - @Field.PlistCoded("color") + @Field.Coded("color", coder: FieldCoders.Plist.self) var color: UIColor? } ``` @@ -65,12 +65,7 @@ extension FieldContainer { @Field.Virtual( "displayName", customGetter: { (object, field) in - if let cached = field.primitiveValue, !cached.isEmpty { - return cached - } - let value = "\(object.$title.value) \(object.$name.value)" - field.primitiveValue = value - return value + return "\(object.$title.value) \(object.$name.value)" } ) var displayName: String = "" diff --git a/Sources/Field.Virtual.swift b/Sources/Field.Virtual.swift index 67f6aa8..fe16fa6 100644 --- a/Sources/Field.Virtual.swift +++ b/Sources/Field.Virtual.swift @@ -43,7 +43,7 @@ extension FieldContainer { @Field.Virtual("pluralName", customGetter: Animal.pluralName(_:)) var pluralName: String = "" - @Field.PlistCoded("color") + @Field.Coded("color", coder: FieldCoders.Plist.self) var color: UIColor? } ``` diff --git a/Sources/Field.swift b/Sources/Field.swift index 19f4df6..7ed8018 100644 --- a/Sources/Field.swift +++ b/Sources/Field.swift @@ -41,7 +41,7 @@ extension DynamicObject where Self: CoreStoreObject { @Field.Stored("nickname") var nickname: String? - @Field.PlistCoded("color") + @Field.Coded("color", coder: FieldCoders.Plist.self) var color: UIColor? } ``` @@ -63,7 +63,7 @@ extension DynamicObject where Self: CoreStoreObject { @Field.Stored("nickname") var nickname: String? - @Field.PlistCoded("color") + @Field.Coded("color", coder: FieldCoders.Plist.self) var color: UIColor? } ``` diff --git a/Sources/KeyPath+Querying.swift b/Sources/KeyPath+Querying.swift index 4cdbb88..1fd8ed2 100644 --- a/Sources/KeyPath+Querying.swift +++ b/Sources/KeyPath+Querying.swift @@ -364,7 +364,7 @@ public func ~= (_ sequence: /** Creates a `Where` clause by comparing if a property is equal to a value ``` - let person = dataStack.fetchOne(From().where(\.nickname == "John")) + let person = dataStack.fetchOne(From().where(\.$nickname == "John")) ``` */ public func == (_ keyPath: KeyPath.Stored>, _ value: V) -> Where { @@ -372,6 +372,28 @@ public func == (_ keyPath: KeyPath.Stored>, _ valu return Where(keyPath, isEqualTo: value) } +/** + Creates a `Where` clause by comparing if a property is not equal to a value + ``` + let person = dataStack.fetchOne(From().where(\.$nickname != "John")) + ``` + */ +public func != (_ keyPath: KeyPath.Stored>, _ value: V) -> Where { + + return !Where(keyPath, isEqualTo: value) +} + +/** + Creates a `Where` clause by checking if a sequence contains the value of a property + ``` + let dog = dataStack.fetchOne(From().where(["Pluto", "Snoopy", "Scooby"] ~= \.nickname)) + ``` + */ +public func ~= (_ sequence: S, _ keyPath: KeyPath.Stored>) -> Where where S.Iterator.Element == V { + + return Where(O.meta[keyPath: keyPath].keyPath, isMemberOf: sequence) +} + // MARK: - KeyPath where Root: CoreStoreObject, Value: ValueContainer.Required From 627a5d4355e3674b7002c3dbb023d2371cdfc47e Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 19 Feb 2020 22:01:46 +0900 Subject: [PATCH 10/20] add OrderBy utilities for Field.Stored --- Sources/OrderBy.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Sources/OrderBy.swift b/Sources/OrderBy.swift index 126f4ae..9e4dd5c 100644 --- a/Sources/OrderBy.swift +++ b/Sources/OrderBy.swift @@ -185,6 +185,14 @@ public struct OrderBy: OrderByClause, FetchClause, QueryClause // MARK: CoreStoreObject Key Paths + + /** + Indicates that the `KeyPathString` should be sorted in ascending order + */ + public static func ascending(_ attribute: KeyPath.Stored>) -> SortKey { + + return .ascending(O.meta[keyPath: attribute].keyPath) + } /** Indicates that the `KeyPathString` should be sorted in ascending order @@ -217,6 +225,14 @@ public struct OrderBy: OrderByClause, FetchClause, QueryClause return .ascending(O.meta[keyPath: attribute].keyPath) } + + /** + Indicates that the `KeyPathString` should be sorted in descending order + */ + public static func descending(_ attribute: KeyPath.Stored>) -> SortKey { + + return .descending(O.meta[keyPath: attribute].keyPath) + } /** Indicates that the `KeyPathString` should be sorted in descending order From dd3fb17dd0f1c777e1ec869bcabd149699cc8315 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Fri, 21 Feb 2020 10:19:42 +0900 Subject: [PATCH 11/20] fix runtime issue with Fields on objects with base classes --- Sources/ObjectProxy.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ObjectProxy.swift b/Sources/ObjectProxy.swift index 7f54b22..8e25731 100644 --- a/Sources/ObjectProxy.swift +++ b/Sources/ObjectProxy.swift @@ -133,11 +133,11 @@ public struct ObjectProxy { let keyPathString = field.keyPath self.getValue = { - return FieldContainer.Stored.read(field: field, for: rawObject) as! V + return FieldContainer.Stored.read(field: field, for: rawObject) as! V } self.setValue = { - FieldContainer.Stored.modify(field: field, for: rawObject, newValue: $0) + FieldContainer.Stored.modify(field: field, for: rawObject, newValue: $0) } self.getPrimitiveValue = { @@ -159,11 +159,11 @@ public struct ObjectProxy { let keyPathString = field.keyPath self.getValue = { - return FieldContainer.Virtual.read(field: field, for: rawObject) as! V + return FieldContainer.Virtual.read(field: field, for: rawObject) as! V } self.setValue = { - FieldContainer.Virtual.modify(field: field, for: rawObject, newValue: $0) + FieldContainer.Virtual.modify(field: field, for: rawObject, newValue: $0) } self.getPrimitiveValue = { @@ -192,11 +192,11 @@ public struct ObjectProxy { let keyPathString = field.keyPath self.getValue = { - return FieldContainer.Coded.read(field: field, for: rawObject) as! V + return FieldContainer.Coded.read(field: field, for: rawObject) as! V } self.setValue = { - FieldContainer.Coded.modify(field: field, for: rawObject, newValue: $0) + FieldContainer.Coded.modify(field: field, for: rawObject, newValue: $0) } self.getPrimitiveValue = { From 12c5aeaaa46b1b5a4055cc87071c46d41f845464 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Fri, 21 Feb 2020 10:36:28 +0900 Subject: [PATCH 12/20] safer casting --- Sources/ObjectProxy.swift | 12 ++++++------ Sources/ObjectPublisher.swift | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Sources/ObjectProxy.swift b/Sources/ObjectProxy.swift index 8e25731..cd2f61e 100644 --- a/Sources/ObjectProxy.swift +++ b/Sources/ObjectProxy.swift @@ -133,11 +133,11 @@ public struct ObjectProxy { let keyPathString = field.keyPath self.getValue = { - return FieldContainer.Stored.read(field: field, for: rawObject) as! V + return type(of: field).read(field: field, for: rawObject) as! V } self.setValue = { - FieldContainer.Stored.modify(field: field, for: rawObject, newValue: $0) + type(of: field).modify(field: field, for: rawObject, newValue: $0) } self.getPrimitiveValue = { @@ -159,11 +159,11 @@ public struct ObjectProxy { let keyPathString = field.keyPath self.getValue = { - return FieldContainer.Virtual.read(field: field, for: rawObject) as! V + return type(of: field).read(field: field, for: rawObject) as! V } self.setValue = { - FieldContainer.Virtual.modify(field: field, for: rawObject, newValue: $0) + type(of: field).modify(field: field, for: rawObject, newValue: $0) } self.getPrimitiveValue = { @@ -192,11 +192,11 @@ public struct ObjectProxy { let keyPathString = field.keyPath self.getValue = { - return FieldContainer.Coded.read(field: field, for: rawObject) as! V + return type(of: field).read(field: field, for: rawObject) as! V } self.setValue = { - FieldContainer.Coded.modify(field: field, for: rawObject, newValue: $0) + type(of: field).modify(field: field, for: rawObject, newValue: $0) } self.getPrimitiveValue = { diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index 2f65fff..286e0bc 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -379,7 +379,8 @@ extension ObjectPublisher where O: CoreStoreObject { return nil } - return FieldContainer.Stored.read(field: object[keyPath: member], for: rawObject) as! V? + let field = object[keyPath: member] + return type(of: field).read(field: field, for: rawObject) as! V? } /** @@ -394,7 +395,8 @@ extension ObjectPublisher where O: CoreStoreObject { return nil } - return FieldContainer.Virtual.read(field: object[keyPath: member], for: rawObject) as! V? + let field = object[keyPath: member] + return type(of: field).read(field: field, for: rawObject) as! V? } /** @@ -409,7 +411,8 @@ extension ObjectPublisher where O: CoreStoreObject { return nil } - return FieldContainer.Coded.read(field: object[keyPath: member], for: rawObject) as! V? + let field = object[keyPath: member] + return type(of: field).read(field: field, for: rawObject) as! V? } /** @@ -419,12 +422,16 @@ extension ObjectPublisher where O: CoreStoreObject { guard let object = self.object, - let rawObject = object.rawObject, - let value = FieldContainer.Relationship.read(field: object[keyPath: member], for: rawObject) as! V? + let rawObject = object.rawObject else { return nil } + let field = object[keyPath: member] + guard let value = type(of: field).read(field: field, for: rawObject) as! V? else { + + return nil + } let nativeValue = V.cs_toNativeType(from: value) let snapshotValue = V.cs_valueForSnapshot(from: nativeValue) return V.cs_toPublishedType(from: snapshotValue, in: self.context) From 0df6c737c1a423155d8b411fdb8380af54c21edb Mon Sep 17 00:00:00 2001 From: John Estropia Date: Fri, 21 Feb 2020 11:17:39 +0900 Subject: [PATCH 13/20] fix wrong optional configuration in Field.Virtual --- Sources/Field.Virtual.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Field.Virtual.swift b/Sources/Field.Virtual.swift index fe16fa6..d2486c4 100644 --- a/Sources/Field.Virtual.swift +++ b/Sources/Field.Virtual.swift @@ -325,7 +325,7 @@ extension FieldContainer.Virtual where V: FieldOptionalType { self.init( keyPath: keyPath, - isOptional: false, + isOptional: true, customGetter: customGetter, customSetter: customSetter, affectedByKeyPaths: affectedByKeyPaths From e1b03b4a8981553f824e7dd161b889db5411ef6e Mon Sep 17 00:00:00 2001 From: John Estropia Date: Fri, 21 Feb 2020 11:20:32 +0900 Subject: [PATCH 14/20] fix wrong optional configuration in Field.Virtual --- Sources/Field.Virtual.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/Field.Virtual.swift b/Sources/Field.Virtual.swift index d2486c4..ad36970 100644 --- a/Sources/Field.Virtual.swift +++ b/Sources/Field.Virtual.swift @@ -91,7 +91,6 @@ extension FieldContainer { self.init( keyPath: keyPath, - isOptional: false, customGetter: customGetter, customSetter: customSetter, affectedByKeyPaths: affectedByKeyPaths @@ -283,7 +282,6 @@ extension FieldContainer { fileprivate init( keyPath: KeyPathString, - isOptional: Bool, customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)?, customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? , affectedByKeyPaths: @escaping () -> Set) { @@ -292,7 +290,7 @@ extension FieldContainer { self.entityDescriptionValues = { ( attributeType: .undefinedAttributeType, - isOptional: isOptional, + isOptional: true, isTransient: true, allowsExternalBinaryDataStorage: false, versionHashModifier: nil, @@ -325,7 +323,6 @@ extension FieldContainer.Virtual where V: FieldOptionalType { self.init( keyPath: keyPath, - isOptional: true, customGetter: customGetter, customSetter: customSetter, affectedByKeyPaths: affectedByKeyPaths From 361dba58c6e01f35ca810faf8bcd93822cb8db28 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Fri, 21 Feb 2020 11:52:11 +0900 Subject: [PATCH 15/20] bypass thread checks depending on location of Field call --- Sources/DynamicObject.swift | 17 ++++++++++++++--- Sources/Field.Coded.swift | 12 ++++++------ Sources/Field.Relationship.swift | 6 +++--- Sources/Field.Stored.swift | 10 +++++----- Sources/Field.Virtual.swift | 8 ++++---- Sources/FieldProtocol.swift | 2 +- Sources/ObjectProxy.swift | 18 +++++++++++++++--- Sources/ObjectPublisher.swift | 8 ++++---- 8 files changed, 52 insertions(+), 29 deletions(-) diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index 5905216..3f09383 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -172,10 +172,17 @@ extension CoreStoreObject { switch child.value { case let property as FieldAttributeProtocol: - attributes[property.keyPath] = type(of: property).read(field: property, for: object.rawObject!) + attributes[property.keyPath] = type(of: property).read( + field: property, + for: object.rawObject!, + bypassThreadCheck: false + ) case let property as FieldRelationshipProtocol: - attributes[property.keyPath] = type(of: property).valueForSnapshot(field: property, for: object.rawObject!) + attributes[property.keyPath] = type(of: property).valueForSnapshot( + field: property, + for: object.rawObject! + ) case let property as AttributeProtocol: attributes[property.keyPath] = property.valueForSnapshot @@ -212,7 +219,11 @@ extension CoreStoreObject { switch property { case let property as FieldAttributeProtocol: - values[property.keyPath] = type(of: property).read(field: property, for: rawObject) + values[property.keyPath] = type(of: property).read( + field: property, + for: rawObject, + bypassThreadCheck: false + ) default: continue diff --git a/Sources/Field.Coded.swift b/Sources/Field.Coded.swift index c79699a..d94e813 100644 --- a/Sources/Field.Coded.swift +++ b/Sources/Field.Coded.swift @@ -82,7 +82,7 @@ extension FieldContainer { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ @@ -161,7 +161,7 @@ extension FieldContainer { instance.rawObject != nil, "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." ) - return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V + return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, bypassThreadCheck: false) as! V } set { @@ -200,10 +200,10 @@ extension FieldContainer { // MARK: FieldProtocol - internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { + internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject, bypassThreadCheck: Bool) -> Any? { Internals.assert( - rawObject.isRunningInAllowedQueue() == true, + bypassThreadCheck || rawObject.isRunningInAllowedQueue() == true, "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." ) let field = field as! Self @@ -397,7 +397,7 @@ extension FieldContainer.Coded where V: FieldOptionalType { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ @@ -485,7 +485,7 @@ extension FieldContainer.Coded where V: DefaultNSSecureCodable { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ diff --git a/Sources/Field.Relationship.swift b/Sources/Field.Relationship.swift index 630941b..f590327 100644 --- a/Sources/Field.Relationship.swift +++ b/Sources/Field.Relationship.swift @@ -99,7 +99,7 @@ extension FieldContainer { instance.rawObject != nil, "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." ) - return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V + return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, bypassThreadCheck: false) as! V } set { @@ -138,10 +138,10 @@ extension FieldContainer { // MARK: FieldProtocol - internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { + internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject, bypassThreadCheck: Bool) -> Any? { Internals.assert( - rawObject.isRunningInAllowedQueue() == true, + bypassThreadCheck || rawObject.isRunningInAllowedQueue() == true, "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." ) let field = field as! Self diff --git a/Sources/Field.Stored.swift b/Sources/Field.Stored.swift index a3bc8dd..29f5742 100644 --- a/Sources/Field.Stored.swift +++ b/Sources/Field.Stored.swift @@ -75,7 +75,7 @@ extension FieldContainer { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ @@ -128,7 +128,7 @@ extension FieldContainer { instance.rawObject != nil, "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." ) - return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V + return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, bypassThreadCheck: false) as! V } set { @@ -167,10 +167,10 @@ extension FieldContainer { // MARK: FieldProtocol - internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { + internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject, bypassThreadCheck: Bool) -> Any? { Internals.assert( - rawObject.isRunningInAllowedQueue() == true, + bypassThreadCheck || rawObject.isRunningInAllowedQueue() == true, "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." ) let field = field as! Self @@ -344,7 +344,7 @@ extension FieldContainer.Stored where V: FieldOptionalType { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ diff --git a/Sources/Field.Virtual.swift b/Sources/Field.Virtual.swift index ad36970..815e18d 100644 --- a/Sources/Field.Virtual.swift +++ b/Sources/Field.Virtual.swift @@ -79,7 +79,7 @@ extension FieldContainer { } ``` - parameter keyPath: the permanent attribute name for this property. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ @@ -139,7 +139,7 @@ extension FieldContainer { instance.rawObject != nil, "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." ) - return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V + return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, bypassThreadCheck: false) as! V } set { @@ -178,10 +178,10 @@ extension FieldContainer { // MARK: FieldProtocol - internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { + internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject, bypassThreadCheck: Bool) -> Any? { Internals.assert( - rawObject.isRunningInAllowedQueue() == true, + bypassThreadCheck || rawObject.isRunningInAllowedQueue() == true, "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." ) let field = field as! Self diff --git a/Sources/FieldProtocol.swift b/Sources/FieldProtocol.swift index 9ae3014..77b1981 100644 --- a/Sources/FieldProtocol.swift +++ b/Sources/FieldProtocol.swift @@ -31,6 +31,6 @@ import CoreData internal protocol FieldProtocol: PropertyProtocol { - static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? + static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject, bypassThreadCheck: Bool) -> Any? static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) } diff --git a/Sources/ObjectProxy.swift b/Sources/ObjectProxy.swift index cd2f61e..89e233e 100644 --- a/Sources/ObjectProxy.swift +++ b/Sources/ObjectProxy.swift @@ -133,7 +133,11 @@ public struct ObjectProxy { let keyPathString = field.keyPath self.getValue = { - return type(of: field).read(field: field, for: rawObject) as! V + return type(of: field).read( + field: field, + for: rawObject, + bypassThreadCheck: true // May be called from NSError logs + ) as! V } self.setValue = { @@ -159,7 +163,11 @@ public struct ObjectProxy { let keyPathString = field.keyPath self.getValue = { - return type(of: field).read(field: field, for: rawObject) as! V + return type(of: field).read( + field: field, + for: rawObject, + bypassThreadCheck: true // May be called from NSError logs + ) as! V } self.setValue = { @@ -192,7 +200,11 @@ public struct ObjectProxy { let keyPathString = field.keyPath self.getValue = { - return type(of: field).read(field: field, for: rawObject) as! V + return type(of: field).read( + field: field, + for: rawObject, + bypassThreadCheck: true // May be called from NSError logs + ) as! V } self.setValue = { diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index 286e0bc..23893c1 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -380,7 +380,7 @@ extension ObjectPublisher where O: CoreStoreObject { return nil } let field = object[keyPath: member] - return type(of: field).read(field: field, for: rawObject) as! V? + return type(of: field).read(field: field, for: rawObject, bypassThreadCheck: false) as! V? } /** @@ -396,7 +396,7 @@ extension ObjectPublisher where O: CoreStoreObject { return nil } let field = object[keyPath: member] - return type(of: field).read(field: field, for: rawObject) as! V? + return type(of: field).read(field: field, for: rawObject, bypassThreadCheck: false) as! V? } /** @@ -412,7 +412,7 @@ extension ObjectPublisher where O: CoreStoreObject { return nil } let field = object[keyPath: member] - return type(of: field).read(field: field, for: rawObject) as! V? + return type(of: field).read(field: field, for: rawObject, bypassThreadCheck: false) as! V? } /** @@ -428,7 +428,7 @@ extension ObjectPublisher where O: CoreStoreObject { return nil } let field = object[keyPath: member] - guard let value = type(of: field).read(field: field, for: rawObject) as! V? else { + guard let value = type(of: field).read(field: field, for: rawObject, bypassThreadCheck: false) as! V? else { return nil } From 231e138ab05776944f0aaf601ff9e7155bcdfa8b Mon Sep 17 00:00:00 2001 From: John Estropia Date: Fri, 21 Feb 2020 13:51:17 +0900 Subject: [PATCH 16/20] performant access of relationship objectIDs for snapshots --- Sources/DynamicObject.swift | 28 ++++++++++++++++++++---- Sources/FIeldRelationshipType.swift | 22 ++++++------------- Sources/Field.Coded.swift | 25 +++++++++++++--------- Sources/Field.Relationship.swift | 32 +++++++++++++++------------- Sources/Field.Stored.swift | 25 +++++++++++++--------- Sources/Field.Virtual.swift | 25 +++++++++++++--------- Sources/FieldAttributeProtocol.swift | 2 ++ Sources/FieldProtocol.swift | 4 +++- Sources/ObjectProxy.swift | 18 +++------------- Sources/ObjectPublisher.swift | 29 +++++++++++++++++-------- 10 files changed, 121 insertions(+), 89 deletions(-) diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index 3f09383..21060df 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -172,13 +172,20 @@ extension CoreStoreObject { switch child.value { case let property as FieldAttributeProtocol: + Internals.assert( + object.rawObject?.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(type(of: property).dynamicObjectType))'s value outside it's designated queue." + ) attributes[property.keyPath] = type(of: property).read( field: property, - for: object.rawObject!, - bypassThreadCheck: false + for: object.rawObject! ) case let property as FieldRelationshipProtocol: + Internals.assert( + object.rawObject?.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(type(of: property).dynamicObjectType))'s value outside it's designated queue." + ) attributes[property.keyPath] = type(of: property).valueForSnapshot( field: property, for: object.rawObject! @@ -219,10 +226,23 @@ extension CoreStoreObject { switch property { case let property as FieldAttributeProtocol: + Internals.assert( + object.rawObject?.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(type(of: property).dynamicObjectType))'s value outside it's designated queue." + ) values[property.keyPath] = type(of: property).read( field: property, - for: rawObject, - bypassThreadCheck: false + for: rawObject + ) + + case let property as FieldRelationshipProtocol: + Internals.assert( + object.rawObject?.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(type(of: property).dynamicObjectType))'s value outside it's designated queue." + ) + values[property.keyPath] = type(of: property).valueForSnapshot( + field: property, + for: object.rawObject! ) default: diff --git a/Sources/FIeldRelationshipType.swift b/Sources/FIeldRelationshipType.swift index b19e751..9272af6 100644 --- a/Sources/FIeldRelationshipType.swift +++ b/Sources/FIeldRelationshipType.swift @@ -47,7 +47,7 @@ public protocol FieldRelationshipType { static func cs_toSnapshotType(from value: PublishedType) -> SnapshotValueType - static func cs_valueForSnapshot(from value: NativeValueType?) -> SnapshotValueType + static func cs_valueForSnapshot(from objectIDs: [DestinationObjectType.ObjectID]) -> SnapshotValueType } public protocol FieldRelationshipToOneType: FieldRelationshipType {} @@ -88,9 +88,9 @@ extension Optional: FieldRelationshipType, FieldRelationshipToOneType where Wrap return value?.objectID() } - public static func cs_valueForSnapshot(from value: NativeValueType?) -> SnapshotValueType { + public static func cs_valueForSnapshot(from objectIDs: [DestinationObjectType.ObjectID]) -> SnapshotValueType { - return value?.objectID + return objectIDs.first } } @@ -129,13 +129,9 @@ extension Array: FieldRelationshipType, FieldRelationshipToManyType, FieldRelati return value.map({ $0.objectID() }) } - public static func cs_valueForSnapshot(from value: NativeValueType?) -> SnapshotValueType { + public static func cs_valueForSnapshot(from objectIDs: [DestinationObjectType.ObjectID]) -> SnapshotValueType { - guard let value = value else { - - return [] - } - return value.map({ ($0 as! NSManagedObject).objectID }) + return objectIDs } } @@ -173,12 +169,8 @@ extension Set: FieldRelationshipType, FieldRelationshipToManyType, FieldRelation return SnapshotValueType(value.map({ $0.objectID() })) } - public static func cs_valueForSnapshot(from value: NativeValueType?) -> SnapshotValueType { + public static func cs_valueForSnapshot(from objectIDs: [DestinationObjectType.ObjectID]) -> SnapshotValueType { - guard let value = value else { - - return [] - } - return .init(value.map({ ($0 as! NSManagedObject).objectID })) + return .init(objectIDs) } } diff --git a/Sources/Field.Coded.swift b/Sources/Field.Coded.swift index d94e813..b02db6b 100644 --- a/Sources/Field.Coded.swift +++ b/Sources/Field.Coded.swift @@ -161,7 +161,11 @@ extension FieldContainer { instance.rawObject != nil, "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." ) - return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, bypassThreadCheck: false) as! V + Internals.assert( + instance.rawObject?.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V } set { @@ -169,6 +173,10 @@ extension FieldContainer { instance.rawObject != nil, "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." ) + Internals.assert( + instance.rawObject?.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue) } } @@ -200,12 +208,13 @@ extension FieldContainer { // MARK: FieldProtocol - internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject, bypassThreadCheck: Bool) -> Any? { + internal static var dynamicObjectType: CoreStoreObject.Type { + + return ObjectType.self + } + + internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { - Internals.assert( - bypassThreadCheck || rawObject.isRunningInAllowedQueue() == true, - "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." - ) let field = field as! Self if let customGetter = field.customGetter { @@ -227,10 +236,6 @@ extension FieldContainer { internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) { - Internals.assert( - rawObject.isRunningInAllowedQueue() == true, - "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." - ) Internals.assert( rawObject.isEditableInContext() == true, "Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction." diff --git a/Sources/Field.Relationship.swift b/Sources/Field.Relationship.swift index f590327..486b207 100644 --- a/Sources/Field.Relationship.swift +++ b/Sources/Field.Relationship.swift @@ -99,7 +99,11 @@ extension FieldContainer { instance.rawObject != nil, "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." ) - return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, bypassThreadCheck: false) as! V + Internals.assert( + instance.rawObject?.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V } set { @@ -107,6 +111,10 @@ extension FieldContainer { instance.rawObject != nil, "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." ) + Internals.assert( + instance.rawObject?.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue) } } @@ -138,12 +146,13 @@ extension FieldContainer { // MARK: FieldProtocol - internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject, bypassThreadCheck: Bool) -> Any? { + internal static var dynamicObjectType: CoreStoreObject.Type { + + return ObjectType.self + } + + internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { - Internals.assert( - bypassThreadCheck || rawObject.isRunningInAllowedQueue() == true, - "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." - ) let field = field as! Self let keyPath = field.keyPath return V.cs_toReturnType( @@ -152,11 +161,7 @@ extension FieldContainer { } internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) { - - Internals.assert( - rawObject.isRunningInAllowedQueue() == true, - "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." - ) + Internals.assert( rawObject.isEditableInContext() == true, "Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction." @@ -182,10 +187,7 @@ extension FieldContainer { "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." ) let field = field as! Self - let keyPath = field.keyPath - return V.cs_valueForSnapshot( - from: rawObject.value(forKey: keyPath) as! V.NativeValueType? - ) + return V.cs_valueForSnapshot(from: rawObject.objectIDs(forRelationshipNamed: field.keyPath)) } diff --git a/Sources/Field.Stored.swift b/Sources/Field.Stored.swift index 29f5742..10e9663 100644 --- a/Sources/Field.Stored.swift +++ b/Sources/Field.Stored.swift @@ -128,7 +128,11 @@ extension FieldContainer { instance.rawObject != nil, "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." ) - return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, bypassThreadCheck: false) as! V + Internals.assert( + instance.rawObject?.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V } set { @@ -136,6 +140,10 @@ extension FieldContainer { instance.rawObject != nil, "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." ) + Internals.assert( + instance.rawObject?.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue) } } @@ -167,12 +175,13 @@ extension FieldContainer { // MARK: FieldProtocol - internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject, bypassThreadCheck: Bool) -> Any? { + internal static var dynamicObjectType: CoreStoreObject.Type { + + return ObjectType.self + } + + internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { - Internals.assert( - bypassThreadCheck || rawObject.isRunningInAllowedQueue() == true, - "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." - ) let field = field as! Self if let customGetter = field.customGetter { @@ -194,10 +203,6 @@ extension FieldContainer { internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) { - Internals.assert( - rawObject.isRunningInAllowedQueue() == true, - "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." - ) Internals.assert( rawObject.isEditableInContext() == true, "Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction." diff --git a/Sources/Field.Virtual.swift b/Sources/Field.Virtual.swift index 815e18d..1437144 100644 --- a/Sources/Field.Virtual.swift +++ b/Sources/Field.Virtual.swift @@ -139,7 +139,11 @@ extension FieldContainer { instance.rawObject != nil, "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." ) - return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, bypassThreadCheck: false) as! V + Internals.assert( + instance.rawObject?.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) + return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V } set { @@ -147,6 +151,10 @@ extension FieldContainer { instance.rawObject != nil, "Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types." ) + Internals.assert( + instance.rawObject?.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue) } } @@ -178,12 +186,13 @@ extension FieldContainer { // MARK: FieldProtocol - internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject, bypassThreadCheck: Bool) -> Any? { + internal static var dynamicObjectType: CoreStoreObject.Type { + + return ObjectType.self + } + + internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? { - Internals.assert( - bypassThreadCheck || rawObject.isRunningInAllowedQueue() == true, - "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." - ) let field = field as! Self if let customGetter = field.customGetter { @@ -205,10 +214,6 @@ extension FieldContainer { internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) { - Internals.assert( - rawObject.isRunningInAllowedQueue() == true, - "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." - ) Internals.assert( rawObject.isEditableInContext() == true, "Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction." diff --git a/Sources/FieldAttributeProtocol.swift b/Sources/FieldAttributeProtocol.swift index 367434f..a2df2bd 100644 --- a/Sources/FieldAttributeProtocol.swift +++ b/Sources/FieldAttributeProtocol.swift @@ -43,6 +43,8 @@ internal protocol FieldAttributeProtocol: FieldProtocol { defaultValue: Any? ) + static var dynamicObjectType: CoreStoreObject.Type { get } + var entityDescriptionValues: () -> EntityDescriptionValues { get } var getter: CoreStoreManagedObject.CustomGetter? { get } var setter: CoreStoreManagedObject.CustomSetter? { get } diff --git a/Sources/FieldProtocol.swift b/Sources/FieldProtocol.swift index 77b1981..83d3aff 100644 --- a/Sources/FieldProtocol.swift +++ b/Sources/FieldProtocol.swift @@ -30,7 +30,9 @@ import CoreData // MARK: - FieldProtocol internal protocol FieldProtocol: PropertyProtocol { + + static var dynamicObjectType: CoreStoreObject.Type { get } - static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject, bypassThreadCheck: Bool) -> Any? + static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) } diff --git a/Sources/ObjectProxy.swift b/Sources/ObjectProxy.swift index 89e233e..cd2f61e 100644 --- a/Sources/ObjectProxy.swift +++ b/Sources/ObjectProxy.swift @@ -133,11 +133,7 @@ public struct ObjectProxy { let keyPathString = field.keyPath self.getValue = { - return type(of: field).read( - field: field, - for: rawObject, - bypassThreadCheck: true // May be called from NSError logs - ) as! V + return type(of: field).read(field: field, for: rawObject) as! V } self.setValue = { @@ -163,11 +159,7 @@ public struct ObjectProxy { let keyPathString = field.keyPath self.getValue = { - return type(of: field).read( - field: field, - for: rawObject, - bypassThreadCheck: true // May be called from NSError logs - ) as! V + return type(of: field).read(field: field, for: rawObject) as! V } self.setValue = { @@ -200,11 +192,7 @@ public struct ObjectProxy { let keyPathString = field.keyPath self.getValue = { - return type(of: field).read( - field: field, - for: rawObject, - bypassThreadCheck: true // May be called from NSError logs - ) as! V + return type(of: field).read(field: field, for: rawObject) as! V } self.setValue = { diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index 23893c1..45a3ad1 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -379,8 +379,12 @@ extension ObjectPublisher where O: CoreStoreObject { return nil } + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) let field = object[keyPath: member] - return type(of: field).read(field: field, for: rawObject, bypassThreadCheck: false) as! V? + return type(of: field).read(field: field, for: rawObject) as! V? } /** @@ -395,8 +399,12 @@ extension ObjectPublisher where O: CoreStoreObject { return nil } + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) let field = object[keyPath: member] - return type(of: field).read(field: field, for: rawObject, bypassThreadCheck: false) as! V? + return type(of: field).read(field: field, for: rawObject) as! V? } /** @@ -411,8 +419,12 @@ extension ObjectPublisher where O: CoreStoreObject { return nil } + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) let field = object[keyPath: member] - return type(of: field).read(field: field, for: rawObject, bypassThreadCheck: false) as! V? + return type(of: field).read(field: field, for: rawObject) as! V? } /** @@ -427,13 +439,12 @@ extension ObjectPublisher where O: CoreStoreObject { return nil } + Internals.assert( + rawObject.isRunningInAllowedQueue() == true, + "Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue." + ) let field = object[keyPath: member] - guard let value = type(of: field).read(field: field, for: rawObject, bypassThreadCheck: false) as! V? else { - - return nil - } - let nativeValue = V.cs_toNativeType(from: value) - let snapshotValue = V.cs_valueForSnapshot(from: nativeValue) + let snapshotValue = V.cs_valueForSnapshot(from: rawObject.objectIDs(forRelationshipNamed: field.keyPath)) return V.cs_toPublishedType(from: snapshotValue, in: self.context) } From 7f928dc684d4c5f969153770f8c631de660e858c Mon Sep 17 00:00:00 2001 From: John Estropia Date: Thu, 19 Mar 2020 14:12:09 +0900 Subject: [PATCH 17/20] docs --- Sources/Field.Virtual.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Field.Virtual.swift b/Sources/Field.Virtual.swift index 1437144..2f64008 100644 --- a/Sources/Field.Virtual.swift +++ b/Sources/Field.Virtual.swift @@ -100,7 +100,7 @@ extension FieldContainer { /** Overload for compiler error message only */ - @available(*, unavailable, message: "Field.Computed properties are not allowed to have initial values, including `nil`.") + @available(*, unavailable, message: "Field.Virtual properties are not allowed to have initial values, including `nil`.") public init( wrappedValue initial: @autoclosure @escaping () -> V, _ keyPath: KeyPathString, From 0d9299f900c8e6da4d61d5b5f6b61bf6a1d3f596 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 25 Mar 2020 14:21:49 +0900 Subject: [PATCH 18/20] WIP: docs --- Sources/Field.Relationship.swift | 1 - Sources/Field.Stored.swift | 7 ++----- Sources/Field.swift | 32 +++++++++++++++++++++++++------- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/Sources/Field.Relationship.swift b/Sources/Field.Relationship.swift index 486b207..c99b07c 100644 --- a/Sources/Field.Relationship.swift +++ b/Sources/Field.Relationship.swift @@ -34,7 +34,6 @@ extension FieldContainer { // MARK: - Relationship @propertyWrapper -// @dynamicMemberLookup public struct Relationship: RelationshipKeyPathStringConvertible, FieldRelationshipProtocol { /** diff --git a/Sources/Field.Stored.swift b/Sources/Field.Stored.swift index 10e9663..99f73c3 100644 --- a/Sources/Field.Stored.swift +++ b/Sources/Field.Stored.swift @@ -40,11 +40,8 @@ extension FieldContainer { @Field.Stored("species") var species = "" - @Field.Virtual("pluralName", customGetter: Animal.pluralName(_:)) - var pluralName: String = "" - - @Field.Coded("color", coder: FieldCoders.Plist.self) - var color: UIColor? + @Field.Stored("nickname") + var nickname: String? } ``` - Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Stored(...) var` syntax will be ignored. diff --git a/Sources/Field.swift b/Sources/Field.swift index 7ed8018..28f439e 100644 --- a/Sources/Field.swift +++ b/Sources/Field.swift @@ -32,9 +32,9 @@ import Foundation extension DynamicObject where Self: CoreStoreObject { /** - The containing type for value propertiess. `Field` properties support any type that conforms to `ImportableAttributeType`. + The containing type for value propertiess. ``` - class Animal: CoreStoreObject { + class Pet: CoreStoreObject { @Field.Stored("species") var species = "" @@ -42,10 +42,19 @@ extension DynamicObject where Self: CoreStoreObject { var nickname: String? @Field.Coded("color", coder: FieldCoders.Plist.self) - var color: UIColor? + var eyeColor: UIColor? + + @Field.Relationship("owner", inverse: \.$pets) + var owner: Person? + + @Field.Relationship("children") + var children: Array + + @Field.Relationship("parents", inverse: \.$children) + var parents: Set } ``` - - Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.*(...) var` syntax will be ignored. + - Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.*(...) var` syntax will result in undefined behavior. */ public typealias Field = FieldContainer } @@ -54,9 +63,9 @@ extension DynamicObject where Self: CoreStoreObject { // MARK: - FieldContainer /** - The containing type for value properties. Use the `DynamicObject.Field` typealias instead for shorter syntax. + The containing type for value properties. Use the `Field` typealias instead for shorter syntax. ``` - class Animal: CoreStoreObject { + class Pet: CoreStoreObject { @Field.Stored("species") var species = "" @@ -64,7 +73,16 @@ extension DynamicObject where Self: CoreStoreObject { var nickname: String? @Field.Coded("color", coder: FieldCoders.Plist.self) - var color: UIColor? + var eyeColor: UIColor? + + @Field.Relationship("owner", inverse: \.$pets) + var owner: Person? + + @Field.Relationship("children") + var children: Array + + @Field.Relationship("parents", inverse: \.$children) + var parents: Set } ``` */ From 97f2a5312414d9c73aca7f8850aa5ed60a29fb7d Mon Sep 17 00:00:00 2001 From: John Estropia Date: Thu, 26 Mar 2020 01:57:32 +0900 Subject: [PATCH 19/20] AppleDocs for Field source files --- README.md | 2 +- Sources/FIeldRelationshipType.swift | 56 ++++++ Sources/Field.Coded.swift | 149 ++++++++-------- Sources/Field.Relationship.swift | 166 ++++++++++++++++++ Sources/Field.Stored.swift | 50 ++---- Sources/Field.Virtual.swift | 76 +++++--- Sources/Field.swift | 2 + Sources/FieldCoderType.swift | 20 +++ .../FieldCoders.DefaultNSSecureCoding.swift | 8 + Sources/FieldCoders.Json.swift | 4 + Sources/FieldCoders.NSCoding.swift | 3 + Sources/FieldCoders.Plist.swift | 4 + Sources/FieldOptionalType.swift | 7 +- Sources/FieldStorableType.swift | 9 +- 14 files changed, 408 insertions(+), 148 deletions(-) diff --git a/README.md b/README.md index 4a2d255..b028477 100644 --- a/README.md +++ b/README.md @@ -1888,7 +1888,7 @@ From the **File** - **Swift Packages** - **Add Package Dependency…** menu, sea ``` CoreStore ``` -where `JohnEstropia` is the *Owner*. Then add to your project. +where `JohnEstropia` is the *Owner* (forks may appear as well). Then add to your project. ### Objective-C support diff --git a/Sources/FIeldRelationshipType.swift b/Sources/FIeldRelationshipType.swift index 9272af6..8c09d85 100644 --- a/Sources/FIeldRelationshipType.swift +++ b/Sources/FIeldRelationshipType.swift @@ -29,37 +29,84 @@ import Foundation // MARK: - FieldRelationshipType +/** + Values to be used for `Field.Relationship` properties. + */ public protocol FieldRelationshipType { + /** + The destination object's type + */ associatedtype DestinationObjectType: CoreStoreObject + /** + The Objective-C native type synthesized by Core Data + */ associatedtype NativeValueType: AnyObject + /** + The corresponding value for this field returned from `ObjectSnapshot` properties. + */ associatedtype SnapshotValueType + /** + The corresponding value for this field returned from `ObjectPublisher` properties. + */ associatedtype PublishedType + /** + Used internally by CoreStore. Do not call directly. + */ static func cs_toReturnType(from value: NativeValueType?) -> Self + /** + Used internally by CoreStore. Do not call directly. + */ static func cs_toPublishedType(from value: SnapshotValueType, in context: NSManagedObjectContext) -> PublishedType + /** + Used internally by CoreStore. Do not call directly. + */ static func cs_toNativeType(from value: Self) -> NativeValueType? + /** + Used internally by CoreStore. Do not call directly. + */ static func cs_toSnapshotType(from value: PublishedType) -> SnapshotValueType + /** + Used internally by CoreStore. Do not call directly. + */ static func cs_valueForSnapshot(from objectIDs: [DestinationObjectType.ObjectID]) -> SnapshotValueType } + +// MARK: - FieldRelationshipToOneType: FieldRelationshipType + public protocol FieldRelationshipToOneType: FieldRelationshipType {} +// MARK: - FieldRelationshipToManyType: FieldRelationshipType where Self: Sequence + public protocol FieldRelationshipToManyType: FieldRelationshipType where Self: Sequence {} + + +// MARK: - FieldRelationshipToManyOrderedType: FieldRelationshipToManyType + public protocol FieldRelationshipToManyOrderedType: FieldRelationshipToManyType {} + + +// MARK: - FieldRelationshipToManyUnorderedType: FieldRelationshipToManyType + public protocol FieldRelationshipToManyUnorderedType: FieldRelationshipToManyType {} +// MARK: - Optional: FieldRelationshipType, FieldRelationshipToOneType where Wrapped: CoreStoreObject + extension Optional: FieldRelationshipType, FieldRelationshipToOneType where Wrapped: CoreStoreObject { + // MARK: FieldRelationshipType + public typealias DestinationObjectType = Wrapped public typealias NativeValueType = NSManagedObject @@ -95,8 +142,12 @@ extension Optional: FieldRelationshipType, FieldRelationshipToOneType where Wrap } +// MARK: - Array: FieldRelationshipType, FieldRelationshipToManyType, FieldRelationshipToManyOrderedType where Element: CoreStoreObject + extension Array: FieldRelationshipType, FieldRelationshipToManyType, FieldRelationshipToManyOrderedType where Element: CoreStoreObject { + // MARK: FieldRelationshipType + public typealias DestinationObjectType = Element public typealias NativeValueType = NSOrderedSet @@ -135,8 +186,13 @@ extension Array: FieldRelationshipType, FieldRelationshipToManyType, FieldRelati } } + +// MARK: - Set: FieldRelationshipType, FieldRelationshipToManyType, FieldRelationshipToManyUnorderedType where Element: CoreStoreObject + extension Set: FieldRelationshipType, FieldRelationshipToManyType, FieldRelationshipToManyUnorderedType where Element: CoreStoreObject { + // MARK: FieldRelationshipType + public typealias DestinationObjectType = Element public typealias NativeValueType = NSSet diff --git a/Sources/Field.Coded.swift b/Sources/Field.Coded.swift index b02db6b..67a3671 100644 --- a/Sources/Field.Coded.swift +++ b/Sources/Field.Coded.swift @@ -34,17 +34,12 @@ extension FieldContainer { // MARK: - Coded /** - The containing type for stored property values. Any type that conforms to `FieldStorableType` are supported. + The containing type for stored property values. Any type supported by the specified encoder/decoder are allowed. ``` class Animal: CoreStoreObject { - @Field.Stored("species") - var species = "" - @Field.Virtual("pluralName", customGetter: Animal.pluralName(_:)) - var pluralName: String = "" - - @Field.Coded("color", coder: FieldCoders.Plist.self) - var color: UIColor? + @Field.Coded("eyeColor", coder: FieldCoders.NSCoding.self) + var eyeColor: UIColor = .black } ``` - Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Stored(...) var` syntax will be ignored. @@ -56,34 +51,18 @@ extension FieldContainer { Initializes the metadata for the property. ``` class Person: CoreStoreObject { - @Field.Stored("title") - var title: String = "Mr." - @Field.Stored("name") - var name: String = "" - - @Field.Virtual("displayName", customGetter: Person.getName(_:)) - var displayName: String = "" - - private static func getName(_ object: ObjectProxy) -> String { - let cachedDisplayName = object.primitiveValue(for: \.$displayName) - if !cachedDisplayName.isEmpty { - return cachedDisplayName - } - let title = object.value(for: \.$title) - let name = object.value(for: \.$name) - let displayName = "\(title) \(name)" - object.setPrimitiveValue(displayName, for: { $0.displayName }) - return displayName - } + @Field.Coded("eyeColor", coder: FieldCoders.NSCoding.self) + var eyeColor: UIColor = .black } ``` - - parameter initial: the initial value for the property when the object is first create + - parameter initial: the initial value for the property when the object is first created. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. + - parameter coder: The `FieldCoderType` to be used for encoding and decoding the value + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -110,6 +89,24 @@ extension FieldContainer { ) } + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + + @Field.Coded("eyeColor", coder: FieldCoders.NSCoding.self) + var eyeColor: UIColor = .black + } + ``` + - parameter initial: the initial value for the property when the object is first created. + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter coder: The closures to be used for encoding and decoding the value + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ public init( wrappedValue initial: @autoclosure @escaping () -> V, _ keyPath: KeyPathString, @@ -376,34 +373,18 @@ extension FieldContainer.Coded where V: FieldOptionalType { Initializes the metadata for the property. ``` class Person: CoreStoreObject { - @Field.Stored("title") - var title: String = "Mr." - @Field.Stored("name") - var name: String = "" - - @Field.Virtual("displayName", customGetter: Person.getName(_:)) - var displayName: String = "" - - private static func getName(_ object: ObjectProxy) -> String { - let cachedDisplayName = object.primitiveValue(for: \.$displayName) - if !cachedDisplayName.isEmpty { - return cachedDisplayName - } - let title = object.value(for: \.$title) - let name = object.value(for: \.$name) - let displayName = "\(title) \(name)" - object.setPrimitiveValue(displayName, for: { $0.displayName }) - return displayName - } + @Field.Coded("eyeColor", coder: FieldCoders.NSCoding.self) + var eyeColor: UIColor? = nil } ``` - - parameter initial: the initial value for the property when the object is first create + - parameter initial: the initial value for the property when the object is first created. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. + - parameter coder: The `FieldCoderType` to be used for encoding and decoding the value + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -430,6 +411,24 @@ extension FieldContainer.Coded where V: FieldOptionalType { ) } + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + + @Field.Coded("eyeColor", coder: FieldCoders.NSCoding.self) + var eyeColor: UIColor? = nil + } + ``` + - parameter initial: the initial value for the property when the object is first created. + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter coder: The closures to be used for encoding and decoding the value + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ public init( wrappedValue initial: @autoclosure @escaping () -> V = nil, _ keyPath: KeyPathString, @@ -461,37 +460,20 @@ extension FieldContainer.Coded where V: FieldOptionalType { extension FieldContainer.Coded where V: DefaultNSSecureCodable { /** - Initializes the metadata for the property. + Initializes the metadata for the property. This overload is for types supported by Core Data's default NSSecureCodable implementation: `NSArray`, `NSDictionary`, `NSSet`, `NSString`, `NSNumber`, `NSDate`, `NSData`, `NSURL`, `NSUUID`, and `NSNull`. ``` class Person: CoreStoreObject { - @Field.Stored("title") - var title: String = "Mr." - @Field.Stored("name") - var name: String = "" - - @Field.Virtual("displayName", customGetter: Person.getName(_:)) - var displayName: String = "" - - private static func getName(_ object: ObjectProxy) -> String { - let cachedDisplayName = object.primitiveValue(for: \.$displayName) - if !cachedDisplayName.isEmpty { - return cachedDisplayName - } - let title = object.value(for: \.$title) - let name = object.value(for: \.$name) - let displayName = "\(title) \(name)" - object.setPrimitiveValue(displayName, for: { $0.displayName }) - return displayName - } + @Field.Coded("customInfo") + var customInfo: NSDictionary = [:] } ``` - - parameter initial: the initial value for the property when the object is first create + - parameter initial: the initial value for the property when the object is first created. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -523,6 +505,23 @@ extension FieldContainer.Coded where V: DefaultNSSecureCodable { extension FieldContainer.Coded where V: FieldOptionalType, V.Wrapped: DefaultNSSecureCodable { + /** + Initializes the metadata for the property. This overload is for types supported by Core Data's default NSSecureCodable implementation: `NSArray`, `NSDictionary`, `NSSet`, `NSString`, `NSNumber`, `NSDate`, `NSData`, `NSURL`, `NSUUID`, and `NSNull`. + ``` + class Person: CoreStoreObject { + + @Field.Coded("customInfo") + var customInfo: NSDictionary? = nil + } + ``` + - parameter initial: the initial value for the property when the object is first created. + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ public init( wrappedValue initial: @autoclosure @escaping () -> V = nil, _ keyPath: KeyPathString, diff --git a/Sources/Field.Relationship.swift b/Sources/Field.Relationship.swift index c99b07c..b79cf62 100644 --- a/Sources/Field.Relationship.swift +++ b/Sources/Field.Relationship.swift @@ -33,6 +33,23 @@ extension FieldContainer { // MARK: - Relationship + /** + The containing type for relationships. Any `CoreStoreObject` subclass can be a destination type. Inverse relationships should be declared from the destination type as well, using the `inverse:` argument for the relationship. + ``` + class Dog: CoreStoreObject { + + @Field.Relationship("master") + var master: Person? + } + + class Person: CoreStoreObject { + + @Field.Relationship("pets", inverse: \.$master) + var pets: Set + } + ``` + - Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Relationship(...) var` syntax will be ignored. + */ @propertyWrapper public struct Relationship: RelationshipKeyPathStringConvertible, FieldRelationshipProtocol { @@ -264,8 +281,32 @@ extension FieldContainer { } } + +// MARK: - FieldContainer.Relationship where V: FieldRelationshipToOneType + extension FieldContainer.Relationship where V: FieldRelationshipToOneType { + /** + Initializes the metadata for the relationship. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. Make sure to declare this relationship's inverse relationship on its destination object. Due to Swift's compiler limitation, only one of the relationship and its inverse can declare an `inverse:` argument. + ``` + class Dog: CoreStoreObject { + + @Field.Relationship("master") + var master: Person? + } + + class Person: CoreStoreObject { + + @Field.Relationship("pets", inverse: \.$master) + var pets: Set + } + ``` + - parameter keyPath: the permanent name for this relationship. + - parameter deleteRule: defines what happens to relationship when an object is deleted. Valid values are `.nullify`, `.cascade`, and `.delete`. Defaults to `.nullify`. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ public init( _ keyPath: KeyPathString, deleteRule: DeleteRule = .nullify, @@ -288,6 +329,28 @@ extension FieldContainer.Relationship where V: FieldRelationshipToOneType { ) } + /** + Initializes the metadata for the relationship. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. Make sure to declare this relationship's inverse relationship on its destination object. Due to Swift's compiler limitation, only one of the relationship and its inverse can declare an `inverse:` argument. + ``` + class Dog: CoreStoreObject { + + @Field.Relationship("master") + var master: Person? + } + + class Person: CoreStoreObject { + + @Field.Relationship("pets", inverse: \.$master) + var pets: Set + } + ``` + - parameter keyPath: the permanent name for this relationship. + - parameter inverse: the inverse relationship that is declared for the destination object. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. + - parameter deleteRule: defines what happens to relationship when an object is deleted. Valid values are `.nullify`, `.cascade`, and `.delete`. Defaults to `.nullify`. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ public init( _ keyPath: KeyPathString, inverse: KeyPath.Relationship>, @@ -312,10 +375,39 @@ extension FieldContainer.Relationship where V: FieldRelationshipToOneType { } } + +// MARK: - FieldContainer.Relationship: ToManyRelationshipKeyPathStringConvertible where V: FieldRelationshipToManyType + extension FieldContainer.Relationship: ToManyRelationshipKeyPathStringConvertible where V: FieldRelationshipToManyType {} + +// MARK: - FieldContainer.Relationship where V: FieldRelationshipToManyOrderedType + extension FieldContainer.Relationship where V: FieldRelationshipToManyOrderedType { + /** + Initializes the metadata for the relationship. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. Make sure to declare this relationship's inverse relationship on its destination object. Due to Swift's compiler limitation, only one of the relationship and its inverse can declare an `inverse:` argument. + ``` + class Dog: CoreStoreObject { + + @Field.Relationship("master") + var master: Person? + } + + class Person: CoreStoreObject { + + @Field.Relationship("pets", inverse: \.$master) + var pets: Array + } + ``` + - parameter keyPath: the permanent name for this relationship. + - parameter minCount: the minimum number of objects in this relationship UNLESS THE RELATIONSHIP IS EMPTY. This means there might be zero objects in the relationship, which might be less than `minCount`. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error. + - parameter maxCount: the maximum number of objects in this relationship. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error. + - parameter deleteRule: defines what happens to relationship when an object is deleted. Valid values are `.nullify`, `.cascade`, and `.delete`. Defaults to `.nullify`. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ public init( _ keyPath: KeyPathString, minCount: Int = 0, @@ -340,6 +432,30 @@ extension FieldContainer.Relationship where V: FieldRelationshipToManyOrderedTyp ) } + /** + Initializes the metadata for the relationship. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. Make sure to declare this relationship's inverse relationship on its destination object. Due to Swift's compiler limitation, only one of the relationship and its inverse can declare an `inverse:` argument. + ``` + class Dog: CoreStoreObject { + + @Field.Relationship("master") + var master: Person? + } + + class Person: CoreStoreObject { + + @Field.Relationship("pets", inverse: \.$master) + var pets: Array + } + ``` + - parameter keyPath: the permanent name for this relationship. + - parameter minCount: the minimum number of objects in this relationship UNLESS THE RELATIONSHIP IS EMPTY. This means there might be zero objects in the relationship, which might be less than `minCount`. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error. + - parameter maxCount: the maximum number of objects in this relationship. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error. + - parameter inverse: the inverse relationship that is declared for the destination object. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. + - parameter deleteRule: defines what happens to relationship when an object is deleted. Valid values are `.nullify`, `.cascade`, and `.delete`. Defaults to `.nullify`. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ public init( _ keyPath: KeyPathString, minCount: Int = 0, @@ -366,8 +482,34 @@ extension FieldContainer.Relationship where V: FieldRelationshipToManyOrderedTyp } } + +// MARK: - FieldContainer.Relationship where V: FieldRelationshipToManyUnorderedType + extension FieldContainer.Relationship where V: FieldRelationshipToManyUnorderedType { + /** + Initializes the metadata for the relationship. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. Make sure to declare this relationship's inverse relationship on its destination object. Due to Swift's compiler limitation, only one of the relationship and its inverse can declare an `inverse:` argument. + ``` + class Dog: CoreStoreObject { + + @Field.Relationship("master") + var master: Person? + } + + class Person: CoreStoreObject { + + @Field.Relationship("pets", inverse: \.$master) + var pets: Set + } + ``` + - parameter keyPath: the permanent name for this relationship. + - parameter minCount: the minimum number of objects in this relationship UNLESS THE RELATIONSHIP IS EMPTY. This means there might be zero objects in the relationship, which might be less than `minCount`. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error. + - parameter maxCount: the maximum number of objects in this relationship. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error. + - parameter deleteRule: defines what happens to relationship when an object is deleted. Valid values are `.nullify`, `.cascade`, and `.delete`. Defaults to `.nullify`. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ public init( _ keyPath: KeyPathString, minCount: Int = 0, @@ -392,6 +534,30 @@ extension FieldContainer.Relationship where V: FieldRelationshipToManyUnorderedT ) } + /** + Initializes the metadata for the relationship. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. Make sure to declare this relationship's inverse relationship on its destination object. Due to Swift's compiler limitation, only one of the relationship and its inverse can declare an `inverse:` argument. + ``` + class Dog: CoreStoreObject { + + @Field.Relationship("master") + var master: Person? + } + + class Person: CoreStoreObject { + + @Field.Relationship("pets", inverse: \.$master) + var pets: Set + } + ``` + - parameter keyPath: the permanent name for this relationship. + - parameter minCount: the minimum number of objects in this relationship UNLESS THE RELATIONSHIP IS EMPTY. This means there might be zero objects in the relationship, which might be less than `minCount`. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error. + - parameter maxCount: the maximum number of objects in this relationship. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error. + - parameter inverse: the inverse relationship that is declared for the destination object. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. + - parameter deleteRule: defines what happens to relationship when an object is deleted. Valid values are `.nullify`, `.cascade`, and `.delete`. Defaults to `.nullify`. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ public init( _ keyPath: KeyPathString, minCount: Int = 0, diff --git a/Sources/Field.Stored.swift b/Sources/Field.Stored.swift index 99f73c3..3a60c27 100644 --- a/Sources/Field.Stored.swift +++ b/Sources/Field.Stored.swift @@ -37,8 +37,9 @@ extension FieldContainer { The containing type for stored property values. Any type that conforms to `FieldStorableType` are supported. ``` class Animal: CoreStoreObject { - @Field.Stored("species") - var species = "" + + @Field.Stored("title") + var title: String = "Mr." @Field.Stored("nickname") var nickname: String? @@ -53,27 +54,17 @@ extension FieldContainer { Initializes the metadata for the property. ``` class Person: CoreStoreObject { + @Field.Stored("title") var title: String = "Mr." - - @Field.Stored("name") - var name: String = "" - - @Field.Virtual( - "displayName", - customGetter: { (object, field) in - return "\(object.$title.value) \(object.$name.value)" - } - ) - var displayName: String = "" } ``` - - parameter initial: the initial value for the property when the object is first create + - parameter initial: the initial value for the property when the object is first created. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -320,34 +311,17 @@ extension FieldContainer.Stored where V: FieldOptionalType { Initializes the metadata for the property. ``` class Person: CoreStoreObject { - @Field.Stored("title") - var title: String = "Mr." - @Field.Stored("name") - var name: String = "" - - @Field.Virtual("displayName", customGetter: Person.getName(_:)) - var displayName: String = "" - - private static func getName(_ object: ObjectProxy) -> String { - let cachedDisplayName = object.primitiveValue(for: \.$displayName) - if !cachedDisplayName.isEmpty { - return cachedDisplayName - } - let title = object.value(for: \.$title) - let name = object.value(for: \.$name) - let displayName = "\(title) \(name)" - object.setPrimitiveValue(displayName, for: { $0.displayName }) - return displayName - } + @Field.Stored("nickname") + var nickname: String? } ``` - - parameter initial: the initial value for the property when the object is first create + - parameter initial: the initial value for the property when the object is first created. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( diff --git a/Sources/Field.Virtual.swift b/Sources/Field.Virtual.swift index 2f64008..eb77f11 100644 --- a/Sources/Field.Virtual.swift +++ b/Sources/Field.Virtual.swift @@ -34,17 +34,20 @@ extension FieldContainer { // MARK: - Virtual /** - The containing type for computed property values. Any type that conforms to `FieldStorableType` are supported. + The containing type for computed property values. Because this value is not persisted to the backing store, any type is supported. `Field.Virtual` properties are not allowed to have initial values, including `nil` for optional types. ``` class Animal: CoreStoreObject { + + @Field.Virtual( + "pluralName", + customGetter: { (object, field) in + return object.$species.value + "s" + } + ) + var pluralName: String + @Field.Stored("species") - var species = "" - - @Field.Virtual("pluralName", customGetter: Animal.pluralName(_:)) - var pluralName: String = "" - - @Field.Coded("color", coder: FieldCoders.Plist.self) - var color: UIColor? + var species: String = "" } ``` - Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Virtual(...) var` syntax will be ignored. @@ -53,34 +56,25 @@ extension FieldContainer { public struct Virtual: AttributeKeyPathStringConvertible, FieldAttributeProtocol { /** - Initializes the metadata for the property. + Initializes the metadata for the property. `Field.Virtual` properties are not allowed to have initial values, including `nil` for optional types. ``` class Person: CoreStoreObject { - @Field.Stored("title") - var title: String = "Mr." - @Field.Stored("name") - var name: String = "" - - @Field.Virtual("displayName", customGetter: Person.getName(_:)) - var displayName: String = "" - - private static func getName(_ object: ObjectProxy) -> String { - let cachedDisplayName = object.primitiveValue(for: \.$displayName) - if !cachedDisplayName.isEmpty { - return cachedDisplayName + @Field.Virtual( + "pluralName", + customGetter: { (object, field) in + return object.$species.value + "s" } - let title = object.value(for: \.$title) - let name = object.value(for: \.$name) - let displayName = "\(title) \(name)" - object.setPrimitiveValue(displayName, for: { $0.displayName }) - return displayName - } + ) + var pluralName: String + + @Field.Stored("species") + var species: String = "" } ``` - parameter keyPath: the permanent attribute name for this property. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -318,8 +312,32 @@ extension FieldContainer { } +// MARK: - FieldContainer.Virtual where V: FieldOptionalType + extension FieldContainer.Virtual where V: FieldOptionalType { + /** + Initializes the metadata for the property. `Field.Virtual` properties are not allowed to have initial values, including `nil` for optional types. + ``` + class Person: CoreStoreObject { + + @Field.Virtual( + "pluralName", + customGetter: { (object, field) in + return object.$species.value + "s" + } + ) + var pluralName: String? + + @Field.Stored("species") + var species: String = "" + } + ``` + - parameter keyPath: the permanent attribute name for this property. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + */ public init( _ keyPath: KeyPathString, customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, diff --git a/Sources/Field.swift b/Sources/Field.swift index 28f439e..eb5c555 100644 --- a/Sources/Field.swift +++ b/Sources/Field.swift @@ -35,6 +35,7 @@ extension DynamicObject where Self: CoreStoreObject { The containing type for value propertiess. ``` class Pet: CoreStoreObject { + @Field.Stored("species") var species = "" @@ -66,6 +67,7 @@ extension DynamicObject where Self: CoreStoreObject { The containing type for value properties. Use the `Field` typealias instead for shorter syntax. ``` class Pet: CoreStoreObject { + @Field.Stored("species") var species = "" diff --git a/Sources/FieldCoderType.swift b/Sources/FieldCoderType.swift index 1189017..80420c9 100644 --- a/Sources/FieldCoderType.swift +++ b/Sources/FieldCoderType.swift @@ -29,10 +29,30 @@ import CoreData // MARK: - FieldCoderType +/** + Types that implement encoding to and decoding from `Data` to be used in `Field.Coded` properties' `coder:` argument. + ``` + class Person: CoreStoreObject { + + @Field.Coded("profile", coder: FieldCoders.Json.self) + var profile: Profile = .init() + } + ``` + */ public protocol FieldCoderType { + /** + The type to encode to and decode from `Data` + */ associatedtype FieldStoredValue + /** + Encodes the value to `Data` + */ static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? + + /** + Decodes the value from `Data` + */ static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? } diff --git a/Sources/FieldCoders.DefaultNSSecureCoding.swift b/Sources/FieldCoders.DefaultNSSecureCoding.swift index 989c4ec..3a38f5c 100644 --- a/Sources/FieldCoders.DefaultNSSecureCoding.swift +++ b/Sources/FieldCoders.DefaultNSSecureCoding.swift @@ -30,6 +30,9 @@ import Foundation extension FieldCoders { + /** + A `FieldCoderType` that implements the default Core Data transformable attribute behavior, which uses a `ValueTransformer` named `.secureUnarchiveFromDataTransformerName`. + */ public struct DefaultNSSecureCoding: FieldCoderType { // MARK: FieldCoderType @@ -64,6 +67,11 @@ extension FieldCoders { } +// MARK: - DefaultNSSecureCodable + +/** + Types that are supported by `FieldCoders.DefaultNSSecureCoding` + */ public protocol DefaultNSSecureCodable: NSObject, NSSecureCoding {} extension NSArray: DefaultNSSecureCodable {} diff --git a/Sources/FieldCoders.Json.swift b/Sources/FieldCoders.Json.swift index f16a224..8d05a5b 100644 --- a/Sources/FieldCoders.Json.swift +++ b/Sources/FieldCoders.Json.swift @@ -32,6 +32,10 @@ extension FieldCoders { // MARK: - Json + /** + A `FieldCoderType` that implements JSON encoding and decoding of `Codable` values. + - Important: Due to restrictions of `JSONEncoder`, the value will be contained in single-item array before encoding. + */ public struct Json: FieldCoderType { // MARK: FieldCoderType diff --git a/Sources/FieldCoders.NSCoding.swift b/Sources/FieldCoders.NSCoding.swift index 150e081..6e57afc 100644 --- a/Sources/FieldCoders.NSCoding.swift +++ b/Sources/FieldCoders.NSCoding.swift @@ -32,6 +32,9 @@ extension FieldCoders { // MARK: - NSCoding + /** + A `FieldCoderType` that implements encoding and decoding of `NSCoding` values + */ public struct NSCoding: FieldCoderType { // MARK: FieldCoderType diff --git a/Sources/FieldCoders.Plist.swift b/Sources/FieldCoders.Plist.swift index 1055ec5..d5ebc2c 100644 --- a/Sources/FieldCoders.Plist.swift +++ b/Sources/FieldCoders.Plist.swift @@ -32,6 +32,10 @@ extension FieldCoders { // MARK: - Plist + /** + A `FieldCoderType` that implements Binary-Plist encoding and decoding of `Codable` values. + - Important: Due to restrictions of `JSONEncoder`, the value will be contained in single-item array before encoding. + */ public struct Plist: FieldCoderType { // MARK: FieldCoderType diff --git a/Sources/FieldOptionalType.swift b/Sources/FieldOptionalType.swift index ae7b80b..f9f07b6 100644 --- a/Sources/FieldOptionalType.swift +++ b/Sources/FieldOptionalType.swift @@ -28,6 +28,9 @@ import Foundation // MARK: - FieldOptionalType +/** + Optional values to be used for `Field` properties. + */ public protocol FieldOptionalType: ExpressibleByNilLiteral { /** @@ -36,7 +39,7 @@ public protocol FieldOptionalType: ExpressibleByNilLiteral { associatedtype Wrapped /** - The wrapped value + Used internally by CoreStore. Do not call directly. */ var cs_wrappedValue: Wrapped? { get } } @@ -46,7 +49,7 @@ public protocol FieldOptionalType: ExpressibleByNilLiteral { extension Optional: FieldOptionalType { - // MARK: Public + // MARK: FieldOptionalType @inlinable public var cs_wrappedValue: Wrapped? { diff --git a/Sources/FieldStorableType.swift b/Sources/FieldStorableType.swift index 5428889..ddaab15 100644 --- a/Sources/FieldStorableType.swift +++ b/Sources/FieldStorableType.swift @@ -30,6 +30,9 @@ import CoreGraphics // MARK: - FieldStorableType +/** + Values to be used for `Field.Stored` properties. + */ public protocol FieldStorableType { /** @@ -38,18 +41,18 @@ public protocol FieldStorableType { associatedtype FieldStoredNativeType /** - The `NSAttributeType` for this type + The `NSAttributeType` for this type. Used internally by CoreStore. Do not call directly. */ static var cs_rawAttributeType: NSAttributeType { get } /** - Creates an instance of this type from raw native value. + Creates an instance of this type from raw native value. Used internally by CoreStore. Do not call directly. */ @inline(__always) static func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self /** - Creates `FieldStoredNativeType` value from this instance. + Creates `FieldStoredNativeType` value from this instance. Used internally by CoreStore. Do not call directly. */ @inline(__always) func cs_toFieldStoredNativeType() -> Any? From 74c1a97af4e46d0647cf2ab2af30fd1d4249a4ea Mon Sep 17 00:00:00 2001 From: John Estropia Date: Thu, 26 Mar 2020 02:04:10 +0900 Subject: [PATCH 20/20] Version bump --- CoreStore.podspec | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CoreStore.podspec b/CoreStore.podspec index a25fdd5..7c1229e 100644 --- a/CoreStore.podspec +++ b/CoreStore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CoreStore" - s.version = "7.0.4" - s.swift_version = "5.1" + s.version = "7.1.0" + s.swift_version = "5.2" s.license = "MIT" s.homepage = "https://github.com/JohnEstropia/CoreStore" s.documentation_url = "https://JohnEstropia.github.io/CoreStore" diff --git a/README.md b/README.md index b028477..f868c38 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Unleashing the real power of Core Data with the elegance and safety of Swift

-* **Swift 5.1:** iOS 10+ / macOS 10.12+ / watchOS 3.0+ / tvOS 10.0+ -* Previously supported Swift versions: [Swift 3.2](https://github.com/JohnEstropia/CoreStore/tree/4.2.3), [Swift 4.2](https://github.com/JohnEstropia/CoreStore/tree/6.2.1), [Swift 5.0](https://github.com/JohnEstropia/CoreStore/tree/6.3.2) +* **Swift 5.2:** iOS 10+ / macOS 10.12+ / watchOS 3.0+ / tvOS 10.0+ +* Previously supported Swift versions: [Swift 3.2](https://github.com/JohnEstropia/CoreStore/tree/4.2.3), [Swift 4.2](https://github.com/JohnEstropia/CoreStore/tree/6.2.1), [Swift 5.0](https://github.com/JohnEstropia/CoreStore/tree/6.3.2), [Swift 5.1](https://github.com/JohnEstropia/CoreStore/tree/7.0.4) Upgrading from CoreStore 6.x (swift 5.0) to 7.x (Swift 5.1)? Check out the [🆕 features](#features) and make sure to read the [Change logs](https://github.com/JohnEstropia/CoreStore/releases).