diff --git a/RealmCrust.xcodeproj/project.pbxproj b/RealmCrust.xcodeproj/project.pbxproj index 0880b29..9ddd98e 100644 --- a/RealmCrust.xcodeproj/project.pbxproj +++ b/RealmCrust.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 173E0D821C6C27CB00A9E9B3 /* PrimaryKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173E0D811C6C27CB00A9E9B3 /* PrimaryKeyTests.swift */; }; 175C9A611C36201F00B0D6E7 /* RealmCrust.h in Headers */ = {isa = PBXBuildFile; fileRef = 175C9A601C36201F00B0D6E7 /* RealmCrust.h */; settings = {ATTRIBUTES = (Public, ); }; }; 175C9A681C36201F00B0D6E7 /* RealmCrust.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 175C9A5D1C36201F00B0D6E7 /* RealmCrust.framework */; }; 175C9A781C36247700B0D6E7 /* RealmMappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175C9A771C36247700B0D6E7 /* RealmMappings.swift */; }; @@ -34,6 +35,7 @@ /* Begin PBXFileReference section */ 0E234D4DFBBC77280B5A7F0C /* Pods-RealmCrustTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RealmCrustTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RealmCrustTests/Pods-RealmCrustTests.debug.xcconfig"; sourceTree = ""; }; + 173E0D811C6C27CB00A9E9B3 /* PrimaryKeyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrimaryKeyTests.swift; sourceTree = ""; }; 175C9A5D1C36201F00B0D6E7 /* RealmCrust.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RealmCrust.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 175C9A601C36201F00B0D6E7 /* RealmCrust.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RealmCrust.h; sourceTree = ""; }; 175C9A621C36201F00B0D6E7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -117,6 +119,7 @@ 175C9A7E1C38547700B0D6E7 /* EmployeeMappingTests.swift */, 175C9A7F1C38547700B0D6E7 /* EmployeeStub.swift */, 175C9A871C38548000B0D6E7 /* Operators.swift */, + 173E0D811C6C27CB00A9E9B3 /* PrimaryKeyTests.swift */, 175C9A891C38548000B0D6E7 /* RealmMappingTest.swift */, 175C9A6E1C36201F00B0D6E7 /* Info.plist */, ); @@ -346,6 +349,7 @@ 175C9A8E1C38548000B0D6E7 /* RealmMappingTest.swift in Sources */, 175C9A841C38547700B0D6E7 /* Employee.swift in Sources */, 175C9A821C38547700B0D6E7 /* CompanyStub.swift in Sources */, + 173E0D821C6C27CB00A9E9B3 /* PrimaryKeyTests.swift in Sources */, 175C9A861C38547700B0D6E7 /* EmployeeStub.swift in Sources */, 175C9A811C38547700B0D6E7 /* CompanyMappingTests.swift in Sources */, 175C9A851C38547700B0D6E7 /* EmployeeMappingTests.swift in Sources */, diff --git a/RealmCrust/RealmMappings.swift b/RealmCrust/RealmMappings.swift index 1428a5c..598ef46 100644 --- a/RealmCrust/RealmMappings.swift +++ b/RealmCrust/RealmMappings.swift @@ -4,12 +4,14 @@ import RealmSwift public class RealmAdaptor { public typealias BaseType = Object - public typealias ResultsType = Results + public typealias ResultsType = Set var realm: Realm + var cache: NSMutableSet public init(realm: Realm) { self.realm = realm + self.cache = [] } public convenience init() throws { @@ -22,22 +24,26 @@ public class RealmAdaptor { public func mappingEnded() throws { try self.realm.commitWrite() + self.cache.removeAllObjects() } public func mappingErrored(error: ErrorType) { if self.realm.inWriteTransaction { self.realm.cancelWrite() } + self.cache.removeAllObjects() } public func createObject(objType: BaseType.Type) throws -> BaseType { let obj = objType.init() + self.cache.addObject(obj) return obj } public func saveObjects(objects: [BaseType]) throws { let saveBlock = { for obj in objects { + self.cache.removeObject(obj) self.realm.add(objects, update: obj.dynamicType.primaryKey() != nil) } } @@ -50,6 +56,7 @@ public class RealmAdaptor { public func deleteObject(obj: BaseType) throws { let deleteBlock = { + self.cache.removeObject(obj) self.realm.delete(obj) } if self.realm.inWriteTransaction { @@ -74,12 +81,17 @@ public class RealmAdaptor { public func fetchObjectsWithType(type: BaseType.Type, predicate: NSPredicate) -> ResultsType? { + let objects = self.cache.filteredSetUsingPredicate(predicate) + if objects.count > 0 { + return objects as? ResultsType + } + if type.primaryKey() != nil { // We're going to build an unstored object and update when saving based on the primary key. return nil } - return realm.objects(type).filter(predicate) + return Set(realm.objects(type).filter(predicate)) } } diff --git a/RealmCrustTests/PrimaryKeyTests.swift b/RealmCrustTests/PrimaryKeyTests.swift new file mode 100644 index 0000000..a4dc01b --- /dev/null +++ b/RealmCrustTests/PrimaryKeyTests.swift @@ -0,0 +1,112 @@ +import XCTest +import Crust +import JSONValueRX +import RealmCrust +import RealmSwift + +class PrimaryObj1: Object { + let class2s = List() + dynamic var uuid: String = "" + + override class func primaryKey() -> String? { + return "uuid" + } +} + +class PrimaryObj2: Object { + dynamic var uuid: String = "" + dynamic var class1: PrimaryObj1? + + override class func primaryKey() -> String? { + return "uuid" + } +} + +class PrimaryObj1Mapping : RealmMapping { + + var adaptor: RealmAdaptor + var primaryKeys: Dictionary? { + return [ "uuid" : "data.uuid" ] + } + + required init(adaptor: RealmAdaptor) { + self.adaptor = adaptor + } + + func mapping(inout tomap: PrimaryObj1, context: MappingContext) { + let obj2Mapping = PrimaryObj2Mapping(adaptor: self.adaptor) + + tomap.class2s <- KeyExtensions.Mapping("class2s", obj2Mapping) >*< + tomap.uuid <- "data.uuid" >*< + context + } +} + +// Until we support optional mappings, have to make a nested version. +class NestedPrimaryObj1Mapping : RealmMapping { + + var adaptor: RealmAdaptor + var primaryKeys: Dictionary? { + return [ "uuid" : "data.uuid" ] + } + + required init(adaptor: RealmAdaptor) { + self.adaptor = adaptor + } + + func mapping(inout tomap: PrimaryObj1, context: MappingContext) { + tomap.uuid <- "data.uuid" >*< + context + } +} + +class PrimaryObj2Mapping : RealmMapping { + + var adaptor: RealmAdaptor + var primaryKeys: Dictionary? { + return [ "uuid" : "data.more_data.uuid" ] + } + + required init(adaptor: RealmAdaptor) { + self.adaptor = adaptor + } + + func mapping(inout tomap: PrimaryObj2, context: MappingContext) { + // TODO: Including this mapping fails. Need to support making some mappings as optional + // so when the recursive cycle of json between these two relationships runs out it doesn't error + // from expecting json. + // + // In the meantime, user can write separate Nested Mappings for the top level object and nested objects. +// let obj1Mapping = PrimaryObj1Mapping(adaptor: self.adaptor) + + let obj1Mapping = NestedPrimaryObj1Mapping(adaptor: self.adaptor) + + tomap.class1 <- KeyExtensions.Mapping("class1", obj1Mapping) >*< + tomap.uuid <- "data.more_data.uuid" >*< + context + } +} + +class PrimaryKeyTests: RealmMappingTest { + + func testMappingsWithPrimaryKeys() { + + var json1Dict = [ "data" : [ "uuid" : "primary1" ] ] as [ String : AnyObject ] + let json2Dict1 = [ "data.more_data.uuid" : "primary2.1", "class1" : json1Dict ] + let json2Dict2 = [ "data.more_data.uuid" : "primary2.2", "class1" : json1Dict ] + + json1Dict["class2s"] = [ json2Dict1, json2Dict2 ] + + XCTAssertEqual(realm!.objects(PrimaryObj1).count, 0) + XCTAssertEqual(realm!.objects(PrimaryObj2).count, 0) + + let json = try! JSONValue(object: json1Dict) + let mapper = CRMapper() + let object = try! mapper.mapFromJSONToExistingObject(json, mapping: PrimaryObj1Mapping(adaptor: adaptor!)) + + XCTAssertEqual(realm!.objects(PrimaryObj1).count, 1) + XCTAssertEqual(realm!.objects(PrimaryObj2).count, 2) + XCTAssertEqual(object.uuid, "primary1") + XCTAssertEqual(object.class2s.count, 2) + } +}