From 258c237100600b5af2e627cb6d2a7f8b6e14280a Mon Sep 17 00:00:00 2001 From: John Estropia Date: Tue, 4 Apr 2017 20:25:40 +0900 Subject: [PATCH] It works! (WIP!) --- CoreStoreTests/DynamicModelTests.swift | 176 ++++++++++++------ .../Internal/NSManagedObjectModel+Setup.swift | 2 +- Sources/Setup/DataStack.swift | 19 +- Sources/Setup/Dynamic Models/Prototype.swift | 85 +++++++-- 4 files changed, 208 insertions(+), 74 deletions(-) diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index d9692e1..1b73f5c 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -9,76 +9,134 @@ import XCTest import CoreData -import CoreStore + +@testable import CoreStore -class DynamicModelTests: XCTestCase { +class Bird: CoreStoreManagedObject { - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } + let species = Attribute.Required("species", default: "Swift") +} +class Mascot: Bird { - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } + let nickname = Attribute.Optional("nickname") + let year = Attribute.Required("year", default: 2016) +} + + +class DynamicModelTests: BaseTestDataTestCase { func testDynamicModels_CanBeDeclaredCorrectly() { - class Bird: CoreStoreManagedObject { + let birdEntity = Entity("Bird") + let mascotEntity = Entity("Mascot") + let dataStack = DataStack( + dynamicModel: ModelVersion( + version: "V1", + entities: [ + Entity("Bird"), + Entity("Mascot") + ] + ) + ) + self.prepareStack(dataStack, configurations: [nil]) { (stack) in - let species = Attribute.Required("species", default: "Swift") - } - class Mascot: Bird { + let k1 = Bird.keyPath({ $0.species }) + XCTAssertEqual(k1, "species") - let nickname = Attribute.Optional("nickname") - let year = Attribute.Required("year", default: 2016) - } - - let k1 = Bird.keyPath({ $0.species }) - XCTAssertEqual(k1, "species") - - let k2 = Mascot.keyPath({ $0.species }) - XCTAssertEqual(k2, "species") - - let k3 = Mascot.keyPath({ $0.nickname }) - XCTAssertEqual(k3, "nickname") - - let entities = [ - "Bird": Entity("Bird").entityDescription, - "Mascot": Entity("Mascot").entityDescription - ] - enum Static { + let k2 = Mascot.keyPath({ $0.species }) + XCTAssertEqual(k2, "species") - static let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) + let k3 = Mascot.keyPath({ $0.nickname }) + XCTAssertEqual(k3, "nickname") + + let expectation = self.expectation(description: "done") + stack.perform( + asynchronous: { (transaction) in + + let bird = Bird(transaction.create(Into(birdEntity.dynamicClass))) + XCTAssertEqual(bird.species*, "Swift") + XCTAssertTrue(type(of: bird.species*) == String.self) + + bird.species .= "Sparrow" + XCTAssertEqual(bird.species*, "Sparrow") + + let mascot = Mascot(transaction.create(Into(mascotEntity.dynamicClass))) + XCTAssertEqual(mascot.species*, "Swift") + XCTAssertEqual(mascot.nickname*, nil) + + mascot.nickname .= "Riko" + XCTAssertEqual(mascot.nickname*, "Riko") + }, + success: { + + print("done") + }, + failure: { _ in + + XCTFail() + } + ) + stack.perform( + asynchronous: { (transaction) in + + let p1 = Bird.where({ $0.species == "Sparrow" }) + XCTAssertEqual(p1.predicate, Where("%K == %@", "species", "Sparrow").predicate) + + let rawBird = transaction.fetchOne(From(birdEntity.dynamicClass), p1) + XCTAssertNotNil(rawBird) + + let bird = Bird(rawBird) + XCTAssertEqual(bird.species*, "Sparrow") + + let p2 = Mascot.where({ $0.nickname == "Riko" }) + XCTAssertEqual(p2.predicate, Where("%K == %@", "nickname", "Riko").predicate) + + let rawMascot = transaction.fetchOne(From(mascotEntity.dynamicClass), p2) + XCTAssertNotNil(rawMascot) + + let mascot = Mascot(rawMascot) + XCTAssertEqual(mascot.nickname*, "Riko") + + let p3 = Mascot.where({ $0.year == 2016 }) + XCTAssertEqual(p3.predicate, Where("%K == %@", "year", 2016).predicate) + }, + success: { + + expectation.fulfill() + withExtendedLifetime(stack, {}) + }, + failure: { _ in + + XCTFail() + } + ) + self.waitAndCheckExpectations() } - let rawBird = NSManagedObject(entity: entities["Bird"]!, insertInto: Static.context) - let rawMascot = NSManagedObject(entity: entities["Mascot"]!, insertInto: Static.context) + } + + @nonobjc + func prepareStack(_ dataStack: DataStack, configurations: [String?] = [nil], _ closure: (_ dataStack: DataStack) -> Void) { - - let bird = Bird(rawBird) - XCTAssertEqual(bird.species*, "Swift") - XCTAssertTrue(type(of: bird.species*) == String.self) - - bird.species .= "Sparrow" - XCTAssertEqual(bird.species*, "Sparrow") - - let mascot = Mascot(rawMascot) - XCTAssertEqual(mascot.species*, "Swift") - XCTAssertEqual(mascot.nickname*, nil) - - mascot.nickname .= "Riko" - XCTAssertEqual(mascot.nickname*, "Riko") - - - let p1 = Bird.where({ $0.species == "Swift" }) - XCTAssertEqual(p1.predicate, Where("%K == %@", "species", "Swift").predicate) - - let p2 = Mascot.where({ $0.nickname == "Riko" }) - XCTAssertEqual(p2.predicate, Where("%K == %@", "nickname", "Riko").predicate) - - let p3 = Mascot.where({ $0.year == 2016 }) - XCTAssertEqual(p3.predicate, Where("%K == %@", "year", 2016).predicate) + do { + + try configurations.forEach { (configuration) in + + try dataStack.addStorageAndWait( + SQLiteStore( + fileURL: SQLiteStore.defaultRootDirectory + .appendingPathComponent(UUID().uuidString) + .appendingPathComponent("\(type(of: self))_\((configuration ?? "-null-")).sqlite"), + configuration: configuration, + localStorageOptions: .recreateStoreOnModelMismatch + ) + ) + } + } + catch let error as NSError { + + XCTFail(error.coreStoreDumpString) + } + closure(dataStack) } } diff --git a/Sources/Internal/NSManagedObjectModel+Setup.swift b/Sources/Internal/NSManagedObjectModel+Setup.swift index 8594d65..3fe06dd 100644 --- a/Sources/Internal/NSManagedObjectModel+Setup.swift +++ b/Sources/Internal/NSManagedObjectModel+Setup.swift @@ -267,7 +267,7 @@ internal extension NSManagedObjectModel { } var mapping = [String: String]() - self.entities.forEach { + self.entities.forEach { // TODO: use AnyEntity as mapping key guard let entityName = $0.name else { diff --git a/Sources/Setup/DataStack.swift b/Sources/Setup/DataStack.swift index 2621f7c..8cf3eb0 100644 --- a/Sources/Setup/DataStack.swift +++ b/Sources/Setup/DataStack.swift @@ -51,6 +51,23 @@ public final class DataStack: Equatable { self.init(model: model, migrationChain: migrationChain) } + public convenience init(dynamicModel: ModelVersion) { + + self.init(model: dynamicModel.createModel()) + } + + public convenience init(dynamicModels: [ModelVersion], migrationChain: MigrationChain = nil) { + + CoreStore.assert( + migrationChain.valid, + "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.init( + model: NSManagedObjectModel(byMerging: dynamicModels.map({ $0.createModel() }))!, + migrationChain: migrationChain + ) + } + /** Initializes a `DataStack` from an `NSManagedObjectModel`. @@ -510,7 +527,7 @@ public final class DataStack: Equatable { // }() private var configurationStoreMapping = [String: NSPersistentStore]() - private var entityConfigurationsMapping = [String: Set]() + private var entityConfigurationsMapping = [String: Set]() // TODO: change key to AnyEntity deinit { diff --git a/Sources/Setup/Dynamic Models/Prototype.swift b/Sources/Setup/Dynamic Models/Prototype.swift index 67a9ee6..8d77453 100644 --- a/Sources/Setup/Dynamic Models/Prototype.swift +++ b/Sources/Setup/Dynamic Models/Prototype.swift @@ -8,6 +8,7 @@ import CoreGraphics import Foundation +import ObjectiveC public protocol ManagedObjectProtocol: class {} @@ -17,7 +18,7 @@ public protocol EntityProtocol { var entityDescription: NSEntityDescription { get } } -protocol AttributeProtocol: class { +internal protocol AttributeProtocol: class { static var attributeType: NSAttributeType { get } var keyPath: String { get } @@ -27,8 +28,8 @@ protocol AttributeProtocol: class { open class CoreStoreManagedObject: ManagedObjectProtocol { - let rawObject: NSManagedObject? - let isMeta: Bool + internal let rawObject: NSManagedObject? + internal let isMeta: Bool public required init(_ object: NSManagedObject?) { @@ -59,12 +60,42 @@ open class CoreStoreManagedObject: ManagedObjectProtocol { public struct Entity: EntityProtocol { public let entityDescription: NSEntityDescription + internal var dynamicClass: AnyClass { + + return NSClassFromString(self.entityDescription.managedObjectClassName!)! + } public init(_ entityName: String) { + let dynamicClassName = String(reflecting: O.self) + .appending("__\(entityName)") + .replacingOccurrences(of: ".", with: "_") + .replacingOccurrences(of: "<", with: "_") + .replacingOccurrences(of: ">", with: "_") + // TODO: assign entityName through ModelVersion and + // TODO: set NSEntityDescription.userInfo AnyEntity + let newClass: AnyClass? + + if NSClassFromString(dynamicClassName) == nil { + + newClass = objc_allocateClassPair(NSManagedObject.self, dynamicClassName, 0) + } + else { + + newClass = nil + } + + defer { + + if let newClass = newClass { + + objc_registerClassPair(newClass) + } + } + let entityDescription = NSEntityDescription() entityDescription.name = entityName - entityDescription.managedObjectClassName = NSStringFromClass(NSManagedObject.self) + entityDescription.managedObjectClassName = dynamicClassName // TODO: return to NSManagedObject entityDescription.properties = type(of: self).initializeAttributes(Mirror(reflecting: O.meta)) self.entityDescription = entityDescription @@ -168,7 +199,7 @@ public extension ManagedObjectProtocol where Self: CoreStoreManagedObject { public typealias Attribute = AttributeContainer - static var meta: Self { + internal static var meta: Self { return self.init(nil) } @@ -250,16 +281,44 @@ public extension AttributeContainer.Optional where V: CVarArg { } } -protocol ModelVersionProtocol: class { +public final class ModelVersion { - static var version: String { get } - static var entities: [EntityProtocol] { get } -} - -extension ModelVersionProtocol { + public let version: String + internal let entities: Set + internal let entityConfigurations: [String: Set] - static func entity(for type: O.Type) -> Entity { + public convenience init(version: String, entities: [EntityProtocol]) { - return self.entities.first(where: { $0 is Entity })! as! Entity + self.init(version: version, configurationEntities: [Into.defaultConfigurationName: entities]) + } + + public required init(version: String, configurationEntities: [String: [EntityProtocol]]) { + + self.version = version + + var entityConfigurations: [String: Set] = [:] + for (configuration, entities) in configurationEntities { + + entityConfigurations[configuration] = Set(entities.map({ $0.entityDescription })) + } + let allEntities = Set(entityConfigurations.map({ $0.value }).joined()) + entityConfigurations[Into.defaultConfigurationName] = allEntities + + self.entityConfigurations = entityConfigurations + self.entities = allEntities + } + + internal func createModel() -> NSManagedObjectModel { + + let model = NSManagedObjectModel() + model.entities = self.entities.sorted(by: { $0.name! < $1.name! }) + for (configuration, entities) in self.entityConfigurations { + + model.setEntities( + entities.sorted(by: { $0.name! < $1.name! }), + forConfigurationName: configuration + ) + } + return model } }