Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Rework implementation of managed object deletion and refetching to us…

…e the refreshed visitor implementation. refs #1111, #1113
  • Loading branch information...
commit 6493282772919c9342199fac84ff2c69cefee947 1 parent 84c6822
@blakewatters blakewatters authored
View
266 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];
}
}
View
61 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"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
View
26 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"
+ }
+ ]
+ }
+ ]
+}
View
136 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];
Please sign in to comment.
Something went wrong with that request. Please try again.