Skip to content
This repository
Browse code

Merge branch 'release/0.20.0-pre6'

  • Loading branch information...
commit 22dd9ab40db337cba60f7ce0937d94c75c0ce040 2 parents 9103b9b + ee00e59
Blake Watters authored January 03, 2013

Showing 71 changed files with 2,105 additions and 723 deletions. Show diff stats Hide diff stats

  1. 4  Code/CoreData/RKConnectionDescription.h
  2. 4  Code/CoreData/RKConnectionDescription.m
  3. 2  Code/CoreData/RKEntityByAttributeCache.h
  4. 2  Code/CoreData/RKEntityCache.h
  5. 13  Code/CoreData/RKEntityMapping.h
  6. 6  Code/CoreData/RKEntityMapping.m
  7. 2  Code/CoreData/RKInMemoryManagedObjectCache.h
  8. 4  Code/CoreData/RKManagedObjectImporter.h
  9. 2  Code/CoreData/RKManagedObjectMappingOperationDataSource.h
  10. 97  Code/CoreData/RKManagedObjectMappingOperationDataSource.m
  11. 6  Code/CoreData/RKManagedObjectStore.h
  12. 4  Code/CoreData/RKManagedObjectStore.m
  13. 2  Code/CoreData/RKRelationshipConnectionOperation.h
  14. 4  Code/CoreData/RKRelationshipConnectionOperation.m
  15. 8  Code/Network/RKHTTPRequestOperation.m
  16. 324  Code/Network/RKManagedObjectRequestOperation.m
  17. 21  Code/Network/RKObjectManager.h
  18. 135  Code/Network/RKObjectManager.m
  19. 4  Code/Network/RKObjectRequestOperation.h
  20. 14  Code/Network/RKObjectRequestOperation.m
  21. 27  Code/Network/RKPaginator.h
  22. 45  Code/Network/RKPaginator.m
  23. 10  Code/Network/RKResponseMapperOperation.h
  24. 17  Code/Network/RKResponseMapperOperation.m
  25. 2  Code/Network/RKRouter.h
  26. 2  Code/ObjectMapping/RKAttributeMapping.m
  27. 75  Code/ObjectMapping/RKDynamicMapping.h
  28. 35  Code/ObjectMapping/RKDynamicMapping.m
  29. 67  Code/ObjectMapping/RKDynamicMappingMatcher.h
  30. 49  Code/ObjectMapping/RKDynamicMappingMatcher.m
  31. 2  Code/ObjectMapping/RKMapperOperation.h
  32. 2  Code/ObjectMapping/RKMappingOperation.h
  33. 71  Code/ObjectMapping/RKMappingOperation.m
  34. 2  Code/ObjectMapping/RKMappingResult.h
  35. 54  Code/ObjectMapping/RKObjectMapping.h
  36. 11  Code/ObjectMapping/RKObjectMapping.m
  37. 75  Code/ObjectMapping/RKObjectMappingMatcher.h
  38. 123  Code/ObjectMapping/RKObjectMappingMatcher.m
  39. 1  Code/Support/RestKit-Prefix.pch
  40. 4  Code/Testing.h
  41. 6  Code/Testing/RKConnectionTestExpectation.h
  42. 7  Code/Testing/RKConnectionTestExpectation.m
  43. 7  Code/Testing/RKMappingTest.h
  44. 14  Code/Testing/RKMappingTest.m
  45. 6  README.md
  46. 6  RestKit.podspec
  47. 51  RestKit.xcodeproj/project.pbxproj
  48. 61  Tests/Fixtures/JSON/humans/nested_self_referential.json
  49. 26  Tests/Fixtures/JSON/humans/self_referential.json
  50. 13  Tests/Fixtures/JSON/humans/with_to_one_relationship.json
  51. 5  Tests/Fixtures/JSON/user.json
  52. 4  Tests/Logic/CoreData/RKEntityMappingTest.m
  53. 45  Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m
  54. 192  Tests/Logic/Network/RKManagedObjectRequestOperationTest.m
  55. 21  Tests/Logic/Network/RKObjectRequestOperationTest.m
  56. 2  Tests/Logic/Network/RKRequestDescriptorTest.m
  57. 146  Tests/Logic/ObjectMapping/RKDynamicMappingTest.m
  58. 89  Tests/Logic/ObjectMapping/RKDynamicObjectMappingTest.m
  59. 442  Tests/Logic/ObjectMapping/RKObjectManagerTest.m
  60. 29  Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m
  61. 21  Tests/Logic/ObjectMapping/RKObjectMappingTest.m
  62. 21  Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m
  63. 36  Tests/Logic/ObjectMapping/RKPaginatorTest.m
  64. 224  Tests/Logic/Testing/RKMappingTestTest.m
  65. BIN  Tests/Models/Data Model.xcdatamodel/elements
  66. BIN  Tests/Models/Data Model.xcdatamodel/layout
  67. 2  Tests/Models/RKTestUser.h
  68. 7  Tests/Models/RKTestUser.m
  69. 9  Tests/Server/server.rb
  70. 2  VERSION
  71. 2  Vendor/AFNetworking
4  Code/CoreData/RKConnectionDescription.h
@@ -88,7 +88,7 @@
88 88
  @param sourceToDestinationEntityAttributes A dictionary specifying how attributes on the source entity correspond to attributes on the destination entity.
89 89
  @return The receiver, initialized with the given relationship and attributes.
90 90
  */
91  
-- (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship attributes:(NSDictionary *)sourceToDestinationEntityAttributes;
  91
+- (id)initWithRelationship:(NSRelationshipDescription *)relationship attributes:(NSDictionary *)sourceToDestinationEntityAttributes;
92 92
 
93 93
 /**
94 94
  The dictionary of attributes specifying how attributes on the source entity for the relationship correspond to attributes on the destination entity.
@@ -115,7 +115,7 @@
115 115
  @param keyPath The key path from which to read the value that is to be set for the relationship.
116 116
  @return The receiver, initialized with the given relationship and key path.
117 117
  */
118  
-- (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship keyPath:(NSString *)keyPath;
  118
+- (id)initWithRelationship:(NSRelationshipDescription *)relationship keyPath:(NSString *)keyPath;
119 119
 
120 120
 /**
121 121
  The key path that is to be evaluated to obtain the value for the relationship.
4  Code/CoreData/RKConnectionDescription.m
@@ -44,7 +44,7 @@ @interface RKConnectionDescription ()
44 44
 
45 45
 @implementation RKConnectionDescription
46 46
 
47  
-- (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship attributes:(NSDictionary *)attributes
  47
+- (id)initWithRelationship:(NSRelationshipDescription *)relationship attributes:(NSDictionary *)attributes
48 48
 {
49 49
     NSParameterAssert(relationship);
50 50
     NSParameterAssert(attributes);
@@ -63,7 +63,7 @@ - (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship a
63 63
     return self;
64 64
 }
65 65
 
66  
-- (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship keyPath:(NSString *)keyPath
  66
+- (id)initWithRelationship:(NSRelationshipDescription *)relationship keyPath:(NSString *)keyPath
67 67
 {
68 68
     NSParameterAssert(relationship);
69 69
     NSParameterAssert(keyPath);
2  Code/CoreData/RKEntityByAttributeCache.h
@@ -46,7 +46,7 @@
46 46
  @return The receiver, initialized with the given entity, attribute, and managed object
47 47
     context.
48 48
  */
49  
-- (instancetype)initWithEntity:(NSEntityDescription *)entity attributes:(NSArray *)attributeNames managedObjectContext:(NSManagedObjectContext *)context;
  49
+- (id)initWithEntity:(NSEntityDescription *)entity attributes:(NSArray *)attributeNames managedObjectContext:(NSManagedObjectContext *)context;
50 50
 
51 51
 ///-----------------------------
52 52
 /// @name Getting Cache Identity
2  Code/CoreData/RKEntityCache.h
@@ -43,7 +43,7 @@
43 43
  @param context The managed object context containing objects to be cached.
44 44
  @returns self, initialized with context.
45 45
  */
46  
-- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)context;
  46
+- (id)initWithManagedObjectContext:(NSManagedObjectContext *)context;
47 47
 
48 48
 /**
49 49
  The managed object context with which the receiver is associated.
13  Code/CoreData/RKEntityMapping.h
@@ -68,7 +68,7 @@
68 68
  @param entity An entity with which to initialize the receiver.
69 69
  @returns The receiver, initialized with the given entity.
70 70
  */
71  
-- (instancetype)initWithEntity:(NSEntityDescription *)entity;
  71
+- (id)initWithEntity:(NSEntityDescription *)entity;
72 72
 
73 73
 /**
74 74
  A convenience initializer that creates and returns an entity mapping for the entity with the given name in
@@ -177,6 +177,17 @@
177 177
  */
178 178
 - (RKConnectionDescription *)connectionForRelationship:(id)relationshipOrName;
179 179
 
  180
+///------------------------------------
  181
+/// @name Flagging Objects for Deletion
  182
+///------------------------------------
  183
+
  184
+/**
  185
+ A predicate that identifies objects for the receiver's entity that are to be deleted from the local store.
  186
+
  187
+ This property provides support for local deletion of managed objects mapped as a 'tombstone' record from the source representation.
  188
+ */
  189
+@property (nonatomic, copy) NSPredicate *deletionPredicate;
  190
+
180 191
 ///------------------------------------------
181 192
 /// @name Retrieving Default Attribute Values
182 193
 ///------------------------------------------
6  Code/CoreData/RKEntityMapping.m
@@ -20,7 +20,7 @@
20 20
 
21 21
 #import "RKEntityMapping.h"
22 22
 #import "RKManagedObjectStore.h"
23  
-#import "RKDynamicMappingMatcher.h"
  23
+#import "RKObjectMappingMatcher.h"
24 24
 #import "RKPropertyInspector+CoreData.h"
25 25
 #import "RKLog.h"
26 26
 #import "RKRelationshipMapping.h"
@@ -153,7 +153,7 @@ + (instancetype)mappingForEntityForName:(NSString *)entityName inManagedObjectSt
153 153
     return [[self alloc] initWithEntity:entity];
154 154
 }
