Permalink
Browse files

Merge branch 'release/0.20.0-pre3'

  • Loading branch information...
2 parents 9b78ca2 + 5c1a461 commit 1c60806a3c26078dc5179e2facf4636e75d9be41 @blakewatters blakewatters committed Dec 8, 2012
Showing with 1,284 additions and 1,062 deletions.
  1. +22 −0 Code/CoreData/RKConnectionDescription.h
  2. +1 −1 Code/CoreData/RKEntityByAttributeCache.h
  3. +52 −25 Code/CoreData/RKEntityByAttributeCache.m
  4. +1 −1 Code/CoreData/RKEntityCache.h
  5. +1 −1 Code/CoreData/RKEntityCache.m
  6. +14 −1 Code/CoreData/RKEntityMapping.m
  7. +18 −9 Code/CoreData/RKFetchRequestManagedObjectCache.m
  8. +1 −1 Code/CoreData/RKInMemoryManagedObjectCache.m
  9. +3 −3 Code/CoreData/RKManagedObjectCaching.h
  10. +5 −5 Code/CoreData/RKManagedObjectMappingOperationDataSource.m
  11. +3 −0 Code/CoreData/RKManagedObjectStore.m
  12. +6 −6 Code/CoreData/RKRelationshipConnectionOperation.m
  13. +2 −2 Code/Network/RKManagedObjectRequestOperation.h
  14. +14 −2 Code/Network/RKObjectManager.h
  15. +74 −3 Code/Network/RKObjectManager.m
  16. +25 −8 Code/Network/RKPaginator.h
  17. +48 −0 Code/Network/RKPaginator.m
  18. +5 −2 Code/Network/RKResponseDescriptor.m
  19. +47 −2 Code/Network/RKResponseMapperOperation.m
  20. +1 −1 Code/Network/RKRouter.h
  21. +0 −1 Code/Network/RKRouter.m
  22. +1 −0 Code/ObjectMapping.h
  23. +11 −5 Code/ObjectMapping/RKMapperOperation.m
  24. +0 −10 Code/Testing/RKTestFixture.h
  25. +0 −7 Code/Testing/RKTestFixture.m
  26. +1 −1 Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m
  27. +1 −1 Rakefile
  28. +2 −2 RestKit.podspec
  29. +6 −2 RestKit.xcodeproj/project.pbxproj
  30. +0 −2 Tests/Fixtures/JSON/SameKeyDifferentTargetClasses.json
  31. +71 −2 Tests/Logic/CoreData/RKEntityByAttributeCacheTest.m
  32. +4 −4 Tests/Logic/CoreData/RKEntityCacheTest.m
  33. +4 −4 Tests/Logic/CoreData/RKFetchRequestMappingCacheTest.m
  34. +599 −0 Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m
  35. +0 −946 Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m
  36. +20 −0 Tests/Logic/CoreData/RKManagedObjectStoreTest.m
  37. +25 −1 Tests/Logic/Network/RKObjectRequestOperationTest.m
  38. +29 −0 Tests/Logic/Network/RKResponseMapperOperationTest.m
  39. +26 −0 Tests/Logic/Network/RKRouterTest.m
  40. +96 −0 Tests/Logic/ObjectMapping/RKObjectManagerTest.m
  41. +19 −0 Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m
  42. +20 −0 Tests/Logic/ObjectMapping/RKPaginatorTest.m
  43. +5 −0 Tests/Server/server.rb
  44. +1 −1 VERSION
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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);
@@ -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]];
@@ -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;
@@ -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
@@ -51,7 +51,7 @@ - (id)init
userInfo:nil];
}
-- (NSArray *)managedObjectsWithEntity:(NSEntityDescription *)entity
+- (NSSet *)managedObjectsWithEntity:(NSEntityDescription *)entity
attributeValues:(NSDictionary *)attributeValues
inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
@@ -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
@@ -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:)]) {
@@ -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];
@@ -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];
Oops, something went wrong.

0 comments on commit 1c60806

Please sign in to comment.