diff --git a/Code/CoreData/RKEntityMapping.h b/Code/CoreData/RKEntityMapping.h index a50638eb..075762e9 100644 --- a/Code/CoreData/RKEntityMapping.h +++ b/Code/CoreData/RKEntityMapping.h @@ -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 ///------------------------------------ diff --git a/Code/CoreData/RKEntityMapping.m b/Code/CoreData/RKEntityMapping.m index d1068693..dc624780 100644 --- a/Code/CoreData/RKEntityMapping.m +++ b/Code/CoreData/RKEntityMapping.m @@ -164,6 +164,7 @@ static BOOL entityIdentificationInferenceEnabled = YES; self = [self initWithClass:objectClass]; if (self) { self.entity = entity; + self.discardsInvalidObjectsOnInsert = NO; if ([RKEntityMapping isEntityIdentificationInferenceEnabled]) self.identificationAttributes = RKIdentificationAttributesInferredFromEntity(entity); } diff --git a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m index 1cb2bf20..e6484108 100644 --- a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m +++ b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m @@ -313,9 +313,10 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName; - (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]; @@ -330,11 +331,11 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName; */ NSOperationQueue *operationQueue = self.operationQueue ?: [NSOperationQueue currentQueue]; __weak NSManagedObjectContext *weakContext = [(NSManagedObject *)mappingOperation.destinationObject managedObjectContext]; - NSBlockOperation *deletionOperation = [NSBlockOperation blockOperationWithBlock:^{ + 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]; diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m index a67d274b..e4bfc9df 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m @@ -1373,6 +1373,7 @@ NSDictionary *representation = @{ @"human": @{ @"name": @"Blake Watters", @"favoriteCatID": @(12345) }, @"cat": @{ @"railsID": @(12345) } }; RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + catMapping.discardsInvalidObjectsOnInsert = YES; RKCat *cat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectContext]; RKMappingOperation *mappingOperation = [[RKMappingOperation alloc] initWithSourceObject:representation destinationObject:cat mapping:catMapping]; NSOperationQueue *operationQueue = [NSOperationQueue new]; diff --git a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m index 3b5aa8c9..beb7d19e 100644 --- a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m @@ -1042,10 +1042,11 @@ NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths); expect(fetchedObjects).to.haveCountOf(1); } -- (void)testManagedObjectRequestOperationCompletesAndIgnoresInvalidObjects +- (void)testManagedObjectRequestOperationCompletesAndIgnoresInvalidObjectsWhenDiscardsInvalidObjectsOnInsertIsYES { RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; RKEntityMapping *postMapping = [RKEntityMapping mappingForEntityForName:@"Post" inManagedObjectStore:managedObjectStore]; + postMapping.discardsInvalidObjectsOnInsert = YES; [postMapping addAttributeMappingsFromArray:@[ @"title", @"body" ]]; RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:postMapping pathPattern:nil keyPath:@"posts" statusCodes:[NSIndexSet indexSetWithIndex:200]]; @@ -1057,6 +1058,23 @@ NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths); expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); } +- (void)testManagedObjectRequestOperationFailsWithValidationErrorWhenDiscardsInvalidObjectsOnInsertIsNO +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *postMapping = [RKEntityMapping mappingForEntityForName:@"Post" inManagedObjectStore:managedObjectStore]; + postMapping.discardsInvalidObjectsOnInsert = NO; + [postMapping addAttributeMappingsFromArray:@[ @"title", @"body" ]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:postMapping pathPattern:nil keyPath:@"posts" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/posts_with_invalid.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).notTo.beNil(); + expect([managedObjectRequestOperation.error code]).to.equal(NSValidationMissingMandatoryPropertyError); + expect([managedObjectRequestOperation.error localizedDescription]).to.equal(@"The operation couldn’t be completed. (Cocoa error 1570.)"); +} + - (void)testThatSuccessfulCompletionSavesManagedObjectIfTargetObjectIsUnsavedEvenIfNoMappingWasPerformed { RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];