Skip to content

Commit a424908

Browse files
committed
Expand test coverage and fix all known issues with managed object refetching and deletion. closes RestKit#1066
1 parent 406504f commit a424908

File tree

4 files changed

+133
-16
lines changed

4 files changed

+133
-16
lines changed

Code/Network/RKManagedObjectRequestOperation.m

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors
5555
if (self) {
5656
self.mutableKeyPaths = [NSMutableSet set];
5757
for (RKResponseDescriptor *responseDescriptor in responseDescriptors) {
58-
[self visitMapping:responseDescriptor.mapping atKeyPath:responseDescriptor.keyPath ?: @""];
58+
[self visitMapping:responseDescriptor.mapping atKeyPath:responseDescriptor.keyPath];
5959
}
6060
}
6161
return self;
@@ -68,10 +68,11 @@ - (NSSet *)keyPaths
6868

6969
- (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
7070
{
71-
if ([self.keyPaths containsObject:keyPath]) return;
71+
id actualKeyPath = keyPath ?: [NSNull null];
72+
if ([self.keyPaths containsObject:actualKeyPath]) return;
7273

7374
if ([mapping isKindOfClass:[RKEntityMapping class]]) {
74-
[self.mutableKeyPaths addObject:keyPath];
75+
[self.mutableKeyPaths addObject:actualKeyPath];
7576
} else {
7677
if ([mapping isKindOfClass:[RKDynamicMapping class]]) {
7778
RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)mapping;
@@ -81,7 +82,7 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
8182
} else if ([mapping isKindOfClass:[RKObjectMapping class]]) {
8283
RKObjectMapping *objectMapping = (RKObjectMapping *)mapping;
8384
for (RKRelationshipMapping *relationshipMapping in objectMapping.relationshipMappings) {
84-
NSString *nestedKeyPath = [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."];
85+
NSString *nestedKeyPath = keyPath ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath;
8586
[self visitMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath];
8687
}
8788
}
@@ -101,16 +102,30 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
101102
return fetchRequests;
102103
}
103104

105+
// When we map the root object, it is returned under the key `[NSNull null]`
106+
static id RKMappedValueForKeyPathInDictionary(NSString *keyPath, NSDictionary *dictionary)
107+
{
108+
return ([keyPath isEqual:[NSNull null]]) ? [dictionary objectForKey:[NSNull null]] : [dictionary valueForKeyPath:keyPath];
109+
}
110+
111+
static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath, NSMutableDictionary *dictionary)
112+
{
113+
[keyPath isEqual:[NSNull null]] ? [dictionary setObject:value forKey:keyPath] : [dictionary setValue:value forKeyPath:keyPath];
114+
}
115+
104116
// Finds the key paths for all entity mappings in the graph whose parent objects are not other managed objects
105117
static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSSet *keyPaths, NSManagedObjectContext *managedObjectContext)
106118
{
119+
if (! [dictionaryOfManagedObjects count]) return dictionaryOfManagedObjects;
107120
NSMutableDictionary *newDictionary = [dictionaryOfManagedObjects mutableCopy];
108121
[managedObjectContext performBlockAndWait:^{
109122
__block NSError *error = nil;
110123

111124
for (NSString *keyPath in keyPaths) {
112-
id value = [dictionaryOfManagedObjects valueForKeyPath:keyPath];
113-
if ([value isKindOfClass:[NSArray class]]) {
125+
id value = RKMappedValueForKeyPathInDictionary(keyPath, dictionaryOfManagedObjects);
126+
if (! value) {
127+
continue;
128+
} else if ([value isKindOfClass:[NSArray class]]) {
114129
BOOL isMutable = [value isKindOfClass:[NSMutableArray class]];
115130
NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]];
116131
for (__strong id object in value) {
@@ -159,7 +174,7 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
159174
NSCAssert(value, @"Failed to find existing object with ID %@ in context %@: %@", [value objectID], managedObjectContext, error);
160175
}
161176

162-
[newDictionary setValue:value forKeyPath:keyPath];
177+
RKSetMappedValueForKeyPathInDictionary(value, keyPath, newDictionary);
163178
}
164179
}];
165180

@@ -359,7 +374,7 @@ - (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result atK
359374
NSMutableSet *managedObjectsInMappingResult = [NSMutableSet set];
360375
NSDictionary *mappingResultDictionary = result.dictionary;
361376
for (NSString *keyPath in keyPaths) {
362-
id managedObjects = [mappingResultDictionary valueForKeyPath:keyPath];
377+
id managedObjects = RKMappedValueForKeyPathInDictionary(keyPath, mappingResultDictionary);
363378
if (! managedObjects) {
364379
continue;
365380
} else if ([managedObjects isKindOfClass:[NSManagedObject class]]) {
@@ -368,6 +383,8 @@ - (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result atK
368383
[managedObjectsInMappingResult unionSet:managedObjects];
369384
} else if ([managedObjects isKindOfClass:[NSArray class]]) {
370385
[managedObjectsInMappingResult addObjectsFromArray:managedObjects];
386+
} else if ([managedObjects isKindOfClass:[NSOrderedSet class]]) {
387+
[managedObjectsInMappingResult addObjectsFromArray:[managedObjects array]];
371388
} else {
372389
[NSException raise:NSInternalInconsistencyException format:@"Unexpected object type '%@' encountered at keyPath '%@': Expected an `NSManagedObject`, `NSArray`, or `NSSet`.", [managedObjects class], keyPath];
373390
}

Code/ObjectMapping/RKDynamicMappingMatcher.m

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ @implementation RKDynamicMappingMatcher
2121

2222
- (id)initWithKeyPath:(NSString *)keyPath expectedValue:(id)expectedValue objectMapping:(RKObjectMapping *)objectMapping
2323
{
24+
NSParameterAssert(keyPath);
25+
NSParameterAssert(expectedValue);
26+
NSParameterAssert(objectMapping);
2427
self = [super init];
2528
if (self) {
2629
self.keyPath = keyPath;
@@ -33,7 +36,9 @@ - (id)initWithKeyPath:(NSString *)keyPath expectedValue:(id)expectedValue object
3336

3437
- (BOOL)matches:(id)object
3538
{
36-
return RKObjectIsEqualToObject([object valueForKeyPath:self.keyPath], self.expectedValue);
39+
id value = [object valueForKeyPath:self.keyPath];
40+
if (value == nil) return NO;
41+
return RKObjectIsEqualToObject(value, self.expectedValue);
3742
}
3843

3944
- (NSString *)description

RestKit.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,12 +363,12 @@
363363
2543A25E1664FD3200821D5B /* RKResponseDescriptorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25CC5C58161DDADD0008BD21 /* RKResponseDescriptorTest.m */; };
364364
2546A95816628EDD0078E044 /* RKConnectionDescriptionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2546A95716628EDD0078E044 /* RKConnectionDescriptionTest.m */; };
365365
2546A95916628EDD0078E044 /* RKConnectionDescriptionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2546A95716628EDD0078E044 /* RKConnectionDescriptionTest.m */; };
366-
2548AC6D162F5E00009E79BF /* RKManagedObjectRequestOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2548AC6C162F5E00009E79BF /* RKManagedObjectRequestOperationTest.m */; };
367366
2548AC6E162F5E00009E79BF /* RKManagedObjectRequestOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2548AC6C162F5E00009E79BF /* RKManagedObjectRequestOperationTest.m */; };
368367
2549D646162B376F003DD135 /* RKRequestDescriptorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2549D645162B376F003DD135 /* RKRequestDescriptorTest.m */; };
369368
2549D647162B376F003DD135 /* RKRequestDescriptorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2549D645162B376F003DD135 /* RKRequestDescriptorTest.m */; };
370369
2551338F167838590017E4B6 /* RKHTTPRequestOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2551338E167838590017E4B6 /* RKHTTPRequestOperationTest.m */; };
371370
25513390167838590017E4B6 /* RKHTTPRequestOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2551338E167838590017E4B6 /* RKHTTPRequestOperationTest.m */; };
371+
255133CF167AC7600017E4B6 /* RKManagedObjectRequestOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2548AC6C162F5E00009E79BF /* RKManagedObjectRequestOperationTest.m */; };
372372
25565956161FC3C300F5BB20 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25565955161FC3C300F5BB20 /* CoreServices.framework */; };
373373
25565959161FC3CD00F5BB20 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25565958161FC3CD00F5BB20 /* SystemConfiguration.framework */; };
374374
25565965161FDD8800F5BB20 /* RKResponseMapperOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25565964161FDD8800F5BB20 /* RKResponseMapperOperationTest.m */; };
@@ -2352,12 +2352,12 @@
23522352
252205CC162B242400F7B11E /* RKHTTPUtilitiesTest.m in Sources */,
23532353
2549D646162B376F003DD135 /* RKRequestDescriptorTest.m in Sources */,
23542354
2506759F162DEA25003210B0 /* RKEntityMappingTest.m in Sources */,
2355-
2548AC6D162F5E00009E79BF /* RKManagedObjectRequestOperationTest.m in Sources */,
23562355
255F87911656B22D00914D57 /* RKPaginatorTest.m in Sources */,
23572356
2546A95816628EDD0078E044 /* RKConnectionDescriptionTest.m in Sources */,
23582357
2543A25D1664FD3100821D5B /* RKResponseDescriptorTest.m in Sources */,
23592358
2536D1FD167270F100DF9BB0 /* RKRouterTest.m in Sources */,
23602359
2551338F167838590017E4B6 /* RKHTTPRequestOperationTest.m in Sources */,
2360+
255133CF167AC7600017E4B6 /* RKManagedObjectRequestOperationTest.m in Sources */,
23612361
);
23622362
runOnlyForDeploymentPostprocessing = 0;
23632363
};

