diff --git a/CoreStoreDemo/CoreStoreDemo.xcodeproj/project.pbxproj b/CoreStoreDemo/CoreStoreDemo.xcodeproj/project.pbxproj index 7a30696..3417a4c 100644 --- a/CoreStoreDemo/CoreStoreDemo.xcodeproj/project.pbxproj +++ b/CoreStoreDemo/CoreStoreDemo.xcodeproj/project.pbxproj @@ -25,7 +25,6 @@ B560070F1B3EC90F00A9A8F9 /* OrganismV2ToV3MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B560070E1B3EC90F00A9A8F9 /* OrganismV2ToV3MigrationPolicy.swift */; }; B566E32A1B117B1F00F4F0C6 /* StackSetupDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B566E3291B117B1F00F4F0C6 /* StackSetupDemoViewController.swift */; }; B566E3321B11DF3200F4F0C6 /* UserAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = B566E3311B11DF3200F4F0C6 /* UserAccount.swift */; }; - B56924091EBAE435007C4DC9 /* OrganismV3ToV2.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = B5125C131B521BA7003A42C7 /* OrganismV3ToV2.xcmappingmodel */; }; B56964C91B20AC780075EE4A /* CustomLoggerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56964C81B20AC780075EE4A /* CustomLoggerViewController.swift */; }; B56964D71B231AE90075EE4A /* StackSetupDemo.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B56964D51B231AE90075EE4A /* StackSetupDemo.xcdatamodeld */; }; B56964DA1B231BCA0075EE4A /* MaleAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56964D91B231BCA0075EE4A /* MaleAccount.swift */; }; @@ -330,7 +329,6 @@ B5EE259B1B3EA4890000406B /* OrganismV3.swift in Sources */, B503FAE11AFDC71700F90881 /* Palette.swift in Sources */, B503FAE21AFDC71700F90881 /* PaletteTableViewCell.swift in Sources */, - B56924091EBAE435007C4DC9 /* OrganismV3ToV2.xcmappingmodel in Sources */, B560070F1B3EC90F00A9A8F9 /* OrganismV2ToV3MigrationPolicy.swift in Sources */, B503FADF1AFDC71700F90881 /* ListObserverDemoViewController.swift in Sources */, B54AAD4F1AF4D26E00848AE0 /* AppDelegate.swift in Sources */, diff --git a/CoreStoreDemo/CoreStoreDemo/MIgrations Demo/MigrationsDemoViewController.swift b/CoreStoreDemo/CoreStoreDemo/MIgrations Demo/MigrationsDemoViewController.swift index 3299eae..980dfb3 100644 --- a/CoreStoreDemo/CoreStoreDemo/MIgrations Demo/MigrationsDemoViewController.swift +++ b/CoreStoreDemo/CoreStoreDemo/MIgrations Demo/MigrationsDemoViewController.swift @@ -228,10 +228,31 @@ class MigrationsDemoViewController: UIViewController, ListObserver, UITableViewD self.setEnabled(false) let progress = dataStack.addStorage( SQLiteStore( - fileName: "MigrationDemo.sqlite"/*, + fileName: "MigrationDemo.sqlite", migrationMappingProviders: [ - CustomSchemaMappingProvider(from: "MigrationDemoV3", to: "MigrationDemoV2") - ]*/ + CustomSchemaMappingProvider( + from: "MigrationDemoV3", + to: "MigrationDemoV2", + entityMappings: [ + .transformEntity( + sourceEntity: "Organism", + destinationEntity: "Organism", + transformer: { (source, createDestination) in + + let destination = createDestination() + destination.enumerateAttributes { (attribute, sourceAttribute) in + + if let sourceAttribute = sourceAttribute { + + destination[attribute] = source[sourceAttribute] + } + } + destination["numberOfFlippers"] = source["numberOfLimbs"] + } + ) + ] + ) + ] ), completion: { [weak self] (result) -> Void in diff --git a/Sources/CoreDataNativeType.swift b/Sources/CoreDataNativeType.swift index c801239..c382a6a 100644 --- a/Sources/CoreDataNativeType.swift +++ b/Sources/CoreDataNativeType.swift @@ -30,7 +30,7 @@ import CoreData // MARK: - CoreDataNativeType /** - Objective-C Foundation types that are nativel supported by Core Data managed attributes all conform to `CoreDataNativeType`. + Objective-C Foundation types that are natively supported by Core Data managed attributes all conform to `CoreDataNativeType`. */ @objc public protocol CoreDataNativeType: class, NSObjectProtocol, AnyObject {} diff --git a/Sources/Migrating/Schema Mapping Providers/CustomSchemaMappingProvider.swift b/Sources/Migrating/Schema Mapping Providers/CustomSchemaMappingProvider.swift index 9dda317..95fef1f 100644 --- a/Sources/Migrating/Schema Mapping Providers/CustomSchemaMappingProvider.swift +++ b/Sources/Migrating/Schema Mapping Providers/CustomSchemaMappingProvider.swift @@ -31,6 +31,27 @@ import Foundation open class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { + public let sourceVersion: ModelVersion + public let destinationVersion: ModelVersion + + public required init(from sourceVersion: ModelVersion, to destinationVersion: ModelVersion, entityMappings: Set = []) { + + CoreStore.assert( + cs_lazy { + + let sources = entityMappings.flatMap({ $0.entityMappingSourceEntity }) + let destinations = entityMappings.flatMap({ $0.entityMappingDestinationEntity }) + return sources.count == Set(sources).count + && destinations.count == Set(destinations).count + }, + "Duplicate source/destination entities found in provided \"entityMappings\" argument." + ) + self.sourceVersion = sourceVersion + self.destinationVersion = destinationVersion + self.entityMappings = entityMappings + } + + // MARK: - CustomMapping public enum CustomMapping: Hashable { @@ -40,7 +61,7 @@ open class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { case deleteEntity(sourceEntity: EntityName) case insertEntity(destinationEntity: EntityName) case copyEntity(sourceEntity: EntityName, destinationEntity: EntityName) - case transformEntity(sourceEntity: EntityName, destinationEntity: EntityName, transformEntity: Transformer) + case transformEntity(sourceEntity: EntityName, destinationEntity: EntityName, transformer: Transformer) static func inferredTransformation(_ sourceObject: UnsafeSourceObject, _ createDestinationObject: () -> UnsafeDestinationObject) throws -> Void { @@ -211,29 +232,6 @@ open class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { } - // MARK: - Public - - public let sourceVersion: ModelVersion - public let destinationVersion: ModelVersion - - public required init(from sourceVersion: ModelVersion, to destinationVersion: ModelVersion, entityMappings: Set = []) { - - CoreStore.assert( - cs_lazy { - - let sources = entityMappings.flatMap({ $0.entityMappingSourceEntity }) - let destinations = entityMappings.flatMap({ $0.entityMappingDestinationEntity }) - return sources.count == Set(sources).count - && destinations.count == Set(destinations).count - }, - "Duplicate source/destination entities found in provided \"entityMappings\" argument." - ) - self.sourceVersion = sourceVersion - self.destinationVersion = destinationVersion - self.entityMappings = entityMappings - } - - // MARK: Equatable public static func == (lhs: CustomSchemaMappingProvider, rhs: CustomSchemaMappingProvider) -> Bool { @@ -607,7 +605,7 @@ open class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { .transformEntity( sourceEntity: sourceEntityName, destinationEntity: destinationEntityName, - transformEntity: CustomMapping.inferredTransformation + transformer: CustomMapping.inferredTransformation ) ) } diff --git a/Sources/Setup/CoreStore+Setup.swift b/Sources/Setup/CoreStore+Setup.swift index 37cc29a..fb80f5e 100644 --- a/Sources/Setup/CoreStore+Setup.swift +++ b/Sources/Setup/CoreStore+Setup.swift @@ -155,7 +155,7 @@ public extension CoreStore { // MARK: Obsolete - @available(*, obsoleted: 3.0.0, renamed: "entityDescription(for:)") + @available(*, obsoleted: 3.1, renamed: "entityDescription(for:)") public static func entityDescriptionForType(_ type: NSManagedObject.Type) -> NSEntityDescription? { return self.entityDescription(for: type) diff --git a/Sources/Setup/DataStack.swift b/Sources/Setup/DataStack.swift index c8a42ff..6ed1cf9 100644 --- a/Sources/Setup/DataStack.swift +++ b/Sources/Setup/DataStack.swift @@ -37,7 +37,7 @@ public final class DataStack: Equatable { /** Initializes a `DataStack` from the model with the specified `modelName` in the specified `bundle`. - - parameter modelName: the name of the (.xcdatamodeld) model file. If not specified, the application name (CFBundleName) will be used if it exists, or "CoreData" if it the bundle name was not set. + - parameter modelName: the name of the (.xcdatamodeld) model file. If not specified, the application name (CFBundleName) will be used if it exists, or "CoreData" if it the bundle name was not set (e.g. in Unit Tests). - parameter bundle: an optional bundle to load models from. If not specified, the main bundle will be used. - parameter migrationChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack. */ @@ -52,6 +52,13 @@ public final class DataStack: Equatable { ) } + /** + Initializes a `DataStack` from a `DynamicSchema` implementation. + + - parameter schema: an instance of `DynamicSchema` + - parameter otherSchema: a list of other `DynamicSchema` instances that represent present/previous/future model versions, in any order + - parameter migrationChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack. + */ public convenience init(_ schema: DynamicSchema, _ otherSchema: DynamicSchema..., migrationChain: MigrationChain = nil) { self.init( @@ -60,24 +67,17 @@ public final class DataStack: Equatable { migrationChain: migrationChain ) ) - } /** - Initializes a `DataStack` from an `NSManagedObjectModel`. + Initializes a `DataStack` from a `SchemaHistory` instance. - - parameter model: the `NSManagedObjectModel` for the stack - - parameter migrationChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack. + - parameter schemaHistory: the `SchemaHistory` for the stack */ public required init(schemaHistory: SchemaHistory) { // TODO: test before release (rolled back) // _ = DataStack.isGloballyInitialized - CoreStore.assert( - schemaHistory.migrationChain.isValid, - "Invalid migration chain passed to the \(cs_typeName(DataStack.self)). Check that the model versions' order is correct and that no repetitions or ambiguities exist." - ) - self.coordinator = NSPersistentStoreCoordinator(managedObjectModel: schemaHistory.rawModel) self.rootSavingContext = NSManagedObjectContext.rootSavingContextForCoordinator(self.coordinator) self.mainContext = NSManagedObjectContext.mainContextForRootContext(self.rootSavingContext) @@ -89,7 +89,7 @@ public final class DataStack: Equatable { } /** - Returns the `DataStack`'s model version. The version string is the same as the name of the version-specific .xcdatamodeld file. + Returns the `DataStack`'s current model version. `StorageInterface`s added to the stack will be migrated to this version. */ public var modelVersion: String { @@ -97,7 +97,7 @@ public final class DataStack: Equatable { } /** - Returns the `DataStack`'s model schema. + Returns the `DataStack`'s current model schema. `StorageInterface`s added to the stack will be migrated to this version. */ public var modelSchema: DynamicSchema { @@ -619,13 +619,13 @@ public final class DataStack: Equatable { // MARK: Obsolete - @available(*, obsoleted: 3.0.0, renamed: "entityDescription(for:)") + @available(*, obsoleted: 3.1, renamed: "entityDescription(for:)") public func entityDescriptionForType(_ type: NSManagedObject.Type) -> NSEntityDescription? { return self.entityDescription(for: type) } - @available(*, obsoleted: 3.0.0, renamed: "objectID(forURIRepresentation:)") + @available(*, obsoleted: 3.1, renamed: "objectID(forURIRepresentation:)") public func objectIDForURIRepresentation(_ url: URL) -> NSManagedObjectID? { return self.objectID(forURIRepresentation: url) diff --git a/Sources/Setup/Dynamic Models/CoreStoreObject.swift b/Sources/Setup/Dynamic Models/CoreStoreObject.swift index 1dfcd9d..f1751e8 100644 --- a/Sources/Setup/Dynamic Models/CoreStoreObject.swift +++ b/Sources/Setup/Dynamic Models/CoreStoreObject.swift @@ -30,7 +30,7 @@ import Foundation // MARK: - CoreStoreObject /** - The `CoreStoreObject` is an abstract class for creating CoreStore-managed objects that are more type-safe and more convenient than `NSManagedObject` subclasses. The model entities for `CoreStoreObject` subclasses are inferred from the subclasses' Swift declaration themselves; no .xcdatamodeld files needed. To declare persisted attributes and relationships for the `CoreStoreObject` subclass, declare properties of type `Value.Required`, `Value.Optional` for values, or `Relationship.ToOne`, `Relationship.ToManyOrdered`, `Relationship.ToManyUnordered` for relationships. + The `CoreStoreObject` is an abstract class for creating CoreStore-managed objects that are more type-safe and more convenient than `NSManagedObject` subclasses. The model entities for `CoreStoreObject` subclasses are inferred from the Swift declaration themselves; no .xcdatamodeld files are needed. To declare persisted attributes and relationships for the `CoreStoreObject` subclass, declare properties of type `Value.Required`, `Value.Optional` for values, or `Relationship.ToOne`, `Relationship.ToManyOrdered`, `Relationship.ToManyUnordered` for relationships. ``` class Animal: CoreStoreObject { let species = Value.Required("species") @@ -44,6 +44,17 @@ import Foundation } ``` `CoreStoreObject` entities for a model version should be added to `CoreStoreSchema` instance. + ``` + CoreStore.defaultStack = DataStack( + CoreStoreSchema( + modelVersion: "V1", + entities: [ + Entity("Animal"), + Entity("Person") + ] + ) + ) + ``` - SeeAlso: CoreStoreSchema - SeeAlso: CoreStoreObject.Value - SeeAlso: CoreStoreObject.Relationship diff --git a/Sources/Setup/Dynamic Models/Dynamic Schema/CoreStoreSchema.swift b/Sources/Setup/Dynamic Models/Dynamic Schema/CoreStoreSchema.swift index 8a6df67..22a5f5d 100644 --- a/Sources/Setup/Dynamic Models/Dynamic Schema/CoreStoreSchema.swift +++ b/Sources/Setup/Dynamic Models/Dynamic Schema/CoreStoreSchema.swift @@ -29,23 +29,134 @@ import Foundation // MARK: - CoreStoreSchema +/** + The `CoreStoreSchema` describes models written for `CoreStoreObject` Swift class declarations for a particular model version. `CoreStoreObject` entities for a model version should be added to `CoreStoreSchema` instance. + ``` + class Animal: CoreStoreObject { + let species = Value.Required("species") + let nickname = Value.Optional("nickname") + let master = Relationship.ToOne("master") + } + + class Person: CoreStoreObject { + let name = Value.Required("name") + let pet = Relationship.ToOne("pet", inverse: { $0.master }) + } + + CoreStore.defaultStack = DataStack( + CoreStoreSchema( + modelVersion: "V1", + entities: [ + Entity("Animal"), + Entity("Person") + ], + versionLock: [ + "Animal": [0x2698c812ebbc3b97, 0x751e3fa3f04cf9, 0x51fd460d3babc82, 0x92b4ba735b5a3053], + "Person": [0xae4060a59f990ef0, 0x8ac83a6e1411c130, 0xa29fea58e2e38ab6, 0x2071bb7e33d77887] + ] + ) + ) + ``` + - SeeAlso: CoreStoreObject + - SeeAlso: Entity + */ public final class CoreStoreSchema: DynamicSchema { - public convenience init(modelVersion: ModelVersion, entities: [DynamicEntity], versionLock: VersionLock? = nil) { + /** + Initializes a `CoreStoreSchema`. Using this initializer only if the entities don't need to be assigned to particular "Configurations". To use multiple configurations (for example, to separate entities in different `StorageInterface`s), use the `init(modelVersion:entitiesByConfiguration:versionLock:)` initializer. + ``` + class Animal: CoreStoreObject { + let species = Value.Required("species") + let nickname = Value.Optional("nickname") + let master = Relationship.ToOne("master") + } + + class Person: CoreStoreObject { + let name = Value.Required("name") + let pet = Relationship.ToOne("pet", inverse: { $0.master }) + } + + CoreStore.defaultStack = DataStack( + CoreStoreSchema( + modelVersion: "V1", + entities: [ + Entity("Animal"), + Entity("Person") + ], + versionLock: [ + "Animal": [0x2698c812ebbc3b97, 0x751e3fa3f04cf9, 0x51fd460d3babc82, 0x92b4ba735b5a3053], + "Person": [0xae4060a59f990ef0, 0x8ac83a6e1411c130, 0xa29fea58e2e38ab6, 0x2071bb7e33d77887] + ] + ) + ) + ``` + - parameter modelVersion: the model version for the schema. This string should be unique from other `DynamicSchema`'s model versions. + - parameter entities: an array of `Entity` pertaining to all `CoreStoreObject` subclasses to be added to the schema version. + - parameter versionLock: an optional list of `VersionLock` hashes for each entity name in the `entities` array. If any `DynamicEntity` doesn't match its version lock hash, an assertion will be raised. + */ + public convenience init(modelVersion: ModelVersion, entities: [DynamicEntity & Hashable], versionLock: VersionLock? = nil) { + var entityConfigurations: [DynamicEntity & Hashable: Set] = [:] + for entity in entities { + + entityConfigurations[entity] = [] + } self.init( modelVersion: modelVersion, - entitiesByConfiguration: [DataStack.defaultConfigurationName: entities], + entityConfigurations: entityConfigurations, versionLock: versionLock ) } - public required init(modelVersion: ModelVersion, entitiesByConfiguration: [String: [DynamicEntity]], versionLock: VersionLock? = nil) { + /** + Initializes a `CoreStoreSchema`. Using this initializer if multiple "Configurations" (for example, to separate entities in different `StorageInterface`s) are needed. To add an entity only to the default configuration, assign an empty set to its configurations list. Note that regardless of the set configurations, all entities will be added to the default configuration. + ``` + class Animal: CoreStoreObject { + let species = Value.Required("species") + let nickname = Value.Optional("nickname") + } + + class Person: CoreStoreObject { + let name = Value.Required("name") + } + + CoreStore.defaultStack = DataStack( + CoreStoreSchema( + modelVersion: "V1", + entityConfigurations: [ + Entity("Animal"): [], + Entity("Person"): ["People"] + ], + versionLock: [ + "Animal": [0x2698c812ebbc3b97, 0x751e3fa3f04cf9, 0x51fd460d3babc82, 0x92b4ba735b5a3053], + "Person": [0xae4060a59f990ef0, 0x8ac83a6e1411c130, 0xa29fea58e2e38ab6, 0x2071bb7e33d77887] + ] + ) + ) + ``` + - parameter modelVersion: the model version for the schema. This string should be unique from other `DynamicSchema`'s model versions. + - parameter entityConfigurations: a dictionary with `Entity` pertaining to all `CoreStoreObject` subclasses and the corresponding list of "Configurations" they should be added to. To add an entity only to the default configuration, assign an empty set to its configurations list. Note that regardless of the set configurations, all entities will be added to the default configuration. + - parameter versionLock: an optional list of `VersionLock` hashes for each entity name in the `entities` array. If any `DynamicEntity` doesn't match its version lock hash, an assertion will be raised. + */ + public required init(modelVersion: ModelVersion, entityConfigurations: [DynamicEntity & Hashable: Set], versionLock: VersionLock? = nil) { var actualEntitiesByConfiguration: [String: Set] = [:] - for (configuration, entities) in entitiesByConfiguration { + for (entity, configurations) in entityConfigurations { - actualEntitiesByConfiguration[configuration] = Set(entities.map(AnyEntity.init)) + for configuration in configurations { + + var entities: Set + if let existingEntities = actualEntitiesByConfiguration[configurations] { + + entities = existingEntities + } + else { + + entities = [] + } + entities.insert(AnyEntity(entity)) + actualEntitiesByConfiguration[configurations] = entities + } } let allEntities = Set(actualEntitiesByConfiguration.values.joined()) actualEntitiesByConfiguration[DataStack.defaultConfigurationName] = allEntities diff --git a/Sources/Setup/Dynamic Models/Dynamic Schema/DynamicSchema.swift b/Sources/Setup/Dynamic Models/Dynamic Schema/DynamicSchema.swift index 09a7bf5..58e0ffc 100644 --- a/Sources/Setup/Dynamic Models/Dynamic Schema/DynamicSchema.swift +++ b/Sources/Setup/Dynamic Models/Dynamic Schema/DynamicSchema.swift @@ -30,10 +30,10 @@ import Foundation // MARK: - DynamicSchema /** - `DynamicSchema` are types that provide `NSManagedObjectModel` instances for a particular model version. CoreStore currently supports concrete types: + `DynamicSchema` are types that provide `NSManagedObjectModel` instances for a particular model version. CoreStore currently supports the following concrete types: - `XcodeDataModelSchema`: describes models loaded from a .xcdatamodeld file. - `LegacyXcodeDataModelSchema`: describes models loaded directly from an existing `NSManagedObjectModel`. It is not advisable to continue using this model as its metadata are not available to CoreStore. - - `CoreStoreSchema`: describes models written in `CoreStoreObject` Swift class declarations. + - `CoreStoreSchema`: describes models written for `CoreStoreObject` Swift class declarations. */ public protocol DynamicSchema { diff --git a/Sources/Setup/Dynamic Models/SchemaHistory.swift b/Sources/Setup/Dynamic Models/SchemaHistory.swift index 155abcc..941401b 100644 --- a/Sources/Setup/Dynamic Models/SchemaHistory.swift +++ b/Sources/Setup/Dynamic Models/SchemaHistory.swift @@ -132,7 +132,10 @@ public final class SchemaHistory: ExpressibleByArrayLiteral { CoreStore.abort("The \"allSchema\" argument of the \(cs_typeName(SchemaHistory.self)) initializer cannot be empty.") } - + CoreStore.assert( + migrationChain.isValid, + "Invalid migration chain passed to the \(cs_typeName(SchemaHistory.self)). Check that the model versions' order are correct and that no repetitions or ambiguities exist." + ) var schemaByVersion: [ModelVersion: DynamicSchema] = [:] for schema in allSchema {