mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-01-15 21:53:39 +01:00
It works! (WIP!)
This commit is contained in:
@@ -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<String>("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<String>("nickname")
|
||||
let year = Attribute.Required<Int>("year", default: 2016)
|
||||
}
|
||||
|
||||
|
||||
class DynamicModelTests: BaseTestDataTestCase {
|
||||
|
||||
func testDynamicModels_CanBeDeclaredCorrectly() {
|
||||
|
||||
class Bird: CoreStoreManagedObject {
|
||||
let birdEntity = Entity<Bird>("Bird")
|
||||
let mascotEntity = Entity<Mascot>("Mascot")
|
||||
let dataStack = DataStack(
|
||||
dynamicModel: ModelVersion(
|
||||
version: "V1",
|
||||
entities: [
|
||||
Entity<Bird>("Bird"),
|
||||
Entity<Mascot>("Mascot")
|
||||
]
|
||||
)
|
||||
)
|
||||
self.prepareStack(dataStack, configurations: [nil]) { (stack) in
|
||||
|
||||
let species = Attribute.Required<String>("species", default: "Swift")
|
||||
}
|
||||
class Mascot: Bird {
|
||||
let k1 = Bird.keyPath({ $0.species })
|
||||
XCTAssertEqual(k1, "species")
|
||||
|
||||
let nickname = Attribute.Optional<String>("nickname")
|
||||
let year = Attribute.Required<Int>("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>("Bird").entityDescription,
|
||||
"Mascot": Entity<Mascot>("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<NSManagedObject>(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<NSManagedObject>(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<NSManagedObject>(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<NSManagedObject>(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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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<String>]()
|
||||
private var entityConfigurationsMapping = [String: Set<String>]() // TODO: change key to AnyEntity
|
||||
|
||||
deinit {
|
||||
|
||||
|
||||
@@ -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<O: CoreStoreManagedObject>: 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<Self>
|
||||
|
||||
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<NSEntityDescription>
|
||||
internal let entityConfigurations: [String: Set<NSEntityDescription>]
|
||||
|
||||
static func entity<O: CoreStoreManagedObject>(for type: O.Type) -> Entity<O> {
|
||||
public convenience init(version: String, entities: [EntityProtocol]) {
|
||||
|
||||
return self.entities.first(where: { $0 is Entity<O> })! as! Entity<O>
|
||||
self.init(version: version, configurationEntities: [Into.defaultConfigurationName: entities])
|
||||
}
|
||||
|
||||
public required init(version: String, configurationEntities: [String: [EntityProtocol]]) {
|
||||
|
||||
self.version = version
|
||||
|
||||
var entityConfigurations: [String: Set<NSEntityDescription>] = [:]
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user