155 155
 
156  
-- (instancetype)initWithEntity:(NSEntityDescription *)entity
  156
+- (id)initWithEntity:(NSEntityDescription *)entity
157 157
 {
158 158
     NSAssert(entity, @"Cannot initialize an RKEntityMapping without an entity. Maybe you want RKObjectMapping instead?");
159 159
     Class objectClass = NSClassFromString([entity managedObjectClassName]);
@@ -167,7 +167,7 @@ - (instancetype)initWithEntity:(NSEntityDescription *)entity
167 167
     return self;
168 168
 }
169 169
 
170  
-- (instancetype)initWithClass:(Class)objectClass
  170
+- (id)initWithClass:(Class)objectClass
171 171
 {
172 172
     self = [super initWithClass:objectClass];
173 173
     if (self) {
2  Code/CoreData/RKInMemoryManagedObjectCache.h
@@ -35,6 +35,6 @@
35 35
  @param managedObjectContext The managed object context with which to initialize the receiver.
36 36
  @return The receiver, initialized with the given managed object context.
37 37
  */
38  
-- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext;
  38
+- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext;
39 39
 
40 40
 @end
4  Code/CoreData/RKManagedObjectImporter.h
@@ -55,7 +55,7 @@
55 55
  @warning As this initialization code path is typical for generating seed databases, the value of
56 56
     `resetsStoreBeforeImporting` is initialized to **YES**.
57 57
  */
58  
-- (instancetype)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel storePath:(NSString *)storePath;
  58
+- (id)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel storePath:(NSString *)storePath;
59 59
 
60 60
 /**
61 61
  Initializes the receiver with a given persistent store in which to persist imported managed objects.
@@ -69,7 +69,7 @@
69 69
     managed object model are determined from the given persistent store and a new managed object context with
70 70
     the private queue concurrency type is constructed.
71 71
  */
72  
-- (instancetype)initWithPersistentStore:(NSPersistentStore *)persistentStore;
  72
+- (id)initWithPersistentStore:(NSPersistentStore *)persistentStore;
73 73
 
74 74
 /**
75 75
  A Boolean value indicating whether existing managed objects in the persistent store should
2  Code/CoreData/RKManagedObjectMappingOperationDataSource.h
@@ -42,7 +42,7 @@
42 42
  @param managedObjectCache The managed object cache used by the receiver to find existing object instances by their identification attributes.
43 43
  @return The receiver, initialized with the given managed object context and managed objet cache.
44 44
  */
45  
-- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id<RKManagedObjectCaching>)managedObjectCache;
  45
+- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id<RKManagedObjectCaching>)managedObjectCache;
46 46
 
47 47
 ///-----------------------------------------------------
48 48
 /// @name Accessing the Managed Object Context and Cache
97  Code/CoreData/RKManagedObjectMappingOperationDataSource.m
@@ -18,22 +18,26 @@
18 18
 //  limitations under the License.
19 19
 //
20 20
 
  21
+#import <objc/runtime.h>
21 22
 #import "RKManagedObjectMappingOperationDataSource.h"
22 23
 #import "RKObjectMapping.h"
23 24
 #import "RKEntityMapping.h"
24 25
 #import "RKLog.h"
25 26
 #import "RKManagedObjectStore.h"
26 27
 #import "RKMappingOperation.h"
27  
-#import "RKDynamicMappingMatcher.h"
  28
+#import "RKObjectMappingMatcher.h"
28 29
 #import "RKManagedObjectCaching.h"
29 30
 #import "RKRelationshipConnectionOperation.h"
30 31
 #import "RKMappingErrors.h"
31 32
 #import "RKValueTransformers.h"
32 33
 #import "RKRelationshipMapping.h"
33 34
 #import "RKObjectUtilities.h"
  35
+#import "NSManagedObject+RKAdditions.h"
34 36
 
35 37
 extern NSString * const RKObjectMappingNestingAttributeKeyName;
36 38
 
  39
+static char kRKManagedObjectMappingOperationDataSourceAssociatedObjectKey;
  40
+
37 41
 id RKTransformedValueWithClass(id value, Class destinationType, NSValueTransformer *dateToStringValueTransformer);
38 42
 NSArray *RKApplyNestingAttributeValueToMappings(NSString *attributeName, id value, NSArray *propertyMappings);
39 43
 
@@ -117,6 +121,58 @@ static id RKValueForAttributeMappingInRepresentation(RKAttributeMapping *attribu
117 121
     return entityIdentifierAttributes;
118 122
 }
119 123
 
  124
+@interface RKManagedObjectDeletionOperation : NSOperation
  125
+
  126
+- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext;
  127
+- (void)addEntityMapping:(RKEntityMapping *)entityMapping;
  128
+@end
  129
+
  130
+@interface RKManagedObjectDeletionOperation ()
  131
+@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;
  132
+@property (nonatomic, strong) NSMutableSet *entityMappings;
  133
+@end
  134
+
  135
+@implementation RKManagedObjectDeletionOperation
  136
+
  137
+- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
  138
+{
  139
+    self = [self init];
  140
+    if (self) {
  141
+        self.managedObjectContext = managedObjectContext;
  142
+        self.entityMappings = [NSMutableSet new];
  143
+    }
  144
+    return self;
  145
+}
  146
+
  147
+- (void)addEntityMapping:(RKEntityMapping *)entityMapping
  148
+{
  149
+    if (! entityMapping.deletionPredicate) return;
  150
+    [self.entityMappings addObject:entityMapping];
  151
+}
  152
+
  153
+- (void)main
  154
+{
  155
+    [self.managedObjectContext performBlockAndWait:^{
  156
+        NSMutableSet *objectsToDelete = [NSMutableSet set];
  157
+        for (RKEntityMapping *entityMapping in self.entityMappings) {
  158
+            NSFetchRequest *fetchRequest = [NSFetchRequest alloc];
  159
+            [fetchRequest setEntity:entityMapping.entity];
  160
+            [fetchRequest setPredicate:entityMapping.deletionPredicate];
  161
+            NSError *error = nil;
  162
+            NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  163
+            if (fetchedObjects) {
  164
+                [objectsToDelete addObjectsFromArray:fetchedObjects];
  165
+            }
  166
+        }
  167
+
  168
+        for (NSManagedObject *managedObject in objectsToDelete) {
  169
+            [self.managedObjectContext deleteObject:managedObject];
  170
+        }
  171
+    }];
  172
+}
  173
