// // DynamicModelTests.swift // CoreStore // // Copyright © 2018 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 XCTest @testable import CoreStore #if os(macOS) typealias Color = NSColor #else typealias Color = UIColor #endif class Animal: CoreStoreObject { let species = Value.Required("species", initial: "Swift") let master = Relationship.ToOne("master") let color = Transformable.Optional("color") } class Dog: Animal { let nickname = Value.Optional("nickname") 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( "displayName", isTransient: true, customGetter: Person.getDisplayName(_:), affectedByKeyPaths: Person.keyPathsAffectingDisplayName() ) let pets = Relationship.ToManyUnordered("pets", inverse: { $0.master }) private static func setTitle(_ partialObject: PartialObject, _ newValue: String) { partialObject.setPrimitiveValue(newValue, for: { $0.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 }) } static func getDisplayName(_ partialObject: PartialObject) -> String? { if let displayName = partialObject.primitiveValue(for: { $0.displayName }) { return displayName } let title = partialObject.value(for: { $0.title }) let name = partialObject.value(for: { $0.name }) let displayName = "\(title) \(name)" partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) return displayName } static func keyPathsAffectingDisplayName() -> Set { return [ String(keyPath: \Person.title), String(keyPath: \Person.name) ] } } // MARK: - DynamicModelTests class DynamicModelTests: BaseTestDataTestCase { @objc dynamic func test_ThatDynamicModels_CanBeDeclaredCorrectly() { let dataStack = DataStack( CoreStoreSchema( modelVersion: "V1", entities: [ Entity("Animal"), Entity("Dog"), Entity("Person") ], versionLock: [ "Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a], "Dog": [0xe3f0afeb109b283a, 0x29998d292938eb61, 0x6aab788333cfc2a3, 0x492ff1d295910ea7], "Person": [0x66d8bbfd8b21561f, 0xcecec69ecae3570f, 0xc4b73d71256214ef, 0x89b99bfe3e013e8b] ] ) ) self.prepareStack(dataStack, configurations: [nil]) { (stack) in let k1 = String(keyPath: \Animal.species) XCTAssertEqual(k1, "species") let k2 = String(keyPath: \Dog.species) XCTAssertEqual(k2, "species") let k3 = String(keyPath: \Dog.nickname) XCTAssertEqual(k3, "nickname") let updateDone = self.expectation(description: "update-done") let fetchDone = self.expectation(description: "fetch-done") let willSetPriorObserverDone = self.expectation(description: "willSet-observe-prior-done") let willSetNotPriorObserverDone = self.expectation(description: "willSet-observe-notPrior-done") let didSetObserverDone = self.expectation(description: "didSet-observe-done") stack.perform( asynchronous: { (transaction) in let animal = transaction.create(Into()) XCTAssertEqual(animal.species.value, "Swift") XCTAssertTrue(type(of: animal.species.value) == String.self) animal.species .= "Sparrow" XCTAssertEqual(animal.species.value, "Sparrow") animal.color .= .yellow XCTAssertEqual(animal.color.value, Color.yellow) let dog = transaction.create(Into()) XCTAssertEqual(dog.species.value, "Swift") XCTAssertEqual(dog.nickname.value, nil) XCTAssertEqual(dog.age.value, 1) let didSetObserver = dog.species.observe(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") didSetObserverDone.fulfill() } let willSetObserver = dog.species.observe(options: [.new, .old, .prior]) { (object, change) in XCTAssertEqual(object, dog) XCTAssertEqual(change.kind, .setting) XCTAssertEqual(change.oldValue, "Swift") if change.isPrior { XCTAssertNil(change.newValue) XCTAssertEqual(object.species.value, "Swift") willSetPriorObserverDone.fulfill() } else { XCTAssertEqual(change.newValue, "Dog") XCTAssertEqual(object.species.value, "Dog") willSetNotPriorObserverDone.fulfill() } } dog.species .= "Dog" XCTAssertEqual(dog.species.value, "Dog") didSetObserver.invalidate() willSetObserver.invalidate() dog.nickname .= "Spot" XCTAssertEqual(dog.nickname.value, "Spot") let person = transaction.create(Into()) XCTAssertTrue(person.pets.value.isEmpty) XCTAssertEqual( cs_dynamicType(of: person.rawObject!).keyPathsForValuesAffectingValue(forKey: "displayName"), ["title", "name"] ) person.name .= "Joe" XCTAssertEqual(person.rawObject!.value(forKey: "name") as! String?, "Joe") XCTAssertEqual(person.rawObject!.value(forKey: "displayName") as! String?, "Mr. Joe") 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.title .= "Sir" XCTAssertEqual(person.displayName.value, "Sir John") person.pets.value.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) }, success: { _ in updateDone.fulfill() }, failure: { _ in XCTFail() } ) stack.perform( asynchronous: { (transaction) in 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") let p2 = Where({ $0.nickname == "Spot" }) XCTAssertEqual(p2.predicate, NSPredicate(format: "%K == %@", "nickname", "Spot")) let dog = try transaction.fetchOne(From().where(\.nickname == "Spot")) XCTAssertNotNil(dog) XCTAssertEqual(dog!.nickname.value, "Spot") XCTAssertEqual(dog!.species.value, "Dog") let person = try transaction.fetchOne(From()) XCTAssertNotNil(person) XCTAssertEqual(person!.pets.value.first, dog) 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))) XCTAssertEqual(totalAge, 1) _ = try transaction.fetchAll( From() .where(\Animal.species == "Dog" && \.age == 10) ) _ = try transaction.fetchAll( From() .where(\.age == 10 && \Animal.species == "Dog") .orderBy(.ascending({ $0.species })) ) _ = try transaction.fetchAll( From(), Where({ $0.age > 10 && $0.age <= 15 }) ) _ = try transaction.fetchAll( From(), Where({ $0.species == "Dog" && $0.age == 10 }) ) _ = try transaction.fetchAll( From(), Where({ $0.age == 10 && $0.species == "Dog" }) ) _ = try transaction.fetchAll( From(), Where({ $0.age > 10 && $0.age <= 15 }) ) _ = try transaction.fetchAll( From(), (\Dog.age > 10 && \Dog.age <= 15) ) }, success: { _ in fetchDone.fulfill() }, failure: { _ in XCTFail() } ) self.waitAndCheckExpectations() } } @objc dynamic func test_ThatDynamicModelKeyPaths_CanBeCreated() { XCTAssertEqual(String(keyPath: \Animal.species), "species") XCTAssertEqual(String(keyPath: \Dog.species), "species") } @nonobjc func prepareStack(_ dataStack: DataStack, configurations: [ModelConfiguration] = [nil], _ closure: (_ dataStack: DataStack) -> Void) { 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) } }