diff --git a/Code/CoreData/RKRelationshipConnectionOperation.m b/Code/CoreData/RKRelationshipConnectionOperation.m index 301e769d..52a6b547 100644 --- a/Code/CoreData/RKRelationshipConnectionOperation.m +++ b/Code/CoreData/RKRelationshipConnectionOperation.m @@ -25,11 +25,18 @@ #import "RKManagedObjectCaching.h" #import "RKDynamicMappingMatcher.h" #import "RKErrors.h" +#import "RKObjectUtilities.h" // Set Logging Component #undef RKLogComponent #define RKLogComponent RKlcl_cRestKitCoreData +static id RKMutableSetValueForRelationship(NSRelationshipDescription *relationship) +{ + if (! [relationship isToMany]) return nil; + return [relationship isOrdered] ? [NSMutableOrderedSet orderedSet] : [NSMutableSet set]; +} + @interface RKRelationshipConnectionOperation () @property (nonatomic, strong, readwrite) NSManagedObject *managedObject; @property (nonatomic, strong, readwrite) RKConnectionMapping *connectionMapping; @@ -81,18 +88,15 @@ // NOTE: This is a nasty hack to work around the fact that NSOrderedSet does not support key-value // collection operators. We try to detect and unpack a doubly wrapped collection - if ([self.connectionMapping.relationship isOrdered] - && [result conformsToProtocol:@protocol(NSFastEnumeration)] - && [[result lastObject] conformsToProtocol:@protocol(NSFastEnumeration)]) { - - NSMutableOrderedSet *set = [NSMutableOrderedSet orderedSet]; + if ([self.connectionMapping.relationship isToMany] && RKObjectIsCollectionOfCollections(result)) { + id mutableSet = RKMutableSetValueForRelationship(self.connectionMapping.relationship); for (id enumerable in result) { for (id object in enumerable) { - [set addObject:object]; + [mutableSet addObject:object]; } } - return set; + return mutableSet; } if ([self.connectionMapping.relationship isToMany]) { @@ -108,6 +112,12 @@ } else { return result; } + } else if ([result isKindOfClass:[NSOrderedSet class]]) { + if ([self.connectionMapping.relationship isOrdered]) { + return result; + } else { + return [(NSOrderedSet *)result set]; + } } else { if ([self.connectionMapping.relationship isOrdered]) { return [NSOrderedSet orderedSetWithObject:result]; diff --git a/Code/ObjectMapping/RKConnectionMapping.h b/Code/ObjectMapping/RKConnectionMapping.h index e83aa88e..1d75c85b 100644 --- a/Code/ObjectMapping/RKConnectionMapping.h +++ b/Code/ObjectMapping/RKConnectionMapping.h @@ -130,6 +130,9 @@ // // @return A new instance of a RKObjectConnectionMapping. // */ -- (id)initWithRelationship:(NSRelationshipDescription *)relationship sourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath matcher:(RKDynamicMappingMatcher *)matcher; +- (id)initWithRelationship:(NSRelationshipDescription *)relationship + sourceKeyPath:(NSString *)sourceKeyPath + destinationKeyPath:(NSString *)destinationKeyPath + matcher:(RKDynamicMappingMatcher *)matcher; @end diff --git a/Code/ObjectMapping/RKConnectionMapping.m b/Code/ObjectMapping/RKConnectionMapping.m index bd2f7989..9e316846 100644 --- a/Code/ObjectMapping/RKConnectionMapping.m +++ b/Code/ObjectMapping/RKConnectionMapping.m @@ -57,14 +57,13 @@ { NSParameterAssert(relationship); NSParameterAssert(sourceKeyPath); - NSParameterAssert(destinationKeyPath); Class connectionClass = [self connectionMappingClassForRelationship:relationship sourceKeyPath:sourceKeyPath destinationKeyPath:destinationKeyPath]; self = [[connectionClass alloc] init]; if (self) { self.relationship = relationship; self.sourceKeyPath = sourceKeyPath; - self.destinationKeyPath = destinationKeyPath; + self.destinationKeyPath = destinationKeyPath ?: [relationship name]; self.matcher = matcher; } diff --git a/Code/ObjectMapping/RKMappingOperation.m b/Code/ObjectMapping/RKMappingOperation.m index 9723f891..1e832b55 100644 --- a/Code/ObjectMapping/RKMappingOperation.m +++ b/Code/ObjectMapping/RKMappingOperation.m @@ -481,10 +481,7 @@ static BOOL RKIsManagedObject(id object) RKLogDebug(@"Mapping one to many relationship value at keyPath '%@' to '%@'", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath); NSMutableArray *relationshipCollection = [NSMutableArray arrayWithCapacity:[value count]]; - id collectionSanityCheckObject = nil; - if ([value respondsToSelector:@selector(anyObject)]) collectionSanityCheckObject = [value anyObject]; - if ([value respondsToSelector:@selector(lastObject)]) collectionSanityCheckObject = [value lastObject]; - if (RKObjectIsCollection(collectionSanityCheckObject)) { + if (RKObjectIsCollectionOfCollections(value)) { RKLogWarning(@"WARNING: Detected a relationship mapping for a collection containing another collection. This is probably not what you want. Consider using a KVC collection operator (such as @unionOfArrays) to flatten your mappable collection."); RKLogWarning(@"Key path '%@' yielded collection containing another collection rather than a collection of objects: %@", relationshipMapping.sourceKeyPath, value); } diff --git a/Code/ObjectMapping/RKObjectUtilities.h b/Code/ObjectMapping/RKObjectUtilities.h index adc94924..37e7b777 100644 --- a/Code/ObjectMapping/RKObjectUtilities.h +++ b/Code/ObjectMapping/RKObjectUtilities.h @@ -69,6 +69,14 @@ BOOL RKObjectIsCollection(id object); */ BOOL RKObjectIsCollectionContainingOnlyManagedObjects(id object); +/** + Returns a Boolean value that indicates if the given object is a collection containing subcollections. + + @param object The object to be tested. + @return `YES` if the object is a collection of collections, else `NO`. + */ +BOOL RKObjectIsCollectionOfCollections(id object); + /** Returns an appropriate class to use for KVC access based on the Objective C runtime type encoding. diff --git a/Code/ObjectMapping/RKObjectUtilities.m b/Code/ObjectMapping/RKObjectUtilities.m index 612cf4c8..adee8548 100644 --- a/Code/ObjectMapping/RKObjectUtilities.m +++ b/Code/ObjectMapping/RKObjectUtilities.m @@ -72,6 +72,14 @@ BOOL RKObjectIsCollectionContainingOnlyManagedObjects(id object) return YES; } +BOOL RKObjectIsCollectionOfCollections(id object) +{ + if (! RKObjectIsCollection(object)) return NO; + id collectionSanityCheckObject = nil; + if ([object respondsToSelector:@selector(anyObject)]) collectionSanityCheckObject = [object anyObject]; + if ([object respondsToSelector:@selector(lastObject)]) collectionSanityCheckObject = [object lastObject]; + return RKObjectIsCollection(collectionSanityCheckObject); +} Class RKKeyValueCodingClassForObjCType(const char *type) { diff --git a/Tests/Logic/CoreData/RKRelationshipConnectionOperationTest.m b/Tests/Logic/CoreData/RKRelationshipConnectionOperationTest.m index 05075681..a3966c72 100644 --- a/Tests/Logic/CoreData/RKRelationshipConnectionOperationTest.m +++ b/Tests/Logic/CoreData/RKRelationshipConnectionOperationTest.m @@ -9,6 +9,8 @@ #import "RKTestEnvironment.h" #import "RKHuman.h" #import "RKCat.h" +#import "RKHouse.h" +#import "RKResident.h" #import "RKRelationshipConnectionOperation.h" #import "RKFetchRequestManagedObjectCache.h" @@ -85,4 +87,121 @@ assertThat(human.cats, is(empty())); } +#pragma mark - Key Path Connections + +- (void)testConnectingToOneRelationshipViaKeyPath +{ + NSEntityDescription *entity = [[[RKTestFactory managedObjectStore] managedObjectModel] entitiesByName][@"RKHuman"]; + NSRelationshipDescription *relationship = [entity relationshipsByName][@"landlord"]; + RKConnectionMapping *connectionMapping = [[RKConnectionMapping alloc] initWithRelationship:relationship sourceKeyPath:@"residence.owner" destinationKeyPath:nil matcher:nil]; + + RKHuman *tenant = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + RKHuman *homeowner = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + RKHouse *house = [RKTestFactory insertManagedObjectForEntityForName:@"RKHouse" inManagedObjectContext:nil withProperties:nil]; + house.owner = homeowner; + tenant.residence = house; + + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:tenant connectionMapping:connectionMapping managedObjectCache:managedObjectCache]; + [operation start]; + + expect(tenant.landlord).to.equal(homeowner); +} + +- (void)testConnectingToManyRelationshipViaKeyPath +{ + NSEntityDescription *entity = [[[RKTestFactory managedObjectStore] managedObjectModel] entitiesByName][@"RKHuman"]; + NSRelationshipDescription *relationship = [entity relationshipsByName][@"roommates"]; + RKConnectionMapping *connectionMapping = [[RKConnectionMapping alloc] initWithRelationship:relationship sourceKeyPath:@"house.residents" destinationKeyPath:nil matcher:nil]; + + RKHuman *human = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + RKHouse *house = [RKTestFactory insertManagedObjectForEntityForName:@"RKHouse" inManagedObjectContext:nil withProperties:nil]; + RKResident *resident1 = [RKTestFactory insertManagedObjectForEntityForName:@"RKResident" inManagedObjectContext:nil withProperties:nil]; + RKResident *resident2 = [RKTestFactory insertManagedObjectForEntityForName:@"RKResident" inManagedObjectContext:nil withProperties:nil]; + + human.house = house; + house.residents = [NSSet setWithObjects:resident1, resident2, nil]; + + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:human connectionMapping:connectionMapping managedObjectCache:managedObjectCache]; + [operation start]; + + NSSet *expectedRoommates = [NSSet setWithObjects:resident1, resident2, nil]; + expect(human.roommates).to.equal(expectedRoommates); +} + +- (void)testConnectingAcrossToManyRelationshipsViaKeyPath +{ + NSEntityDescription *entity = [[[RKTestFactory managedObjectStore] managedObjectModel] entitiesByName][@"RKHuman"]; + NSRelationshipDescription *relationship = [entity relationshipsByName][@"friends"]; + RKConnectionMapping *connectionMapping = [[RKConnectionMapping alloc] initWithRelationship:relationship sourceKeyPath:@"housesResidedAt.ownersInChronologicalOrder" destinationKeyPath:nil matcher:nil]; + + RKHuman *human = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + + // Create 2 houses with 2 previous owners + RKHouse *house1 = [RKTestFactory insertManagedObjectForEntityForName:@"RKHouse" inManagedObjectContext:nil withProperties:nil]; + RKHuman *homeowner1 = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + RKHuman *homeowner2 = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + house1.ownersInChronologicalOrder = [NSOrderedSet orderedSetWithObjects:homeowner1, homeowner2, nil]; + + RKHouse *house2 = [RKTestFactory insertManagedObjectForEntityForName:@"RKHouse" inManagedObjectContext:nil withProperties:nil]; + RKHuman *homeowner3 = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + RKHuman *homeowner4 = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + house2.ownersInChronologicalOrder = [NSOrderedSet orderedSetWithObjects:homeowner3, homeowner4, nil]; + + human.housesResidedAt = [NSOrderedSet orderedSetWithObjects:house1, house2, nil]; + + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:human connectionMapping:connectionMapping managedObjectCache:managedObjectCache]; + [operation start]; + + NSSet *expectedFriends = [NSSet setWithObjects:homeowner1, homeowner2, homeowner3, homeowner4, nil]; + expect(human.friends).to.haveCountOf(4); + expect(human.friends).to.equal(expectedFriends); +} + +- (void)testConnectingToManyOrderedSetRelationshipViaKeyPath +{ + NSEntityDescription *entity = [[[RKTestFactory managedObjectStore] managedObjectModel] entitiesByName][@"RKHuman"]; + NSRelationshipDescription *relationship = [entity relationshipsByName][@"friendsInTheOrderWeMet"]; + RKConnectionMapping *connectionMapping = [[RKConnectionMapping alloc] initWithRelationship:relationship sourceKeyPath:@"housesResidedAt.ownersInChronologicalOrder" destinationKeyPath:nil matcher:nil]; + + RKHuman *human = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + + // Create 2 houses with 2 previous owners + RKHouse *house1 = [RKTestFactory insertManagedObjectForEntityForName:@"RKHouse" inManagedObjectContext:nil withProperties:nil]; + RKHuman *homeowner1 = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + RKHuman *homeowner2 = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + house1.ownersInChronologicalOrder = [NSOrderedSet orderedSetWithObjects:homeowner1, homeowner2, nil]; + + RKHouse *house2 = [RKTestFactory insertManagedObjectForEntityForName:@"RKHouse" inManagedObjectContext:nil withProperties:nil]; + RKHuman *homeowner3 = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + RKHuman *homeowner4 = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + house2.ownersInChronologicalOrder = [NSOrderedSet orderedSetWithObjects:homeowner3, homeowner4, nil]; + + human.housesResidedAt = [NSOrderedSet orderedSetWithObjects:house1, house2, nil]; + + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:human connectionMapping:connectionMapping managedObjectCache:managedObjectCache]; + [operation start]; + + NSOrderedSet *expectedFriends = [NSOrderedSet orderedSetWithObjects:homeowner1, homeowner2, homeowner3, homeowner4, nil]; + expect(human.friendsInTheOrderWeMet).to.equal(expectedFriends); +} + +- (void)testConnectingToManyOrderedSetRelationshipWithEmptyTargetViaKeyPath +{ + NSEntityDescription *entity = [[[RKTestFactory managedObjectStore] managedObjectModel] entitiesByName][@"RKHuman"]; + NSRelationshipDescription *relationship = [entity relationshipsByName][@"friendsInTheOrderWeMet"]; + RKConnectionMapping *connectionMapping = [[RKConnectionMapping alloc] initWithRelationship:relationship sourceKeyPath:@"housesResidedAt.ownersInChronologicalOrder" destinationKeyPath:nil matcher:nil]; + + RKHuman *human = [RKTestFactory insertManagedObjectForEntityForName:@"RKHuman" inManagedObjectContext:nil withProperties:nil]; + + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:human connectionMapping:connectionMapping managedObjectCache:managedObjectCache]; + [operation start]; + + expect([human.friendsInTheOrderWeMet set]).to.beEmpty(); +} + @end diff --git a/Tests/Models/Data Model.xcdatamodel/elements b/Tests/Models/Data Model.xcdatamodel/elements index 738988bc..f3d203d1 100644 Binary files a/Tests/Models/Data Model.xcdatamodel/elements and b/Tests/Models/Data Model.xcdatamodel/elements differ diff --git a/Tests/Models/Data Model.xcdatamodel/layout b/Tests/Models/Data Model.xcdatamodel/layout index e53af474..c8f6f8a7 100644 Binary files a/Tests/Models/Data Model.xcdatamodel/layout and b/Tests/Models/Data Model.xcdatamodel/layout differ diff --git a/Tests/Models/RKHouse.h b/Tests/Models/RKHouse.h index aadc9d68..ab3e3a11 100644 --- a/Tests/Models/RKHouse.h +++ b/Tests/Models/RKHouse.h @@ -21,9 +21,9 @@ #import -@interface RKHouse : NSManagedObject { +@class RKHuman; -} +@interface RKHouse : NSManagedObject @property (nonatomic, strong) NSString *city; @property (nonatomic, strong) NSDate *createdAt; @@ -34,4 +34,8 @@ @property (nonatomic, strong) NSDate *updatedAt; @property (nonatomic, strong) NSString *zip; +@property (nonatomic, strong) RKHuman *owner; +@property (nonatomic, strong) NSSet *residents; +@property (nonatomic, strong) NSOrderedSet *ownersInChronologicalOrder; + @end diff --git a/Tests/Models/RKHouse.m b/Tests/Models/RKHouse.m index 1f3afdbf..6088c13f 100644 --- a/Tests/Models/RKHouse.m +++ b/Tests/Models/RKHouse.m @@ -32,6 +32,8 @@ @dynamic street; @dynamic updatedAt; @dynamic zip; - +@dynamic owner; +@dynamic residents; +@dynamic ownersInChronologicalOrder; @end diff --git a/Tests/Models/RKHuman.h b/Tests/Models/RKHuman.h index 3fe0b1b4..d320d277 100644 --- a/Tests/Models/RKHuman.h +++ b/Tests/Models/RKHuman.h @@ -20,7 +20,7 @@ #import -@class RKCat; +@class RKCat, RKHouse, RKResident; @interface RKHuman : NSManagedObject @@ -41,6 +41,16 @@ @property (nonatomic, strong) NSArray *catIDs; @property (nonatomic, strong) NSOrderedSet *catsInOrderByAge; +@property (nonatomic, strong) RKHouse *house; +@property (nonatomic, strong) RKHuman *landlord; +@property (nonatomic, strong) NSSet *roommates; +@property (nonatomic, strong) NSSet *tenants; +@property (nonatomic, strong) RKHouse *residence; +@property (nonatomic, strong) NSSet *housesResidedAt; + +@property (nonatomic, strong) NSSet *friends; +@property (nonatomic, strong) NSOrderedSet *friendsInTheOrderWeMet; + @end @interface RKHuman (CoreDataGeneratedAccessors) @@ -48,5 +58,4 @@ - (void)removeCatsObject:(RKCat *)value; - (void)addCats:(NSSet *)value; - (void)removeCats:(NSSet *)value; - @end diff --git a/Tests/Models/RKHuman.m b/Tests/Models/RKHuman.m index 374c10e9..4e641116 100644 --- a/Tests/Models/RKHuman.m +++ b/Tests/Models/RKHuman.m @@ -39,9 +39,13 @@ @dynamic catIDs; @dynamic catsInOrderByAge; -- (NSString *)polymorphicResourcePath -{ - return @"/this/is/the/path"; -} +@dynamic house; +@dynamic landlord; +@dynamic roommates; +@dynamic tenants; +@dynamic residence; +@dynamic housesResidedAt; +@dynamic friends; +@dynamic friendsInTheOrderWeMet; @end