diff --git a/Code/Network/RKManagedObjectRequestOperation.m b/Code/Network/RKManagedObjectRequestOperation.m index 2668123563..393f157434 100644 --- a/Code/Network/RKManagedObjectRequestOperation.m +++ b/Code/Network/RKManagedObjectRequestOperation.m @@ -35,6 +35,23 @@ #undef RKLogComponent #define RKLogComponent RKlcl_cRestKitCoreData +@interface RKMappingGraphVisitation : NSObject +@property (nonatomic, strong) id rootKey; // Will be [NSNull null] or a string value +@property (nonatomic, strong) NSString *keyPath; +@property (nonatomic, assign, getter = isCyclic) BOOL cyclic; +@property (nonatomic, strong) RKMapping *mapping; +@end + +@implementation RKMappingGraphVisitation + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p rootKey=%@ keyPath=%@ isCylic=%@ mapping=%@>", + [self class], self, self.rootKey, self.keyPath, self.isCyclic ? @"YES" : @"NO", self.mapping]; +} + +@end + /** This class implements Tarjan's algorithm to efficiently visit all nodes within the mapping graph and detect cycles in the graph. @@ -47,15 +64,15 @@ */ @interface RKNestedManagedObjectKeyPathMappingGraphVisitor : NSObject -@property (nonatomic, readonly) NSSet *keyPaths; +@property (nonatomic, readonly, strong) NSMutableArray *visitations; - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors; @end @interface RKNestedManagedObjectKeyPathMappingGraphVisitor () -@property (nonatomic, strong) NSMutableSet *mutableKeyPaths; @property (nonatomic, strong) NSMutableArray *visitationStack; @property (nonatomic, strong) NSMutableDictionary *lowValues; @property (nonatomic, strong) NSNumber *numberOfDecriptors; +@property (nonatomic, strong, readwrite) NSMutableArray *visitations; @end @implementation RKNestedManagedObjectKeyPathMappingGraphVisitor @@ -65,37 +82,62 @@ - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors self = [self init]; if (self) { self.numberOfDecriptors = @([responseDescriptors count]); - self.mutableKeyPaths = [NSMutableSet set]; self.visitationStack = [NSMutableArray array]; self.lowValues = [NSMutableDictionary dictionary]; + self.visitations = [NSMutableArray array]; for (RKResponseDescriptor *responseDescriptor in responseDescriptors) { [self visitMapping:responseDescriptor.mapping atKeyPath:responseDescriptor.keyPath]; } } + return self; } +- (RKMappingGraphVisitation *)visitationForMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath +{ + RKMappingGraphVisitation *visitation = [RKMappingGraphVisitation new]; + visitation.mapping = mapping; + if ([self.visitationStack count] == 0) { + // If we are the first item in the stack, we are visiting the rootKey + visitation.rootKey = keyPath ?: [NSNull null]; + } else { + // Take the root key from the visitation stack + visitation.rootKey = [[self.visitationStack objectAtIndex:0] rootKey]; + visitation.keyPath = keyPath; + } + + return visitation; +} + // Traverse the mappings graph using Tarjan's algorithm - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath { NSValue *dictionaryKey = [NSValue valueWithNonretainedObject:mapping]; if ([self.lowValues objectForKey:dictionaryKey]) { - // This key path points to a cycle back into the graph - if ([mapping isKindOfClass:[RKEntityMapping class]]) [self.mutableKeyPaths addObject:keyPath]; + if (![ mapping isKindOfClass:[RKEntityMapping class]]) return; + + NSArray *keyPathComponents = [[self.visitationStack valueForKey:@"keyPath"] arrayByAddingObject:keyPath]; + NSString *keyPath = [[keyPathComponents subarrayWithRange:NSMakeRange(1, [keyPathComponents count] - 1)] componentsJoinedByString:@"."]; + + RKMappingGraphVisitation *cyclicVisitation = [self visitationForMapping:mapping atKeyPath:keyPath]; + cyclicVisitation.cyclic = YES; + [self.visitations addObject:cyclicVisitation]; + return; } NSNumber *lowValue = @([self.lowValues count]); [self.lowValues setObject:lowValue forKey:dictionaryKey]; + NSUInteger stackPosition = [self.visitationStack count]; - [self.visitationStack addObject:@{ @"mapping": mapping, @"keyPath": keyPath ?: [NSNull null] }]; + RKMappingGraphVisitation *visitation = [self visitationForMapping:mapping atKeyPath:keyPath]; + [self.visitationStack addObject:visitation]; if ([mapping isKindOfClass:[RKObjectMapping class]]) { RKObjectMapping *objectMapping = (RKObjectMapping *)mapping; for (RKRelationshipMapping *relationshipMapping in objectMapping.relationshipMappings) { - NSString *nestedKeyPath = keyPath ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath; - [self visitMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath]; + [self visitMapping:relationshipMapping.mapping atKeyPath:relationshipMapping.destinationKeyPath]; // We want the minimum value NSValue *relationshipKey = [NSValue valueWithNonretainedObject:relationshipMapping.mapping]; @@ -105,36 +147,38 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath } } } else if ([mapping isKindOfClass:[RKDynamicMapping class]]) { - RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)mapping; - for (RKMapping *nestedMapping in dynamicMapping.objectMappings) { + // Pop the visitation stack to remove the dynamic mapping, since each mapping within the dynamic mapping + // is rooted at the same point in the graph + [self.visitationStack removeLastObject]; + + for (RKMapping *nestedMapping in [(RKDynamicMapping *)mapping objectMappings]) { [self visitMapping:nestedMapping atKeyPath:keyPath]; } } if ([[self.lowValues objectForKey:dictionaryKey] isEqualToNumber:lowValue]) { NSRange range = NSMakeRange(stackPosition, [self.visitationStack count] - stackPosition); - NSArray *mappingDetails = [self.visitationStack subarrayWithRange:range]; + NSArray *visitations = [self.visitationStack subarrayWithRange:range]; [self.visitationStack removeObjectsInRange:range]; - NSArray *mappings = [mappingDetails valueForKey:@"mapping"]; - for (NSDictionary *dictionary in mappingDetails) { - NSString *keyPath = [dictionary objectForKey:@"keyPath"]; - NSString *mapping = [dictionary objectForKey:@"mapping"]; - if ([mapping isKindOfClass:[RKEntityMapping class]]) [self.mutableKeyPaths addObject:keyPath]; - } + // Take everything left on the stack + NSArray *keyPathComponents = [self.visitationStack valueForKey:@"keyPath"]; + NSString *nestingKeyPath = ([keyPathComponents count] > 1) ? [[keyPathComponents subarrayWithRange:NSMakeRange(1, [keyPathComponents count] - 1)] componentsJoinedByString:@"."] : nil; - for (RKMapping *mapping in mappings) { + [visitations enumerateObjectsUsingBlock:^(RKMappingGraphVisitation *visitation, NSUInteger idx, BOOL *stop) { + // If this is an entity mapping, collect the complete key path + if ([visitation.mapping isKindOfClass:[RKEntityMapping class]]) { + visitation.keyPath = nestingKeyPath ? [@[ nestingKeyPath, visitation.keyPath ] componentsJoinedByString:@"."] : visitation.keyPath; + [self.visitations addObject:visitation]; + } + + // Update the low value NSValue *relationshipKey = [NSValue valueWithNonretainedObject:mapping]; [self.lowValues setObject:self.numberOfDecriptors forKey:relationshipKey]; - } + }]; } } -- (NSSet *)keyPaths -{ - return self.mutableKeyPaths; -} - @end NSArray *RKArrayOfFetchRequestFromBlocksWithURL(NSArray *fetchRequestBlocks, NSURL *URL) @@ -148,6 +192,49 @@ - (NSSet *)keyPaths return fetchRequests; } +static NSSet *RKFlattenCollectionToSet(id collection) +{ + NSMutableSet *mutableSet = [NSMutableSet set]; + if ([collection conformsToProtocol:@protocol(NSFastEnumeration)]) { + for (id nestedObject in collection) { + if ([nestedObject conformsToProtocol:@protocol(NSFastEnumeration)]) { + if ([nestedObject isKindOfClass:[NSArray class]]) { + [mutableSet unionSet:RKFlattenCollectionToSet([NSSet setWithArray:nestedObject])]; + } else if ([nestedObject isKindOfClass:[NSSet class]]) { + [mutableSet unionSet:RKFlattenCollectionToSet(nestedObject)]; + } else if ([nestedObject isKindOfClass:[NSOrderedSet class]]) { + [mutableSet unionSet:RKFlattenCollectionToSet([(NSOrderedSet *)nestedObject set])]; + } + } else { + [mutableSet addObject:nestedObject]; + } + } + } else if (collection) { + [mutableSet addObject:collection]; + } + + return mutableSet; +} + +/** + Traverses a set of cyclic key paths within the mapping result. Because these relationships are cyclic, we continue collecting managed objects and traversing until the values returned by the key path are a complete subset of all objects already in the set. + */ +static void RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(id graph, NSSet *cyclicKeyPaths, NSMutableSet *mutableSet) +{ + if ([graph respondsToSelector:@selector(count)] && [graph count] == 0) return; + + for (NSString *cyclicKeyPath in cyclicKeyPaths) { + NSSet *objectsAtCyclicKeyPath = RKFlattenCollectionToSet([graph valueForKeyPath:cyclicKeyPath]); + if ([objectsAtCyclicKeyPath count] == 0 || [objectsAtCyclicKeyPath isEqualToSet:[NSSet setWithObject:[NSNull null]]]) continue; + if (! [objectsAtCyclicKeyPath isSubsetOfSet:mutableSet]) { + [mutableSet unionSet:objectsAtCyclicKeyPath]; + for (id nestedValue in objectsAtCyclicKeyPath) { + RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(nestedValue, cyclicKeyPaths, mutableSet); + } + } + } +} + /** Returns the set of keys containing the outermost nesting keypath for all children. For example, given a set containing: 'this', 'this.that', 'another.one.test', 'another.two.test', 'another.one.test.nested' @@ -170,28 +257,17 @@ - (NSSet *)keyPaths }]; } -// When we map the root object, it is returned under the key `[NSNull null]` -static id RKMappedValueForKeyPathInDictionary(NSString *keyPath, NSDictionary *dictionary) -{ - @try { - return ([keyPath isEqual:[NSNull null]]) ? [dictionary objectForKey:[NSNull null]] : [dictionary valueForKeyPath:keyPath]; - } - @catch (NSException *exception) { - if ([[exception name] isEqualToString:NSUndefinedKeyException]) { - RKLogWarning(@"Caught undefined key exception for keyPath '%@' in mapping result: This likely indicates an ambiguous keyPath is used across response descriptor or dynamic mappings.", keyPath); - return nil; - } - - [exception raise]; - } -} - -static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath, NSMutableDictionary *dictionary) +static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSString *keyPath, NSMutableDictionary *dictionary) { NSCParameterAssert(value); - NSCParameterAssert(keyPath); + NSCParameterAssert(rootKey); NSCParameterAssert(dictionary); - [keyPath isEqual:[NSNull null]] ? [dictionary setObject:value forKey:keyPath] : [dictionary setValue:value forKeyPath:keyPath]; + if (keyPath && ![keyPath isEqual:[NSNull null]]) { + id valueAtRootKey = [dictionary objectForKey:rootKey]; + [valueAtRootKey setValue:value forKeyPath:keyPath]; + } else { + [dictionary setObject:value forKey:rootKey]; + } } // Precondition: Must be called from within the correct context @@ -210,44 +286,52 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath, } // Finds the key paths for all entity mappings in the graph whose parent objects are not other managed objects -static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSSet *keyPaths, NSManagedObjectContext *managedObjectContext) +static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsInVisitationsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSArray *visitations, NSManagedObjectContext *managedObjectContext) { if (! [dictionaryOfManagedObjects count]) return dictionaryOfManagedObjects; NSMutableDictionary *newDictionary = [dictionaryOfManagedObjects mutableCopy]; [managedObjectContext performBlockAndWait:^{ - for (NSString *keyPath in keyPaths) { - id value = RKMappedValueForKeyPathInDictionary(keyPath, dictionaryOfManagedObjects); - if (! value) { - continue; - } else if ([value isKindOfClass:[NSArray class]]) { - BOOL isMutable = [value isKindOfClass:[NSMutableArray class]]; - NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]]; - for (__strong id object in value) { - if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); - if (object) [newValue addObject:object]; - } - value = (isMutable) ? newValue : [newValue copy]; - } else if ([value isKindOfClass:[NSSet class]]) { - BOOL isMutable = [value isKindOfClass:[NSMutableSet class]]; - NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]]; - for (__strong id object in value) { - if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); - if (object) [newValue addObject:object]; + NSArray *rootKeys = [visitations valueForKey:@"rootKey"]; + for (id rootKey in rootKeys) { + NSSet *keyPaths = [visitations valueForKey:@"keyPath"]; + // If keyPaths contains null, then the root object is a managed object and we only need to refetch it + NSSet *nonNestedKeyPaths = ([keyPaths containsObject:[NSNull null]]) ? [NSSet setWithObject:[NSNull null]] : RKSetByRemovingSubkeypathsFromSet(keyPaths); + + NSDictionary *mappingResultsAtRootKey = [dictionaryOfManagedObjects objectForKey:rootKey]; + for (NSString *keyPath in nonNestedKeyPaths) { + id value = [keyPath isEqual:[NSNull null]] ? mappingResultsAtRootKey : [mappingResultsAtRootKey valueForKeyPath:keyPath]; + if (! value) { + continue; + } else if ([value isKindOfClass:[NSArray class]]) { + BOOL isMutable = [value isKindOfClass:[NSMutableArray class]]; + NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]]; + for (__strong id object in value) { + if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); + if (object) [newValue addObject:object]; + } + value = (isMutable) ? newValue : [newValue copy]; + } else if ([value isKindOfClass:[NSSet class]]) { + BOOL isMutable = [value isKindOfClass:[NSMutableSet class]]; + NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]]; + for (__strong id object in value) { + if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); + if (object) [newValue addObject:object]; + } + value = (isMutable) ? newValue : [newValue copy]; + } else if ([value isKindOfClass:[NSOrderedSet class]]) { + BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]]; + NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet]; + [(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) { + if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); + if (object) [newValue setObject:object atIndex:index]; + }]; + value = (isMutable) ? newValue : [newValue copy]; + } else if ([value isKindOfClass:[NSManagedObject class]]) { + value = RKRefetchManagedObjectInContext(value, managedObjectContext); } - value = (isMutable) ? newValue : [newValue copy]; - } else if ([value isKindOfClass:[NSOrderedSet class]]) { - BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]]; - NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet]; - [(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) { - if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); - if (object) [newValue setObject:object atIndex:index]; - }]; - value = (isMutable) ? newValue : [newValue copy]; - } else if ([value isKindOfClass:[NSManagedObject class]]) { - value = RKRefetchManagedObjectInContext(value, managedObjectContext); + + if (value) RKSetMappedValueForKeyPathInDictionary(value, rootKey, keyPath, newDictionary); } - - if (value) RKSetMappedValueForKeyPathInDictionary(value, keyPath, newDictionary); } }]; @@ -426,7 +510,7 @@ - (NSSet *)localObjectsFromFetchRequestsMatchingRequestURL:(NSError **)error return localObjects; } -- (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result atKeyPaths:(NSSet *)keyPaths error:(NSError **)error +- (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result withVisitor:(RKNestedManagedObjectKeyPathMappingGraphVisitor *)visitor error:(NSError **)error { if (! self.deletesOrphanedObjects) { RKLogDebug(@"Skipping deletion of orphaned objects: disabled as deletesOrphanedObjects=NO"); @@ -446,20 +530,16 @@ - (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result atK // Build an aggregate collection of all the managed objects in the mapping result NSMutableSet *managedObjectsInMappingResult = [NSMutableSet set]; NSDictionary *mappingResultDictionary = result.dictionary; - for (NSString *keyPath in keyPaths) { - id managedObjects = RKMappedValueForKeyPathInDictionary(keyPath, mappingResultDictionary); - if (! managedObjects) { - continue; - } else if ([managedObjects isKindOfClass:[NSManagedObject class]]) { - [managedObjectsInMappingResult addObject:managedObjects]; - } else if ([managedObjects isKindOfClass:[NSSet class]]) { - [managedObjectsInMappingResult unionSet:managedObjects]; - } else if ([managedObjects isKindOfClass:[NSArray class]]) { - [managedObjectsInMappingResult addObjectsFromArray:managedObjects]; - } else if ([managedObjects isKindOfClass:[NSOrderedSet class]]) { - [managedObjectsInMappingResult addObjectsFromArray:[managedObjects array]]; - } else { - [NSException raise:NSInternalInconsistencyException format:@"Unexpected object type '%@' encountered at keyPath '%@': Expected an `NSManagedObject`, `NSArray`, or `NSSet`.", [managedObjects class], keyPath]; + + for (RKMappingGraphVisitation *visitation in visitor.visitations) { + id objectsAtRoot = [mappingResultDictionary objectForKey:visitation.rootKey]; + id managedObjects = visitation.keyPath ? [objectsAtRoot valueForKeyPath:visitation.keyPath] : objectsAtRoot; + [managedObjectsInMappingResult unionSet:RKFlattenCollectionToSet(managedObjects)]; + + if (visitation.isCyclic) { + NSSet *cyclicKeyPaths = [NSSet setWithArray:[visitation valueForKeyPath:@"mapping.relationshipMappings.destinationKeyPath"]]; + [managedObjectsInMappingResult unionSet:RKFlattenCollectionToSet(managedObjects)]; + RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(managedObjects, cyclicKeyPaths, managedObjectsInMappingResult); } } @@ -530,7 +610,6 @@ - (void)willFinish // Construct a set of key paths to all of the managed objects in the mapping result RKNestedManagedObjectKeyPathMappingGraphVisitor *visitor = [[RKNestedManagedObjectKeyPathMappingGraphVisitor alloc] initWithResponseDescriptors:self.responseDescriptors]; - NSSet *managedObjectMappingResultKeyPaths = visitor.keyPaths; // Handle any cleanup success = [self deleteTargetObjectIfAppropriate:&error]; @@ -539,7 +618,7 @@ - (void)willFinish return; } - success = [self deleteLocalObjectsMissingFromMappingResult:self.mappingResult atKeyPaths:managedObjectMappingResultKeyPaths error:&error]; + success = [self deleteLocalObjectsMissingFromMappingResult:self.mappingResult withVisitor:visitor error:&error]; if (! success) { self.error = error; return; @@ -556,8 +635,7 @@ - (void)willFinish // Refetch all managed objects nested at key paths within the results dictionary before returning if (self.mappingResult) { - NSSet *nonNestedKeyPaths = RKSetByRemovingSubkeypathsFromSet(managedObjectMappingResultKeyPaths); - NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext([self.mappingResult dictionary], nonNestedKeyPaths, self.managedObjectContext); + NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsInVisitationsRefetchedInContext([self.mappingResult dictionary], visitor.visitations, self.managedObjectContext); self.mappingResult = [[RKMappingResult alloc] initWithDictionary:resultsDictionaryFromOriginalContext]; } } diff --git a/Tests/Fixtures/JSON/humans/nested_self_referential.json b/Tests/Fixtures/JSON/humans/nested_self_referential.json new file mode 100644 index 0000000000..d5f9258d5b --- /dev/null +++ b/Tests/Fixtures/JSON/humans/nested_self_referential.json @@ -0,0 +1,61 @@ +{ + "houses": [ + { + "houseID": 1, + "city": "New York City", + "state": "New York", + "owner": { + "humanID": 1, + "name": "Blake" + }, + "occupants": [ + { + "humanID": 2, + "name": "John", + "landlord": { + "humanID": 1, + "name": "Blake" + }, + "roommates": [ + { + "humanID": 3, + "name": "Mary", + "landlord": { + "humanID": 1, + "name": "Blake" + }, + "roommates": [ + { + "humanID": 2, + "name": "John" + }, + { + "humanID": 4, + "name": "Edward" + } + ] + }, + { + "humanID": 4, + "name": "Edward", + "landlord": { + "humanID": 1, + "name": "Blake" + }, + "roommates": [ + { + "humanID": 2, + "name": "John" + }, + { + "humanID": 3, + "name": "Mary" + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Tests/Fixtures/JSON/humans/self_referential.json b/Tests/Fixtures/JSON/humans/self_referential.json new file mode 100644 index 0000000000..a9188c3fb6 --- /dev/null +++ b/Tests/Fixtures/JSON/humans/self_referential.json @@ -0,0 +1,26 @@ +{ + "name": "Blake", + "id": 1, + "friends": [ + { + "name": "Sarah", + "id": 2, + "friends": [ + { + "name": "Monkey", + "id": 3 + } + ] + }, + { + "name": "Colin", + "id": 4, + "friends": [ + { + "id": "3", + "name": "Monkey" + } + ] + } + ] +} \ No newline at end of file diff --git a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m index 60462bcc8e..bd89100315 100644 --- a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m @@ -555,12 +555,148 @@ - (void)testPathVisitationDoesNotRecurseInfinitelyForSelfReferentialMappings }; managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ]; managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + managedObjectRequestOperation.managedObjectCache = managedObjectCache; [managedObjectRequestOperation start]; expect(managedObjectRequestOperation.error).to.beNil(); expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); expect(orphanedHuman.managedObjectContext).to.beNil(); } +- (void)testDeletionOfObjectsMappedFindsObjectsMappedBySelfReferentialMappings +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + entityMapping.identificationAttributes = @[ @"railsID" ]; + [entityMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"id": @"railsID" }]; + [entityMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:entityMapping]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:entityMapping pathPattern:nil keyPath:nil statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + // Create Blake, Sarah, Colin, Monkey & Orphan + NSManagedObjectContext *context = managedObjectStore.persistentStoreManagedObjectContext; + NSUInteger count = [context countForEntityForName:@"Human" predicate:nil error:nil]; + expect(count).to.equal(0); + RKHuman *blake = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(1), @"name": @"Blake" }]; + RKHuman *sarah = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(2), @"name": @"Sarah" }]; + RKHuman *monkey = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(3), @"name": @"Monkey" }]; + RKHuman *colin = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(4), @"name": @"Colin" }]; + RKHuman *orphan = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(5), @"name": @"Orphan" }]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/self_referential.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Human"]; + }; + managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + managedObjectRequestOperation.managedObjectCache = managedObjectCache; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).to.beNil(); + expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); + + // Verify that orphan was deleted + count = [context countForEntityForName:@"Human" predicate:nil error:nil]; + expect(count).to.equal(4); + + expect(blake.managedObjectContext).notTo.beNil(); + expect(sarah.managedObjectContext).notTo.beNil(); + expect(monkey.managedObjectContext).notTo.beNil(); + expect(colin.managedObjectContext).notTo.beNil(); + expect(orphan.managedObjectContext).to.beNil(); +} + +- (void)testDeletionOfObjectsMappedFindsObjectsMappedByNestedSelfReferentialMappings +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *houseMapping = [RKEntityMapping mappingForEntityForName:@"House" inManagedObjectStore:managedObjectStore]; + [houseMapping addAttributeMappingsFromDictionary:@{ @"houseID": @"railsID" }]; + [houseMapping addAttributeMappingsFromArray:@[ @"city", @"state" ]]; + houseMapping.identificationAttributes = @[ @"railsID" ]; + + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"humanID": @"railsID" }]; + [humanMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"roommates" toKeyPath:@"friends" withMapping:humanMapping]]; + [humanMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"landlord" toKeyPath:@"landlord" withMapping:humanMapping]]; + + [houseMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"owner" toKeyPath:@"owner" withMapping:humanMapping]]; + [houseMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"occupants" toKeyPath:@"occupants" withMapping:humanMapping]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:houseMapping pathPattern:nil keyPath:@"houses" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + // Create Blake, Sarah, Colin, Monkey & Orphan + NSManagedObjectContext *context = managedObjectStore.persistentStoreManagedObjectContext; + RKHuman *orphan = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(5), @"name": @"Orphan" }]; + RKHuman *edward = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(4), @"name": @"Edward" }]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/nested_self_referential.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + RKFetchRequestBlock humanFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Human"]; + }; + RKFetchRequestBlock houseFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"House"]; + }; + managedObjectRequestOperation.fetchRequestBlocks = @[ humanFetchRequestBlock, houseFetchRequestBlock ]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + managedObjectRequestOperation.deletesOrphanedObjects = YES; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + managedObjectRequestOperation.managedObjectCache = managedObjectCache; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).to.beNil(); + expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); + + NSUInteger count = [context countForEntityForName:@"Human" predicate:nil error:nil]; + expect(count).to.equal(4); + + count = [context countForEntityForName:@"House" predicate:nil error:nil]; + expect(count).to.equal(1); + + expect(edward.managedObjectContext).notTo.beNil(); + expect(orphan.managedObjectContext).to.beNil(); +} + +- (void)testMappingWithDynamicMappingContainingIncompatibleEntityMappingsAtSameKeyPath +{ + RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"humanID": @"railsID" }]; + [humanMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"roommates" toKeyPath:@"friends" withMapping:humanMapping]]; + + RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; + RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; + parentMapping.identificationAttributes = @[ @"railsID" ]; + [parentMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"humanID": @"railsID" }]; + [parentMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"children" toKeyPath:@"children" withMapping:childMapping]]; + + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"invalid" expectedValue:@"whatever" objectMapping:humanMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"name" expectedValue:@"Blake" objectMapping:parentMapping]]; + + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping pathPattern:nil keyPath:@"houses.owner" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/nested_self_referential.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + RKFetchRequestBlock humanFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Human"]; + }; + RKFetchRequestBlock parentFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Parent"]; + }; + RKFetchRequestBlock childFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Child"]; + }; + managedObjectRequestOperation.fetchRequestBlocks = @[ humanFetchRequestBlock, parentFetchRequestBlock, childFetchRequestBlock ]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + managedObjectRequestOperation.deletesOrphanedObjects = YES; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + managedObjectRequestOperation.managedObjectCache = managedObjectCache; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).to.beNil(); + expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); +} + - (void)testThatMappingObjectsWithTheSameIdentificationAttributesAcrossTwoObjectRequestOperationConcurrentlyDoesNotCreateDuplicateObjects { RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];