+
  174
+@end
  175
+
120 176
 // Set Logging Component
121 177
 #undef RKLogComponent
122 178
 #define RKLogComponent RKlcl_cRestKitCoreData
@@ -126,11 +182,12 @@ static id RKValueForAttributeMappingInRepresentation(RKAttributeMapping *attribu
126 182
 @interface RKManagedObjectMappingOperationDataSource ()
127 183
 @property (nonatomic, strong, readwrite) NSManagedObjectContext *managedObjectContext;
128 184
 @property (nonatomic, strong, readwrite) id<RKManagedObjectCaching> managedObjectCache;
  185
+@property (nonatomic, strong) NSMutableArray *deletionPredicates;
129 186
 @end
130 187
 
131 188
 @implementation RKManagedObjectMappingOperationDataSource
132 189
 
133  
-- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id<RKManagedObjectCaching>)managedObjectCache
  190
+- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id<RKManagedObjectCaching>)managedObjectCache
134 191
 {
135 192
     NSParameterAssert(managedObjectContext);
136 193
 
@@ -169,7 +226,7 @@ - (id)mappingOperation:(RKMappingOperation *)mappingOperation targetObjectForRep
169 226
                       "Unable to update existing object instances by identification attributes. Duplicate objects may be created.");
170 227
     }
171 228
 
172  
-    // If we have found the entity identifier attributes, try to find an existing instance to update
  229
+    // If we have found the entity identification attributes, try to find an existing instance to update
173 230
     NSEntityDescription *entity = [entityMapping entity];
174 231
     NSManagedObject *managedObject = nil;
175 232
     if ([entityIdentifierAttributes count]) {
@@ -187,8 +244,7 @@ - (id)mappingOperation:(RKMappingOperation *)mappingOperation targetObjectForRep
187 244
     }
188 245
 
189 246
     if (managedObject == nil) {
190  
-        managedObject = [[NSManagedObject alloc] initWithEntity:entity
191  
-                           insertIntoManagedObjectContext:self.managedObjectContext];
  247
+        managedObject = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];
192 248
         [managedObject setValuesForKeysWithDictionary:entityIdentifierAttributes];
193 249
 