Tests/Logic/Network/RKManagedObjectRequestOperationTest.m

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -291,10 +291,105 @@ - (void)testThatManagedObjectMappedAsTheRelationshipOfNonManagedObjectsAreRefetc
291291
expect([[testUser.friends lastObject] managedObjectContext]).to.equal(managedObjectStore.persistentStoreManagedObjectContext);
292292
}
293293

294-
// TODO: Test with dynamic mapping
295-
// TODO: Test with nil root key path
296-
// TODO: Test with NSSet
297-
// TODO: Test with NSOrderedSet
298-
// TODO: Test with NSSet
294+
- (void)testThatManagedObjectMappedAsTheRelationshipOfNonManagedObjectsWithADynamicMappingAreRefetchedFromTheParentContext
295+
{
296+
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
297+
RKObjectMapping *userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]];
298+
RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore];
299+
[entityMapping addAttributeMappingsFromArray:@[ @"name" ]];
300+
[userMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"favorite_cat" toKeyPath:@"friends" withMapping:entityMapping]];
301+
RKDynamicMapping *dynamicMapping = [RKDynamicMapping new];
302+
[dynamicMapping setObjectMapping:userMapping whenValueOfKeyPath:@"name" isEqualTo:@"Blake Watters"];
303+
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping pathPattern:nil keyPath:@"human" statusCodes:[NSIndexSet indexSetWithIndex:200]];
304+
305+
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/with_to_one_relationship.json" relativeToURL:[RKTestFactory baseURL]]];
306+
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]];
307+
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
308+
[managedObjectRequestOperation start];
309+
expect(managedObjectRequestOperation.error).to.beNil();
310+
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
311+
RKTestUser *testUser = [managedObjectRequestOperation.mappingResult firstObject];
312+
expect([[testUser.friends lastObject] managedObjectContext]).to.equal(managedObjectStore.persistentStoreManagedObjectContext);
313+
}
314+
315+
- (void)testThatManagedObjectMappedFromRootKeyPathAreRefetchedFromTheParentContext
316+
{
317+
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
318+
RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore];
319+
[entityMapping addAttributeMappingsFromDictionary:@{ @"human.name": @"name" }];
320+
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:entityMapping pathPattern:nil keyPath:nil statusCodes:[NSIndexSet indexSetWithIndex:200]];
321+
322+
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/with_to_one_relationship.json" relativeToURL:[RKTestFactory baseURL]]];
323+
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]];
324+
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
325+
[managedObjectRequestOperation start];
326+
expect(managedObjectRequestOperation.error).to.beNil();
327+
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
328+
NSManagedObject *managedObject = [managedObjectRequestOperation.mappingResult firstObject];
329+
expect([managedObject managedObjectContext]).to.equal(managedObjectStore.persistentStoreManagedObjectContext);
330+
}
331+
332+
- (void)testThatManagedObjectMappedToNSSetRelationshipOfNonManagedObjectsAreRefetchedFromTheParentContext
333+
{
334+
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
335+
RKObjectMapping *userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]];
336+
RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore];
337+
[entityMapping addAttributeMappingsFromArray:@[ @"name" ]];
338+
[userMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"favorite_cat" toKeyPath:@"friendsSet" withMapping:entityMapping]];
339+
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:userMapping pathPattern:nil keyPath:@"human" statusCodes:[NSIndexSet indexSetWithIndex:200]];
340+
341+
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/with_to_one_relationship.json" relativeToURL:[RKTestFactory baseURL]]];
342+
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]];
343+
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
344+
[managedObjectRequestOperation start];
345+
expect(managedObjectRequestOperation.error).to.beNil();
346+
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
347+
RKTestUser *testUser = [managedObjectRequestOperation.mappingResult firstObject];
348+
expect([[testUser.friendsSet anyObject] managedObjectContext]).to.equal(managedObjectStore.persistentStoreManagedObjectContext);
349+
}
350+
351+
- (void)testThatManagedObjectMappedToNSOrderedSetRelationshipOfNonManagedObjectsAreRefetchedFromTheParentContext
352+
{
353+
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
354+
RKObjectMapping *userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]];
355+
RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore];
356+
[entityMapping addAttributeMappingsFromArray:@[ @"name" ]];
357+
[userMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"favorite_cat" toKeyPath:@"friendsOrderedSet" withMapping:entityMapping]];
358+
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:userMapping pathPattern:nil keyPath:@"human" statusCodes:[NSIndexSet indexSetWithIndex:200]];
359+
360+
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/with_to_one_relationship.json" relativeToURL:[RKTestFactory baseURL]]];
361+
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]];
362+
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
363+
[managedObjectRequestOperation start];
364+
expect(managedObjectRequestOperation.error).to.beNil();
365+
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
366+
RKTestUser *testUser = [managedObjectRequestOperation.mappingResult firstObject];
367+
expect([[testUser.friendsOrderedSet firstObject] managedObjectContext]).to.equal(managedObjectStore.persistentStoreManagedObjectContext);
368+
}
369+
370+
- (void)testDeletionOfOrphanedObjectsMappedOnRelationships
371+
{
372+
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
373+
RKHuman *orphanedHuman = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
374+
RKObjectMapping *userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]];
375+
RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore];
376+
[entityMapping addAttributeMappingsFromArray:@[ @"name" ]];
377+
[userMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"favorite_cat" toKeyPath:@"friends" withMapping:entityMapping]];
378+
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:userMapping pathPattern:nil keyPath:@"human" statusCodes:[NSIndexSet indexSetWithIndex:200]];
379+
380+
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/with_to_one_relationship.json" relativeToURL:[RKTestFactory baseURL]]];
381+
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]];
382+
RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) {
383+
return [NSFetchRequest fetchRequestWithEntityName:@"Human"];
384+
};
385+
managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ];
386+
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
387+
[managedObjectRequestOperation start];
388+
expect(managedObjectRequestOperation.error).to.beNil();
389+
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
390+
expect(orphanedHuman.managedObjectContext).to.beNil();
391+
}
392+
393+
// TODO: test deletion of nested objects
299394

300395
@end

0 commit comments

Comments
 (0)