From 2b9c7ca2754ea62cdbc0e7c831d901128a6db9fc Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 2 Dec 2012 15:56:45 -0500 Subject: [PATCH 01/18] Bump version of Podspec to pre3 on development --- RestKit.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RestKit.podspec b/RestKit.podspec index ce4a19a089..0e1ce0e9c0 100644 --- a/RestKit.podspec +++ b/RestKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'RestKit' - s.version = '0.20.0pre1' + s.version = '0.20.0pre3' s.summary = 'RestKit is a framework for consuming and modeling RESTful web resources on iOS and OS X.' s.homepage = 'http://www.restkit.org' s.author = { 'Blake Watters' => 'blakewatters@gmail.com' } From 1e23b1c03739de3154ea8dd16e1e3365faca2021 Mon Sep 17 00:00:00 2001 From: Lasse Bang Mikkelsen Date: Mon, 3 Dec 2012 20:56:35 +0100 Subject: [PATCH 02/18] Bumped AFNetworking version to 1.0.1 --- RestKit.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RestKit.podspec b/RestKit.podspec index 0e1ce0e9c0..3de5fa1c35 100644 --- a/RestKit.podspec +++ b/RestKit.podspec @@ -37,7 +37,7 @@ Pod::Spec.new do |s| ns.ios.frameworks = 'CFNetwork', 'Security', 'MobileCoreServices', 'SystemConfiguration' ns.osx.frameworks = 'CoreServices', 'Security', 'SystemConfiguration' ns.dependency 'SOCKit' - ns.dependency 'AFNetworking', '1.0' + ns.dependency 'AFNetworking', '1.0.1' ns.dependency 'RestKit/ObjectMapping' ns.dependency 'RestKit/Support' end From 9a04de975ee6c10816a6535e98b2e13a8b384164 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Wed, 5 Dec 2012 21:48:02 -0500 Subject: [PATCH 03/18] Re-enable publishing of the Atom feed --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 472c41d490..28956fc5f7 100644 --- a/Rakefile +++ b/Rakefile @@ -166,7 +166,7 @@ namespace :docs do run(command) if $?.exitstatus == 0 - command = "rsync -rvpPe ssh Docs/API/publish/*.xar #{destination}" + command = "rsync -rvpPe ssh Docs/API/publish/* #{destination}" run(command) end end From 9672e0a4713c79ff3f429af8ccc9db35a3f3d3f6 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Wed, 5 Dec 2012 23:13:13 -0500 Subject: [PATCH 04/18] Remove use of keyed subscript to access RKEntityIdentificationAttributesUserInfoKey --- Code/CoreData/RKEntityMapping.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Code/CoreData/RKEntityMapping.m b/Code/CoreData/RKEntityMapping.m index c88b41a762..c0f000fb9b 100644 --- a/Code/CoreData/RKEntityMapping.m +++ b/Code/CoreData/RKEntityMapping.m @@ -36,7 +36,7 @@ static NSArray *RKEntityIdentificationAttributesFromUserInfoOfEntity(NSEntityDescription *entity) { - id userInfoValue = [entity userInfo][RKEntityIdentificationAttributesUserInfoKey]; + id userInfoValue = [[entity userInfo] valueForKey:RKEntityIdentificationAttributesUserInfoKey]; if (userInfoValue) { NSArray *attributeNames = [userInfoValue isKindOfClass:[NSArray class]] ? userInfoValue : @[ userInfoValue ]; NSMutableArray *attributes = [NSMutableArray arrayWithCapacity:[attributeNames count]]; From 79e31b524a8e02aa0d8822eb4da01950250aec7c Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 6 Dec 2012 23:43:06 -0500 Subject: [PATCH 05/18] Refactored managed object caches and connection support to enable connecting by multiple atributes that are specified as arrays. * Migrated the caches to return `NSSet` to eliminate duplicate objects when your cache keys overlap * Introduced new recursive strategy for building cache keys in `RKInMemoryManagedObjectCache` * Added support for array cache key values in `RKFetchRequestManagedObjectCache` * Re-enabled a slew of tests that were disabled during 0.20 development --- Code/CoreData/RKEntityByAttributeCache.h | 2 +- Code/CoreData/RKEntityByAttributeCache.m | 77 +- Code/CoreData/RKEntityCache.h | 2 +- Code/CoreData/RKEntityCache.m | 2 +- Code/CoreData/RKEntityMapping.m | 13 + .../RKFetchRequestManagedObjectCache.m | 27 +- Code/CoreData/RKInMemoryManagedObjectCache.m | 2 +- Code/CoreData/RKManagedObjectCaching.h | 6 +- ...KManagedObjectMappingOperationDataSource.m | 10 +- .../RKRelationshipConnectionOperation.m | 12 +- RestKit.xcodeproj/project.pbxproj | 2 - .../CoreData/RKEntityByAttributeCacheTest.m | 73 +- Tests/Logic/CoreData/RKEntityCacheTest.m | 8 +- .../CoreData/RKFetchRequestMappingCacheTest.m | 8 +- ...agedObjectMappingOperationDataSourceTest.m | 589 +++++++++++ .../RKManagedObjectMappingOperationTest.m | 946 ------------------ 16 files changed, 769 insertions(+), 1010 deletions(-) delete mode 100644 Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m diff --git a/Code/CoreData/RKEntityByAttributeCache.h b/Code/CoreData/RKEntityByAttributeCache.h index 514f37d931..06f3faf7bc 100644 --- a/Code/CoreData/RKEntityByAttributeCache.h +++ b/Code/CoreData/RKEntityByAttributeCache.h @@ -153,7 +153,7 @@ @param context The managed object context to retrieve the objects from. @return An array of objects with the value of attribute matching attributeValue or an empty array. */ -- (NSArray *)objectsWithAttributeValues:(NSDictionary *)attributeValues inContext:(NSManagedObjectContext *)context; +- (NSSet *)objectsWithAttributeValues:(NSDictionary *)attributeValues inContext:(NSManagedObjectContext *)context; ///------------------------------ /// @name Managing Cached Objects diff --git a/Code/CoreData/RKEntityByAttributeCache.m b/Code/CoreData/RKEntityByAttributeCache.m index 05198761c9..6bec2fcd33 100644 --- a/Code/CoreData/RKEntityByAttributeCache.m +++ b/Code/CoreData/RKEntityByAttributeCache.m @@ -27,6 +27,7 @@ #import "RKPropertyInspector.h" #import "RKPropertyInspector+CoreData.h" #import "NSManagedObject+RKAdditions.h" +#import "RKObjectUtilities.h" // Set Logging Component #undef RKLogComponent @@ -54,6 +55,32 @@ static id RKCacheKeyValueForEntityAttributeWithValue(NSEntityDescription *entity return [sortedValues componentsJoinedByString:@":"]; } +/* + This function recursively calculates a set of cache keys given a dictionary of attribute values. The basic premise is that we wish to decompose all arrays of values within the dictionary into a distinct cache key, as each object within the cache will appear for only one key. + */ +static NSArray *RKCacheKeysForEntityFromAttributeValues(NSEntityDescription *entity, NSDictionary *attributeValues) +{ + NSMutableArray *cacheKeys = [NSMutableArray array]; + NSSet *collectionKeys = [attributeValues keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) { + return RKObjectIsCollection(obj); + }]; + + if ([collectionKeys count] > 0) { + for (NSString *attributeName in collectionKeys) { + id attributeValue = [attributeValues objectForKey:attributeName]; + for (id value in attributeValue) { + NSMutableDictionary *mutableAttributeValues = [attributeValues mutableCopy]; + [mutableAttributeValues setValue:value forKey:attributeName]; + [cacheKeys addObjectsFromArray:RKCacheKeysForEntityFromAttributeValues(entity, mutableAttributeValues)]; + } + } + } else { + [cacheKeys addObject:RKCacheKeyForEntityWithAttributeValues(entity, attributeValues)]; + } + + return cacheKeys; +} + @interface RKEntityByAttributeCache () @property (nonatomic, strong) NSMutableDictionary *cacheKeysToObjectIDs; @end @@ -193,39 +220,39 @@ - (NSManagedObject *)objectForObjectID:(NSManagedObjectID *)objectID inContext:( - (NSManagedObject *)objectWithAttributeValues:(NSDictionary *)attributeValues inContext:(NSManagedObjectContext *)context { - NSArray *objects = [self objectsWithAttributeValues:attributeValues inContext:context]; - return ([objects count] > 0) ? [objects objectAtIndex:0] : nil; + NSSet *objects = [self objectsWithAttributeValues:attributeValues inContext:context]; + return ([objects count] > 0) ? [objects anyObject] : nil; } -- (NSArray *)objectsWithAttributeValues:(NSDictionary *)attributeValues inContext:(NSManagedObjectContext *)context +- (NSSet *)objectsWithAttributeValues:(NSDictionary *)attributeValues inContext:(NSManagedObjectContext *)context { // TODO: Assert that the attribute values contains all of the cache attributes!!! - NSString *cacheKey = RKCacheKeyForEntityWithAttributeValues(self.entity, attributeValues); - NSArray *objectIDs = nil; - @synchronized(self.cacheKeysToObjectIDs) { - objectIDs = [[NSArray alloc] initWithArray:[self.cacheKeysToObjectIDs objectForKey:cacheKey] copyItems:YES]; - } - if ([objectIDs count]) { - /** - NOTE: - In my benchmarking, retrieving the objects one at a time using existingObjectWithID: is significantly faster - than issuing a single fetch request against all object ID's. - */ - NSMutableArray *objects = [NSMutableArray arrayWithCapacity:[objectIDs count]]; - for (NSManagedObjectID *objectID in objectIDs) { - NSManagedObject *object = [self objectForObjectID:objectID inContext:context]; - if (object) { - [objects addObject:object]; - } else { - RKLogDebug(@"Evicting objectID association for attributes %@ of Entity '%@': %@", attributeValues, self.entity.name, objectID); - [self removeObjectID:objectID forAttributeValues:attributeValues]; + NSMutableSet *objects = [NSMutableSet set]; + NSArray *cacheKeys = RKCacheKeysForEntityFromAttributeValues(self.entity, attributeValues); + for (NSString *cacheKey in cacheKeys) { + NSArray *objectIDs = nil; + @synchronized(self.cacheKeysToObjectIDs) { + objectIDs = [[NSArray alloc] initWithArray:[self.cacheKeysToObjectIDs objectForKey:cacheKey] copyItems:YES]; + } + if ([objectIDs count]) { + /** + NOTE: + In my benchmarking, retrieving the objects one at a time using existingObjectWithID: is significantly faster + than issuing a single fetch request against all object ID's. + */ + for (NSManagedObjectID *objectID in objectIDs) { + NSManagedObject *object = [self objectForObjectID:objectID inContext:context]; + if (object) { + [objects addObject:object]; + } else { + RKLogDebug(@"Evicting objectID association for attributes %@ of Entity '%@': %@", attributeValues, self.entity.name, objectID); + [self removeObjectID:objectID forAttributeValues:attributeValues]; + } } } - - return objects; } - return [NSArray array]; + return objects; } - (void)setObjectID:(NSManagedObjectID *)objectID forAttributeValues:(NSDictionary *)attributeValues diff --git a/Code/CoreData/RKEntityCache.h b/Code/CoreData/RKEntityCache.h index d063845867..d791b86dba 100644 --- a/Code/CoreData/RKEntityCache.h +++ b/Code/CoreData/RKEntityCache.h @@ -91,7 +91,7 @@ @return All matching managed object instances or nil. @raise NSInvalidArgumentException Raised if instances of the entity and attribute have not been cached. */ -- (NSArray *)objectsForEntity:(NSEntityDescription *)entity withAttributeValues:(NSDictionary *)attributeValues inContext:(NSManagedObjectContext *)context; +- (NSSet *)objectsForEntity:(NSEntityDescription *)entity withAttributeValues:(NSDictionary *)attributeValues inContext:(NSManagedObjectContext *)context; ///----------------------------------------------------------------------------- // @name Accessing Underlying Caches diff --git a/Code/CoreData/RKEntityCache.m b/Code/CoreData/RKEntityCache.m index 663f905a9e..90920277d5 100644 --- a/Code/CoreData/RKEntityCache.m +++ b/Code/CoreData/RKEntityCache.m @@ -81,7 +81,7 @@ - (NSManagedObject *)objectForEntity:(NSEntityDescription *)entity withAttribute return nil; } -- (NSArray *)objectsForEntity:(NSEntityDescription *)entity withAttributeValues:(NSDictionary *)attributeValues inContext:(NSManagedObjectContext *)context +- (NSSet *)objectsForEntity:(NSEntityDescription *)entity withAttributeValues:(NSDictionary *)attributeValues inContext:(NSManagedObjectContext *)context { NSParameterAssert(entity); NSParameterAssert(attributeValues); diff --git a/Code/CoreData/RKEntityMapping.m b/Code/CoreData/RKEntityMapping.m index c0f000fb9b..3a4a763283 100644 --- a/Code/CoreData/RKEntityMapping.m +++ b/Code/CoreData/RKEntityMapping.m @@ -270,6 +270,19 @@ - (Class)classForProperty:(NSString *)propertyName return propertyClass; } +- (Class)classForKeyPath:(NSString *)keyPath +{ + NSArray *components = [keyPath componentsSeparatedByString:@"."]; + Class propertyClass = self.objectClass; + for (NSString *property in components) { + propertyClass = [[RKPropertyInspector sharedInspector] classForPropertyNamed:property ofClass:propertyClass]; + if (! propertyClass) propertyClass = [[RKPropertyInspector sharedInspector] classForPropertyNamed:property ofEntity:self.entity]; + if (! propertyClass) break; + } + + return propertyClass; +} + + (void)setEntityIdentificationInferenceEnabled:(BOOL)enabled { entityIdentificationInferenceEnabled = enabled; diff --git a/Code/CoreData/RKFetchRequestManagedObjectCache.m b/Code/CoreData/RKFetchRequestManagedObjectCache.m index f8e82e2242..aa351c0865 100644 --- a/Code/CoreData/RKFetchRequestManagedObjectCache.m +++ b/Code/CoreData/RKFetchRequestManagedObjectCache.m @@ -10,24 +10,33 @@ #import "RKLog.h" #import "RKPropertyInspector.h" #import "RKPropertyInspector+CoreData.h" +#import "RKObjectUtilities.h" // Set Logging Component #undef RKLogComponent #define RKLogComponent RKlcl_cRestKitCoreData +/* + NOTE: At the moment this cache key assume that the structure of the values for each key in the `attributeValues` in constant + i.e. if you have `userID`, it will always be a single value, or `userIDs` will always be an array. + It will need to be reimplemented if changes in attribute values occur during the life of a single cache + */ static NSString *RKPredicateCacheKeyForAttributes(NSArray *attributeNames) { return [[attributeNames sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] componentsJoinedByString:@":"]; } // NOTE: We build a dynamic format string here because `NSCompoundPredicate` does not support use of substiution variables -static NSPredicate *RKPredicateWithSubsitutionVariablesForAttributes(NSArray *attributeNames) +static NSPredicate *RKPredicateWithSubsitutionVariablesForAttributeValues(NSDictionary *attributeValues) { + NSArray *attributeNames = [attributeValues allKeys]; NSMutableArray *formatFragments = [NSMutableArray arrayWithCapacity:[attributeNames count]]; - for (NSString *attributeName in attributeNames) { - NSString *formatFragment = [NSString stringWithFormat:@"%@ = $%@", attributeName, attributeName]; + [attributeValues enumerateKeysAndObjectsUsingBlock:^(NSString *attributeName, id value, BOOL *stop) { + NSString *formatFragment = RKObjectIsCollection(value) + ? [NSString stringWithFormat:@"%@ IN $%@", attributeName, attributeName] + : [NSString stringWithFormat:@"%@ = $%@", attributeName, attributeName]; [formatFragments addObject:formatFragment]; - } + }]; return [NSPredicate predicateWithFormat:[formatFragments componentsJoinedByString:@" AND "]]; } @@ -47,9 +56,9 @@ - (id)init return self; } -- (NSArray *)managedObjectsWithEntity:(NSEntityDescription *)entity - attributeValues:(NSDictionary *)attributeValues - inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +- (NSSet *)managedObjectsWithEntity:(NSEntityDescription *)entity + attributeValues:(NSDictionary *)attributeValues + inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext { NSAssert(entity, @"Cannot find existing managed object without a target class"); @@ -59,7 +68,7 @@ - (NSArray *)managedObjectsWithEntity:(NSEntityDescription *)entity NSString *predicateCacheKey = RKPredicateCacheKeyForAttributes([attributeValues allKeys]); NSPredicate *substitutionPredicate = [self.predicateCache objectForKey:predicateCacheKey]; if (! substitutionPredicate) { - substitutionPredicate = RKPredicateWithSubsitutionVariablesForAttributes([attributeValues allKeys]); + substitutionPredicate = RKPredicateWithSubsitutionVariablesForAttributeValues(attributeValues); [self.predicateCache setObject:substitutionPredicate forKey:predicateCacheKey]; } @@ -72,7 +81,7 @@ - (NSArray *)managedObjectsWithEntity:(NSEntityDescription *)entity } RKLogDebug(@"Found objects '%@' using fetchRequest '%@'", objects, fetchRequest); - return objects; + return [NSSet setWithArray:objects]; } @end diff --git a/Code/CoreData/RKInMemoryManagedObjectCache.m b/Code/CoreData/RKInMemoryManagedObjectCache.m index 80e1634204..bb99ba5872 100644 --- a/Code/CoreData/RKInMemoryManagedObjectCache.m +++ b/Code/CoreData/RKInMemoryManagedObjectCache.m @@ -51,7 +51,7 @@ - (id)init userInfo:nil]; } -- (NSArray *)managedObjectsWithEntity:(NSEntityDescription *)entity +- (NSSet *)managedObjectsWithEntity:(NSEntityDescription *)entity attributeValues:(NSDictionary *)attributeValues inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext { diff --git a/Code/CoreData/RKManagedObjectCaching.h b/Code/CoreData/RKManagedObjectCaching.h index 4637e27568..2fcfcadc48 100644 --- a/Code/CoreData/RKManagedObjectCaching.h +++ b/Code/CoreData/RKManagedObjectCaching.h @@ -38,9 +38,9 @@ @param attributeValues A dictionary specifying the attribute criteria for retrieving managed objects. @param managedObjectContext The context to fetch the matching objects in. */ -- (NSArray *)managedObjectsWithEntity:(NSEntityDescription *)entity - attributeValues:(NSDictionary *)attributeValues - inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; +- (NSSet *)managedObjectsWithEntity:(NSEntityDescription *)entity + attributeValues:(NSDictionary *)attributeValues + inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; ///--------------------------------------------------- /// @name Handling Managed Object Change Notifications diff --git a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m index ad00313f29..9e5c65cbfd 100644 --- a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m +++ b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m @@ -161,12 +161,12 @@ - (id)mappingOperation:(RKMappingOperation *)mappingOperation targetObjectForRep NSEntityDescription *entity = [entityMapping entity]; NSManagedObject *managedObject = nil; if ([entityIdentifierAttributes count]) { - NSArray *objects = [self.managedObjectCache managedObjectsWithEntity:entity - attributeValues:entityIdentifierAttributes - inManagedObjectContext:self.managedObjectContext]; - if (entityMapping.identificationPredicate) objects = [objects filteredArrayUsingPredicate:entityMapping.identificationPredicate]; + NSSet *objects = [self.managedObjectCache managedObjectsWithEntity:entity + attributeValues:entityIdentifierAttributes + inManagedObjectContext:self.managedObjectContext]; + if (entityMapping.identificationPredicate) objects = [objects filteredSetUsingPredicate:entityMapping.identificationPredicate]; if ([objects count] > 0) { - managedObject = objects[0]; + managedObject = [objects anyObject]; if ([objects count] > 1) RKLogWarning(@"Managed object cache returned %ld objects for the identifier configured for the '%@' entity, expected 1.", (long) [objects count], [entity name]); } if (managedObject && [self.managedObjectCache respondsToSelector:@selector(didFetchObject:)]) { diff --git a/Code/CoreData/RKRelationshipConnectionOperation.m b/Code/CoreData/RKRelationshipConnectionOperation.m index f8416931f0..6a25de9b28 100644 --- a/Code/CoreData/RKRelationshipConnectionOperation.m +++ b/Code/CoreData/RKRelationshipConnectionOperation.m @@ -140,15 +140,15 @@ - (id)findConnected id connectionResult = nil; if ([self.connection isForeignKeyConnection]) { NSDictionary *attributeValues = RKConnectionAttributeValuesWithObject(self.connection, self.managedObject); - NSArray *managedObjects = [self.managedObjectCache managedObjectsWithEntity:[self.connection.relationship destinationEntity] - attributeValues:attributeValues - inManagedObjectContext:self.managedObjectContext]; - if (self.connection.predicate) managedObjects = [managedObjects filteredArrayUsingPredicate:self.connection.predicate]; + NSSet *managedObjects = [self.managedObjectCache managedObjectsWithEntity:[self.connection.relationship destinationEntity] + attributeValues:attributeValues + inManagedObjectContext:self.managedObjectContext]; + if (self.connection.predicate) managedObjects = [managedObjects filteredSetUsingPredicate:self.connection.predicate]; if ([self.connection.relationship isToMany]) { connectionResult = managedObjects; } else { - if ([managedObjects count] > 1) RKLogWarning(@"Retrieved %ld objects satisfying connection criteria for one-to-one relationship connection: only the first result will be connected.", (long) [managedObjects count]); - if ([managedObjects count]) connectionResult = managedObjects[0]; + if ([managedObjects count] > 1) RKLogWarning(@"Retrieved %ld objects satisfying connection criteria for one-to-one relationship connection: only object will be connected.", (long) [managedObjects count]); + if ([managedObjects count]) connectionResult = [managedObjects anyObject]; } } else if ([self.connection isKeyPathConnection]) { connectionResult = [self.managedObject valueForKeyPath:self.connection.keyPath]; diff --git a/RestKit.xcodeproj/project.pbxproj b/RestKit.xcodeproj/project.pbxproj index be286dbb12..5df55f2590 100644 --- a/RestKit.xcodeproj/project.pbxproj +++ b/RestKit.xcodeproj/project.pbxproj @@ -697,7 +697,6 @@ 25160F7B145657220060A5C5 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = SDKs/MacOSX10.7.sdk/System/Library/Frameworks/SystemConfiguration.framework; sourceTree = DEVELOPER_DIR; }; 25160F7D1456572F0060A5C5 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = SDKs/MacOSX10.7.sdk/System/Library/Frameworks/Cocoa.framework; sourceTree = DEVELOPER_DIR; }; 25160FC71456F2330060A5C5 /* RKManagedObjectLoaderTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectLoaderTest.m; sourceTree = ""; }; - 25160FC81456F2330060A5C5 /* RKManagedObjectMappingOperationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectMappingOperationTest.m; sourceTree = ""; }; 25160FC91456F2330060A5C5 /* RKEntityMappingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKEntityMappingTest.m; sourceTree = ""; }; 25160FCB1456F2330060A5C5 /* RKManagedObjectStoreTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectStoreTest.m; sourceTree = ""; }; 25160FCF1456F2330060A5C5 /* blake.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = blake.png; sourceTree = ""; }; @@ -1290,7 +1289,6 @@ isa = PBXGroup; children = ( 25160FC71456F2330060A5C5 /* RKManagedObjectLoaderTest.m */, - 25160FC81456F2330060A5C5 /* RKManagedObjectMappingOperationTest.m */, 25160FC91456F2330060A5C5 /* RKEntityMappingTest.m */, 25160FCB1456F2330060A5C5 /* RKManagedObjectStoreTest.m */, 25E36E0115195CED00F9E448 /* RKFetchRequestMappingCacheTest.m */, diff --git a/Tests/Logic/CoreData/RKEntityByAttributeCacheTest.m b/Tests/Logic/CoreData/RKEntityByAttributeCacheTest.m index 00fd610852..909079afbe 100644 --- a/Tests/Logic/CoreData/RKEntityByAttributeCacheTest.m +++ b/Tests/Logic/CoreData/RKEntityByAttributeCacheTest.m @@ -155,11 +155,80 @@ - (void)testRetrievalOfObjectsWithAttributeValue NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; childContext.parentContext = self.managedObjectContext; - NSArray *objects = [self.cache objectsWithAttributeValues:@{ @"railsID": @(12345) } inContext:childContext]; + NSSet *objects = [self.cache objectsWithAttributeValues:@{ @"railsID": @(12345) } inContext:childContext]; assertThat(objects, hasCountOf(2)); - assertThat([objects objectAtIndex:0], is(instanceOf([NSManagedObject class]))); + assertThat([objects anyObject], is(instanceOf([NSManagedObject class]))); } +- (void)testRetrievalOfObjectsWithCollectionAttributeValue +{ + RKHuman *human1 = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:self.managedObjectStore.persistentStoreManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:self.managedObjectStore.persistentStoreManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:5678]; + [self.managedObjectStore.persistentStoreManagedObjectContext save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + + NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; + childContext.parentContext = self.managedObjectContext; + NSSet *objects = [self.cache objectsWithAttributeValues:@{ @"railsID": @[ @(12345), @(5678) ] } inContext:childContext]; + assertThat(objects, hasCountOf(2)); + assertThat([objects anyObject], is(instanceOf([NSManagedObject class]))); +} + +- (void)testRetrievalOfObjectsWithMoreThanOneCollectionAttributeValue +{ + NSEntityDescription *entity = [NSEntityDescription entityForName:@"Human" inManagedObjectContext:self.managedObjectContext]; + self.cache = [[RKEntityByAttributeCache alloc] initWithEntity:entity + attributes:@[ @"railsID", @"name" ] + managedObjectContext:self.managedObjectContext]; + // Disable cache monitoring. Tested in specific cases. + self.cache.monitorsContextForChanges = NO; + + RKHuman *human1 = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:self.managedObjectStore.persistentStoreManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + human1.name = @"Blake"; + RKHuman *human2 = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:self.managedObjectStore.persistentStoreManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:5678]; + human2.name = @"Jeff"; + RKHuman *human3 = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:self.managedObjectStore.persistentStoreManagedObjectContext]; + human3.railsID = [NSNumber numberWithInteger:9999]; + human3.name = @"Blake"; + [self.managedObjectStore.persistentStoreManagedObjectContext save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + [self.cache addObject:human3]; + + NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; + childContext.parentContext = self.managedObjectContext; + NSSet *objects = [self.cache objectsWithAttributeValues:@{ @"railsID": @[ @(12345), @(5678) ], @"name": @"Blake" } inContext:childContext]; + // should find human1 + assertThat(objects, hasCountOf(1)); + assertThat([objects anyObject], is(instanceOf([NSManagedObject class]))); + assertThat([[objects anyObject] objectID], equalTo([human1 objectID])); + + objects = [self.cache objectsWithAttributeValues:@{ @"railsID": @[ @(12345), @(9999) ], @"name": @"Blake" } inContext:childContext]; + // should be human1 and human3 + assertThat(objects, hasCountOf(2)); + assertThat([objects valueForKey:@"objectID"], hasItems([human1 objectID], [human3 objectID], nil)); + + objects = [self.cache objectsWithAttributeValues:@{ @"railsID": @[ @(12345), @(9999) ], @"name": @[ @"Blake", @"Jeff" ] } inContext:childContext]; + // should be human1, human2 and human3 + assertThat(objects, hasCountOf(2)); + assertThat([objects valueForKey:@"objectID"], hasItems([human1 objectID], [human3 objectID], nil)); + + objects = [self.cache objectsWithAttributeValues:@{ @"railsID": @[ @(31337), @(8888) ], @"name": @[ @"Blake", @"Jeff" ] } inContext:childContext]; + // should be none + assertThat(objects, hasCountOf(0)); +} + +// Do this with 3 attributes, 2 that are arrays and 1 that is not +// check +// Test blowing up if you request objects without enough cache keys + - (void)testAddingObjectToCache { RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:self.managedObjectStore.persistentStoreManagedObjectContext]; diff --git a/Tests/Logic/CoreData/RKEntityCacheTest.m b/Tests/Logic/CoreData/RKEntityCacheTest.m index 38117c3706..ed0d13d5a9 100644 --- a/Tests/Logic/CoreData/RKEntityCacheTest.m +++ b/Tests/Logic/CoreData/RKEntityCacheTest.m @@ -93,7 +93,7 @@ - (void)testRetrievalOfObjectsForEntityWithAttributeValue [_cache cacheObjectsForEntity:self.entity byAttributes:@[ @"railsID" ]]; NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; childContext.parentContext = self.managedObjectStore.persistentStoreManagedObjectContext; - NSArray *objects = [self.cache objectsForEntity:self.entity withAttributeValues:@{ @"railsID": @(12345) } inContext:childContext]; + NSSet *objects = [self.cache objectsForEntity:self.entity withAttributeValues:@{ @"railsID": @(12345) } inContext:childContext]; assertThat(objects, hasCountOf(2)); assertThat([objects valueForKey:@"objectID"], containsInAnyOrder(human1.objectID, human2.objectID, nil)); } @@ -114,7 +114,7 @@ - (void)testThatFlushEmptiesAllUnderlyingAttributeCaches NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; childContext.parentContext = self.managedObjectStore.persistentStoreManagedObjectContext; - NSArray *objects = [self.cache objectsForEntity:self.entity withAttributeValues:@{ @"railsID": @(12345) } inContext:childContext]; + NSSet *objects = [self.cache objectsForEntity:self.entity withAttributeValues:@{ @"railsID": @(12345) } inContext:childContext]; assertThat(objects, hasCountOf(2)); assertThat([objects valueForKey:@"objectID"], containsInAnyOrder(human1.objectID, human2.objectID, nil)); @@ -154,7 +154,7 @@ - (void)testAddingObjectAddsToEachUnderlyingEntityAttributeCaches NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; childContext.parentContext = self.managedObjectStore.persistentStoreManagedObjectContext; - NSArray *objects = [self.cache objectsForEntity:self.entity withAttributeValues:@{ @"railsID": @(12345) } inContext:childContext]; + NSSet *objects = [self.cache objectsForEntity:self.entity withAttributeValues:@{ @"railsID": @(12345) } inContext:childContext]; assertThat(objects, hasCountOf(2)); assertThat([objects valueForKey:@"objectID"], containsInAnyOrder(human1.objectID, human2.objectID, nil)); @@ -188,7 +188,7 @@ - (void)testRemovingObjectRemovesFromUnderlyingEntityAttributeCaches NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; childContext.parentContext = self.managedObjectStore.persistentStoreManagedObjectContext; - NSArray *objects = [self.cache objectsForEntity:self.entity withAttributeValues:@{ @"railsID": @(12345) } inContext:childContext]; + NSSet *objects = [self.cache objectsForEntity:self.entity withAttributeValues:@{ @"railsID": @(12345) } inContext:childContext]; assertThat(objects, hasCountOf(2)); assertThat([objects valueForKey:@"objectID"], containsInAnyOrder(human1.objectID, human2.objectID, nil)); diff --git a/Tests/Logic/CoreData/RKFetchRequestMappingCacheTest.m b/Tests/Logic/CoreData/RKFetchRequestMappingCacheTest.m index 7fa83b48e6..b9c31b265c 100644 --- a/Tests/Logic/CoreData/RKFetchRequestMappingCacheTest.m +++ b/Tests/Logic/CoreData/RKFetchRequestMappingCacheTest.m @@ -30,10 +30,10 @@ - (void)testFetchRequestMappingCacheReturnsObjectsWithNumericPrimaryKey reginald.railsID = [NSNumber numberWithInt:123456]; [managedObjectStore.persistentStoreManagedObjectContext save:nil]; - NSArray *managedObjects = [cache managedObjectsWithEntity:entity + NSSet *managedObjects = [cache managedObjectsWithEntity:entity attributeValues:@{ @"railsID": @123456 } inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; - NSArray *cats = @[ reginald ]; + NSSet *cats = [NSSet setWithObject:reginald]; expect(managedObjects).to.equal(cats); } @@ -50,10 +50,10 @@ - (void)testFetchRequestMappingCacheReturnsObjectsWithStringPrimaryKey birthday.eventID = @"e-1234-a8-b12"; [managedObjectStore.persistentStoreManagedObjectContext save:nil]; - NSArray *managedObjects = [cache managedObjectsWithEntity:entity + NSSet *managedObjects = [cache managedObjectsWithEntity:entity attributeValues:@{ @"eventID": @"e-1234-a8-b12" } inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; - NSArray *birthdays = @[ birthday ]; + NSSet *birthdays = [NSSet setWithObject:birthday]; expect(managedObjects).to.equal(birthdays); } diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m index a8747b8efb..9d22cb8b0b 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m @@ -13,6 +13,11 @@ #import "RKManagedObjectMappingOperationDataSource.h" #import "RKMappableObject.h" #import "RKMappingErrors.h" +#import "RKCat.h" +#import "RKHuman.h" +#import "RKChild.h" +#import "RKParent.h" +//#import "RKBenchmark.h" @interface RKManagedObjectMappingOperationDataSourceTest : RKTestCase @@ -478,4 +483,588 @@ - (void)testEntityIdentifierWithPredicate expect(object).to.equal(human1); } +- (void)testMappingInPrivateQueue +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; + managedObjectContext.parentContext = managedObjectStore.persistentStoreManagedObjectContext; + managedObjectContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectContext + cache:managedObjectCache]; + + __block BOOL success; + __block NSError *error; + NSDictionary *sourceObject = @{ @"name" : @"Blake Watters" }; + [managedObjectContext performBlockAndWait:^{ + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"Human" inManagedObjectContext:managedObjectContext]; + RKHuman *human = [[RKHuman alloc] initWithEntity:entity insertIntoManagedObjectContext:managedObjectContext]; + RKMappingOperation *mappingOperation = [[RKMappingOperation alloc] initWithSourceObject:sourceObject destinationObject:human mapping:humanMapping]; + mappingOperation.dataSource = mappingOperationDataSource; + success = [mappingOperation performMapping:&error]; + + assertThatBool(success, is(equalToBool(YES))); + assertThat(human.name, is(equalTo(@"Blake Watters"))); + }]; +} + +- (void)testShouldConnectRelationshipsByPrimaryKey +{ + /* Connect a new human to a cat */ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + catMapping.identificationAttributes = @[ @"railsID" ]; + [catMapping addAttributeMappingsFromArray:@[@"name"]]; + + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; + [humanMapping addConnectionForRelationship:@"favoriteCat" connectedBy:@{ @"favoriteCatID": @"railsID" }]; + + // Create a cat to connect + RKCat *cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + cat.name = @"Asia"; + cat.railsID = @31337; + [managedObjectStore.persistentStoreManagedObjectContext save:nil]; + + NSDictionary *mappableData = @{ @"name": @"Blake", @"favoriteCatID": @31337 }; + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + mappingOperationDataSource.operationQueue = [NSOperationQueue new]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; + operation.dataSource = mappingOperationDataSource; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + [mappingOperationDataSource.operationQueue waitUntilAllOperationsAreFinished]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(human.favoriteCat, isNot(nilValue())); + assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); +} + +- (void)testShouldConnectRelationshipsByPrimaryKeyReverse +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSManagedObjectContext *managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; + + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + catMapping.identificationAttributes = @[ @"railsID" ]; + [catMapping addAttributeMappingsFromArray:@[@"name", @"railsID"]]; + [catMapping addConnectionForRelationship:@"favoriteOfHumans" connectedBy:@{ @"railsID": @"favoriteCatID" }]; + + // Create some humans to connect + RKHuman *blake = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + blake.name = @"Blake"; + blake.favoriteCatID = [NSNumber numberWithInt:31340]; + + RKHuman *jeremy = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + jeremy.name = @"Jeremy"; + jeremy.favoriteCatID = [NSNumber numberWithInt:31340]; + + [managedObjectStore.persistentStoreManagedObjectContext save:nil]; + + NSDictionary *mappableData = @{ @"name": @"Asia", @"railsID": @31340 }; + RKCat *cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + NSOperationQueue *operationQueue = [NSOperationQueue new]; + [operationQueue setSuspended:YES]; + mappingOperationDataSource.operationQueue = operationQueue; + __block BOOL success; + [managedObjectContext performBlockAndWait:^{ + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:cat mapping:catMapping]; + operation.dataSource = mappingOperationDataSource; + NSError *error = nil; + success = [operation performMapping:&error]; + }]; + + [operationQueue setSuspended:NO]; + [operationQueue waitUntilAllOperationsAreFinished]; + + assertThatBool(success, is(equalToBool(YES))); + assertThat(cat.favoriteOfHumans, isNot(nilValue())); + assertThat([cat.favoriteOfHumans valueForKeyPath:@"name"], containsInAnyOrder(blake.name, jeremy.name, nil)); +} + +- (void)testConnectionOfHasManyRelationshipsByPrimaryKey +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + catMapping.identificationAttributes = @[ @"railsID" ]; + [catMapping addAttributeMappingsFromArray:@[@"name"]]; + + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; + [humanMapping addConnectionForRelationship:@"favoriteCat" connectedBy:@{ @"favoriteCatID": @"railsID" }]; + + // Create a cat to connect + RKCat *cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + cat.name = @"Asia"; + cat.railsID = @31337; + [managedObjectStore.persistentStoreManagedObjectContext save:nil]; + + NSDictionary *mappableData = @{ @"name": @"Blake", @"favoriteCatID": @31337 }; + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + mappingOperationDataSource.operationQueue = [NSOperationQueue new]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; + operation.dataSource = mappingOperationDataSource; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + [mappingOperationDataSource.operationQueue waitUntilAllOperationsAreFinished]; + assertThat(human.favoriteCat, isNot(nilValue())); + assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); +} + +- (void)testConnectingARelationshipFromASourceAttributeWhoseValueIsACollectionWithFetchRequestCache +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + catMapping.identificationAttributes = @[ @"railsID" ]; + [catMapping addAttributeMappingsFromArray:@[@"name"]]; + + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromArray:@[@"name", @"catIDs"]]; + [humanMapping addConnectionForRelationship:@"cats" connectedBy:@{ @"catIDs": @"railsID" }]; + + // Create a couple of cats to connect + RKCat *asia = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + asia.name = @"Asia"; + asia.railsID = @31337; + + RKCat *roy = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + roy.name = @"Reginald Royford Williams III"; + roy.railsID = [NSNumber numberWithInt:31338]; + + [managedObjectStore.persistentStoreManagedObjectContext save:nil]; + + NSArray *catIDs = [NSArray arrayWithObjects:@31337, [NSNumber numberWithInt:31338], nil]; + NSDictionary *mappableData = @{ @"name": @"Blake", @"catIDs": catIDs }; + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + mappingOperationDataSource.operationQueue = [NSOperationQueue new]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; + operation.dataSource = mappingOperationDataSource; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + [mappingOperationDataSource.operationQueue waitUntilAllOperationsAreFinished]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(human.cats, isNot(nilValue())); + assertThat([human.cats valueForKeyPath:@"name"], containsInAnyOrder(@"Asia", @"Reginald Royford Williams III", nil)); +} + +- (void)testConnectingARelationshipFromASourceAttributeWhoseValueIsACollectionWithInMemoryCache +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + catMapping.identificationAttributes = @[ @"railsID" ]; + [catMapping addAttributeMappingsFromArray:@[@"name"]]; + + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromArray:@[@"name", @"catIDs"]]; + [humanMapping addConnectionForRelationship:@"cats" connectedBy:@{ @"catIDs": @"railsID" }]; + + // Create a couple of cats to connect + RKCat *asia = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + asia.name = @"Asia"; + asia.railsID = @31337; + + RKCat *roy = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + roy.name = @"Reginald Royford Williams III"; + roy.railsID = [NSNumber numberWithInt:31338]; + + [managedObjectStore.persistentStoreManagedObjectContext save:nil]; + + NSArray *catIDs = [NSArray arrayWithObjects:@31337, [NSNumber numberWithInt:31338], nil]; + NSDictionary *mappableData = @{ @"name": @"Blake", @"catIDs": catIDs }; + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + + RKInMemoryManagedObjectCache *managedObjectCache = [[RKInMemoryManagedObjectCache alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + mappingOperationDataSource.operationQueue = [NSOperationQueue new]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; + operation.dataSource = mappingOperationDataSource; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + [mappingOperationDataSource.operationQueue waitUntilAllOperationsAreFinished]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(human.cats, isNot(nilValue())); + assertThat([human.cats valueForKeyPath:@"name"], containsInAnyOrder(@"Asia", @"Reginald Royford Williams III", nil)); +} + +- (void)testShouldConnectRelationshipsByPrimaryKeyWithDifferentSourceAndDestinationKeyPathsReverse +{ + /* Connect a new cat to a human */ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromArray:@[@"name", @"railsID"]]; + + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + catMapping.identificationAttributes = @[ @"railsID" ]; + [catMapping addAttributeMappingsFromArray:@[@"name", @"humanId"]]; + [catMapping addConnectionForRelationship:@"human" connectedBy:@{ @"humanId": @"railsID" }]; + + // Create a human to connect + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + human.name = @"Blake"; + human.railsID = @31337; + [managedObjectStore.persistentStoreManagedObjectContext save:nil]; + + NSDictionary *mappableData = @{ @"name": @"Asia", @"humanId": @31337 }; + RKCat *cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + mappingOperationDataSource.operationQueue = [NSOperationQueue new]; + RKMappingOperation* operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:cat mapping:catMapping]; + operation.dataSource = mappingOperationDataSource; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + [mappingOperationDataSource.operationQueue waitUntilAllOperationsAreFinished]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(cat.human, isNot(nilValue())); + assertThat(cat.human.name, is(equalTo(@"Blake"))); +} + +- (void)testShouldLoadNestedHasManyRelationship +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + catMapping.identificationAttributes = @[ @"railsID" ]; + [catMapping addAttributeMappingsFromArray:@[@"name"]]; + + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; + [humanMapping addRelationshipMappingWithSourceKeyPath:@"cats" mapping:catMapping]; + + NSArray *catsData = [NSArray arrayWithObject:[NSDictionary dictionaryWithObject:@"Asia" forKey:@"name"]]; + NSDictionary *mappableData = @{ @"name": @"Blake", @"favoriteCatID": @31337, @"cats": catsData }; + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; + operation.dataSource = mappingOperationDataSource; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); +} + +- (void)testShouldLoadOrderedHasManyRelationship +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + catMapping.identificationAttributes = @[ @"railsID" ]; + [catMapping addAttributeMappingsFromArray:@[@"name"]]; + + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; + [humanMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"cats" toKeyPath:@"catsInOrderByAge" withMapping:catMapping]];; + + NSArray *catsData = [NSArray arrayWithObject:[NSDictionary dictionaryWithObject:@"Asia" forKey:@"name"]]; + NSDictionary *mappableData = @{ @"name": @"Blake", @"favoriteCatID": @31337, @"cats": catsData }; + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; + operation.dataSource = mappingOperationDataSource; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat([human catsInOrderByAge], isNot(empty())); +} + +- (void)testShouldMapNullToAHasManyRelationship +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + [catMapping addAttributeMappingsFromArray:@[@"name"]]; + + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; + [humanMapping addRelationshipMappingWithSourceKeyPath:@"cats" mapping:catMapping]; + + NSDictionary *mappableData = @{ @"name": @"Blake", @"cats": [NSNull null] }; + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; + operation.dataSource = mappingOperationDataSource; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(human.cats, is(empty())); +} + +- (void)testShouldLoadNestedHasManyRelationshipWithoutABackingClass +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *cloudMapping = [RKEntityMapping mappingForEntityForName:@"Cloud" inManagedObjectStore:managedObjectStore]; + [cloudMapping addAttributeMappingsFromArray:@[@"name"]]; + + RKEntityMapping *stormMapping = [RKEntityMapping mappingForEntityForName:@"Storm" inManagedObjectStore:managedObjectStore]; + [stormMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; + [stormMapping addRelationshipMappingWithSourceKeyPath:@"clouds" mapping:cloudMapping]; + + NSArray *cloudsData = [NSArray arrayWithObject:[NSDictionary dictionaryWithObject:@"Nimbus" forKey:@"name"]]; + NSDictionary *mappableData = @{ @"name": @"Hurricane", @"clouds": cloudsData }; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"Storm" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + NSManagedObject *storm = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:storm mapping:stormMapping]; + operation.dataSource = mappingOperationDataSource; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); +} + +- (void)testShouldConnectManyToManyRelationships +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; + childMapping.identificationAttributes = @[ @"name" ]; + [childMapping addAttributeMappingsFromArray:@[@"name"]]; + + RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; + parentMapping.identificationAttributes = @[ @"railsID" ]; + [parentMapping addAttributeMappingsFromArray:@[@"name", @"age"]]; + [parentMapping addRelationshipMappingWithSourceKeyPath:@"children" mapping:childMapping]; + + NSArray *childMappableData = @[ @{ @"name": @"Maya" }, @{ @"name": @"Brady" } ]; + NSDictionary *parentMappableData = @{ @"name": @"Win", @"age": @34, @"children": childMappableData }; + RKParent *parent = [NSEntityDescription insertNewObjectForEntityForName:@"Parent" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + mappingOperationDataSource.operationQueue = [NSOperationQueue new]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:parentMappableData destinationObject:parent mapping:parentMapping]; + operation.dataSource = mappingOperationDataSource; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(parent.children, isNot(nilValue())); + assertThatUnsignedInteger([parent.children count], is(equalToInt(2))); + assertThat([[parent.children anyObject] parents], isNot(nilValue())); + assertThatBool([[[parent.children anyObject] parents] containsObject:parent], is(equalToBool(YES))); + assertThatUnsignedInteger([[[parent.children anyObject] parents] count], is(equalToInt(1))); +} + +- (void)testShouldConnectRelationshipsByPrimaryKeyRegardlessOfOrder +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSManagedObjectContext *managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + + RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; + [parentMapping addAttributeMappingsFromArray:@[@"parentID"]]; + parentMapping.identificationAttributes = @[ @"parentID" ]; + + RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; + [childMapping addAttributeMappingsFromArray:@[@"fatherID"]]; + [childMapping addConnectionForRelationship:@"father" connectedBy:@{ @"fatherID": @"parentID" }]; + + NSDictionary *mappingsDictionary = @{ @"parents": parentMapping, @"children": childMapping }; + NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"ConnectingParents.json"]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectContext + cache:managedObjectCache]; + NSOperationQueue *operationQueue = [NSOperationQueue new]; + [operationQueue setSuspended:YES]; + mappingOperationDataSource.operationQueue = operationQueue; + [managedObjectContext performBlockAndWait:^{ + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; + mapper.mappingOperationDataSource = mappingOperationDataSource; + [mapper start]; + }]; + + [operationQueue setSuspended:NO]; + [operationQueue waitUntilAllOperationsAreFinished]; + + [managedObjectContext performBlockAndWait:^{ + NSError *error; + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Parent"]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"parentID = %@", @1]; + fetchRequest.fetchLimit = 1; + NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:&error]; + RKParent *parent = [results lastObject]; + assertThat(parent, is(notNilValue())); + NSSet *children = [parent fatheredChildren]; + assertThat(children, hasCountOf(1)); + RKChild *child = [children anyObject]; + assertThat(child.father, is(notNilValue())); + }]; +} + +- (void)testMappingAPayloadContainingRepeatedObjectsDoesNotYieldDuplicatesWithFetchRequestMappingCache +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + managedObjectStore.managedObjectCache = managedObjectCache; + + RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; + childMapping.identificationAttributes = @[ @"childID" ]; + [childMapping addAttributeMappingsFromArray:@[@"name", @"childID"]]; + + RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; + [parentMapping addAttributeMappingsFromArray:@[@"parentID", @"name"]]; + parentMapping.identificationAttributes = @[ @"parentID" ]; + [parentMapping addRelationshipMappingWithSourceKeyPath:@"children" mapping:childMapping]; + + NSDictionary *mappingsDictionary = @{ @"parents": parentMapping }; + + NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"parents_and_children.json"]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; + mapper.mappingOperationDataSource = mappingOperationDataSource; + [mapper start]; + + NSUInteger parentCount = [managedObjectStore.persistentStoreManagedObjectContext countForEntityForName:@"Parent" predicate:nil error:nil]; + NSUInteger childrenCount = [managedObjectStore.persistentStoreManagedObjectContext countForEntityForName:@"Child" predicate:nil error:nil]; + assertThatInteger(parentCount, is(equalToInteger(2))); + assertThatInteger(childrenCount, is(equalToInteger(4))); +} + +- (void)testMappingAPayloadContainingRepeatedObjectsDoesNotYieldDuplicatesWithInMemoryMappingCache +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKInMemoryManagedObjectCache *managedObjectCache = [[RKInMemoryManagedObjectCache alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:managedObjectCache]; + managedObjectStore.managedObjectCache = managedObjectCache; + + RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; + childMapping.identificationAttributes = @[ @"childID" ]; + [childMapping addAttributeMappingsFromArray:@[@"name", @"childID"]]; + + RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; + [parentMapping addAttributeMappingsFromArray:@[@"parentID", @"name"]]; + parentMapping.identificationAttributes = @[ @"parentID" ]; + [parentMapping addRelationshipMappingWithSourceKeyPath:@"children" mapping:childMapping]; + + // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary + // keys are not guaranteed to return in any particular order + NSDictionary *mappingsDictionary = @{ @"parents": parentMapping }; + + NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"parents_and_children.json"]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; + mapper.mappingOperationDataSource = mappingOperationDataSource; + [mapper start]; + + NSError *error = nil; + BOOL success = [managedObjectStore.persistentStoreManagedObjectContext save:&error]; + assertThatBool(success, is(equalToBool(YES))); + NSLog(@"Failed to save MOC: %@", error); + assertThat(error, is(nilValue())); + + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Parent"]; + NSUInteger parentCount = [managedObjectStore.persistentStoreManagedObjectContext countForFetchRequest:fetchRequest error:&error]; + NSUInteger childrenCount = [managedObjectStore.persistentStoreManagedObjectContext countForEntityForName:@"Child" predicate:nil error:nil]; + assertThatInteger(parentCount, is(equalToInteger(2))); + assertThatInteger(childrenCount, is(equalToInteger(4))); +} + +// TODO: Import bencharmk utility somehow... +//- (void)testMappingAPayloadContainingRepeatedObjectsPerformsAcceptablyWithFetchRequestMappingCache +//{ +// RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; +// RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; +// RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext +// cache:managedObjectCache]; +// managedObjectStore.managedObjectCache = managedObjectCache; +// +// RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; +// childMapping.identificationAttributes = @[ @"childID" ]; +// [childMapping addAttributeMappingsFromArray:@[@"name", @"childID"]]; +// +// RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; +// [parentMapping addAttributeMappingsFromArray:@[@"parentID", @"name"]]; +// parentMapping.identificationAttributes = @[ @"parentID" ]; +// [parentMapping addRelationshipMappingWithSourceKeyPath:@"children" mapping:childMapping]; +// +// +// NSDictionary *mappingsDictionary = @{ @"parents": parentMapping }; +// NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"benchmark_parents_and_children.json"]; +// RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; +// mapper.mappingOperationDataSource = mappingOperationDataSource; +// +// RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelOff); +// RKLogConfigureByName("RestKit/CoreData", RKLogLevelOff); +// +// [RKBenchmark report:@"Mapping with Fetch Request Cache" executionBlock:^{ +// for (NSUInteger i = 0; i < 50; i++) { +// [mapper performMapping]; +// } +// }]; +// NSUInteger parentCount = [managedObjectStore.persistentStoreManagedObjectContext countForEntityForName:@"Parent" predicate:nil error:nil]; +// NSUInteger childrenCount = [managedObjectStore.persistentStoreManagedObjectContext countForEntityForName:@"Child" predicate:nil error:nil]; +// assertThatInteger(parentCount, is(equalToInteger(25))); +// assertThatInteger(childrenCount, is(equalToInteger(51))); +//} +// +//- (void)testMappingAPayloadContainingRepeatedObjectsPerformsAcceptablyWithInMemoryMappingCache +//{ +// RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; +// RKInMemoryManagedObjectCache *managedObjectCache = [[RKInMemoryManagedObjectCache alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; +// RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext +// cache:managedObjectCache]; +// managedObjectStore.managedObjectCache = managedObjectCache; +// +// RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; +// childMapping.identificationAttributes = @[ @"childID" ]; +// [childMapping addAttributeMappingsFromArray:@[@"name", @"childID"]]; +// +// RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; +// [parentMapping addAttributeMappingsFromArray:@[@"parentID", @"name"]]; +// parentMapping.identificationAttributes = @[ @"parentID" ]; +// [parentMapping addRelationshipMappingWithSourceKeyPath:@"children" mapping:childMapping]; +// +// NSDictionary *mappingsDictionary = @{ @"parents": parentMapping }; +// NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"benchmark_parents_and_children.json"]; +// RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; +// mapper.mappingOperationDataSource = mappingOperationDataSource; +// RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelOff); +// RKLogConfigureByName("RestKit/CoreData", RKLogLevelOff); +// +// [RKBenchmark report:@"Mapping with In Memory Cache" executionBlock:^{ +// for (NSUInteger i = 0; i < 50; i++) { +// [mapper performMapping]; +// } +// }]; +// NSUInteger parentCount = [managedObjectStore.persistentStoreManagedObjectContext countForEntityForName:@"Parent" predicate:nil error:nil]; +// NSUInteger childrenCount = [managedObjectStore.persistentStoreManagedObjectContext countForEntityForName:@"Child" predicate:nil error:nil]; +// assertThatInteger(parentCount, is(equalToInteger(25))); +// assertThatInteger(childrenCount, is(equalToInteger(51))); +//} + @end diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m deleted file mode 100644 index c3be3f2264..0000000000 --- a/Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m +++ /dev/null @@ -1,946 +0,0 @@ -// -// RKObjectMappingOperationTest.m -// RestKit -// -// Created by Blake Watters on 5/31/11. -// Copyright (c) 2009-2012 RestKit. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import "RKTestEnvironment.h" -#import "RKEntityMapping.h" -#import "RKManagedObjectMappingOperationDataSource.h" -#import "RKCat.h" -#import "RKHuman.h" -#import "RKChild.h" -#import "RKParent.h" -#import "RKBenchmark.h" - -@interface RKManagedObjectMappingOperationTest : RKTestCase -@end - -@implementation RKManagedObjectMappingOperationTest - -- (void)setUp -{ - [RKTestFactory setUp]; -} - -- (void)tearDown -{ - [RKTestFactory tearDown]; -} - -- (void)testMappingInPrivateQueue -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - NSManagedObjectContext *managedObjectContext = [[[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType] autorelease]; - managedObjectContext.parentContext = managedObjectStore.persistentStoreManagedObjectContextContext; - managedObjectContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectContext - cache:managedObjectCache]; - - __block BOOL success; - __block NSError *error; - NSDictionary *sourceObject = @{ @"name" : @"Blake Watters" }; - [managedObjectContext performBlockAndWait:^{ - RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - NSEntityDescription *entity = [NSEntityDescription entityForName:@"Human" inManagedObjectContext:managedObjectContext]; - RKHuman *human = [[RKHuman alloc] initWithEntity:entity insertIntoManagedObjectContext:managedObjectContext]; - RKMappingOperation *mappingOperation = [RKMappingOperation mappingOperationFromObject:sourceObject toObject:human withMapping:humanMapping]; - mappingOperation.dataSource = mappingOperationDataSource; - success = [mappingOperation performMapping:&error]; - - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.name, is(equalTo(@"Blake Watters"))); - }]; -} - -- (void)testShouldConnectRelationshipsByPrimaryKey -{ - /* Connect a new human to a cat */ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping connectRelationship:@"favoriteCat" fromKeyPath:@"favoriteCatID" toKeyPath:@"railsID" withMapping:catMapping]; - - // Create a cat to connect - RKCat *cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - cat.name = @"Asia"; - cat.railsID = [NSNumber numberWithInt:31337]; - [managedObjectStore.persistentStoreManagedObjectContextContext save:nil]; - - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], nil]; - RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.favoriteCat, isNot(nilValue())); - assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); -} - -- (void)testShouldConnectRelationshipsByPrimaryKeyReverse -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - NSManagedObjectContext *managedObjectContext = managedObjectStore.persistentStoreManagedObjectContextContext; - - RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - - RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name", @"railsID"]]; - [catMapping connectRelationship:@"favoriteOfHumans" fromKeyPath:@"railsID" toKeyPath:@"favoriteCatID" withMapping:humanMapping]; - - // Create some humans to connect - RKHuman *blake = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - blake.name = @"Blake"; - blake.favoriteCatID = [NSNumber numberWithInt:31340]; - - RKHuman *jeremy = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - jeremy.name = @"Jeremy"; - jeremy.favoriteCatID = [NSNumber numberWithInt:31340]; - - [managedObjectStore.persistentStoreManagedObjectContextContext save:nil]; - - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Asia", @"railsID", [NSNumber numberWithInt:31340], nil]; - RKCat *cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - NSOperationQueue *operationQueue = [NSOperationQueue new]; - [operationQueue setSuspended:YES]; - mappingOperationDataSource.operationQueue = operationQueue; - __block BOOL success; - [managedObjectContext performBlockAndWait:^{ - RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:cat mapping:catMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - success = [operation performMapping:&error]; - }]; - - [operationQueue setSuspended:NO]; - [operationQueue waitUntilAllOperationsAreFinished]; - - assertThatBool(success, is(equalToBool(YES))); - assertThat(cat.favoriteOfHumans, isNot(nilValue())); - assertThat([cat.favoriteOfHumans valueForKeyPath:@"name"], containsInAnyOrder(blake.name, jeremy.name, nil)); -} - -- (void)testConnectRelationshipsDoesNotLeakMemory -{ - RKManagedObjectStore* managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping connectRelationship:@"favoriteCat" fromKeyPath:@"favoriteCatID" toKeyPath:@"railsID" withMapping:catMapping]; - - // Create a cat to connect - RKCat *cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - cat.name = @"Asia"; - cat.railsID = [NSNumber numberWithInt:31337]; - [managedObjectStore.persistentStoreManagedObjectContextContext save:nil]; - - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], nil]; - RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - mappingOperationDataSource.operationQueue = [[NSOperationQueue new] autorelease]; - RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - [operation performMapping:&error]; - - assertThatInteger([operation retainCount], is(equalToInteger(1))); -} - -- (void)testConnectionOfHasManyRelationshipsByPrimaryKey -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping connectRelationship:@"favoriteCat" fromKeyPath:@"favoriteCatID" toKeyPath:@"railsID" withMapping:catMapping]; - - // Create a cat to connect - RKCat *cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - cat.name = @"Asia"; - cat.railsID = [NSNumber numberWithInt:31337]; - [managedObjectStore.persistentStoreManagedObjectContextContext save:nil]; - - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], nil]; - RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.favoriteCat, isNot(nilValue())); - assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); -} - -- (void)testShouldConnectRelationshipsByPrimaryKeyWithDifferentSourceAndDestinationKeyPaths -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"catIDs"]]; - [humanMapping connectRelationship:@"cats" fromKeyPath:@"catIDs" toKeyPath:@"railsID" withMapping:catMapping]; - - // Create a couple of cats to connect - RKCat *asia = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - asia.name = @"Asia"; - asia.railsID = [NSNumber numberWithInt:31337]; - - RKCat *roy = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - roy.name = @"Reginald Royford Williams III"; - roy.railsID = [NSNumber numberWithInt:31338]; - - [managedObjectStore.persistentStoreManagedObjectContextContext save:nil]; - - NSArray *catIDs = [NSArray arrayWithObjects:[NSNumber numberWithInt:31337], [NSNumber numberWithInt:31338], nil]; - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"catIDs", catIDs, nil]; - RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.cats, isNot(nilValue())); - assertThat([human.cats valueForKeyPath:@"name"], containsInAnyOrder(@"Asia", @"Reginald Royford Williams III", nil)); -} - -- (void)testShouldConnectRelationshipsByPrimaryKeyWithDifferentSourceAndDestinationKeyPathsReverse -{ - /* Connect a new cat to a human */ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"railsID"]]; - - RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name", @"humanId"]]; - [catMapping connectRelationship:@"human" fromKeyPath:@"humanId" toKeyPath:@"railsID" withMapping:humanMapping]; - - // Create a human to connect - RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - human.name = @"Blake"; - human.railsID = [NSNumber numberWithInt:31337]; - [managedObjectStore.persistentStoreManagedObjectContextContext save:nil]; - - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Asia", @"humanId", [NSNumber numberWithInt:31337], nil]; - RKCat *cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation* operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:cat mapping:catMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(cat.human, isNot(nilValue())); - assertThat(cat.human.name, is(equalTo(@"Blake"))); -} - -- (void)testShouldLoadNestedHasManyRelationship -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping hasMany:@"cats" withMapping:catMapping]; - - NSArray *catsData = [NSArray arrayWithObject:[NSDictionary dictionaryWithObject:@"Asia" forKey:@"name"]]; - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], @"cats", catsData, nil]; - RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); -} - -- (void)testShouldLoadOrderedHasManyRelationship -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"cats" toKeyPath:@"catsInOrderByAge" withMapping:catMapping]];; - - NSArray *catsData = [NSArray arrayWithObject:[NSDictionary dictionaryWithObject:@"Asia" forKey:@"name"]]; - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], @"cats", catsData, nil]; - RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat([human catsInOrderByAge], isNot(empty())); -} - -- (void)testShouldMapNullToAHasManyRelationship -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping hasMany:@"cats" withMapping:catMapping]; - - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"cats", [NSNull null], nil]; - RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.cats, is(empty())); -} - -- (void)testShouldLoadNestedHasManyRelationshipWithoutABackingClass -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - RKEntityMapping *cloudMapping = [RKEntityMapping mappingForEntityForName:@"Cloud" inManagedObjectStore:managedObjectStore]; - [cloudMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping *stormMapping = [RKEntityMapping mappingForEntityForName:@"Storm" inManagedObjectStore:managedObjectStore]; - [stormMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [stormMapping hasMany:@"clouds" withMapping:cloudMapping]; - - NSArray *cloudsData = [NSArray arrayWithObject:[NSDictionary dictionaryWithObject:@"Nimbus" forKey:@"name"]]; - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Hurricane", @"clouds", cloudsData, nil]; - NSEntityDescription *entity = [NSEntityDescription entityForName:@"Storm" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - NSManagedObject *storm = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:storm mapping:stormMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); -} - -- (void)testShouldDynamicallyConnectRelationshipsByPrimaryKeyWhenMatchingSucceeds -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping connectRelationship:@"favoriteCat" fromKeyPath:@"favoriteCatID" toKeyPath:@"railsID" withMapping:catMapping whenValueOfKeyPath:@"name" isEqualTo:@"Blake"]; - - // Create a cat to connect - RKCat *cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - cat.name = @"Asia"; - cat.railsID = [NSNumber numberWithInt:31337]; - [managedObjectStore.persistentStoreManagedObjectContextContext save:nil]; - - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], nil]; - RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.favoriteCat, isNot(nilValue())); - assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); -} - -- (void)testShouldNotDynamicallyConnectRelationshipsByPrimaryKeyWhenMatchingFails -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping connectRelationship:@"favoriteCat" fromKeyPath:@"favoriteCatID" toKeyPath:@"railsID" withMapping:catMapping whenValueOfKeyPath:@"name" isEqualTo:@"Jeff"]; - - // Create a cat to connect - RKCat *cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - cat.name = @"Asia"; - cat.railsID = [NSNumber numberWithInt:31337]; - [managedObjectStore.persistentStoreManagedObjectContextContext save:nil]; - - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], nil]; - RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.favoriteCat, is(nilValue())); -} - -- (void)testShouldConnectManyToManyRelationships -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; - childmapping.identificationAttributes = @[ @"railsID" ]; - [childMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; - parentmapping.identificationAttributes = @[ @"railsID" ]; - [parentMapping addAttributeMappingsFromArray:@[@"name", @"age"]]; - [parentMapping hasMany:@"children" withMapping:childMapping]; - - NSArray *childMappableData = [NSArray arrayWithObjects:[NSDictionary dictionaryWithKeysAndObjects:@"name", @"Maya", nil], - [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Brady", nil], nil]; - NSDictionary *parentMappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Win", - @"age", [NSNumber numberWithInt:34], - @"children", childMappableData, nil]; - RKParent *parent = [NSEntityDescription insertNewObjectForEntityForName:@"Parent" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:parentMappableData destinationObject:parent mapping:parentMapping]; - operation.dataSource = mappingOperationDataSource; - NSError *error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(parent.children, isNot(nilValue())); - assertThatUnsignedInteger([parent.children count], is(equalToInt(2))); - assertThat([[parent.children anyObject] parents], isNot(nilValue())); - assertThatBool([[[parent.children anyObject] parents] containsObject:parent], is(equalToBool(YES))); - assertThatUnsignedInteger([[[parent.children anyObject] parents] count], is(equalToInt(1))); -} - -- (void)testShouldConnectRelationshipsByPrimaryKeyRegardlessOfOrder -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - NSManagedObjectContext *managedObjectContext = managedObjectStore.persistentStoreManagedObjectContextContext; - - RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; - [parentMapping addAttributeMappingsFromArray:@[@"parentID"]]; - parentMapping.primaryKeyAttribute = @"parentID"; - - RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; - [childMapping addAttributeMappingsFromArray:@[@"fatherID"]]; - [childMapping connectRelationship:@"father" fromKeyPath:@"fatherID" toKeyPath:@"parentID" withMapping:parentMapping]; - - RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider new]; - // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary - // keys are not guaranteed to return in any particular order - [mappingProvider setMapping:parentMapping forKeyPath:@"parents"]; - [mappingProvider setMapping:childMapping forKeyPath:@"children"]; - - NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"ConnectingParents.json"]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectContext - cache:managedObjectCache]; - NSOperationQueue *operationQueue = [NSOperationQueue new]; - [operationQueue setSuspended:YES]; - mappingOperationDataSource.operationQueue = operationQueue; - [managedObjectContext performBlockAndWait:^{ - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; - mapper.mappingOperationDataSource = mappingOperationDataSource; - [mapper performMapping]; - }]; - - [operationQueue setSuspended:NO]; - [operationQueue waitUntilAllOperationsAreFinished]; - - [managedObjectContext performBlockAndWait:^{ - NSError *error; - NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Parent"]; - fetchRequest.predicate = [NSPredicate predicateWithFormat:@"parentID = %@", @1]; - fetchRequest.fetchLimit = 1; - NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:&error]; - RKParent *parent = [results lastObject]; - assertThat(parent, is(notNilValue())); - NSSet *children = [parent fatheredChildren]; - assertThat(children, hasCountOf(1)); - RKChild *child = [children anyObject]; - assertThat(child.father, is(notNilValue())); - }]; -} - -- (void)testMappingAPayloadContainingRepeatedObjectsDoesNotYieldDuplicatesWithFetchRequestMappingCache -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - managedObjectStore.managedObjectCache = managedObjectCache; - - RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; - childMapping.primaryKeyAttribute = @"childID"; - [childMapping addAttributeMappingsFromArray:@[@"name", @"childID"]]; - - RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; - [parentMapping addAttributeMappingsFromArray:@[@"parentID", @"name"]]; - parentMapping.primaryKeyAttribute = @"parentID"; - [parentMapping mapRelationship:@"children" withMapping:childMapping]; - - RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider new]; - // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary - // keys are not guaranteed to return in any particular order - [mappingProvider setObjectMapping:parentMapping forKeyPath:@"parents"]; - - NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"parents_and_children.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; - mapper.mappingOperationDataSource = mappingOperationDataSource; - [mapper performMapping]; - - NSUInteger parentCount = [managedObjectStore.persistentStoreManagedObjectContextContext countForEntityForName:@"Parent" predicate:nil error:nil]; - NSUInteger childrenCount = [managedObjectStore.persistentStoreManagedObjectContextContext countForEntityForName:@"Child" predicate:nil error:nil]; - assertThatInteger(parentCount, is(equalToInteger(2))); - assertThatInteger(childrenCount, is(equalToInteger(4))); -} - -- (void)testMappingAPayloadContainingRepeatedObjectsDoesNotYieldDuplicatesWithInMemoryMappingCache -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - RKInMemoryManagedObjectCache *managedObjectCache = [[RKInMemoryManagedObjectCache alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - managedObjectStore.managedObjectCache = managedObjectCache; - - RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; - childMapping.primaryKeyAttribute = @"childID"; - [childMapping addAttributeMappingsFromArray:@[@"name", @"childID"]]; - - RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; - [parentMapping addAttributeMappingsFromArray:@[@"parentID", @"name"]]; - parentMapping.primaryKeyAttribute = @"parentID"; - [parentMapping mapRelationship:@"children" withMapping:childMapping]; - - RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider new]; - // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary - // keys are not guaranteed to return in any particular order - [mappingProvider setObjectMapping:parentMapping forKeyPath:@"parents"]; - - NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"parents_and_children.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; - mapper.mappingOperationDataSource = mappingOperationDataSource; - [mapper performMapping]; - - NSError *error = nil; - BOOL success = [managedObjectStore.persistentStoreManagedObjectContextContext save:&error]; - assertThatBool(success, is(equalToBool(YES))); - NSLog(@"Failed to save MOC: %@", error); - assertThat(error, is(nilValue())); - - NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Parent"]; - NSUInteger parentCount = [managedObjectStore.persistentStoreManagedObjectContextContext countForFetchRequest:fetchRequest error:&error]; - NSUInteger childrenCount = [managedObjectStore.persistentStoreManagedObjectContextContext countForEntityForName:@"Child" predicate:nil error:nil]; - assertThatInteger(parentCount, is(equalToInteger(2))); - assertThatInteger(childrenCount, is(equalToInteger(4))); -} - -- (void)testMappingAPayloadContainingRepeatedObjectsPerformsAcceptablyWithFetchRequestMappingCache -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - managedObjectStore.managedObjectCache = managedObjectCache; - - RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; - childMapping.primaryKeyAttribute = @"childID"; - [childMapping addAttributeMappingsFromArray:@[@"name", @"childID"]]; - - RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; - [parentMapping addAttributeMappingsFromArray:@[@"parentID", @"name"]]; - parentMapping.primaryKeyAttribute = @"parentID"; - [parentMapping mapRelationship:@"children" withMapping:childMapping]; - - RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider new]; - // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary - // keys are not guaranteed to return in any particular order - [mappingProvider setObjectMapping:parentMapping forKeyPath:@"parents"]; - - NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"benchmark_parents_and_children.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; - mapper.mappingOperationDataSource = mappingOperationDataSource; - - RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelOff); - RKLogConfigureByName("RestKit/CoreData", RKLogLevelOff); - - [RKBenchmark report:@"Mapping with Fetch Request Cache" executionBlock:^{ - for (NSUInteger i = 0; i < 50; i++) { - [mapper performMapping]; - } - }]; - NSUInteger parentCount = [managedObjectStore.persistentStoreManagedObjectContextContext countForEntityForName:@"Parent" predicate:nil error:nil]; - NSUInteger childrenCount = [managedObjectStore.persistentStoreManagedObjectContextContext countForEntityForName:@"Child" predicate:nil error:nil]; - assertThatInteger(parentCount, is(equalToInteger(25))); - assertThatInteger(childrenCount, is(equalToInteger(51))); -} - -- (void)testMappingAPayloadContainingRepeatedObjectsPerformsAcceptablyWithInMemoryMappingCache -{ - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - RKInMemoryManagedObjectCache *managedObjectCache = [[RKInMemoryManagedObjectCache alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - managedObjectStore.managedObjectCache = managedObjectCache; - - RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; - childMapping.primaryKeyAttribute = @"childID"; - [childMapping addAttributeMappingsFromArray:@[@"name", @"childID"]]; - - RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; - [parentMapping addAttributeMappingsFromArray:@[@"parentID", @"name"]]; - parentMapping.primaryKeyAttribute = @"parentID"; - [parentMapping mapRelationship:@"children" withMapping:childMapping]; - - RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider new]; - // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary - // keys are not guaranteed to return in any particular order - [mappingProvider setObjectMapping:parentMapping forKeyPath:@"parents"]; - - NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"benchmark_parents_and_children.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; - mapper.mappingOperationDataSource = mappingOperationDataSource; - RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelOff); - RKLogConfigureByName("RestKit/CoreData", RKLogLevelOff); - - [RKBenchmark report:@"Mapping with In Memory Cache" executionBlock:^{ - for (NSUInteger i = 0; i < 50; i++) { - [mapper performMapping]; - } - }]; - NSUInteger parentCount = [managedObjectStore.persistentStoreManagedObjectContextContext countForEntityForName:@"Parent" predicate:nil error:nil]; - NSUInteger childrenCount = [managedObjectStore.persistentStoreManagedObjectContextContext countForEntityForName:@"Child" predicate:nil error:nil]; - assertThatInteger(parentCount, is(equalToInteger(25))); - assertThatInteger(childrenCount, is(equalToInteger(51))); -} - -/* Test deprecated connectionMapping API */ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -- (void)testShouldConnectRelationshipsByPrimaryKeyDeprecated { - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping* catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping* humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping hasOne:@"favoriteCat" withMapping:catMapping]; - [humanMapping connectRelationship:@"favoriteCat" withObjectForPrimaryKeyAttribute:@"favoriteCatID"]; - - // Create a cat to connect - RKCat* cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - cat.name = @"Asia"; - cat.railsID = [NSNumber numberWithInt:31337]; - [managedObjectStore save:nil]; - - NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], nil]; - RKHuman* human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation* operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError* error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.favoriteCat, isNot(nilValue())); - assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); -} - -- (void)testConnectRelationshipsDoesNotLeakMemoryDeprecated { - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping* catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping* humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping hasOne:@"favoriteCat" withMapping:catMapping]; - [humanMapping connectRelationship:@"favoriteCat" withObjectForPrimaryKeyAttribute:@"favoriteCatID"]; - - // Create a cat to connect - RKCat* cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - cat.name = @"Asia"; - cat.railsID = [NSNumber numberWithInt:31337]; - [managedObjectStore save:nil]; - - NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], nil]; - RKHuman* human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - mappingOperationDataSource.operationQueue = [[NSOperationQueue new] autorelease]; - RKMappingOperation* operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError* error = nil; - [operation performMapping:&error]; - - assertThatInteger([operation retainCount], is(equalToInteger(1))); -} - -- (void)testConnectionOfHasManyRelationshipsByPrimaryKeyDeprecated { - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping* catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping* humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping hasOne:@"favoriteCat" withMapping:catMapping]; - [humanMapping connectRelationship:@"favoriteCat" withObjectForPrimaryKeyAttribute:@"favoriteCatID"]; - - // Create a cat to connect - RKCat* cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - cat.name = @"Asia"; - cat.railsID = [NSNumber numberWithInt:31337]; - [managedObjectStore save:nil]; - - NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], nil]; - RKHuman* human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation* operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - NSError* error = nil; - operation.dataSource = mappingOperationDataSource; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.favoriteCat, isNot(nilValue())); - assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); -} - -- (void)testShouldConnectRelationshipsByPrimaryKeyWithDifferentSourceAndDestinationKeyPathsDeprecated { - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping* catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping* humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID", @"catIDs"]]; - [humanMapping mapRelationship:@"cats" withMapping:catMapping]; - [humanMapping connectRelationship:@"cats" withObjectForPrimaryKeyAttribute:@"catIDs"]; - - // Create a couple of cats to connect - RKCat* asia = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - asia.name = @"Asia"; - asia.railsID = [NSNumber numberWithInt:31337]; - - RKCat* roy = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - roy.name = @"Reginald Royford Williams III"; - roy.railsID = [NSNumber numberWithInt:31338]; - - [managedObjectStore save:nil]; - - NSArray *catIDs = [NSArray arrayWithObjects:[NSNumber numberWithInt:31337], [NSNumber numberWithInt:31338], nil]; - NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"catIDs", catIDs, nil]; - RKHuman* human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation* operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError* error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.cats, isNot(nilValue())); - assertThat([human.cats valueForKeyPath:@"name"], containsInAnyOrder(@"Asia", @"Reginald Royford Williams III", nil)); -} - -- (void)testShouldDynamicallyConnectRelationshipsByPrimaryKeyWhenMatchingSucceedsDeprecated { - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping* catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping* humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping hasOne:@"favoriteCat" withMapping:catMapping]; - [humanMapping connectRelationship:@"favoriteCat" withObjectForPrimaryKeyAttribute:@"favoriteCatID" whenValueOfKeyPath:@"name" isEqualTo:@"Blake"]; - - // Create a cat to connect - RKCat* cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - cat.name = @"Asia"; - cat.railsID = [NSNumber numberWithInt:31337]; - [managedObjectStore save:nil]; - - NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], nil]; - RKHuman* human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation* operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError* error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.favoriteCat, isNot(nilValue())); - assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); -} - -- (void)testShouldNotDynamicallyConnectRelationshipsByPrimaryKeyWhenMatchingFailsDeprecated { - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - - RKEntityMapping* catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; - catmapping.identificationAttributes = @[ @"railsID" ]; - [catMapping addAttributeMappingsFromArray:@[@"name"]]; - - RKEntityMapping* humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; - humanmapping.identificationAttributes = @[ @"railsID" ]; - [humanMapping addAttributeMappingsFromArray:@[@"name", @"favoriteCatID"]]; - [humanMapping hasOne:@"favoriteCat" withMapping:catMapping]; - [humanMapping connectRelationship:@"favoriteCat" withObjectForPrimaryKeyAttribute:@"favoriteCatID" whenValueOfKeyPath:@"name" isEqualTo:@"Jeff"]; - - // Create a cat to connect - RKCat* cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - cat.name = @"Asia"; - cat.railsID = [NSNumber numberWithInt:31337]; - [managedObjectStore save:nil]; - - NSDictionary *mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], nil]; - RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - RKMappingOperation* operation = [[RKMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - operation.dataSource = mappingOperationDataSource; - NSError* error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.favoriteCat, is(nilValue())); -} - -- (void)testShouldConnectRelationshipsByPrimaryKeyRegardlessOfOrderDeprecated { - RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; - NSManagedObjectContext *managedObjectContext = managedObjectStore.persistentStoreManagedObjectContextContext; - RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; - [parentMapping addAttributeMappingsFromArray:@[@"parentID"]]; - parentMapping.primaryKeyAttribute = @"parentID"; - - RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; - [childMapping addAttributeMappingsFromArray:@[@"fatherID"]]; - [childMapping mapRelationship:@"father" withMapping:parentMapping]; - [childMapping connectRelationship:@"father" withObjectForPrimaryKeyAttribute:@"fatherID"]; - - RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider new]; - // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary - // keys are not guaranteed to return in any particular order - [mappingProvider setMapping:parentMapping forKeyPath:@"parents"]; - [mappingProvider setMapping:childMapping forKeyPath:@"children"]; - - NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"ConnectingParents.json"]; - RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; - RKManagedObjectMappingOperationDataSource *mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContextContext - cache:managedObjectCache]; - - NSOperationQueue *operationQueue = [NSOperationQueue new]; - [operationQueue setSuspended:YES]; - mappingOperationDataSource.operationQueue = operationQueue; - - __block RKMappingResult *result; - [managedObjectContext performBlockAndWait:^{ - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; - mapper.mappingOperationDataSource = mappingOperationDataSource; - result = [mapper performMapping]; - }]; - - [operationQueue setSuspended:NO]; - [operationQueue waitUntilAllOperationsAreFinished]; - - NSArray *children = [[result asDictionary] valueForKey:@"children"]; - assertThat(children, hasCountOf(1)); - RKChild *child = [children lastObject]; - assertThat(child.father, is(notNilValue())); -} - -#pragma GCC diagnostic pop - -@end From fe6e8597ee62b18e93d786d935a37b5afbf5703f Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 6 Dec 2012 23:52:08 -0500 Subject: [PATCH 06/18] Fix incorrectly configuring identification attribute in Twitter Core Data example --- Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m b/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m index 54241a784c..3ff177dad9 100644 --- a/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m +++ b/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m @@ -49,7 +49,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [userMapping addAttributeMappingsFromArray:@[ @"name" ]]; RKEntityMapping *tweetMapping = [RKEntityMapping mappingForEntityForName:@"Tweet" inManagedObjectStore:managedObjectStore]; - userMapping.identificationAttributes = @[ @"statusID" ]; + tweetMapping.identificationAttributes = @[ @"statusID" ]; [tweetMapping addAttributeMappingsFromDictionary:@{ @"id": @"statusID", @"created_at": @"createdAt", From 0210424c6c05b8a620e6d1c2cbf00b6f1b158dac Mon Sep 17 00:00:00 2001 From: Sam Krishna Date: Mon, 3 Dec 2012 23:25:19 -0800 Subject: [PATCH 07/18] Added proper support for RKDynamicMapping as a container for RKEntityMappings inside of RKObjectManager. --- Code/Network/RKObjectManager.m | 10 ++++++++++ Code/ObjectMapping.h | 1 + 2 files changed, 11 insertions(+) diff --git a/Code/Network/RKObjectManager.m b/Code/Network/RKObjectManager.m index 413edb1a00..76db9a8d77 100644 --- a/Code/Network/RKObjectManager.m +++ b/Code/Network/RKObjectManager.m @@ -31,6 +31,7 @@ #import "RKPathMatcher.h" #import "RKMappingErrors.h" #import "RKPaginator.h" +#import "RKDynamicMapping.h" #if !__has_feature(objc_arc) #error RestKit must be built with ARC. @@ -92,6 +93,15 @@ static BOOL RKDoesArrayOfResponseDescriptorsContainEntityMapping(NSArray *respon if ([responseDescriptor.mapping isKindOfClass:[RKEntityMapping class]]) { return YES; } + + if ([responseDescriptor.mapping isKindOfClass:[RKDynamicMapping class]]) { + RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)responseDescriptor.mapping; + for (RKMapping *mapping in dynamicMapping.objectMappings) { + if ([mapping isKindOfClass:[RKEntityMapping class]]) { + return YES; + } + } + } } return NO; diff --git a/Code/ObjectMapping.h b/Code/ObjectMapping.h index 9bff91a570..3f81814571 100644 --- a/Code/ObjectMapping.h +++ b/Code/ObjectMapping.h @@ -25,3 +25,4 @@ #import "RKObjectParameterization.h" #import "RKMappingResult.h" #import "RKMapperOperation.h" +#import "RKDynamicMapping.h" \ No newline at end of file From 997158e9e63038a03113ca48329f4d2cb48a0d5a Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 7 Dec 2012 12:40:02 -0500 Subject: [PATCH 08/18] Fix issues with incorrect determination of the appropriate object request operation. fixes #1054, #1056 * Expands test coverage for the `appropriateObjectRequestOperationWithObject:method:path:parameters:` * Uses an object graph visitor to completely navigate the mapping graph, ensuring that an `RKEntityMapping` appearing at any nesting level will be correctly handled --- Code/Network/RKObjectManager.m | 80 ++++++++++++++++--- Code/Network/RKResponseDescriptor.m | 3 +- .../Logic/ObjectMapping/RKObjectManagerTest.m | 71 ++++++++++++++++ 3 files changed, 144 insertions(+), 10 deletions(-) diff --git a/Code/Network/RKObjectManager.m b/Code/Network/RKObjectManager.m index 76db9a8d77..67aa1cd677 100644 --- a/Code/Network/RKObjectManager.m +++ b/Code/Network/RKObjectManager.m @@ -32,6 +32,7 @@ #import "RKMappingErrors.h" #import "RKPaginator.h" #import "RKDynamicMapping.h" +#import "RKRelationshipMapping.h" #if !__has_feature(objc_arc) #error RestKit must be built with ARC. @@ -82,24 +83,85 @@ } /** - Returns `YES` if the given array of `RKResponseDescriptor` objects contains an `RKEntityMapping`. + Visits all mappings accessible via relationships or dynamic mapping in an object graph starting from a given mapping. + */ +@interface RKMappingGraphVisitor : NSObject + +@property (nonatomic, readonly) NSSet *mappings; + +- (id)initWithMapping:(RKMapping *)mapping; + +@end + +@interface RKMappingGraphVisitor () +@property (nonatomic, readwrite) NSMutableSet *mutableMappings; +@end + +@implementation RKMappingGraphVisitor + +- (id)initWithMapping:(RKMapping *)mapping +{ + self = [super init]; + if (self) { + self.mutableMappings = [NSMutableSet set]; + [self visitMapping:mapping]; + } + return self; +} + +- (NSSet *)mappings +{ + return self.mutableMappings; +} + +- (void)visitMapping:(RKMapping *)mapping +{ + if ([self.mappings containsObject:mapping]) return; + [self.mutableMappings addObject:mapping]; + + if ([mapping isKindOfClass:[RKDynamicMapping class]]) { + RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)mapping; + for (RKMapping *nestedMapping in dynamicMapping.objectMappings) { + [self visitMapping:nestedMapping]; + } + } else if ([mapping isKindOfClass:[RKObjectMapping class]]) { + RKObjectMapping *objectMapping = (RKObjectMapping *)mapping; + for (RKRelationshipMapping *relationshipMapping in objectMapping.relationshipMappings) { + [self visitMapping:relationshipMapping.mapping]; + } + } +} + +@end + +/** + Returns `YES` if the given array of `RKResponseDescriptor` objects contains an `RKEntityMapping` anywhere in its object graph. @param responseDescriptor An array of `RKResponseDescriptor` objects. @return `YES` if the `mapping` property of any of the response descriptor objects in the given array is an instance of `RKEntityMapping`, else `NO`. */ static BOOL RKDoesArrayOfResponseDescriptorsContainEntityMapping(NSArray *responseDescriptors) { + // Visit all mappings accessible from the object graphs of all response descriptors + NSMutableSet *accessibleMappings = [NSMutableSet set]; for (RKResponseDescriptor *responseDescriptor in responseDescriptors) { - if ([responseDescriptor.mapping isKindOfClass:[RKEntityMapping class]]) { + if (! [accessibleMappings containsObject:responseDescriptor.mapping]) { + RKMappingGraphVisitor *graphVisitor = [[RKMappingGraphVisitor alloc] initWithMapping:responseDescriptor.mapping]; + [accessibleMappings unionSet:graphVisitor.mappings]; + } + } + + // Enumerate all mappings and search for an `RKEntityMapping` + for (RKMapping *mapping in accessibleMappings) { + if ([mapping isKindOfClass:[RKEntityMapping class]]) { return YES; } - - if ([responseDescriptor.mapping isKindOfClass:[RKDynamicMapping class]]) { - RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)responseDescriptor.mapping; - for (RKMapping *mapping in dynamicMapping.objectMappings) { - if ([mapping isKindOfClass:[RKEntityMapping class]]) { - return YES; - } + + if ([mapping isKindOfClass:[RKDynamicMapping class]]) { + RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)mapping; + if ([dynamicMapping.objectMappings count] == 0) { + // Likely means that there is a representation block, assume `YES` + return YES; } } } diff --git a/Code/Network/RKResponseDescriptor.m b/Code/Network/RKResponseDescriptor.m index f9e908ba68..c4593b74a6 100644 --- a/Code/Network/RKResponseDescriptor.m +++ b/Code/Network/RKResponseDescriptor.m @@ -24,6 +24,7 @@ // Cloned from AFStringFromIndexSet -- method should be non-static for reuse static NSString *RKStringFromIndexSet(NSIndexSet *indexSet) { + NSCParameterAssert(indexSet); NSMutableString *string = [NSMutableString string]; NSRange range = NSMakeRange([indexSet firstIndex], 1); @@ -80,7 +81,7 @@ + (RKResponseDescriptor *)responseDescriptorWithMapping:(RKMapping *)mapping - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p pathPattern=%@ keyPath=%@ statusCodes=%@ : %@>", - NSStringFromClass([self class]), self, self.pathPattern, self.keyPath, RKStringFromIndexSet(self.statusCodes), self.mapping]; + NSStringFromClass([self class]), self, self.pathPattern, self.keyPath, self.statusCodes ? RKStringFromIndexSet(self.statusCodes) : self.statusCodes, self.mapping]; } - (BOOL)matchesPath:(NSString *)path diff --git a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m index 0b16d06faa..0309ecc4ed 100644 --- a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m @@ -25,6 +25,7 @@ #import "RKHuman.h" #import "RKCat.h" #import "RKObjectMapperTestModel.h" +#import "RKDynamicMapping.h" @interface RKSubclassedTestModel : RKObjectMapperTestModel @end @@ -526,6 +527,76 @@ - (void)testThatRegisteringARequestDescriptorForASubclassSecondWillMatchAppropri expect(dictionary).to.equal(@{ @"subclassed": @{ @"age": @(30) } }); } +- (void)testThatResponseDescriptorWithUnmanagedMappingTriggersCreationOfObjectRequestOperation +{ + RKObjectMapping *vanillaMapping = [RKObjectMapping requestMapping]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:vanillaMapping pathPattern:nil keyPath:nil statusCodes:nil]; + RKObjectManager *manager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:@"http://restkit.org"]]; + manager.managedObjectStore = [RKTestFactory managedObjectStore]; + [manager addResponseDescriptor:responseDescriptor]; + RKObjectRequestOperation *objectRequestOperation = [manager appropriateObjectRequestOperationWithObject:nil method:RKRequestMethodGET path:@"/something" parameters:nil]; + expect(objectRequestOperation).to.beInstanceOf([RKObjectRequestOperation class]); +} + +- (void)testThatResponseDescriptorWithDynamicMappingContainingEntityMappingsTriggersCreationOfManagedObjectRequestOperation +{ + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:_objectManager.managedObjectStore]; + RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; + [dynamicMapping setObjectMapping:humanMapping whenValueOfKeyPath:@"whatever" isEqualTo:@"whatever"]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping pathPattern:nil keyPath:nil statusCodes:nil]; + RKObjectManager *manager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:@"http://restkit.org"]]; + manager.managedObjectStore = [RKTestFactory managedObjectStore]; + [manager addResponseDescriptor:responseDescriptor]; + RKObjectRequestOperation *objectRequestOperation = [manager appropriateObjectRequestOperationWithObject:nil method:RKRequestMethodGET path:@"/something" parameters:nil]; + expect(objectRequestOperation).to.beInstanceOf([RKManagedObjectRequestOperation class]); +} + +- (void)testThatResponseDescriptorWithDynamicMappingUsingABlockTriggersCreationOfManagedObjectRequestOperation +{ + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:_objectManager.managedObjectStore]; + RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; + [dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) { + return humanMapping; + }]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping pathPattern:nil keyPath:nil statusCodes:nil]; + RKObjectManager *manager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:@"http://restkit.org"]]; + manager.managedObjectStore = [RKTestFactory managedObjectStore]; + [manager addResponseDescriptor:responseDescriptor]; + RKObjectRequestOperation *objectRequestOperation = [manager appropriateObjectRequestOperationWithObject:nil method:RKRequestMethodGET path:@"/something" parameters:nil]; + expect(objectRequestOperation).to.beInstanceOf([RKManagedObjectRequestOperation class]); +} + +- (void)testThatResponseDescriptorWithUnmanagedMappingContainingRelationshipMappingWithEntityMappingsTriggersCreationOfManagedObjectRequestOperation +{ + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:_objectManager.managedObjectStore]; + RKObjectMapping *objectMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + [objectMapping addRelationshipMappingWithSourceKeyPath:@"relationship" mapping:humanMapping]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:objectMapping pathPattern:nil keyPath:nil statusCodes:nil]; + RKObjectManager *manager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:@"http://restkit.org"]]; + manager.managedObjectStore = [RKTestFactory managedObjectStore]; + [manager addResponseDescriptor:responseDescriptor]; + RKObjectRequestOperation *objectRequestOperation = [manager appropriateObjectRequestOperationWithObject:nil method:RKRequestMethodGET path:@"/something" parameters:nil]; + expect(objectRequestOperation).to.beInstanceOf([RKManagedObjectRequestOperation class]); +} + +- (void)testThatResponseDescriptorWithUnmanagedMappingContainingRelationshipMappingWithEntityMappingsDeepWithinObjectGraphTriggersCreationOfManagedObjectRequestOperation +{ + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:_objectManager.managedObjectStore]; + RKObjectMapping *objectMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + [objectMapping addRelationshipMappingWithSourceKeyPath:@"relationship" mapping:humanMapping]; + RKObjectMapping *objectMapping2 = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + [objectMapping2 addRelationshipMappingWithSourceKeyPath:@"relationship" mapping:objectMapping]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:objectMapping2 pathPattern:nil keyPath:nil statusCodes:nil]; + RKObjectManager *manager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:@"http://restkit.org"]]; + manager.managedObjectStore = [RKTestFactory managedObjectStore]; + [manager addResponseDescriptor:responseDescriptor]; + RKObjectRequestOperation *objectRequestOperation = [manager appropriateObjectRequestOperationWithObject:nil method:RKRequestMethodGET path:@"/something" parameters:nil]; + expect(objectRequestOperation).to.beInstanceOf([RKManagedObjectRequestOperation class]); +} + +// Test with relationship 2 levels deep +// Test with recursive relationships + //- (void)testShouldHandleConnectionFailures //{ // NSString *localBaseURL = [NSString stringWithFormat:@"http://127.0.0.1:3001"]; From b1f57612e1f07b6a503c83e4d452ba904d9b758d Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 7 Dec 2012 13:49:18 -0500 Subject: [PATCH 09/18] Add support for replacing the HTTP client on the object manager and mutating the `baseURL` on the router. closes #1051 --- Code/Network/RKObjectManager.h | 2 +- Code/Network/RKObjectManager.m | 1 - Code/Network/RKRouter.h | 2 +- Code/Network/RKRouter.m | 1 - RestKit.xcodeproj/project.pbxproj | 6 +++++ Tests/Logic/Network/RKRouterTest.m | 26 +++++++++++++++++++ .../Logic/ObjectMapping/RKObjectManagerTest.m | 7 +++++ 7 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 Tests/Logic/Network/RKRouterTest.m diff --git a/Code/Network/RKObjectManager.h b/Code/Network/RKObjectManager.h index 8007d1cffe..e3c340ffd4 100644 --- a/Code/Network/RKObjectManager.h +++ b/Code/Network/RKObjectManager.h @@ -255,7 +255,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; /** The AFNetworking HTTP client with which the receiver makes requests. */ -@property (nonatomic, strong, readonly) AFHTTPClient *HTTPClient; +@property (nonatomic, strong, readwrite) AFHTTPClient *HTTPClient; /** The base URL of the underlying HTTP client. diff --git a/Code/Network/RKObjectManager.m b/Code/Network/RKObjectManager.m index 67aa1cd677..54834dfc85 100644 --- a/Code/Network/RKObjectManager.m +++ b/Code/Network/RKObjectManager.m @@ -194,7 +194,6 @@ static BOOL RKDoesArrayOfResponseDescriptorsContainEntityMapping(NSArray *respon /////////////////////////////////// @interface RKObjectManager () -@property (nonatomic, strong, readwrite) AFHTTPClient *HTTPClient; @property (nonatomic, strong) NSMutableArray *mutableRequestDescriptors; @property (nonatomic, strong) NSMutableArray *mutableResponseDescriptors; @property (nonatomic, strong) NSMutableArray *mutableFetchRequestBlocks; diff --git a/Code/Network/RKRouter.h b/Code/Network/RKRouter.h index 5e1e42637c..4bb3cdb4a4 100644 --- a/Code/Network/RKRouter.h +++ b/Code/Network/RKRouter.h @@ -106,7 +106,7 @@ /** The base URL that all URLs constructed by the receiver are relative to. */ -@property (nonatomic, strong, readonly) NSURL *baseURL; +@property (nonatomic, strong, readwrite) NSURL *baseURL; /** A route set defining all the routes addressable through the receiver. diff --git a/Code/Network/RKRouter.m b/Code/Network/RKRouter.m index dfbe52d43a..777f13d438 100644 --- a/Code/Network/RKRouter.m +++ b/Code/Network/RKRouter.m @@ -25,7 +25,6 @@ #import @interface RKRouter () -@property (nonatomic, strong, readwrite) NSURL *baseURL; @property (nonatomic, strong, readwrite) RKRouteSet *routeSet; @end diff --git a/RestKit.xcodeproj/project.pbxproj b/RestKit.xcodeproj/project.pbxproj index 5df55f2590..85fecae787 100644 --- a/RestKit.xcodeproj/project.pbxproj +++ b/RestKit.xcodeproj/project.pbxproj @@ -321,6 +321,8 @@ 2534781715FFD4A6002C0E4E /* RKURLEncodedSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = 2534781415FFD4A6002C0E4E /* RKURLEncodedSerialization.m */; }; 2534781815FFD4A6002C0E4E /* RKURLEncodedSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = 2534781515FFD4A6002C0E4E /* RKURLEncodedSerialization.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2534781915FFD4A6002C0E4E /* RKURLEncodedSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = 2534781515FFD4A6002C0E4E /* RKURLEncodedSerialization.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2536D1FD167270F100DF9BB0 /* RKRouterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2536D1FC167270F100DF9BB0 /* RKRouterTest.m */; }; + 2536D1FE167270F100DF9BB0 /* RKRouterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2536D1FC167270F100DF9BB0 /* RKRouterTest.m */; }; 253B495214E35D1A00B0483F /* RKTestFixture.h in Headers */ = {isa = PBXBuildFile; fileRef = 252EFB2014D9B35D004863C8 /* RKTestFixture.h */; settings = {ATTRIBUTES = (Public, ); }; }; 253B495F14E35EC300B0483F /* RKTestFixture.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFB2114D9B35D004863C8 /* RKTestFixture.m */; }; 254372A815F54995006E8424 /* RKObjectParameterization.h in Headers */ = {isa = PBXBuildFile; fileRef = 254372A615F54995006E8424 /* RKObjectParameterization.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -798,6 +800,7 @@ 253477F615FFBD2E002C0E4E /* NSBundle+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSBundle+RKAdditions.h"; path = "Testing/NSBundle+RKAdditions.h"; sourceTree = ""; }; 2534781415FFD4A6002C0E4E /* RKURLEncodedSerialization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKURLEncodedSerialization.m; sourceTree = ""; }; 2534781515FFD4A6002C0E4E /* RKURLEncodedSerialization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKURLEncodedSerialization.h; sourceTree = ""; }; + 2536D1FC167270F100DF9BB0 /* RKRouterTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKRouterTest.m; sourceTree = ""; }; 254372A615F54995006E8424 /* RKObjectParameterization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectParameterization.h; sourceTree = ""; }; 254372A715F54995006E8424 /* RKObjectParameterization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectParameterization.m; sourceTree = ""; }; 254372AA15F54C3F006E8424 /* RKHTTPRequestOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKHTTPRequestOperation.h; sourceTree = ""; }; @@ -1444,6 +1447,7 @@ 259AC480162B05C80012D2F9 /* RKObjectRequestOperationTest.m */, 2549D645162B376F003DD135 /* RKRequestDescriptorTest.m */, 2548AC6C162F5E00009E79BF /* RKManagedObjectRequestOperationTest.m */, + 2536D1FC167270F100DF9BB0 /* RKRouterTest.m */, ); name = Network; path = Logic/Network; @@ -2337,6 +2341,7 @@ 255F87911656B22D00914D57 /* RKPaginatorTest.m in Sources */, 2546A95816628EDD0078E044 /* RKConnectionDescriptionTest.m in Sources */, 2543A25D1664FD3100821D5B /* RKResponseDescriptorTest.m in Sources */, + 2536D1FD167270F100DF9BB0 /* RKRouterTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2484,6 +2489,7 @@ 255F87921656B22F00914D57 /* RKPaginatorTest.m in Sources */, 2546A95916628EDD0078E044 /* RKConnectionDescriptionTest.m in Sources */, 2543A25E1664FD3200821D5B /* RKResponseDescriptorTest.m in Sources */, + 2536D1FE167270F100DF9BB0 /* RKRouterTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Tests/Logic/Network/RKRouterTest.m b/Tests/Logic/Network/RKRouterTest.m new file mode 100644 index 0000000000..c9d0e19274 --- /dev/null +++ b/Tests/Logic/Network/RKRouterTest.m @@ -0,0 +1,26 @@ +// +// RKRouterTest.m +// RestKit +// +// Created by Blake Watters on 12/7/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "RKRouter.h" + +@interface RKRouterTest : RKTestCase +@end + +@implementation RKRouterTest + +- (void)testChangingBaseURL +{ + NSURL *originalURL = [NSURL URLWithString:@"http://restkit.org/"]; + NSURL *newURL = [NSURL URLWithString:@"http://google.com/"]; + RKRouter *router = [[RKRouter alloc] initWithBaseURL:originalURL]; + router.baseURL = newURL; + expect(router.baseURL).to.equal(newURL); +} + +@end diff --git a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m index 0309ecc4ed..75253a4561 100644 --- a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m @@ -594,6 +594,13 @@ - (void)testThatResponseDescriptorWithUnmanagedMappingContainingRelationshipMapp expect(objectRequestOperation).to.beInstanceOf([RKManagedObjectRequestOperation class]); } +- (void)testChangingHTTPClient +{ + RKObjectManager *manager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:@"http://restkit.org"]]; + manager.HTTPClient = [AFHTTPClient clientWithBaseURL:[NSURL URLWithString:@"http://google.com/"]]; + expect([manager.baseURL absoluteString]).to.equal(@"http://google.com/"); +} + // Test with relationship 2 levels deep // Test with recursive relationships From 0eb875679d8ecbfc7e83f9dc3a4a9773c5aca890 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 7 Dec 2012 14:16:50 -0500 Subject: [PATCH 10/18] Add proxy attributes enabling pagination mapping configuration under iOS 5. Expand documentation. closes #1040 --- Code/Network/RKPaginator.h | 33 ++++++++++---- Code/Network/RKPaginator.m | 48 +++++++++++++++++++++ Tests/Logic/ObjectMapping/RKPaginatorTest.m | 20 +++++++++ 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/Code/Network/RKPaginator.h b/Code/Network/RKPaginator.h index 53d10ca027..ada67ed97b 100644 --- a/Code/Network/RKPaginator.h +++ b/Code/Network/RKPaginator.h @@ -24,12 +24,27 @@ #import "RKMappingResult.h" /** - Instances of RKPaginator retrieve paginated collections of mappable data - from remote systems via HTTP. Paginators perform GET requests and use a patterned - URL to construct a full URL reflecting the state of the paginator. Paginators rely - on an instance of RKObjectMappingProvider to determine how to perform object mapping - on the retrieved data. Paginators can load Core Data backed models provided that an - instance of RKManagedObjectStore is assigned to the paginator. + Instances of `RKPaginator` retrieve paginated collections of mappable data from remote systems via HTTP. Paginators perform GET requests and use a patterned URL to construct a full URL reflecting the state of the paginator. Paginators rely on an instance of RKObjectMappingProvider to determine how to perform object mapping on the retrieved data. Paginators can load Core Data backed models provided that an instance of RKManagedObjectStore is assigned to the paginator. + + ## Configuring Pagination Mapping + + The paginator must be configured with a `paginationMapping` specifying how configuration metadata is to be mapped out of the response payload. The configured mapping must have an `objectClass` of `RKPaginator` and should include attribute mappings for the `currentPage`, `pageCount`, `perPage`, and `objectCount`. For example, given a paginated resource loaded from '/articles?page=1' with the followibg JSON: + + { "pagination": { "per_page": 10, "total_pages": 25, "total_objects": 250 }, "articles": [ // Array of articles ] } + + The pagination mapping would be configured as: + + RKObjectMapping *paginationMapping = [RKObjectMapping mappingForClass:[RKPaginator class]]; + [paginationMapping addAttributeMappingsFromDictionary:@{ + @"pagination.per_page", @"perPage", + @"pagination.total_pages", @"pageCount", + @"pagination.total_objects", @"objectCount", + }]; + + ## iOS 5 Compatibility Caveats + + The paginator is compatible with iOS 5.x through the use of proxy attributes. In iOS 6.0 and greater, key-value coding supports the automatic boxing and unboxing of primitive values. This enables direct mapping configuration for the `currentPage`, `pageCount`, `perPage`, and `objectCount` attributes. Under iOS 5, where autoboxing is not available, mapping configuration must target special proxy attributes instead. For each of the above properties, a private `NSNumber` property is implemented by the class. Each proxy property has 'Number' appended as a suffix to the property name: `currentPageNumber`, `pageCountNumber`, `perPageNumber`, and `objectCountNumber`. + */ @interface RKPaginator : NSObject @@ -109,6 +124,8 @@ /** The object mapping defining how pagination metadata is to be mapped from a paginated response onto the paginator object. + See the documentation in the "Configuring Pagination Mapping" section for details about the pagination mapping. + @warning The `objectClass` of the given mapping must be `RKPaginator`. */ @property (nonatomic, strong) RKObjectMapping *paginationMapping; @@ -160,7 +177,7 @@ Returns the number of pages in the total resource collection. @return A count of the number of pages in the resource collection. - @exception NSInternalInconsistencyException Raised if hasPageCount is `NO`. + @exception NSInternalInconsistencyException Raised if `hasPageCount` is `NO`. */ @property (nonatomic, readonly) NSUInteger pageCount; @@ -168,7 +185,7 @@ Returns the total number of objects in the collection @return A count of the number of objects in the resource collection. - @exception NSInternalInconsistencyException Raised if hasObjectCount is `NO`. + @exception NSInternalInconsistencyException Raised if `hasObjectCount` is `NO`. */ @property (nonatomic, readonly) NSUInteger objectCount; diff --git a/Code/Network/RKPaginator.m b/Code/Network/RKPaginator.m index 47c98ce937..8894baae85 100644 --- a/Code/Network/RKPaginator.m +++ b/Code/Network/RKPaginator.m @@ -41,6 +41,12 @@ @interface RKPaginator () @property (nonatomic, strong, readwrite) RKMappingResult *mappingResult; @property (nonatomic, strong, readwrite) NSError *error; +// iOS 5.x compatible proxy attributes +@property (nonatomic, assign, readwrite) NSNumber *perPageNumber; +@property (nonatomic, assign, readwrite) NSNumber *currentPageNumber; +@property (nonatomic, assign, readwrite) NSNumber *pageCountNumber; +@property (nonatomic, assign, readwrite) NSNumber *objectCountNumber; + @property (nonatomic, copy) void (^successBlock)(RKPaginator *paginator, NSArray *objects, NSUInteger page); @property (nonatomic, copy) void (^failureBlock)(RKPaginator *paginator, NSError *error); @end @@ -239,4 +245,46 @@ - (void)cancel [self.objectRequestOperation cancel]; } +#pragma mark - iOS 5 proxy attributes + +- (NSNumber *)perPageNumber +{ + return [NSNumber numberWithUnsignedInteger:self.perPage]; +} + +- (void)setPerPageNumber:(NSNumber *)perPageNumber +{ + self.perPage = [perPageNumber unsignedIntegerValue]; +} + +- (NSNumber *)currentPageNumber +{ + return [NSNumber numberWithUnsignedInteger:self.currentPage]; +} + +- (void)setCurrentPageNumber:(NSNumber *)currentPageNumber +{ + self.currentPage = [currentPageNumber unsignedIntegerValue]; +} + +- (NSNumber *)pageCountNumber +{ + return [NSNumber numberWithUnsignedInteger:self.pageCount]; +} + +- (void)setPageCountNumber:(NSNumber *)pageCountNumber +{ + self.pageCount = [pageCountNumber unsignedIntegerValue]; +} + +- (NSNumber *)objectCountNumber +{ + return [NSNumber numberWithUnsignedInteger:self.objectCount]; +} + +- (void)setObjectCountNumber:(NSNumber *)objectCountNumber +{ + self.objectCount = [objectCountNumber unsignedIntegerValue]; +} + @end diff --git a/Tests/Logic/ObjectMapping/RKPaginatorTest.m b/Tests/Logic/ObjectMapping/RKPaginatorTest.m index 6d0b8e3a87..87fe2df8e4 100644 --- a/Tests/Logic/ObjectMapping/RKPaginatorTest.m +++ b/Tests/Logic/ObjectMapping/RKPaginatorTest.m @@ -350,4 +350,24 @@ - (void)testKnowledgeOfPreviousPage assertThatBool([mockPaginator hasPreviousPage], is(equalToBool(NO))); } +- (void)testProxyAttributes +{ + RKPaginator *paginator = [RKPaginator new]; + [paginator setValue:@(12345) forKey:@"pageCountNumber"]; + expect(paginator.pageCount).to.equal(12345); + expect([paginator valueForKey:@"pageCountNumber"]).to.equal(12345); + + [paginator setValue:@(1) forKey:@"currentPageNumber"]; + expect(paginator.currentPage).to.equal(1); + expect([paginator valueForKey:@"currentPageNumber"]).to.equal(1); + + [paginator setValue:@(25) forKey:@"objectCountNumber"]; + expect(paginator.objectCount).to.equal(25); + expect([paginator valueForKey:@"objectCountNumber"]).to.equal(25); + + [paginator setValue:@(10) forKey:@"perPageNumber"]; + expect(paginator.perPage).to.equal(10); + expect([paginator valueForKey:@"perPageNumber"]).to.equal(10); +} + @end From 7eef2f21743240b055210a16496568b3b364e41d Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 7 Dec 2012 14:46:23 -0500 Subject: [PATCH 11/18] Expand documentation about connecting relationships with collection values --- Code/CoreData/RKConnectionDescription.h | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Code/CoreData/RKConnectionDescription.h b/Code/CoreData/RKConnectionDescription.h index 56baa7cf82..6445130cd6 100644 --- a/Code/CoreData/RKConnectionDescription.h +++ b/Code/CoreData/RKConnectionDescription.h @@ -46,6 +46,28 @@ Any number of attribute pairs may be specified, but all values must match for the connection to be satisfied and the relationship's value to be set. + ### Connecting with Collection Values + + Connections can be established by a collection of values. For example, imagine that the previously described project representation has been extended to include a list of team members who are working on the project: + + { "project": + { "id": 12345, + "name": "My Project", + "userID": 1, + "teamMemberIDs": [1, 2, 3, 4] + } + } + + The 'teamMemberIDs' contains an array specifying the ID's of the `User` objects who are collaborating on the project, which corresponds to a to-many relationship named 'teamMembers' on the `Project` entity. In this case, the 'teamMemberIDs' could be mapped on to an `NSArray` or `NSSet` property on the `Project` entity and then connected: + + NSEntityDescription *projectEntity = [NSEntityDescription entityForName:@"Project" inManagedObjectContext:managedObjectContext]; + NSRelationshipDescription *teamMembers = [projectEntity relationshipsByName][@"teamMembers"]; // To many relationship for the `User` entity + RKConnectionDescription *connection = [[RKConnectionDescription alloc] initWithRelationship:relationship attributes:@{ @"teamMemberIDs": @"userID" }]; + + When evaluating the above JSON, the connection would be established for the 'teamMembers' relationship to the `User` entities whose userID's are 1, 2, 3 or 4. + + Note that collections of attribute values are always interpetted as logic OR's, but compound connections are aggregated as a logical AND. For example, if we were to add a second connecting attribute for the "gender" property and include `"gender": "male"` in the JSON, the connection would be made to all `User` managed objects whose ID is 1, 2, 3, OR 4 AND whose gender is "male". + ## Key Path Connections A key path connection is established by evaluating the key path of the connection against the managed object being connected. The returned value has type transformation applied and is then assigned to the relationship. From f4457f294896ed933a01df57ff01018ff6422ee1 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 7 Dec 2012 15:06:13 -0500 Subject: [PATCH 12/18] Add MOC reset to the beginning of `resetPersistentStores:`. closes #1018 --- Code/CoreData/RKManagedObjectStore.m | 3 +++ .../Logic/CoreData/RKManagedObjectStoreTest.m | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/Code/CoreData/RKManagedObjectStore.m b/Code/CoreData/RKManagedObjectStore.m index 334855c61e..00cdf6a4f0 100644 --- a/Code/CoreData/RKManagedObjectStore.m +++ b/Code/CoreData/RKManagedObjectStore.m @@ -212,6 +212,9 @@ - (void)recreateManagedObjectContexts - (BOOL)resetPersistentStores:(NSError **)error { + [self.mainQueueManagedObjectContext reset]; + [self.persistentStoreManagedObjectContext reset]; + NSError *localError; for (NSPersistentStore *persistentStore in self.persistentStoreCoordinator.persistentStores) { NSURL *URL = [self.persistentStoreCoordinator URLForPersistentStore:persistentStore]; diff --git a/Tests/Logic/CoreData/RKManagedObjectStoreTest.m b/Tests/Logic/CoreData/RKManagedObjectStoreTest.m index 360e75a4d5..703081d192 100644 --- a/Tests/Logic/CoreData/RKManagedObjectStoreTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectStoreTest.m @@ -251,4 +251,24 @@ - (void)testThatAddingASQLiteStoreExcludesThePathFromiCloudBackups } #endif +- (void)testResetPersistentStoresDoesNotTriggerDeadlock +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSManagedObject *managedObject1 = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext]; + NSManagedObject *managedObject2 = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Human"]; + [fetchRequest setSortDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES] ]]; + NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectStore.mainQueueManagedObjectContext sectionNameKeyPath:nil cacheName:nil]; + NSError *error = nil; + [fetchedResultsController performFetch:&error]; + BOOL success = [managedObjectStore resetPersistentStores:&error]; + expect(success).to.equal(YES); + expect(error).to.beNil(); + [fetchedResultsController performFetch:&error]; + [managedObject1 setValue:@"Blake" forKey:@"name"]; + [managedObject2.managedObjectContext performBlockAndWait:^{ + [managedObject2 setValue:@"Blake" forKey:@"name"]; + }]; +} + @end From d79dec118aee7087c7279359b38489f8e5b7de3c Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 7 Dec 2012 17:39:02 -0500 Subject: [PATCH 13/18] Greatly improve error output when all response descriptors fail to match. closes #1060 --- Code/Network/RKResponseDescriptor.m | 4 +- Code/Network/RKResponseMapperOperation.m | 49 ++++++++++++++++++- ...agedObjectMappingOperationDataSourceTest.m | 10 ++++ .../Network/RKObjectRequestOperationTest.m | 26 +++++++++- .../Network/RKResponseMapperOperationTest.m | 29 +++++++++++ 5 files changed, 114 insertions(+), 4 deletions(-) diff --git a/Code/Network/RKResponseDescriptor.m b/Code/Network/RKResponseDescriptor.m index c4593b74a6..b19621ce6b 100644 --- a/Code/Network/RKResponseDescriptor.m +++ b/Code/Network/RKResponseDescriptor.m @@ -23,7 +23,9 @@ #import "RKHTTPUtilities.h" // Cloned from AFStringFromIndexSet -- method should be non-static for reuse -static NSString *RKStringFromIndexSet(NSIndexSet *indexSet) { +NSString *RKStringFromIndexSet(NSIndexSet *indexSet); +NSString *RKStringFromIndexSet(NSIndexSet *indexSet) +{ NSCParameterAssert(indexSet); NSMutableString *string = [NSMutableString string]; diff --git a/Code/Network/RKResponseMapperOperation.m b/Code/Network/RKResponseMapperOperation.m index d9c43f9548..c122c03cef 100644 --- a/Code/Network/RKResponseMapperOperation.m +++ b/Code/Network/RKResponseMapperOperation.m @@ -58,6 +58,39 @@ return [[NSError alloc] initWithDomain:RKErrorDomain code:NSURLErrorBadServerResponse userInfo:userInfo]; } +NSString *RKStringFromIndexSet(NSIndexSet *indexSet); // Defined in RKResponseDescriptor.m +static NSString *RKMatchFailureDescriptionForResponseDescriptorWithResponse(RKResponseDescriptor *responseDescriptor, NSHTTPURLResponse *response) +{ + if (responseDescriptor.statusCodes && ![responseDescriptor.statusCodes containsIndex:response.statusCode]) { + return [NSString stringWithFormat:@"response status code %ld is not within the range %@", (long) response.statusCode, RKStringFromIndexSet(responseDescriptor.statusCodes)]; + } + + NSString *pathAndQueryString = RKPathAndQueryStringFromURLRelativeToURL(response.URL, responseDescriptor.baseURL); + if (responseDescriptor.baseURL && !RKURLIsRelativeToURL(response.URL, responseDescriptor.baseURL)) { + // Not relative to the baseURL + return [NSString stringWithFormat:@"response URL '%@' is not relative to the baseURL '%@'.", response.URL, responseDescriptor.baseURL]; + } + + // Must be a path pattern mismatch + return [NSString stringWithFormat:@"response path '%@' did not match the path pattern '%@'.", pathAndQueryString, responseDescriptor.pathPattern]; +} + +static NSString *RKFailureReasonErrorStringForResponseDescriptorsMismatchWithResponse(NSArray *responseDescriptors, NSHTTPURLResponse *response) +{ + NSMutableString *failureReason = [NSMutableString string]; + [failureReason appendFormat:@"A %ld response was loaded from the URL '%@', which failed to match all (%ld) response descriptors:", + (long) response.statusCode, response.URL, (long) [responseDescriptors count]]; + + for (RKResponseDescriptor *responseDescriptor in responseDescriptors) { + [failureReason appendFormat:@"\n failed to match: %@", + responseDescriptor, responseDescriptor.baseURL, responseDescriptor.pathPattern, + responseDescriptor.statusCodes ? RKStringFromIndexSet(responseDescriptor.statusCodes) : responseDescriptor.statusCodes, + RKMatchFailureDescriptionForResponseDescriptorWithResponse(responseDescriptor, response)]; + } + + return failureReason; +} + /** A serial dispatch queue used for all deserialization of response bodies */ @@ -201,7 +234,7 @@ - (void)main self.error = error; return; } - if (self.isCancelled) return; + if (self.isCancelled) return; // Invoke the will map deserialized response block if (self.willMapDeserializedResponseBlock) { @@ -215,7 +248,7 @@ - (void)main } // Object map the response - self.mappingResult = [self performMappingWithObject:parsedBody error:&error]; + self.mappingResult = [self performMappingWithObject:parsedBody error:&error]; // If the response is a client error return either the mapping error or the mapped result to the caller as the error if (isClientError) { @@ -229,6 +262,18 @@ - (void)main return; } + // Fail if no response descriptors matched + if (error.code == RKMappingErrorNotFound && [self.responseMappingsDictionary count] == 0) { + NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: NSLocalizedString(@"No response descriptors match the response loaded.", nil), + NSLocalizedFailureReasonErrorKey: RKFailureReasonErrorStringForResponseDescriptorsMismatchWithResponse(self.responseDescriptors, self.response), + RKMappingErrorKeyPathErrorKey: [NSNull null], + NSURLErrorFailingURLErrorKey: self.response.URL, + NSURLErrorFailingURLStringErrorKey: [self.response.URL absoluteString], + NSUnderlyingErrorKey: error}; + self.error = [[NSError alloc] initWithDomain:RKErrorDomain code:RKMappingErrorNotFound userInfo:userInfo]; + return; + } + if (! self.mappingResult) { self.error = error; return; diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m index 9d22cb8b0b..b51be8e889 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m @@ -994,6 +994,16 @@ - (void)testMappingAPayloadContainingRepeatedObjectsDoesNotYieldDuplicatesWithIn assertThatInteger(childrenCount, is(equalToInteger(4))); } +- (void)testConnectingToSubentitiesByFetchRequestCache +{ + +} + +- (void)testConnectingToSubentitiesByInMemoryCache +{ + +} + // TODO: Import bencharmk utility somehow... //- (void)testMappingAPayloadContainingRepeatedObjectsPerformsAcceptablyWithFetchRequestMappingCache //{ diff --git a/Tests/Logic/Network/RKObjectRequestOperationTest.m b/Tests/Logic/Network/RKObjectRequestOperationTest.m index b3e28f0ff2..99a951037a 100644 --- a/Tests/Logic/Network/RKObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKObjectRequestOperationTest.m @@ -463,6 +463,31 @@ - (void)testThatAnEmptyStringResponseBodyForAnObjectRequestOperationWithATargetO expect([requestOperation.mappingResult array]).to.contain(targetObject); } +- (void)testErrorReportingForPathPatternMismatch +{ + RKObjectMapping *userMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + [userMapping addAttributeMappingsFromArray:@[@"firstname"]]; + + NSURL *baseURL = [NSURL URLWithString:@"http://restkit.org/api/v1"]; + RKResponseDescriptor *responseDescriptor1 = [RKResponseDescriptor responseDescriptorWithMapping:userMapping pathPattern:@"/users/empty" keyPath:@"firstUser" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]; + responseDescriptor1.baseURL = baseURL; + RKResponseDescriptor *responseDescriptor2 = [RKResponseDescriptor responseDescriptorWithMapping:userMapping pathPattern:@"/users/empty" keyPath:@"secondUser" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]; + responseDescriptor2.baseURL = baseURL; + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/users/empty" relativeToURL:[RKTestFactory baseURL]]]; + RKObjectRequestOperation *requestOperation = [[RKObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor1, responseDescriptor2 ]]; + NSOperationQueue *operationQueue = [NSOperationQueue new]; + [operationQueue addOperation:requestOperation]; + [operationQueue waitUntilAllOperationsAreFinished]; + expect(requestOperation.error).notTo.beNil(); + NSString *failureReason = [[requestOperation.error userInfo] valueForKey:NSLocalizedFailureReasonErrorKey]; + assertThat(failureReason, containsString(@"A 200 response was loaded from the URL 'http://127.0.0.1:4567/users/empty', which failed to match all (2) response descriptors:")); + assertThat(failureReason, containsString(@"failed to match: response URL 'http://127.0.0.1:4567/users/empty' is not relative to the baseURL 'http://restkit.org/api/v1'.")); + assertThat(failureReason, containsString(@"failed to match: response URL 'http://127.0.0.1:4567/users/empty' is not relative to the baseURL 'http://restkit.org/api/v1'.")); +} + +// Test trailing slash on the baseURL + #pragma mark - Block Tests - (void)testInvocationOfSuccessBlock @@ -501,7 +526,6 @@ - (void)testInvocationOfFailureBlock [operationQueue waitUntilAllOperationsAreFinished]; } - #pragma mark - Will Map Data Block - (void)testShouldAllowMutationOfTheParsedDataInWillMapData diff --git a/Tests/Logic/Network/RKResponseMapperOperationTest.m b/Tests/Logic/Network/RKResponseMapperOperationTest.m index 7d5e1be50c..ec66ecbf9c 100644 --- a/Tests/Logic/Network/RKResponseMapperOperationTest.m +++ b/Tests/Logic/Network/RKResponseMapperOperationTest.m @@ -200,4 +200,33 @@ - (void)testThatResponseMapperMatchesBaseURLWithPathAndQueryParametersAppropriat expect(mapper.responseMappingsDictionary).to.equal(expectedMappingsDictionary); } +- (void)testThatResponseDescriptorMismatchesIncludeHelpfulError +{ + NSURL *responseURL = [NSURL URLWithString:@"http://restkit.org/api/v1/users"]; + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:responseURL statusCode:200 HTTPVersion:@"1.1" headerFields:@{@"Content-Type": @"application/json"}]; + NSData *data = [@"{\"some\": \"Data\"}" dataUsingEncoding:NSUTF8StringEncoding]; + NSURL *baseURL = [NSURL URLWithString:@"http://restkit.org/api/v1/"]; + + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + RKResponseDescriptor *responseDescriptor1 = [RKResponseDescriptor responseDescriptorWithMapping:mapping pathPattern:@"/users" keyPath:@"this" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + responseDescriptor1.baseURL = baseURL; + RKResponseDescriptor *responseDescriptor2 = [RKResponseDescriptor responseDescriptorWithMapping:mapping pathPattern:@"/users" keyPath:@"that" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + responseDescriptor2.baseURL = [NSURL URLWithString:@"http://google.com"]; + RKResponseDescriptor *responseDescriptor3 = [RKResponseDescriptor responseDescriptorWithMapping:mapping pathPattern:@"users" keyPath:@"that" statusCodes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(202, 5)]]; + responseDescriptor3.baseURL = baseURL; + + RKObjectResponseMapperOperation *mapper = [[RKObjectResponseMapperOperation alloc] initWithResponse:response data:data responseDescriptors:@[ responseDescriptor1, responseDescriptor2, responseDescriptor3 ]]; + [mapper start]; + expect(mapper.error).notTo.beNil(); + expect([mapper.error code]).to.equal(RKMappingErrorNotFound); + NSString *failureReason = [[mapper.error userInfo] valueForKey:NSLocalizedFailureReasonErrorKey]; + assertThat(failureReason, containsString(@"A 200 response was loaded from the URL 'http://restkit.org/api/v1/users', which failed to match all (3) response descriptors")); + assertThat(failureReason, containsString(@"failed to match: response path 'users' did not match the path pattern '/users'.")); + assertThat(failureReason, containsString(@"failed to match: response URL 'http://restkit.org/api/v1/users' is not relative to the baseURL 'http://google.com'.")); + assertThat(failureReason, containsString(@"failed to match: response status code 200 is not within the range 202-206")); + + NSDictionary *expectedMappingsDictionary = @{}; + expect(mapper.responseMappingsDictionary).to.equal(expectedMappingsDictionary); +} + @end From 5685a5fb60853a1d55322baf4839726be3c72dfb Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 7 Dec 2012 18:25:47 -0500 Subject: [PATCH 14/18] Add test case investigating #921 --- Code/Testing/RKTestFixture.h | 10 ---------- Code/Testing/RKTestFixture.m | 7 ------- .../JSON/SameKeyDifferentTargetClasses.json | 2 -- .../RKObjectMappingNextGenTest.m | 19 +++++++++++++++++++ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Code/Testing/RKTestFixture.h b/Code/Testing/RKTestFixture.h index 0428450e85..2790bf13e9 100644 --- a/Code/Testing/RKTestFixture.h +++ b/Code/Testing/RKTestFixture.h @@ -53,16 +53,6 @@ */ + (NSString *)pathForFixture:(NSString *)fixtureName; -#if TARGET_OS_IPHONE -/** - Creates and returns an image object by loading the image data from the fixture identified by the specified file name. - - @param fixtureName The name of the fixture file. - @return A new image object for the specified fixture, or nil if the method could not initialize the image from the specified file. - */ -//+ (UIImage *)imageWithContentsOfFixture:(NSString *)fixtureName; -#endif - /** Creates and returns a string object by reading data from the fixture identified by the specified file name using UTF-8 encoding. diff --git a/Code/Testing/RKTestFixture.m b/Code/Testing/RKTestFixture.m index 26287e3c2d..eda7a9ee1e 100644 --- a/Code/Testing/RKTestFixture.m +++ b/Code/Testing/RKTestFixture.m @@ -42,13 +42,6 @@ + (NSString *)pathForFixture:(NSString *)fixtureName return [[self fixtureBundle] pathForResource:fixtureName ofType:nil]; } -//#if TARGET_OS_IPHONE -//+ (UIImage *)imageWithContentsOfFixture:(NSString *)fixtureName -//{ -// return [[self fixtureBundle] imageWithContentsOfResource:fixtureName withExtension:nil]; -//} -//#endif - + (NSString *)stringWithContentsOfFixture:(NSString *)fixtureName { return [[self fixtureBundle] stringWithContentsOfResource:fixtureName withExtension:nil encoding:NSUTF8StringEncoding]; diff --git a/Tests/Fixtures/JSON/SameKeyDifferentTargetClasses.json b/Tests/Fixtures/JSON/SameKeyDifferentTargetClasses.json index ddac615035..9cacae1459 100644 --- a/Tests/Fixtures/JSON/SameKeyDifferentTargetClasses.json +++ b/Tests/Fixtures/JSON/SameKeyDifferentTargetClasses.json @@ -1,5 +1,3 @@ -// TODO: Map this to a different class for each translation key - { "products": [ { "prodId": 5, diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m index 37664773ac..2f11a6b6b3 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m @@ -2063,4 +2063,23 @@ - (void)testUpdatingArrayOfExistingCats assertThat([humans objectAtIndex:1], is(equalTo(human2))); } +- (void)testMappingMultipleKeyPathsAtRootOfObject +{ + RKObjectMapping *mapping1 = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping1 addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:@"prodId" toKeyPath:@"userID"]]; + + RKObjectMapping *mapping2 = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping2 addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:@"catId" toKeyPath:@"userID"]]; + + NSDictionary *dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"SameKeyDifferentTargetClasses.json"]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:dictionary mappingsDictionary:@{ @"products": mapping1, @"categories": mapping2 }]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + mapper.mappingOperationDataSource = dataSource; + [mapper start]; + + expect(mapper.error).to.beNil(); + expect(mapper.mappingResult).notTo.beNil(); + expect([mapper.mappingResult array]).to.haveCountOf(4); +} + @end From 61102be1a4ec35ec098eb9469316b4c8263d10b1 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 7 Dec 2012 18:56:14 -0500 Subject: [PATCH 15/18] Fix incorrect mapping results for secondary key paths when more than one response descriptor matches and there is a targetObject. fixes #1057 --- Code/ObjectMapping/RKMapperOperation.m | 16 +++++++++----- .../Logic/ObjectMapping/RKObjectManagerTest.m | 22 +++++++++++++++++-- Tests/Server/server.rb | 5 +++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Code/ObjectMapping/RKMapperOperation.m b/Code/ObjectMapping/RKMapperOperation.m index fd57b97188..dd8db29397 100644 --- a/Code/ObjectMapping/RKMapperOperation.m +++ b/Code/ObjectMapping/RKMapperOperation.m @@ -132,12 +132,18 @@ - (id)mapObject:(id)mappableObject atKeyPath:(NSString *)keyPath usingMapping:(R } else { NSAssert(objectMapping, @"Encountered unknown mapping type '%@'", NSStringFromClass([mapping class])); } + if (NO == [[self.targetObject class] isSubclassOfClass:objectMapping.objectClass]) { - NSString *errorMessage = [NSString stringWithFormat: - @"Expected an object mapping for class of type '%@', provider returned one for '%@'", - NSStringFromClass([self.targetObject class]), NSStringFromClass(objectMapping.objectClass)]; - [self addErrorWithCode:RKMappingErrorTypeMismatch message:errorMessage keyPath:keyPath userInfo:nil]; - return nil; + if ([_mappingsDictionary count] == 1) { + NSString *errorMessage = [NSString stringWithFormat: + @"Expected an object mapping for class of type '%@', provider returned one for '%@'", + NSStringFromClass([self.targetObject class]), NSStringFromClass(objectMapping.objectClass)]; + [self addErrorWithCode:RKMappingErrorTypeMismatch message:errorMessage keyPath:keyPath userInfo:nil]; + return nil; + } else { + // There is more than one mapping present. We are likely mapping secondary key paths to new objects + destinationObject = [self objectWithMapping:mapping andData:mappableObject]; + } } } else { destinationObject = [self objectWithMapping:mapping andData:mappableObject]; diff --git a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m index 75253a4561..02ebd844cb 100644 --- a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m @@ -24,6 +24,7 @@ #import "RKEntityMapping.h" #import "RKHuman.h" #import "RKCat.h" +#import "RKTestUser.h" #import "RKObjectMapperTestModel.h" #import "RKDynamicMapping.h" @@ -601,8 +602,25 @@ - (void)testChangingHTTPClient expect([manager.baseURL absoluteString]).to.equal(@"http://google.com/"); } -// Test with relationship 2 levels deep -// Test with recursive relationships +- (void)testPostingOneObjectAndGettingResponseMatchingMultipleDescriptors +{ + RKObjectManager *manager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKObjectMapping *userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [userMapping addAttributeMappingsFromDictionary:@{ @"fullname": @"name" }]; + RKResponseDescriptor *userResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:userMapping pathPattern:nil keyPath:@"data.STUser" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]; + RKObjectMapping *metaMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + [metaMapping addAttributeMappingsFromArray:@[ @"status", @"version" ]]; + RKResponseDescriptor *metaResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:metaMapping pathPattern:nil keyPath:@"meta" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]; + + [manager addResponseDescriptorsFromArray:@[ userResponseDescriptor, metaResponseDescriptor ]]; + RKTestUser *user = [RKTestUser new]; + RKObjectRequestOperation *requestOperation = [manager appropriateObjectRequestOperationWithObject:user method:RKRequestMethodPOST path:@"/ComplexUser" parameters:nil]; + [requestOperation start]; + [requestOperation waitUntilFinished]; + + expect(requestOperation.mappingResult).notTo.beNil(); + expect([requestOperation.mappingResult array]).to.haveCountOf(2); +} //- (void)testShouldHandleConnectionFailures //{ diff --git a/Tests/Server/server.rb b/Tests/Server/server.rb index eb1bc3b06e..156e2d5dd5 100755 --- a/Tests/Server/server.rb +++ b/Tests/Server/server.rb @@ -283,6 +283,11 @@ def render_fixture(path, options = {}) status 200 ' ' end + + post '/ComplexUser' do + content_type 'application/json' + render_fixture('/JSON/ComplexNestedUser.json', :status => 200) + end # start the server if ruby file executed directly run! if app_file == $0 From 3d6c211417681cd1f8fafc54da6fe7bb1ff00726 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 8 Dec 2012 00:34:15 -0500 Subject: [PATCH 16/18] Add documentation notes about how easy it is to fuck things up with relative paths --- Code/Network/RKObjectManager.h | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Code/Network/RKObjectManager.h b/Code/Network/RKObjectManager.h index e3c340ffd4..26de59cedf 100644 --- a/Code/Network/RKObjectManager.h +++ b/Code/Network/RKObjectManager.h @@ -36,10 +36,22 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; Object request operations model the lifecycle of an object mapped HTTP request from start to finish. They are initialized with a fully configured `NSURLRequest` object and a set of `RKResponseDescriptor` objects that specify how an HTTP response is to be mapped into local domain objects. Object request operations may be constructed as standalone objects, but are often constructed through an `RKObjectManager` object. The object request operation encapsulates the functionality of two underlying operations that perform the bulk of the work. The HTTP request and response loading is handled by an `RKHTTPRequestOperation`, which is responsible for the HTTP transport details. Once a response has been successfully loaded, the object request operation starts an `RKResponseMapperOperation` that is responsible for handling the mapping of the response body. When working with Core Data, the `RKManagedObjectRequestOperation` class is used. The object manager encapsulates the Core Data configuration details and provides an interface that will return the appropriate object request operation for a request through the `appropriateObjectRequestOperationWithObject:method:path:parameters:` method. - ## The Base URL and Path Patterns + ## Base URL, Relative Paths and Path Patterns Each object manager is configured with a base URL that defines the URL that all request sent through the manager will be relative to. The base URL is configured directly through the `managerWithBaseURL:` method or is inherited from an AFNetworking `AFHTTPClient` object if the manager is initialized via the `initWithHTTPClient:` method. The base URL can point directly at the root of a URL or may include a path. + Many of the methods of the object manager accept a path argument, either directly or in the form of a path pattern. Whenever a path is provided to the object manager directly, as part of a request or response descriptor (see "Request and Response Descriptors"), or via a route (see the "Routing" section), the path is used to construct an `NSURL` object with `[NSURL URLWithString:relativeToURL:]`. The rules for the evaluation of a relative URL can at times be surprising and many configuration errors result from incorrectly configuring the `baseURL` and relative paths thereof. For reference, here are some examples borrowed from the AFNetworking documentation detailing how base URL's and relative paths interact: + + NSURL *baseURL = [NSURL URLWithString:@"http://example.com/v1/"]; + [NSURL URLWithString:@"foo" relativeToURL:baseURL]; // http://example.com/v1/foo + [NSURL URLWithString:@"foo?bar=baz" relativeToURL:baseURL]; // http://example.com/v1/foo?bar=baz + [NSURL URLWithString:@"/foo" relativeToURL:baseURL]; // http://example.com/foo + [NSURL URLWithString:@"foo/" relativeToURL:baseURL]; // http://example.com/v1/foo + [NSURL URLWithString:@"/foo/" relativeToURL:baseURL]; // http://example.com/foo/ + [NSURL URLWithString:@"http://example2.com/" relativeToURL:baseURL]; // http://example2.com/ + + Keep these rules in mind when providing relative paths to the object manager. + Path patterns are a common unit of abstraction in RestKit for describing the path portion of URL's. When working with API's, there is typically one or more dynamic portions of the URL that correspond to primary keys or other identifying resource attributes. For example, a blogging application may represent articles in a URL structure such as '/articles/1234' and comments about an article might appear at '/articles/1234/comments'. These path structures could be represented as the path patterns '/articles/:articleID' and '/articles/:articleID/comments', substituing the dynamic key ':articleID' in place of the primary key of in the path. These keys can be used to interpolate a path with an object's property values using key-value coding or be used to match a string. Path patterns appear throughout RestKit, but the most fundamental uses are for the dynamic generation of URL paths from objects and the matching of request and response URLs for mapping configuration. When generating a URL, a path pattern is interpolated with the value of an object. Consider this example: From c06a1a314d4c6e1cfad0d8e79cb9546260ec2e6e Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 8 Dec 2012 00:44:28 -0500 Subject: [PATCH 17/18] Add documentation notes about the importance of coercing your values from the path matcher --- Code/Network/RKManagedObjectRequestOperation.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Code/Network/RKManagedObjectRequestOperation.h b/Code/Network/RKManagedObjectRequestOperation.h index 285518e7c4..9b1863117b 100644 --- a/Code/Network/RKManagedObjectRequestOperation.h +++ b/Code/Network/RKManagedObjectRequestOperation.h @@ -67,7 +67,7 @@ airportID = [argsDict objectForKey:@"airport_id"]; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Terminal"]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"Airport" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; - fetchRequest.predicate = [entity predicateForPrimaryKeyAttributeWithValue:airportID]; + fetchRequest.predicate = [NSPredication predicateWithFormat:@"airportID = %@", @([airportID integerValue])]; // NOTE: Coerced from string to number fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES] ]; return fetchRequest; } @@ -75,7 +75,7 @@ return nil; }]; - The above example code defines an `RKFetchRequestBlock` block object that will match an `NSURL` with a relative path matching the pattern `@"/airports/:airport_id/terminals.json"`. If a match is found, the block extracts the `airport_id` key from the matched arguments and uses it to construct an `NSPredicate` for the primary key attribute of `GGAirport` entity. It then constructs an `NSFetchRequest` for the `GGTerminal` entity that will retrieve all the managed objects with an airport ID attribute whose value is equal to `airport_id` encoded within the URL's path. + The above example code defines an `RKFetchRequestBlock` block object that will match an `NSURL` with a relative path matching the pattern `@"/airports/:airport_id/terminals.json"`. If a match is found, the block extracts the `airport_id` key from the matched arguments, coerces its value into a number, and uses it to construct an `NSPredicate` for the primary key attribute of `GGAirport` entity. Take note that the value of the 'airport_id' was coerced from an `NSString` to an `NSNumber` -- failure to so would result in a predicate whose value is equal to `airportID == '1234'` vs. `airportID == 1234`, which will prevent fetch requests from evaluating correctly. Once coerced, the value is used to construct a `NSFetchRequest` object for the `GGTerminal` entity that will retrieve all the managed objects with an airport ID attribute whose value is equal to `airport_id` encoded within the URL's path. In more concrete terms, given the URL `http://restkit.org/airports/1234/terminals.json` the block would return an `NSFetchRequest` equal to: From 5c1a461cfe55c2350aae13f6f2bc40881b5f9c50 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 8 Dec 2012 00:46:19 -0500 Subject: [PATCH 18/18] Bump VERSION to 0.20.0-pre3 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index e7ee464d27..e2f6c29a83 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.20.0-pre2 \ No newline at end of file +0.20.0-pre3 \ No newline at end of file