Permalink
Browse files

Merge branch 'release/0.20.0'

  • Loading branch information...
2 parents 0166576 + 255c1c7 commit e0c6ed24c5a83dbc9044cafff0a6afd061cc278f @blakewatters blakewatters committed Mar 21, 2013
Showing with 1,559 additions and 662 deletions.
  1. +16 −0 Code/CoreData/NSManagedObjectContext+RKAdditions.m
  2. +2 −2 Code/CoreData/RKConnectionDescription.h
  3. +126 −78 Code/CoreData/RKEntityByAttributeCache.m
  4. +14 −1 Code/CoreData/RKEntityMapping.h
  5. +1 −0 Code/CoreData/RKEntityMapping.m
  6. +10 −0 Code/CoreData/RKInMemoryManagedObjectCache.m
  7. +1 −0 Code/CoreData/RKManagedObjectImporter.m
  8. +24 −13 Code/CoreData/RKManagedObjectMappingOperationDataSource.m
  9. +2 −2 Code/CoreData/RKRelationshipConnectionOperation.h
  10. +16 −4 Code/CoreData/RKRelationshipConnectionOperation.m
  11. +0 −5 Code/Network/RKHTTPRequestOperation.h
  12. +11 −12 Code/Network/RKHTTPRequestOperation.m
  13. +10 −0 Code/Network/RKManagedObjectRequestOperation.h
  14. +114 −25 Code/Network/RKManagedObjectRequestOperation.m
  15. +1 −1 Code/Network/RKObjectManager.h
  16. +13 −9 Code/Network/RKObjectManager.m
  17. +9 −1 Code/Network/RKObjectRequestOperation.h
  18. +29 −2 Code/Network/RKObjectRequestOperation.m
  19. +8 −0 Code/Network/RKPaginator.h
  20. +25 −1 Code/Network/RKPaginator.m
  21. +1 −1 Code/Network/RKResponseMapperOperation.h
  22. +5 −1 Code/Network/RKResponseMapperOperation.m
  23. +1 −2 Code/Network/RKRouteSet.m
  24. +7 −0 Code/ObjectMapping/RKHTTPUtilities.h
  25. +16 −1 Code/ObjectMapping/RKHTTPUtilities.m
  26. +1 −0 Code/ObjectMapping/RKMapperOperation.m
  27. +22 −5 Code/ObjectMapping/RKMappingOperation.m
  28. +3 −3 Code/ObjectMapping/RKObjectMapping.h
  29. +2 −2 Code/Search/RKSearchIndexer.m
  30. +5 −0 Code/Support/RKMIMETypeSerialization.m
  31. +23 −9 Code/Support/RKPathMatcher.m
  32. +1 −0 Code/Support/lcl_config_components_RK.h
  33. +0 −67 Code/Testing/NSBundle+RKAdditions.h
  34. +0 −82 Code/Testing/NSBundle+RKAdditions.m
  35. +111 −0 Code/Testing/RKBenchmark.h
  36. +129 −0 Code/Testing/RKBenchmark.m
  37. +3 −1 Code/Testing/RKTestFactory.h
  38. +3 −3 Code/Testing/RKTestFactory.m
  39. +2 −5 Code/Testing/RKTestFixture.h
  40. +38 −7 Code/Testing/RKTestFixture.m
  41. +1 −1 Examples/RKTwitter/Classes/RKTwitterAppDelegate.m
  42. +1 −1 Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m
  43. +4 −4 Gemfile
  44. +41 −33 Gemfile.lock
  45. +1 −1 Podfile.lock
  46. +5 −4 README.md
  47. +6 −9 Rakefile
  48. +2 −2 RestKit.podspec
  49. +20 −16 RestKit.xcodeproj/project.pbxproj
  50. +23 −8 Tests/Logic/CoreData/RKEntityByAttributeCacheTest.m
  51. +18 −0 Tests/Logic/CoreData/RKEntityMappingTest.m
  52. +177 −73 Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m
  53. +181 −6 Tests/Logic/Network/RKManagedObjectRequestOperationTest.m
  54. +60 −0 Tests/Logic/Network/RKObjectRequestOperationTest.m
  55. +22 −0 Tests/Logic/Network/RKResponseMapperOperationTest.m
  56. +11 −0 Tests/Logic/Network/RKRouteSetTest.m
  57. +59 −1 Tests/Logic/ObjectMapping/RKObjectManagerTest.m
  58. +17 −0 Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m
  59. +46 −0 Tests/Logic/ObjectMapping/RKPaginatorTest.m
  60. +7 −0 Tests/Logic/Support/RKHTTPUtilitiesTest.m
  61. +28 −1 Tests/Logic/Support/RKPathMatcherTest.m
  62. BIN Tests/Models/Data Model.xcdatamodel/elements
  63. BIN Tests/Models/Data Model.xcdatamodel/layout
  64. +1 −0 Tests/Models/RKHuman.h
  65. +1 −0 Tests/Models/RKHuman.m
  66. +1 −0 Tests/Models/RKTestUser.h
  67. +0 −146 Tests/RunPlatformUnitTests
  68. +14 −7 Tests/Server/server.rb
  69. +6 −3 Tests/cibuild
  70. +1 −1 VERSION