194 250
         if ([self.managedObjectCache respondsToSelector:@selector(didCreateObject:)]) {
@@ -220,6 +276,16 @@ - (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation
220 276
     if ([mappingOperation.objectMapping isKindOfClass:[RKEntityMapping class]]) {
221 277
         [self emitDeadlockWarningIfNecessary];
222 278
         
  279
+        // Validate unsaved objects
  280
+        if ([mappingOperation.destinationObject isKindOfClass:[NSManagedObject class]] && [(NSManagedObject *)mappingOperation.destinationObject isNew]) {
  281
+            NSError *validationError = nil;
  282
+            if (! [(NSManagedObject *)mappingOperation.destinationObject validateForInsert:&validationError]) {
  283
+                RKLogDebug(@"Unsaved NSManagedObject failed `validateForInsert:` - Deleting object from context: %@", validationError);
  284
+                [self.managedObjectContext deleteObject:mappingOperation.destinationObject];
  285
+                return YES;
  286
+            }
  287
+        }
  288
+        
223 289
         NSArray *connections = [(RKEntityMapping *)mappingOperation.objectMapping connections];
224 290
         if ([connections count] > 0 && self.managedObjectCache == nil) {
225 291
             NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Cannot map an entity mapping that contains connection mappings with a data source whose managed object cache is nil." };
@@ -246,6 +312,27 @@ - (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation
246 312
             [operationQueue addOperation:operation];
247 313
             RKLogTrace(@"Enqueued %@ dependent upon parent operation %@ to operation queue %@", operation, self.parentOperation, operationQueue);
248 314
         }
  315
+
  316
+        // Handle tombstone deletion by predicate
  317
+        if ([(RKEntityMapping *)mappingOperation.objectMapping deletionPredicate]) {
  318
+            RKManagedObjectDeletionOperation *deletionOperation = nil;
  319
+            if (self.parentOperation) {
  320
+                // Attach a deletion operation for execution after the parent operation completes
  321
+                deletionOperation = (RKManagedObjectDeletionOperation *)objc_getAssociatedObject(self.parentOperation, &kRKManagedObjectMappingOperationDataSourceAssociatedObjectKey);
  322
+                if (! deletionOperation) {
  323
+                    deletionOperation = [[RKManagedObjectDeletionOperation alloc] initWithManagedObjectContext:self.managedObjectContext];
  324
+                    objc_setAssociatedObject(self.parentOperation, &kRKManagedObjectMappingOperationDataSourceAssociatedObjectKey, deletionOperation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  325
+                    [deletionOperation addDependency:self.parentOperation];
  326
+                    NSOperationQueue *operationQueue = self.operationQueue ?: [NSOperationQueue currentQueue];
  327
+                    [operationQueue addOperation:deletionOperation];
  328
+                }
  329
+                [deletionOperation addEntityMapping:(RKEntityMapping *)mappingOperation.objectMapping];
  330
+            } else {
  331
+                deletionOperation = [[RKManagedObjectDeletionOperation alloc] initWithManagedObjectContext:self.managedObjectContext];
  332
+                [deletionOperation addEntityMapping:(RKEntityMapping *)mappingOperation.objectMapping];
  333
+                [deletionOperation start];
  334
+            }
  335
+        }
249 336
     }
250 337
     
251 338
     return YES;
6  Code/CoreData/RKManagedObjectStore.h
@@ -76,7 +76,7 @@
76 76
     RKManagedObjectStore *managedObjectStore = [[RKManagedObjectStore alloc] initWithManagedObjectModel:managedObjectModel];
77 77
  
78 78
  */
79  
-- (instancetype)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel;
  79
+- (id)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel;
80 80
 
81 81
 /**
82 82
  Initializes the receiver with an existing persistent store coordinator.
@@ -88,7 +88,7 @@
88 88
  @param persistentStoreCoordinator The persistent store coordinator with which to initialize the receiver.
89 89
  @return The receiver, initialized with the managed object model of the given persistent store coordinator and the persistent store coordinator.
90 90
  */
91  
-- (instancetype)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator;
  91
+- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator;
92 92
 
93 93
 /**
94 94
  Initializes the receiver with a managed object model obtained by merging the models from all of the application's non-framework bundles.
@@ -98,7 +98,7 @@
98 98
 
99 99
  @warning Obtaining a managed object model by merging all bundles may result in an application error if versioned object models are in use.
100 100
  */
101  
-- (instancetype)init;
  101
+- (id)init;
102 102
 
103 103
 ///-----------------------------------------------------------------------------
104 104
 /// @name Configuring Persistent Stores
4  Code/CoreData/RKManagedObjectStore.m
@@ -57,7 +57,7 @@ + (void)setDefaultStore:(RKManagedObjectStore *)managedObjectStore
57 57
     }
58 58
 }
59 59
 
60  
-- (instancetype)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel
  60
+- (id)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel
61 61
 {
62 62
     self = [super init];
63 63
     if (self) {
@@ -73,7 +73,7 @@ - (instancetype)initWithManagedObjectModel:(NSManagedObjectModel *)managedObject
73 73
     return self;
74 74
 }
75 75
 
76  
-- (instancetype)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator
  76
+- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator
77 77
 {
78 78
     self = [self initWithManagedObjectModel:persistentStoreCoordinator.managedObjectModel];
79 79
     if (self) {
2  Code/CoreData/RKRelationshipConnectionOperation.h
@@ -44,7 +44,7 @@
44 44
  @param managedObjectCache The managed object cache from which to attempt to fetch a matching object to satisfy the connection.
45 45
  @return The receiver, initialized with the given managed object, connection mapping, and managed object cache.
46 46
  */
47  
-- (instancetype)initWithManagedObject:(NSManagedObject *)managedObject
  47
+- (id)initWithManagedObject:(NSManagedObject *)managedObject
48 48
                            connection:(RKConnectionDescription *)connection
49 49
                    managedObjectCache:(id<RKManagedObjectCaching>)managedObjectCache;
50 50
 
4  Code/CoreData/RKRelationshipConnectionOperation.m
@@ -24,7 +24,7 @@
24 24
 #import "RKEntityMapping.h"
25 25
 #import "RKLog.h"
26 26
 #import "RKManagedObjectCaching.h"
27  
-#import "RKDynamicMappingMatcher.h"
  27
+#import "RKObjectMappingMatcher.h"
28 28
 #import "RKErrors.h"
29 29
 #import "RKObjectUtilities.h"
30 30
 
@@ -70,7 +70,7 @@ @interface RKRelationshipConnectionOperation ()
70 70
 
71 71
 @implementation RKRelationshipConnectionOperation
72 72
 
73  
-- (instancetype)initWithManagedObject:(NSManagedObject *)managedObject
  73
+- (id)initWithManagedObject:(NSManagedObject *)managedObject
74 74
                            connection:(RKConnectionDescription *)connection
75 75
                    managedObjectCache:(id<RKManagedObjectCaching>)managedObjectCache;
76 76
 {
8  Code/Network/RKHTTPRequestOperation.m
@@ -210,14 +210,14 @@ - (NSError *)error
210 210
             if (![self hasAcceptableStatusCode] || ![self hasAcceptableContentType]) {
211 211
                 NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
212 212
                 
213  
-                if (error.code == NSURLErrorBadServerResponse) {
  213
+                if (error.code == NSURLErrorBadServerResponse && ![self hasAcceptableStatusCode]) {
214 214
                     // Replace the NSLocalizedDescriptionKey
215 215
                     NSUInteger statusCode = ([self.response isKindOfClass:[NSHTTPURLResponse class]]) ? (NSUInteger)[self.response statusCode] : 200;
216  
-                    [userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected status code in (%@), got %d", nil), RKStringFromIndexSet([self acceptableStatusCodes]), statusCode] forKey:NSLocalizedDescriptionKey];
  216
+                    [userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected status code in (%@), got %d", nil), RKStringFromIndexSet(self.acceptableStatusCodes ?: [NSMutableIndexSet indexSet]), statusCode] forKey:NSLocalizedDescriptionKey];
217 217
                     self.HTTPError = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:NSURLErrorBadServerResponse userInfo:userInfo];
218  
-                } else if (error.code == NSURLErrorCannotDecodeContentData) {
  218
+                } else if (error.code == NSURLErrorCannotDecodeContentData && ![self hasAcceptableContentType]) {
219 219
                     // Because we have shifted the Acceptable Content Types and Status Codes
220  
-                    [userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected content type %@, got %@", nil), [self acceptableContentTypes], [self.response MIMEType]] forKey:NSLocalizedDescriptionKey];
  220
+                    [userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected content type %@, got %@", nil), self.acceptableContentTypes, [self.response MIMEType]] forKey:NSLocalizedDescriptionKey];
221 221
                     self.HTTPError = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo];
222 222
                 }
223 223
             }
324  Code/Network/RKManagedObjectRequestOperation.m
@@ -35,16 +35,45 @@
35 35
 #undef RKLogComponent
36 36
 #define RKLogComponent RKlcl_cRestKitCoreData
37 37
 
38  
-@interface RKNestedManagedObjectKeyPathMappingGraphVisitor : NSObject
  38
+@interface RKMappingGraphVisitation : NSObject
  39
+@property (nonatomic, strong) id rootKey; // Will be [NSNull null] or a string value
  40
+@property (nonatomic, strong) NSString *keyPath;
  41
+@property (nonatomic, assign, getter = isCyclic) BOOL cyclic;
  42
+@property (nonatomic, strong) RKMapping *mapping;
  43
+@end
39 44
 
40  
-@property (nonatomic, readonly) NSSet *keyPaths;
  45
+@implementation RKMappingGraphVisitation
41 46
 
42  
-- (id)initWithResponseDescriptors:(NSArray *)responseDescriptors;
  47
+- (NSString *)description
  48
+{
  49
+    return [NSString stringWithFormat:@"<%@: %p rootKey=%@ keyPath=%@ isCylic=%@ mapping=%@>",
  50
+            [self class], self, self.rootKey, self.keyPath, self.isCyclic ? @"YES" : @"NO", self.mapping];
  51
+}
43 52
 
44 53
 @end
45 54
 
  55
+/**
  56
+ This class implements Tarjan's algorithm to efficiently visit all nodes within the mapping graph and detect cycles in the graph.
  57
+ 
  58
+ For more details on the algorithm, refer to the Wikipedia page: http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
  59
+ 
  60
+ The following reference implementations were used when building out an Objective-C implementation:
  61
+ 
  62
+ 1. http://algowiki.net/wiki/index.php?title=Tarjan%27s_algorithm
  63
+ 1. http://www.logarithmic.net/pfh-files/blog/01208083168/tarjan.py
  64
+ 
  65
+ */
  66
+@interface RKNestedManagedObjectKeyPathMappingGraphVisitor : NSObject
  67
+@property (nonatomic, readonly, strong) NSMutableArray *visitations;
  68
+- (id)initWithResponseDescriptors:(NSArray *)responseDescriptors;
  69
+@end
  70
+
46 71
 @interface RKNestedManagedObjectKeyPathMappingGraphVisitor ()
47  
-@property (nonatomic, strong) NSMutableSet *mutableKeyPaths;
  72
+@property (nonatomic, assign) NSUInteger indexCounter;
  73
+@property (nonatomic, strong) NSMutableArray *visitationStack;
  74
+@property (nonatomic, strong) NSMutableDictionary *index;
  75
+@property (nonatomic, strong) NSMutableDictionary *lowLinks;
  76
+@property (nonatomic, strong, readwrite) NSMutableArray *visitations;
48 77
 @end
49 78
 
50 79
 @implementation RKNestedManagedObjectKeyPathMappingGraphVisitor
@@ -53,34 +82,108 @@ - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors
53 82
 {
54 83
     self = [self init];
55 84
     if (self) {
56  
-        self.mutableKeyPaths = [NSMutableSet set];
  85
+        self.indexCounter = 0;
  86
+        self.visitationStack = [NSMutableArray array];
  87
+        self.index = [NSMutableDictionary dictionary];
  88
+        self.lowLinks = [NSMutableDictionary dictionary];
  89
+        self.visitations = [NSMutableArray array];
  90
+        
57 91
         for (RKResponseDescriptor *responseDescriptor in responseDescriptors) {
  92
+            self.indexCounter = 0;
  93
+            [self.visitationStack removeAllObjects];
  94
+            [self.index removeAllObjects];
  95
+            [self.lowLinks removeAllObjects];
58 96
             [self visitMapping:responseDescriptor.mapping atKeyPath:responseDescriptor.keyPath];
59 97
         }
60 98
     }
  99
+    
61 100
     return self;
62 101
 }
63 102
 
64  
-- (NSSet *)keyPaths
  103
+- (RKMappingGraphVisitation *)visitationForMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
65 104
 {
66  
-    return self.mutableKeyPaths;
  105
+    RKMappingGraphVisitation *visitation = [RKMappingGraphVisitation new];
  106
+    visitation.mapping = mapping;
  107
+    if ([self.visitationStack count] == 0) {
  108
+        // If we are the first item in the stack, we are visiting the rootKey
  109
+        visitation.rootKey = keyPath ?: [NSNull null];
  110
+    } else {
  111
+        // Take the root key from the visitation stack
  112
+        visitation.rootKey = [[self.visitationStack objectAtIndex:0] rootKey];
  113
+        visitation.keyPath = keyPath;
  114
+    }    
  115
+    
  116
+    return visitation;
67 117
 }
68 118
 
  119
+// Traverse the mappings graph using Tarjan's algorithm
69 120
 - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
70 121
 {
71  
-    id actualKeyPath = keyPath ?: [NSNull null];
72  
-    if ([self.keyPaths containsObject:actualKeyPath]) return;    
73  
-    if ([mapping isKindOfClass:[RKEntityMapping class]]) [self.mutableKeyPaths addObject:actualKeyPath];
74  
-    if ([mapping isKindOfClass:[RKDynamicMapping class]]) {
75  
-        RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)mapping;
76  
-        for (RKMapping *nestedMapping in dynamicMapping.objectMappings) {
77  
-            [self visitMapping:nestedMapping atKeyPath:keyPath];
78  
-        }
79  
-    } else if ([mapping isKindOfClass:[RKObjectMapping class]]) {
  122
+    // Track the visit to each node in the graph. Note that we do not pop the stack as we traverse back up
  123
+    NSValue *dictionaryKey = [NSValue valueWithNonretainedObject:mapping];
  124
+    [self.index setObject:@(self.indexCounter) forKey:dictionaryKey];
  125
+    [self.lowLinks setObject:@(self.indexCounter) forKey:dictionaryKey];
  126
+    self.indexCounter++;
  127
+    
  128
+    RKMappingGraphVisitation *visitation = [self visitationForMapping:mapping atKeyPath:keyPath];
  129
+    [self.visitationStack addObject:visitation];
  130
+    
  131
+    if ([mapping isKindOfClass:[RKObjectMapping class]]) {
80 132
         RKObjectMapping *objectMapping = (RKObjectMapping *)mapping;
81 133
         for (RKRelationshipMapping *relationshipMapping in objectMapping.relationshipMappings) {
82  
-            NSString *nestedKeyPath = keyPath ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath;
83  
-            [self visitMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath];
  134
+            // Check if the successor relationship appears in the lowlinks
  135
+            NSValue *relationshipKey = [NSValue valueWithNonretainedObject:relationshipMapping.mapping];
  136
+            NSNumber *relationshipLowValue = [self.lowLinks objectForKey:relationshipKey];
  137
+            NSString *nestedKeyPath = ([self.visitationStack count] > 1 && keyPath) ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath;
  138
+            if (relationshipLowValue == nil) {
  139
+                // The relationship has not yet been visited, recurse
  140
+                [self visitMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath];
  141
+                
  142
+                // Set the lowlink value for parent mapping to the lower value for us or the child mapping we just recursed on
  143
+                NSNumber *lowLinkForMapping = [self.lowLinks objectForKey:dictionaryKey];
  144
+                NSNumber *lowLinkForSuccessor = [self.lowLinks objectForKey:relationshipKey];
  145
+                
  146
+                if ([lowLinkForMapping compare:lowLinkForSuccessor] == NSOrderedDescending) {
  147
+                    [self.lowLinks setObject:lowLinkForSuccessor forKey:dictionaryKey];
  148
+                }
  149
+            } else {
  150
+                // The child mapping is already in the stack, so it is part of a strongly connected component
  151
+                NSNumber *lowLinkForMapping = [self.lowLinks objectForKey:dictionaryKey];
  152
+                NSNumber *indexValueForSuccessor = [self.index objectForKey:relationshipKey];
  153
+                if ([lowLinkForMapping compare:indexValueForSuccessor] == NSOrderedDescending) {
  154
+                    [self.lowLinks setObject:indexValueForSuccessor forKey:dictionaryKey];
  155
+                }
  156
+                
  157
+                // Since this mapping already appears in lowLinks, we have a cycle at this point in the graph
  158
+                if ([relationshipMapping.mapping isKindOfClass:[RKEntityMapping class]]) {
  159
+                    RKMappingGraphVisitation *cyclicVisitation = [self visitationForMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath];
  160
+                    cyclicVisitation.cyclic = YES;
  161
+                    [self.visitations addObject:cyclicVisitation];
  162
+                }
  163
+            }
  164
+        }
  165
+    } else if ([mapping isKindOfClass:[RKDynamicMapping class]]) {
  166
+        // Pop the dynamic mapping off of the stack so that our children are rooted at the same level
  167
+        [self.visitationStack removeLastObject];
  168
+        
  169
+        // Dynamic mappings appear at the same point in the graph, so we recurse with the same keyPath
  170
+        for (RKMapping *nestedMapping in [(RKDynamicMapping *)mapping objectMappings]) {
  171
+            [self visitMapping:nestedMapping atKeyPath:keyPath];
  172
+        }
  173
+    }
  174
+    
  175
+    // If the current mapping is a root node, then pop the stack to create an SCC
  176
+    NSNumber *lowLinkValueForMapping = [self.lowLinks objectForKey:dictionaryKey];
  177
+    NSNumber *indexValueForMapping = [self.index objectForKey:dictionaryKey];
  178
+    if ([lowLinkValueForMapping isEqualToNumber:indexValueForMapping]) {
  179
+        NSUInteger index = [self.visitationStack indexOfObject:visitation];
  180
+        if (index != NSNotFound) {
  181
+            NSRange removalRange = NSMakeRange(index, [self.visitationStack count] - index);
  182
+            [self.visitationStack removeObjectsInRange:removalRange];
  183
+        }
  184
+        
  185
+        if ([visitation.mapping isKindOfClass:[RKEntityMapping class]]) {
  186
+            [self.visitations addObject:visitation];
84 187
         }
85 188
     }
86 189
 }
@@ -98,6 +201,49 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
98 201
     return fetchRequests;
99 202
 }
100 203
 
  204
+static NSSet *RKFlattenCollectionToSet(id collection)
  205
+{
  206
+    NSMutableSet *mutableSet = [NSMutableSet set];
  207
+    if ([collection conformsToProtocol:@protocol(NSFastEnumeration)]) {
  208
+        for (id nestedObject in collection) {
  209
+            if ([nestedObject conformsToProtocol:@protocol(NSFastEnumeration)]) {
  210
+                if ([nestedObject isKindOfClass:[NSArray class]]) {
  211
+                    [mutableSet unionSet:RKFlattenCollectionToSet([NSSet setWithArray:nestedObject])];
  212
+                } else if ([nestedObject isKindOfClass:[NSSet class]]) {
  213
+                    [mutableSet unionSet:RKFlattenCollectionToSet(nestedObject)];
  214
+                } else if ([nestedObject isKindOfClass:[NSOrderedSet class]]) {
  215
+                    [mutableSet unionSet:RKFlattenCollectionToSet([(NSOrderedSet *)nestedObject set])];
  216
+                }
  217
+            } else {
  218
+                [mutableSet addObject:nestedObject];
  219
+            }
  220
+        }
  221
+    } else if (collection) {
  222
+        [mutableSet addObject:collection];
  223
+    }
  224
+    
  225
+    return mutableSet;
  226
+}
  227
+
  228
+/**
  229
+ 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.
  230
+ */
  231
+static void RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(id graph, NSSet *cyclicKeyPaths, NSMutableSet *mutableSet)
  232
+{
  233
+    if ([graph respondsToSelector:@selector(count)] && [graph count] == 0) return;
  234
+    
  235
+    for (NSString *cyclicKeyPath in cyclicKeyPaths) {
  236
+        NSSet *objectsAtCyclicKeyPath = RKFlattenCollectionToSet([graph valueForKeyPath:cyclicKeyPath]);
  237
+        if ([objectsAtCyclicKeyPath count] == 0 || [objectsAtCyclicKeyPath isEqualToSet:[NSSet setWithObject:[NSNull null]]]) continue;
  238
+        if (! [objectsAtCyclicKeyPath isSubsetOfSet:mutableSet]) {
  239
+            [mutableSet unionSet:objectsAtCyclicKeyPath];
  240
+            for (id nestedValue in objectsAtCyclicKeyPath) {
  241
+                RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(nestedValue, cyclicKeyPaths, mutableSet);
  242
+            }
  243
+        }
  244
+    }
  245
+}
  246
+
101 247
 /**
102 248
  Returns the set of keys containing the outermost nesting keypath for all children.
103 249
  For example, given a set containing: 'this', 'this.that', 'another.one.test', 'another.two.test', 'another.one.test.nested'
@@ -120,18 +266,17 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
120 266
     }];
121 267
 }
122 268
 
123  
-// When we map the root object, it is returned under the key `[NSNull null]`
124  
-static id RKMappedValueForKeyPathInDictionary(NSString *keyPath, NSDictionary *dictionary)
125  
-{
126  
-    return ([keyPath isEqual:[NSNull null]]) ? [dictionary objectForKey:[NSNull null]] : [dictionary valueForKeyPath:keyPath];
127  
-}
128  
-
129  
-static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath, NSMutableDictionary *dictionary)
  269
+static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSString *keyPath, NSMutableDictionary *dictionary)
130 270
 {
131 271
     NSCParameterAssert(value);
132  
-    NSCParameterAssert(keyPath);
  272
+    NSCParameterAssert(rootKey);
133 273
     NSCParameterAssert(dictionary);
134  
-    [keyPath isEqual:[NSNull null]] ? [dictionary setObject:value forKey:keyPath] : [dictionary setValue:value forKeyPath:keyPath];
  274
+    if (keyPath && ![keyPath isEqual:[NSNull null]]) {
  275
+        id valueAtRootKey = [dictionary objectForKey:rootKey];
  276
+        [valueAtRootKey setValue:value forKeyPath:keyPath];
  277
+    } else {
  278
+        [dictionary setObject:value forKey:rootKey];
  279
+    }
135 280
 }
136 281
 
137 282
 // Precondition: Must be called from within the correct context
@@ -150,44 +295,56 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath,
150 295
 }
151 296
 
152 297
 // Finds the key paths for all entity mappings in the graph whose parent objects are not other managed objects
153  
-static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSSet *keyPaths, NSManagedObjectContext *managedObjectContext)
  298
+static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsInVisitationsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSArray *visitations, NSManagedObjectContext *managedObjectContext)
154 299
 {
155  
-    if (! [dictionaryOfManagedObjects count]) return dictionaryOfManagedObjects;    
  300
+    if (! [dictionaryOfManagedObjects count]) return dictionaryOfManagedObjects;
  301
+    
156 302
     NSMutableDictionary *newDictionary = [dictionaryOfManagedObjects mutableCopy];
157 303
     [managedObjectContext performBlockAndWait:^{
158  
-        for (NSString *keyPath in keyPaths) {
159  
-            id value = RKMappedValueForKeyPathInDictionary(keyPath, dictionaryOfManagedObjects);
160  
-            if (! value) {
161  
-                continue;
162  
-            } else if ([value isKindOfClass:[NSArray class]]) {
163  
-                BOOL isMutable = [value isKindOfClass:[NSMutableArray class]];
164  
-                NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]];
165  
-                for (__strong id object in value) {
166  
-                    if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);                    
167  
-                    if (object) [newValue addObject:object];
  304
+        NSSet *rootKeys = [NSSet setWithArray:[visitations valueForKey:@"rootKey"]];
  305
+        for (id rootKey in rootKeys) {
  306
+            NSArray *visitationsForRootKey = [visitations filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"rootKey = %@", rootKey]];
  307
+            NSSet *keyPaths = [visitationsForRootKey valueForKey:@"keyPath"];
  308
+            // If keyPaths contains null, then the root object is a managed object and we only need to refetch it
  309
+            NSSet *nonNestedKeyPaths = ([keyPaths containsObject:[NSNull null]]) ? [NSSet setWithObject:[NSNull null]] : RKSetByRemovingSubkeypathsFromSet(keyPaths);
  310
+            
  311
+            NSDictionary *mappingResultsAtRootKey = [dictionaryOfManagedObjects objectForKey:rootKey];
  312
+            for (NSString *keyPath in nonNestedKeyPaths) {
  313
+                id value = [keyPath isEqual:[NSNull null]] ? mappingResultsAtRootKey : [mappingResultsAtRootKey valueForKeyPath:keyPath];
  314
+                if (! value) {
  315
+                    continue;
  316
+                } else if ([value isKindOfClass:[NSArray class]]) {
  317
+                    BOOL isMutable = [value isKindOfClass:[NSMutableArray class]];
  318
+                    NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]];
  319
+                    for (__strong id object in value) {
  320
+                        if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
  321
+                        if (object) [newValue addObject:object];
  322
+                    }
  323
+                    value = (isMutable) ? newValue : [newValue copy];
  324
+                } else if ([value isKindOfClass:[NSSet class]]) {
  325
+                    BOOL isMutable = [value isKindOfClass:[NSMutableSet class]];
  326
+                    NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]];
  327
+                    for (__strong id object in value) {
  328
+                        if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
  329
+                        if (object) [newValue addObject:object];
  330
+                    }
  331
+                    value = (isMutable) ? newValue : [newValue copy];
  332
+                } else if ([value isKindOfClass:[NSOrderedSet class]]) {
  333
+                    BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]];
  334
+                    NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet];
  335
+                    [(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
  336
+                        if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
  337
+                        if (object) [newValue setObject:object atIndex:index];
  338
+                    }];
  339
+                    value = (isMutable) ? newValue : [newValue copy];
  340
+                } else if ([value isKindOfClass:[NSManagedObject class]]) {
  341
+                    value = RKRefetchManagedObjectInContext(value, managedObjectContext);
168 342
                 }
169  
-                value = (isMutable) ? newValue : [newValue copy];
170  
-            } else if ([value isKindOfClass:[NSSet class]]) {
171  
-                BOOL isMutable = [value isKindOfClass:[NSMutableSet class]];
172  
-                NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]];
173  
-                for (__strong id object in value) {
174  
-                    if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);                    
175  
-                    if (object) [newValue addObject:object];
  343
+                
  344
+                if (value) {
  345
+                    RKSetMappedValueForKeyPathInDictionary(value, rootKey, keyPath, newDictionary);
176 346
                 }
177  
-                value = (isMutable) ? newValue : [newValue copy];
178  
-            } else if ([value isKindOfClass:[NSOrderedSet class]]) {
179  
-                BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]];
180  
-                NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet];
181  
-                [(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
182  
-                    if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
183  
-                    if (object) [newValue setObject:object atIndex:index];
184  
-                }];
185  
-                value = (isMutable) ? newValue : [newValue copy];
186  
-            } else if ([value isKindOfClass:[NSManagedObject class]]) {
187  
-                value = RKRefetchManagedObjectInContext(value, managedObjectContext);
188 347
             }
189  
-            
190  
-            if (value) RKSetMappedValueForKeyPathInDictionary(value, keyPath, newDictionary);
191 348
         }
192 349
     }];
193 350
     
@@ -366,7 +523,7 @@ - (NSSet *)localObjectsFromFetchRequestsMatchingRequestURL:(NSError **)error
366 523
     return localObjects;
367 524
 }
368 525
 
369  
-- (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result atKeyPaths:(NSSet *)keyPaths error:(NSError **)error
  526
+- (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result withVisitor:(RKNestedManagedObjectKeyPathMappingGraphVisitor *)visitor error:(NSError **)error
370 527
 {
371 528
     if (! self.deletesOrphanedObjects) {
372 529
         RKLogDebug(@"Skipping deletion of orphaned objects: disabled as deletesOrphanedObjects=NO");
@@ -386,20 +543,26 @@ - (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result atK
386 543
     // Build an aggregate collection of all the managed objects in the mapping result
387 544
     NSMutableSet *managedObjectsInMappingResult = [NSMutableSet set];
388 545
     NSDictionary *mappingResultDictionary = result.dictionary;
389  
-    for (NSString *keyPath in keyPaths) {
390  
-        id managedObjects = RKMappedValueForKeyPathInDictionary(keyPath, mappingResultDictionary);
391  
-        if (! managedObjects) {
392  
-            continue;
393  
-        } else if ([managedObjects isKindOfClass:[NSManagedObject class]]) {
394  
-            [managedObjectsInMappingResult addObject:managedObjects];
395  
-        } else if ([managedObjects isKindOfClass:[NSSet class]]) {
396  
-            [managedObjectsInMappingResult unionSet:managedObjects];
397  
-        } else if ([managedObjects isKindOfClass:[NSArray class]]) {
398  
-            [managedObjectsInMappingResult addObjectsFromArray:managedObjects];
399  
-        } else if ([managedObjects isKindOfClass:[NSOrderedSet class]]) {
400  
-            [managedObjectsInMappingResult addObjectsFromArray:[managedObjects array]];
401  
-        } else {
402  
-            [NSException raise:NSInternalInconsistencyException format:@"Unexpected object type '%@' encountered at keyPath '%@': Expected an `NSManagedObject`, `NSArray`, or `NSSet`.", [managedObjects class], keyPath];
  546
+    
  547
+    for (RKMappingGraphVisitation *visitation in visitor.visitations) {
  548
+        id objectsAtRoot = [mappingResultDictionary objectForKey:visitation.rootKey];
  549
+        id managedObjects = nil;
  550
+        @try {
  551
+            managedObjects = visitation.keyPath ? [objectsAtRoot valueForKeyPath:visitation.keyPath] : objectsAtRoot;
  552
+        }
  553
+        @catch (NSException *exception) {
  554
+            if ([exception.name isEqualToString:NSUndefinedKeyException]) {
  555
+                RKLogWarning(@"Caught undefined key exception for keyPath '%@' in mapping result: This likely indicates an ambiguous keyPath is used across response descriptor or dynamic mappings.", visitation.keyPath);
  556
+                continue;
  557
+            }
  558
+            [exception raise];
  559
+        }
  560
+        [managedObjectsInMappingResult unionSet:RKFlattenCollectionToSet(managedObjects)];
  561
+        
  562
+        if (visitation.isCyclic) {
  563
+            NSSet *cyclicKeyPaths = [NSSet setWithArray:[visitation valueForKeyPath:@"mapping.relationshipMappings.destinationKeyPath"]];
  564
+            [managedObjectsInMappingResult unionSet:RKFlattenCollectionToSet(managedObjects)];
  565
+            RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(managedObjects, cyclicKeyPaths, managedObjectsInMappingResult);
403 566
         }
404 567
     }
405 568
 
@@ -469,8 +632,7 @@ - (void)willFinish
469 632
     NSError *error = nil;
470 633
 
471 634
     // Construct a set of key paths to all of the managed objects in the mapping result
472  
-    RKNestedManagedObjectKeyPathMappingGraphVisitor *visitor = [[RKNestedManagedObjectKeyPathMappingGraphVisitor alloc] initWithResponseDescriptors:self.responseDescriptors];
473  
-    NSSet *managedObjectMappingResultKeyPaths = visitor.keyPaths;
  635
+    RKNestedManagedObjectKeyPathMappingGraphVisitor *visitor = [[RKNestedManagedObjectKeyPathMappingGraphVisitor alloc] initWithResponseDescriptors:self.responseMapperOperation.matchingResponseDescriptors];
474 636
 
475 637
     // Handle any cleanup
476 638
     success = [self deleteTargetObjectIfAppropriate:&error];
@@ -479,7 +641,7 @@ - (void)willFinish
479 641
         return;
480 642
     }
481 643
 
482  
-    success = [self deleteLocalObjectsMissingFromMappingResult:self.mappingResult atKeyPaths:managedObjectMappingResultKeyPaths error:&error];
  644
+    success = [self deleteLocalObjectsMissingFromMappingResult:self.mappingResult withVisitor:visitor error:&error];
483 645
     if (! success) {
484 646
         self.error = error;
485 647
         return;
@@ -492,12 +654,14 @@ - (void)willFinish
492 654
         return;
493 655
     }
494 656
     success = [self saveContext:&error];
495  
-    if (! success) self.error = error;
  657
+    if (! success) {
  658
+        self.error = error;
  659
+        return;
  660
+    }
496 661
     
497 662
     // Refetch all managed objects nested at key paths within the results dictionary before returning
498 663
     if (self.mappingResult) {
499  
-        NSSet *nonNestedKeyPaths = RKSetByRemovingSubkeypathsFromSet(managedObjectMappingResultKeyPaths);
500  
-        NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext([self.mappingResult dictionary], nonNestedKeyPaths, self.managedObjectContext);
  664
+        NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsInVisitationsRefetchedInContext([self.mappingResult dictionary], visitor.visitations, self.managedObjectContext);
501 665
         self.mappingResult = [[RKMappingResult alloc] initWithDictionary:resultsDictionaryFromOriginalContext];
502 666
     }
503 667
 }
21  Code/Network/RKObjectManager.h
@@ -146,6 +146,15 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor;
146 146
  
147 147
  In the above example, request and response mapping configurations were described for a simple data model and then used to perform a basic POST operation and map the results. An arbitrary number of request and response descriptors may be added to the manager to accommodate your application's needs.
148 148
  
  149
+ ## Multi-object Parameterization
  150
+
  151
+ The object manager provides support for the parameterization of multiple objects provided as an array. The `requestWithObject:method:path:parameters:` and `multipartFormRequestWithObject:method:path:parameters:constructingBodyWithBlock:` methods can parameterize an array of objects for you provided that the `RKRequestDescriptor` objects are configured in a compatible way. The rules for multi-object parameterization are simple:
  152
+
  153
+ 1. If a `nil` root key path is used, then it must be used for all objects in the array. This is because the objects will be parameterized into a dictionary and then each dictionary will be added to an array. This array is then serialized for transport, so objects parameterized to a non-nil key path cannot be merged with the array.
  154
+ 1. If a `nil` root key path is used to parameterize the array of objects, then you cannot provide additional parameters to be merged with the request. This is again because you cannot merge a dictionary with an array.
  155
+
  156
+ If non-nil key paths are used, then each object will be set in the parameters dictionary at the specified key-path. If more than one object uses the same root key path, then the parameters will be combined into an array for transport.
  157
+
149 158
  ## MIME Types
150 159
  
151 160
  MIME Types serve an important function to the object manager. They are used to identify how content is to be serialized when constructing request bodies and also used to set the 'Accept' header for content negotiation. RestKit aspires to be content type agnostic by leveraging the pluggable `RKMIMESerialization` class to handle content serialization and deserialization.
@@ -262,7 +271,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor;
262 271
  @param client The AFNetworking HTTP client with which to initialize the receiver.
263 272
  @return The receiver, initialized with the given client.
264 273
  */
265  
-- (instancetype)initWithHTTPClient:(AFHTTPClient *)client;
  274
+- (id)initWithHTTPClient:(AFHTTPClient *)client;
266 275
 
267 276
 ///------------------------------------------
268 277
 /// @name Accessing Object Manager Properties
@@ -613,7 +622,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor;
613 622
  
614 623
  The type of object request operation created is determined by invoking `appropriateObjectRequestOperationWithObject:method:path:parameters:`.
615 624
  
616  
- @param object The object with which to construct the object request operation. Cannot be nil.
  625
+ @param object The object with which to construct the object request operation. If `nil`, then the path must be provided.
617 626
  @param path The path to be appended to the HTTP client's base URL and used as the request URL. If nil, the request URL will be obtained by consulting the router for a route registered for the given object's class and the `RKRequestMethodGET` request method.
618 627
  @param parameters The parameters to be encoded and appended as the query string for the request URL.
619 628
  @param success A block object to be executed when the object request operation finishes successfully. This block has no return value and takes two arguments: the created object request operation and the `RKMappingResult` object created by object mapping the response data of request.
@@ -631,7 +640,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor;
631 640
 /**
632 641
  Creates an `RKObjectRequestOperation` with a `POST` request for the given object, and enqueues it to the manager's operation queue.
633 642
  
634  
- @param object The object with which to construct the object request operation. Cannot be nil.
  643
+ @param object The object with which to construct the object request operation. If `nil`, then the path must be provided.
635 644
  @param path The path to be appended to the HTTP client's base URL and used as the request URL. If nil, the request URL will be obtained by consulting the router for a route registered for the given object's class and the `RKRequestMethodPOST` method.
636 645
  @param parameters The parameters to be reverse merged with the parameterization of the given object and set as the request body.
637 646
  @param success A block object to be executed when the object request operation finishes successfully. This block has no return value and takes two arguments: the created object request operation and the `RKMappingResult` object created by object mapping the response data of request.
@@ -649,7 +658,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor;
649 658
 /**
650 659
  Creates an `RKObjectRequestOperation` with a `PUT` request for the given object, and enqueues it to the manager's operation queue.
651 660
  
652  
- @param object The object with which to construct the object request operation. Cannot be nil.
  661
+ @param object The object with which to construct the object request operation. If `nil`, then the path must be provided.
653 662
  @param path The path to be appended to the HTTP client's base URL and used as the request URL. If nil, the request URL will be obtained by consulting the router for a route registered for the given object's class and the `RKRequestMethodPUT` method.
654 663
  @param parameters The parameters to be reverse merged with the parameterization of the given object and set as the request body.
655 664
  @param success A block object to be executed when the object request operation finishes successfully. This block has no return value and takes two arguments: the created object request operation and the `RKMappingResult` object created by object mapping the response data of request.
@@ -667,7 +676,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor;
667 676
 /**
668 677
  Creates an `RKObjectRequestOperation` with a `PATCH` request for the given object, and enqueues it to the manager's operation queue.
669 678
  
670  
- @param object The object with which to construct the object request operation. Cannot be nil.
  679
+ @param object The object with which to construct the object request operation. If `nil`, then the path must be provided.
671 680
  @param path The path to be appended to the HTTP client's base URL and used as the request URL. If nil, the request URL will be obtained by consulting the router for a route registered for the given object's class and the `RKRequestMethodPATCH` method.
672 681
  @param parameters The parameters to be reverse merged with the parameterization of the given object and set as the request body.
673 682
  @param success A block object to be executed when the object request operation finishes successfully. This block has no return value and takes two arguments: the created object request operation and the `RKMappingResult` object created by object mapping the response data of request.
@@ -687,7 +696,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor;
687 696
  
688 697
  The type of object request operation created is determined by invoking `appropriateObjectRequestOperationWithObject:method:path:parameters:`.
689 698
  
690  
- @param object The object with which to construct the object request operation. Cannot be nil.
  699
+ @param object The object with which to construct the object request operation. If `nil`, then the path must be provided.
691 700
  @param path The path to be appended to the HTTP client's base URL and used as the request URL. If nil, the request URL will be obtained by consulting the router for a route registered for the given object's class and the `RKRequestMethodDELETE` request method.
692 701
  @param parameters The parameters to be encoded and appended as the query string for the request URL.
693 702
  @param success A block object to be executed when the object request operation finishes successfully. This block has no return value and takes two arguments: the created object request operation and the `RKMappingResult` object created by object mapping the response data of request.
135  Code/Network/RKObjectManager.m
@@ -82,6 +82,58 @@
82 82
     return nil;
83 83
 }
84 84
 
  85
+@interface RKObjectParameters : NSObject
  86
+
  87
+@property (nonatomic, strong) NSMutableDictionary *parameters;
  88
+- (void)addParameters:(NSDictionary *)serialization atRootKeyPath:(NSString *)rootKeyPath;
  89
+
  90
+@end
  91
+
  92
+@implementation RKObjectParameters
  93
+
  94
+- (id)init
  95
+{
  96
+    self = [super init];
  97
+    if (self) {
  98
+        self.parameters = [NSMutableDictionary new];
  99
+    }
  100
+    return self;
  101
+}
  102
+
  103
+- (void)addParameters:(NSDictionary *)parameters atRootKeyPath:(NSString *)rootKeyPath
  104
+{
  105
+    id rootKey = rootKeyPath ?: [NSNull null];
  106
+    id nonNestedParameters = rootKeyPath ? [parameters objectForKey:rootKeyPath] : parameters;
  107
+    id value = [self.parameters objectForKey:rootKey];
  108
+    if (value) {
  109
+        if ([value isKindOfClass:[NSMutableArray class]]) {
  110
+            [value addObject:nonNestedParameters];
  111
+        } else if ([value isKindOfClass:[NSDictionary class]]) {
  112
+            NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:value, nonNestedParameters, nil];
  113
+            [self.parameters setObject:mutableArray forKey:rootKey];
  114
+        } else {
  115
+            [NSException raise:NSInvalidArgumentException format:@"Unexpected argument of type '%@': expected an NSDictionary or NSArray.", [value class]];
  116
+        }
  117
+    } else {
  118
+        [self.parameters setObject:nonNestedParameters forKey:rootKey];
  119
+    }
  120
+}
  121
+
  122
+- (id)requestParameters
  123
+{
  124
+    if ([self.parameters count] == 0) return nil;
  125
+    id valueAtNullKey = [self.parameters objectForKey:[NSNull null]];
  126
+    if (valueAtNullKey) {
  127
+        if ([self.parameters count] == 1) return valueAtNullKey;
  128
+
  129
+        // If we have values at `[NSNull null]` and other key paths, we have an invalid configuration
  130
+        [NSException raise:NSInvalidArgumentException format:@"Invalid request descriptor configuration: The request descriptors specify that multiple objects be serialized at incompatible key paths. Cannot serialize objects at the `nil` root key path in the same request as objects with a non-nil root key path. Please check your request descriptors and try again."];
  131
+    }
  132
+    return self.parameters;
  133
+}
  134
+
  135
+@end
  136
+
85 137
 /**
86 138
  Visits all mappings accessible via relationships or dynamic mapping in an object graph starting from a given mapping.
87 139
  */
@@ -330,30 +382,44 @@ - (NSMutableURLRequest *)requestWithPathForRelationship:(NSString *)relationship
330 382
     return [self requestWithMethod:RKStringFromRequestMethod(method) path:[URL relativeString] parameters:parameters];
331 383
 }
332 384
 
  385
+- (id)mergedParametersWithObject:(id)object method:(RKRequestMethod)method parameters:(NSDictionary *)parameters
  386
+{
  387
+    NSArray *objectsToParameterize = ([object isKindOfClass:[NSArray class]] || object == nil) ? object : @[ object ];
  388
+    RKObjectParameters *objectParameters = [RKObjectParameters new];
  389
+    for (id object in objectsToParameterize) {
  390
+        RKRequestDescriptor *requestDescriptor = RKRequestDescriptorFromArrayMatchingObject(self.requestDescriptors, object);
  391
+        if ((method != RKRequestMethodGET && method != RKRequestMethodDELETE) && requestDescriptor) {
  392
+            NSError *error = nil;
  393
+            NSDictionary *parametersForObject = [RKObjectParameterization parametersWithObject:object requestDescriptor:requestDescriptor error:&error];
  394
+            if (error) {
  395
+                RKLogError(@"Object parameterization failed while building %@ request for object '%@': %@", RKStringFromRequestMethod(method), object, error);
  396
+                return nil;
  397
+            }
  398
+            [objectParameters addParameters:parametersForObject atRootKeyPath:requestDescriptor.rootKeyPath];
  399
+        }
  400
+    }
  401
+    id requestParameters = [objectParameters requestParameters];
  402
+
  403
+    // Merge the extra parameters if possible
  404
+    if ([requestParameters isKindOfClass:[NSArray class]] && parameters) {
  405
+        [NSException raise:NSInvalidArgumentException format:@"Cannot merge parameters with array of object representations serialized with a nil root key path."];
  406
+    } else if (requestParameters && parameters) {
  407
+        requestParameters = RKDictionaryByMergingDictionaryWithDictionary(requestParameters, parameters);
  408
+    } else if (parameters && !requestParameters) {
  409
+        requestParameters = parameters;
  410
+    }
  411
+
  412
+    return requestParameters;
  413
+}
  414
+
333 415
 - (NSMutableURLRequest *)requestWithObject:(id)object
334 416
                                     method:(RKRequestMethod)method
335 417
                                       path:(NSString *)path
336 418
                                 parameters:(NSDictionary *)parameters;
337 419
 {
338 420
     NSString *requestPath = (path) ? path : [[self.router URLForObject:object method:method] relativeString];
339  
-    NSString *stringMethod = RKStringFromRequestMethod(method);
340  
-    NSDictionary *requestParameters = nil;
341  
-    RKRequestDescriptor *requestDescriptor = RKRequestDescriptorFromArrayMatchingObject(self.requestDescriptors, object);
342  
-    if ((method != RKRequestMethodGET && method != RKRequestMethodDELETE) && requestDescriptor) {
343  
-        NSError *error = nil;
344  
-        requestParameters = [[RKObjectParameterization parametersWithObject:object requestDescriptor:requestDescriptor error:&error] mutableCopy];
345  
-        if (error) {