@@ -42,6 +42,22 @@ - (BOOL)saveToPersistentStore:(NSError **)error
NSManagedObjectContext *contextToSave = self;
while (contextToSave) {
__block BOOL success;
+
+ /**
+ To work around issues in ios 5 first obtain permanent object ids for any inserted objects. If we don't do this then its easy to get an `NSObjectInaccessibleException`. This happens when:
+
+ 1. Create new object on main context and save it.
+ 2. At this point you may or may not call obtainPermanentIDsForObjects for the object, it doesn't matter
+ 3. Update the object in a private child context.
+ 4. Save the child context to the parent context (the main one) which will work,
+ 5. Save the main context - a NSObjectInaccessibleException will occur and Core Data will either crash your app or lock it up (a semaphore is not correctly released on the first error so the next fetch request will block forever.
+ */
+ [contextToSave obtainPermanentIDsForObjects:[[contextToSave insertedObjects] allObjects] error:&localError];
+ if (localError) {
+ if (error) *error = localError;
+ return NO;
+ }
+
[contextToSave performBlockAndWait:^{
success = [contextToSave save:&localError];
if (! success && localError == nil) RKLogWarning(@"Saving of managed object context failed, but a `nil` value for the `error` argument was returned. This typically indicates an invalid implementation of a key-value validation method exists within your model. This violation of the API contract may result in the save operation being mis-interpretted by callers that rely on the availability of the error.");
@@ -40,7 +40,7 @@
NSEntityDescription *projectEntity = [NSEntityDescription entityForName:@"Project" inManagedObjectContext:managedObjectContext];
NSRelationshipDescription *userRelationship = [projectEntity relationshipsByName][@"user"];
- RKConnectionDescription *connection = [[RKConnectionDescription alloc] initWithRelationship:relationship attributes:@{ @"userID": @"userID" }];
+ RKConnectionDescription *connection = [[RKConnectionDescription alloc] initWithRelationship:userRelationship attributes:@{ @"userID": @"userID" }];
Note that the value for the `attributes` argument is provided as a dictionary. Each pair within the dictionary correspond to an attribute pair in which the key is an attribute on the source entity (in this case, the `Project`) and the value is the destination entity (in this case, the `User`).
@@ -62,7 +62,7 @@
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" }];
+ RKConnectionDescription *connection = [[RKConnectionDescription alloc] initWithRelationship:teamMembers 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.

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -147,7 +147,7 @@
// JSON looks like {"project": { "name": "Project Name", "userID": 1234, "projectID": 1 } }
RKEntityMapping *mapping = [RKEntityMapping mappingForEntityForName:@"Project" inManagedObjectStore:managedObjectStore];
- [mapping addAttributeMappings:@[ @"name", @"userID", @"projectID" ]];
+ [mapping addAttributeMappingsFromArray:@[ @"name", @"userID", @"projectID" ]];
// Find a 'User' whose value for the 'userID' object is equal to the value stored on the 'userID' attribute of the 'Project' and assign it to the relationship
// In other words, "Find the User whose userID == 1234 and assign that object to the 'user' relationship"
@@ -177,6 +177,19 @@
*/
- (RKConnectionDescription *)connectionForRelationship:(id)relationshipOrName;
+///-----------------------------
+/// @name Configuring Validation
+///-----------------------------
+
+/**
+ A Boolean value that determines if newly created `NSManagedObject` instances mapped with the receiver should be discarded when they fail `validateForInsert:`.
+
+ This property allows for the deletion of managed objects that fail validation such that `NSManagedObjectContext` save will complete successfully. Typically an invalid managed object in the graph will result in a failure to save the `NSManagedObjectContext` due to an NSValidation error. In some cases it is desirable to persist only the subset of objects that pass validation and discard the invalid content rather than failing the entire operation. Setting this property to `YES` will result in the deletion of in any newly created `NSManagedObject` instances that fail to return `YES` when sent the `validateForInsert:` message.
+
+ **Default**: `NO`
+ */
+@property (nonatomic, assign) BOOL discardsInvalidObjectsOnInsert;
+
///------------------------------------
/// @name Flagging Objects for Deletion
///------------------------------------
@@ -164,6 +164,7 @@ - (id)initWithEntity:(NSEntityDescription *)entity
self = [self initWithClass:objectClass];
if (self) {
self.entity = entity;
+ self.discardsInvalidObjectsOnInsert = NO;
if ([RKEntityMapping isEntityIdentificationInferenceEnabled]) self.identificationAttributes = RKIdentificationAttributesInferredFromEntity(entity);
}
@@ -77,6 +77,16 @@ - (void)didFetchObject:(NSManagedObject *)object
- (void)didCreateObject:(NSManagedObject *)object
{
+ /**
+ NOTE: When an object with a temporary ID is added to the cache, we obtain a permanent ID to avoid the potential for duplicated objects being created due to concurrenct access
+ */
+ if ([[object objectID] isTemporaryID]) {
+ NSError *error = nil;
+ RKLogTrace(@"Obtaining permanent managed object ID for temporary managed object: %@", object);
+ if (! [[object managedObjectContext] obtainPermanentIDsForObjects:@[ object ] error:&error]) {
+ RKLogError(@"Failed to obtain permanent managed object ID: %@", error);
+ }
+ }
[self.entityCache addObject:object];
}
@@ -215,6 +215,7 @@ - (NSUInteger)importObjectsFromFileAtPath:(NSString *)path withMapping:(RKMappin
NSDictionary *mappingDictionary = @{ (keyPath ?: [NSNull null]) : mapping };
RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:parsedData mappingsDictionary:mappingDictionary];
mapper.mappingOperationDataSource = self.mappingOperationDataSource;
+ self.mappingOperationDataSource.parentOperation = mapper;
__block RKMappingResult *mappingResult;
[self.managedObjectContext performBlockAndWait:^{
[mapper start];
@@ -123,6 +123,7 @@ static id RKMutableCollectionValueWithObjectForKeyPath(id object, NSString *keyP
return nil;
}
+// Pre-condition: invoked from the managed object context of the given object
static BOOL RKDeleteInvalidNewManagedObject(NSManagedObject *managedObject)
{
if ([managedObject isKindOfClass:[NSManagedObject class]] && [managedObject managedObjectContext] && [managedObject isNew]) {
@@ -246,13 +247,15 @@ - (id)mappingOperation:(RKMappingOperation *)mappingOperation targetObjectForRep
NSManagedObject *managedObject = nil;
// If we are mapping within a relationship, try to find an existing object without identifying attributes
+ // NOTE: We avoid doing the mutable(Array|Set|OrderedSet)ValueForKey if there are identification attributes for performance (see issue GH-1232)
if (relationship) {
- id mutableArrayOrSetValueForExistingObjects = RKMutableCollectionValueWithObjectForKeyPath(mappingOperation.destinationObject, relationship.destinationKeyPath);
NSArray *identificationAttributes = [entityMapping.identificationAttributes valueForKey:@"name"];
- for (NSManagedObject *existingObject in mutableArrayOrSetValueForExistingObjects) {
+ id existingObjectsOfRelationship = identificationAttributes ? [mappingOperation.destinationObject valueForKeyPath:relationship.destinationKeyPath] : RKMutableCollectionValueWithObjectForKeyPath(mappingOperation.destinationObject, relationship.destinationKeyPath);
+ if (existingObjectsOfRelationship && !RKObjectIsCollection(existingObjectsOfRelationship)) existingObjectsOfRelationship = @[ existingObjectsOfRelationship ];
+ for (NSManagedObject *existingObject in existingObjectsOfRelationship) {
if (! identificationAttributes) {
managedObject = existingObject;
- [mutableArrayOrSetValueForExistingObjects removeObject:managedObject];
+ [existingObjectsOfRelationship removeObject:managedObject];
break;
}
@@ -310,25 +313,33 @@ - (void)emitDeadlockWarningIfNecessary
- (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation error:(NSError **)error
{
if ([mappingOperation.objectMapping isKindOfClass:[RKEntityMapping class]]) {
- [self emitDeadlockWarningIfNecessary];
+ [self emitDeadlockWarningIfNecessary];
- NSArray *connections = [(RKEntityMapping *)mappingOperation.objectMapping connections];
+ RKEntityMapping *entityMapping = (RKEntityMapping *)mappingOperation.objectMapping;
+ NSArray *connections = [entityMapping connections];
if ([connections count] > 0 && self.managedObjectCache == nil) {
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Cannot map an entity mapping that contains connection mappings with a data source whose managed object cache is nil." };
NSError *localError = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorNilManagedObjectCache userInfo:userInfo];
if (error) *error = localError;
return NO;
}
- // Delete the object immediately if there are no connections that may make it valid
- if ([connections count] == 0 && RKDeleteInvalidNewManagedObject(mappingOperation.destinationObject)) return YES;
-
- // Attempt to establish the connections and delete the object if its invalid once we are done
+ /**
+ Attempt to establish the connections and delete the object if its invalid once we are done
+
+ NOTE: We obtain a weak reference to the MOC to avoid a potential crash under iOS 5 if the MOC is deallocated before the operation executes. Under iOS 6, the object returns a nil `managedObjectContext` and the `performBlockAndWait:` message is sent to nil.
+ */
NSOperationQueue *operationQueue = self.operationQueue ?: [NSOperationQueue currentQueue];
- NSBlockOperation *deletionOperation = [NSBlockOperation blockOperationWithBlock:^{
- RKDeleteInvalidNewManagedObject(mappingOperation.destinationObject);
- }];
+ __weak NSManagedObjectContext *weakContext = [(NSManagedObject *)mappingOperation.destinationObject managedObjectContext];
+ NSBlockOperation *deletionOperation = entityMapping.discardsInvalidObjectsOnInsert ? [NSBlockOperation blockOperationWithBlock:^{
+ [weakContext performBlockAndWait:^{
+ RKDeleteInvalidNewManagedObject(mappingOperation.destinationObject);
+ }];
+ }] : nil;
+ // Add a dependency on the parent operation. If we are being mapped as part of a relationship, then the assignment of the mapped object to a parent may well fulfill the validation requirements. This ensures that the relationship mapping has completed before we evaluate the object for deletion.
+ if (self.parentOperation) [deletionOperation addDependency:self.parentOperation];
+
for (RKConnectionDescription *connection in connections) {
RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:mappingOperation.destinationObject connection:connection managedObjectCache:self.managedObjectCache];
[operation setConnectionBlock:^(RKRelationshipConnectionOperation *operation, id connectedValue) {
@@ -400,7 +411,7 @@ - (void)updateCacheWithChangesFromContextWillSaveNotification:(NSNotification *)
}
}
- if ([self.managedObjectCache respondsToSelector:@selector(didDeleteObject::)]) {
+ if ([self.managedObjectCache respondsToSelector:@selector(didDeleteObject:)]) {
for (NSManagedObject *managedObject in [self.managedObjectContext deletedObjects]) {
[self.managedObjectCache didDeleteObject:managedObject];
}
@@ -45,8 +45,8 @@
@return The receiver, initialized with the given managed object, connection mapping, and managed object cache.
*/
- (id)initWithManagedObject:(NSManagedObject *)managedObject
- connection:(RKConnectionDescription *)connection
- managedObjectCache:(id<RKManagedObjectCaching>)managedObjectCache;
+ connection:(RKConnectionDescription *)connection
+ managedObjectCache:(id<RKManagedObjectCaching>)managedObjectCache;
///--------------------------------------------
/// @name Accessing Details About the Operation
@@ -181,16 +181,28 @@ - (id)findConnected:(BOOL *)shouldConnectRelationship
- (void)main
{
- if (self.isCancelled) return;
+ if (self.isCancelled || [self.managedObject isDeleted]) return;
NSString *relationshipName = self.connection.relationship.name;
RKLogTrace(@"Connecting relationship '%@' with mapping: %@", relationshipName, self.connection);
[self.managedObjectContext performBlockAndWait:^{
+ if (self.isCancelled || [self.managedObject isDeleted]) return;
+
BOOL shouldConnect = YES;
self.connectedValue = [self findConnected:&shouldConnect];
if (shouldConnect) {
- [self.managedObject setValue:self.connectedValue forKeyPath:relationshipName];
- RKLogDebug(@"Connected relationship '%@' to object '%@'", relationshipName, self.connectedValue);
- if (self.connectionBlock) self.connectionBlock(self, self.connectedValue);
+ @try {
+ [self.managedObject setValue:self.connectedValue forKeyPath:relationshipName];
+ RKLogDebug(@"Connected relationship '%@' to object '%@'", relationshipName, self.connectedValue);
+ if (self.connectionBlock) self.connectionBlock(self, self.connectedValue);
+ }
+ @catch (NSException *exception) {
+ if ([[exception name] isEqualToString:NSObjectInaccessibleException]) {
+ // Object has been deleted
+ RKLogDebug(@"Rescued an `NSObjectInaccessibleException` exception while attempting to establish a relationship.");
+ } else {
+ [exception raise];
+ }
+ }
}
}];
}
@@ -57,9 +57,4 @@
*/
@property (nonatomic, strong) NSSet *acceptableContentTypes;
-/**
- Whether the response received a 304 response, whether via the initial request, or by virtue of cache revalidation occurring from `NSURLCache`.
- */
-@property (nonatomic, readonly) BOOL wasNotModified;
-
@end
@@ -172,7 +172,11 @@ - (void)HTTPOperationDidFinish:(NSNotification *)notification
if ((_RKlcl_component_level[(__RKlcl_log_symbol(RKlcl_cRestKitNetwork))]) >= (__RKlcl_log_symbol(RKlcl_vTrace))) {
RKLogError(@"%@ '%@' %@:\nerror=%@\nresponse.body=%@", [operation.request HTTPMethod], [[operation.request URL] absoluteString], statusCodeAndElapsedTime, operation.error, operation.responseString);
} else {
- RKLogError(@"%@ '%@' %@: %@", [operation.request HTTPMethod], [[operation.request URL] absoluteString], statusCodeAndElapsedTime, operation.error);
+ if (operation.error.code == NSURLErrorCancelled) {
+ RKLogError(@"%@ '%@' %@: Cancelled", [operation.request HTTPMethod], [[operation.request URL] absoluteString], statusCodeAndElapsedTime);
+ } else {
+ RKLogError(@"%@ '%@' %@: %@", [operation.request HTTPMethod], [[operation.request URL] absoluteString], statusCodeAndElapsedTime, operation.error);
+ }
}
} else {
if ((_RKlcl_component_level[(__RKlcl_log_symbol(RKlcl_cRestKitNetwork))]) >= (__RKlcl_log_symbol(RKlcl_vTrace))) {
@@ -185,12 +189,12 @@ - (void)HTTPOperationDidFinish:(NSNotification *)notification
@end
-@interface AFURLConnectionOperation ()
+@interface AFURLConnectionOperation () <NSURLConnectionDataDelegate>
@property (readwrite, nonatomic, strong) NSRecursiveLock *lock;
@end
@interface RKHTTPRequestOperation ()
-@property (readwrite, nonatomic, strong) NSError *HTTPError;
+@property (readwrite, nonatomic, strong) NSError *rkHTTPError;
@end
@implementation RKHTTPRequestOperation
@@ -230,7 +234,7 @@ - (NSError *)error
{
[self.lock lock];
- if (!self.HTTPError && self.response) {
+ if (!self.rkHTTPError && self.response) {
if (![self hasAcceptableStatusCode] || ![self hasAcceptableContentType]) {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[userInfo setValue:self.responseString forKey:NSLocalizedRecoverySuggestionErrorKey];
@@ -241,25 +245,20 @@ - (NSError *)error
if (![self hasAcceptableStatusCode]) {
NSUInteger statusCode = ([self.response isKindOfClass:[NSHTTPURLResponse class]]) ? (NSUInteger)[self.response statusCode] : 200;
[userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected status code in (%@), got %d", nil), RKStringFromIndexSet(self.acceptableStatusCodes ?: [NSMutableIndexSet indexSet]), statusCode] forKey:NSLocalizedDescriptionKey];
- self.HTTPError = [[NSError alloc] initWithDomain:RKErrorDomain code:NSURLErrorBadServerResponse userInfo:userInfo];
+ self.rkHTTPError = [[NSError alloc] initWithDomain:RKErrorDomain code:NSURLErrorBadServerResponse userInfo:userInfo];
} else if (![self hasAcceptableContentType] && self.response.statusCode != 204) {
// NOTE: 204 responses come back as text/plain, which we don't want
[userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected content type %@, got %@", nil), self.acceptableContentTypes, [self.response MIMEType]] forKey:NSLocalizedDescriptionKey];
- self.HTTPError = [[NSError alloc] initWithDomain:RKErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo];
+ self.rkHTTPError = [[NSError alloc] initWithDomain:RKErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo];
}
}
}
- NSError *error = self.HTTPError ?: [super error];
+ NSError *error = self.rkHTTPError ?: [super error];
[self.lock unlock];
return error;
}
-- (BOOL)wasNotModified
-{
- return [(NSString *)[[self.response allHeaderFields] objectForKey:@"Status"] isEqualToString:@"304 Not Modified"];
-}
-
#pragma mark - NSURLConnectionDelegate methods
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
Oops, something went wrong.

0 comments on commit e0c6ed2

Please sign in to comment.