From 92458a1e880bf78a01f305fbfa5a7cd8cac21ecd Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Wed, 26 Dec 2012 17:56:39 -0500 Subject: [PATCH] Enable support for mapping a relationship flexibly via assignment policies. You can now map a relationship and assign its value by setting, replacing, or unioning (combining) the relationship. closes #1073, closes #989 --- ...KManagedObjectMappingOperationDataSource.m | 28 +++ Code/ObjectMapping/RKMappingErrors.h | 3 +- Code/ObjectMapping/RKMappingOperation.m | 40 ++++ .../RKMappingOperationDataSource.h | 12 +- Code/ObjectMapping/RKRelationshipMapping.h | 28 +++ Code/ObjectMapping/RKRelationshipMapping.m | 10 + .../RKObjectMappingNextGenTest.m | 178 +++++++++++++++++- 7 files changed, 294 insertions(+), 5 deletions(-) diff --git a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m index 61591bef..3339c0ba 100644 --- a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m +++ b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m @@ -29,6 +29,8 @@ #import "RKRelationshipConnectionOperation.h" #import "RKMappingErrors.h" #import "RKValueTransformers.h" +#import "RKRelationshipMapping.h" +#import "RKObjectUtilities.h" extern NSString * const RKObjectMappingNestingAttributeKeyName; @@ -280,4 +282,30 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName; } } +- (BOOL)mappingOperation:(RKMappingOperation *)mappingOperation deleteExistingValueOfRelationshipWithMapping:(RKRelationshipMapping *)relationshipMapping error:(NSError **)error +{ + // Validate the assignment policy + if (! relationshipMapping.assignmentPolicy == RKReplaceAssignmentPolicy) { + NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Unable to satisfy deletion request: Relationship mapping was expected to have an assignment policy of `RKReplaceAssignmentPolicy`, but did not." }; + NSError *localError = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorInvalidAssignmentPolicy userInfo:userInfo]; + if (error) *error = localError; + return NO; + } + + // Delete any managed objects at the destination key path from the context + id existingValue = [mappingOperation.destinationObject valueForKeyPath:relationshipMapping.destinationKeyPath]; + if ([existingValue isKindOfClass:[NSManagedObject class]]) { + [self.managedObjectContext deleteObject:existingValue]; + } else { + if (RKObjectIsCollection(existingValue)) { + for (NSManagedObject *managedObject in existingValue) { + if (! [managedObject isKindOfClass:[NSManagedObject class]]) continue; + [self.managedObjectContext deleteObject:managedObject]; + } + } + } + + return YES; +} + @end diff --git a/Code/ObjectMapping/RKMappingErrors.h b/Code/ObjectMapping/RKMappingErrors.h index 25c9c264..794e7034 100644 --- a/Code/ObjectMapping/RKMappingErrors.h +++ b/Code/ObjectMapping/RKMappingErrors.h @@ -30,7 +30,8 @@ enum { RKMappingErrorUnableToDetermineMapping = 1006, // The mapping operation was unable to obtain a concrete object mapping from a given dynamic mapping RKMappingErrorNilDestinationObject = 1007, // The mapping operation failed due to a nil destination object. RKMappingErrorNilManagedObjectCache = 1008, // A managed object cache is required to satisfy the mapping, but none was given. - RKMappingErrorMappingDeclined = 1009 // Mapping was declined by a callback. + RKMappingErrorMappingDeclined = 1009, // Mapping was declined by a callback. + RKMappingErrorInvalidAssignmentPolicy = 1010, // The assignment policy for the relationship is invalid. }; extern NSString * const RKMappingErrorKeyPathErrorKey; // The key path the error is associated with diff --git a/Code/ObjectMapping/RKMappingOperation.m b/Code/ObjectMapping/RKMappingOperation.m index df6e2764..fb78e794 100644 --- a/Code/ObjectMapping/RKMappingOperation.m +++ b/Code/ObjectMapping/RKMappingOperation.m @@ -444,10 +444,35 @@ NSArray *RKApplyNestingAttributeValueToMappings(NSString *attributeName, id valu return YES; } +- (BOOL)applyReplaceAssignmentPolicyForRelationshipMapping:(RKRelationshipMapping *)relationshipMapping +{ + if (relationshipMapping.assignmentPolicy == RKReplaceAssignmentPolicy) { + if ([self.dataSource respondsToSelector:@selector(mappingOperation:deleteExistingValueOfRelationshipWithMapping:error:)]) { + NSError *error = nil; + BOOL success = [self.dataSource mappingOperation:self deleteExistingValueOfRelationshipWithMapping:relationshipMapping error:&error]; + if (! success) { + RKLogError(@"Failed to delete existing value of relationship mapped with RKReplaceAssignmentPolicy: %@", error); + self.error = error; + return NO; + } + } else { + RKLogWarning(@"Requested mapping with `RKReplaceAssignmentPolicy` assignment policy, but the data source does not support it. Mapping has proceeded identically to the `RKSetAssignmentPolicy`."); + } + } + + return YES; +} + - (BOOL)mapOneToOneRelationshipWithValue:(id)value mapping:(RKRelationshipMapping *)relationshipMapping { // One to one relationship RKLogDebug(@"Mapping one to one relationship value at keyPath '%@' to '%@'", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath); + + if (relationshipMapping.assignmentPolicy == RKUnionAssignmentPolicy) { + NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Invalid assignment policy: cannot union a one-to-one relationship." }; + self.error = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorInvalidAssignmentPolicy userInfo:userInfo]; + return NO; + } id destinationObject = [self destinationObjectForMappingRepresentation:value withMapping:relationshipMapping.mapping]; if (! destinationObject) { @@ -458,6 +483,10 @@ NSArray *RKApplyNestingAttributeValueToMappings(NSString *attributeName, id valu // If the relationship has changed, set it if ([self shouldSetValue:&destinationObject atKeyPath:relationshipMapping.destinationKeyPath]) { + if (! [self applyReplaceAssignmentPolicyForRelationshipMapping:relationshipMapping]) { + return NO; + } + RKLogTrace(@"Mapped relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, destinationObject); [self.destinationObject setValue:destinationObject forKey:relationshipMapping.destinationKeyPath]; } else { @@ -500,6 +529,14 @@ NSArray *RKApplyNestingAttributeValueToMappings(NSString *attributeName, id valu RKLogWarning(@"WARNING: Detected a relationship mapping for a collection containing another collection. This is probably not what you want. Consider using a KVC collection operator (such as @unionOfArrays) to flatten your mappable collection."); RKLogWarning(@"Key path '%@' yielded collection containing another collection rather than a collection of objects: %@", relationshipMapping.sourceKeyPath, value); } + + if (relationshipMapping.assignmentPolicy == RKUnionAssignmentPolicy) { + RKLogDebug(@"Mapping relationship with union assignment policy: constructing combined relationship value."); + id existingObjects = [self.destinationObject valueForKeyPath:relationshipMapping.destinationKeyPath]; + NSArray *existingObjectsArray = RKTransformedValueWithClass(existingObjects, [NSArray class], nil); + [relationshipCollection addObjectsFromArray:existingObjectsArray]; + } + for (id nestedObject in value) { id mappableObject = [self destinationObjectForMappingRepresentation:nestedObject withMapping:relationshipMapping.mapping]; if (! mappableObject) { @@ -520,6 +557,9 @@ NSArray *RKApplyNestingAttributeValueToMappings(NSString *attributeName, id valu // If the relationship has changed, set it if ([self shouldSetValue:&valueForRelationship atKeyPath:relationshipMapping.destinationKeyPath]) { + if (! [self applyReplaceAssignmentPolicyForRelationshipMapping:relationshipMapping]) { + return NO; + } if (! [self mapCoreDataToManyRelationshipValue:valueForRelationship withMapping:relationshipMapping]) { RKLogTrace(@"Mapped relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, valueForRelationship); [self.destinationObject setValue:valueForRelationship forKeyPath:relationshipMapping.destinationKeyPath]; diff --git a/Code/ObjectMapping/RKMappingOperationDataSource.h b/Code/ObjectMapping/RKMappingOperationDataSource.h index de2d4267..df9ed009 100644 --- a/Code/ObjectMapping/RKMappingOperationDataSource.h +++ b/Code/ObjectMapping/RKMappingOperationDataSource.h @@ -20,7 +20,7 @@ #import -@class RKObjectMapping, RKMappingOperation; +@class RKObjectMapping, RKMappingOperation, RKRelationshipMapping; /** An object that adopts the `RKMappingOperationDataSource` protocol is responsible for the retrieval or creation of target objects within an `RKMapperOperation` or `RKMappingOperation`. A data source is responsible for meeting the requirements of the underlying data store implementation and must return a key-value coding compliant object instance that can be used as the target object of a mapping operation. It is also responsible for commiting any changes necessary to the underlying data store once a mapping operation has completed its work. @@ -56,4 +56,14 @@ */ - (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation error:(NSError **)error; +/** + Tells the data source to delete the existing value for a relationship that has been mapped with an assignment policy of `RKReplaceAssignmentPolicy`. + + @param mappingOperation The mapping operation that is executing. + @param relationshipMapping The relationship mapping for which the existing value is being replaced. + @param error A pointer to an error to be set in the event that the deletion operation could not be completed. + @return A Boolean value indicating if the existing objects for the relationship were successfully deleted. + */ +- (BOOL)mappingOperation:(RKMappingOperation *)mappingOperation deleteExistingValueOfRelationshipWithMapping:(RKRelationshipMapping *)relationshipMapping error:(NSError **)error; + @end diff --git a/Code/ObjectMapping/RKRelationshipMapping.h b/Code/ObjectMapping/RKRelationshipMapping.h index 0f0b96b4..73ff8b97 100644 --- a/Code/ObjectMapping/RKRelationshipMapping.h +++ b/Code/ObjectMapping/RKRelationshipMapping.h @@ -22,6 +22,12 @@ @class RKMapping; +typedef enum { + RKSetAssignmentPolicy, // Set the relationship to the new value and leave the existing objects alone, breaking the relationship to existing objects at the destination. This is the default policy for `RKRelationshipMapping`. + RKReplaceAssignmentPolicy, // Set the relationship to the new value and destroy the previous value, replacing the existing objects at the destination of the relationship. + RKUnionAssignmentPolicy, // Set the relationship to the union of the existing value and the new value being assigned. Only applicable for to-many relationships. +} RKAssignmentPolicy; + /** The `RKRelationshipMapping` class is used to describe relationships of a class in an `RKObjectMapping` or an entity in an `RKEntityMapping` object. @@ -32,6 +38,15 @@ ## Mapping a Non-nested Relationship from the Parent Representation It can often be desirable to map data for a relationship directly from the parent object representation, rather than under a nested key path. When a relationship mapping is constructed with a `nil` value for the source key path, then the `RKMapping` object is evaluated against the parent representation. + + ## Assignment Policy + + When mapping a relationship, the typical desired behavior is to set the destination of the relationship to the newly mapped values from the object representation being processed. There are times in which it is desirable to use different assignment behaviors. The way in which the relationship is assigned can be controlled by the assignmentPolicy property. There are currently three distinct assignment policies available: + + 1. `RKSetAssignmentPolicy` - Instructs the mapper to assign the new destination value to the relationship directly. No further action is taken and the relationship to the old objects is broken. This is the default assignment policy. + 1. `RKReplaceAssignmentPolicy` - Instructs the mapper to assign the new destination value to the relationship and delete any existing object or objects at the destination. The deletion behavior is contextual based on the type of objects being mapped (i.e. Core Data vs NSObject) and is delegated to the mapping operation data source. + 1. `RKUnionAssignmentPolicy` - Instructs the mapper to build a new value for the relationship by unioning the existing value with the new value and set the combined value to the relationship. The union assignment policy is only appropriate for use with a to-many relationship. + */ @interface RKRelationshipMapping : RKPropertyMapping @@ -59,4 +74,17 @@ */ @property (nonatomic, strong, readonly) RKMapping *mapping; +///---------------------------------------- +/// @name Configuring the Assignment Policy +///---------------------------------------- + +/** + The assignment policy to use when applying the relationship mapping. + + The assignment policy determines how a relationship is set when there are existing objects at the destination of the relationship. The existing values can be disconnected from the parent and left in the graph (`RKSetAssignmentPolicy`), deleted and replaced by the new value (`RKReplaceAssignmentPolicy`), or the new value can be unioned with the existing objects to create a new combined value (`RKUnionAssignmentPolicy`). + + **Default**: `RKSetAssignmentPolicy` + */ +@property (nonatomic, assign) RKAssignmentPolicy assignmentPolicy; + @end diff --git a/Code/ObjectMapping/RKRelationshipMapping.m b/Code/ObjectMapping/RKRelationshipMapping.m index cd909251..6d69512d 100644 --- a/Code/ObjectMapping/RKRelationshipMapping.m +++ b/Code/ObjectMapping/RKRelationshipMapping.m @@ -38,10 +38,20 @@ return relationshipMapping; } +- (id)init +{ + self = [super init]; + if (self) { + self.assignmentPolicy = RKSetAssignmentPolicy; + } + return self; +} + - (id)copyWithZone:(NSZone *)zone { RKRelationshipMapping *copy = [super copyWithZone:zone]; copy.mapping = self.mapping; + copy.assignmentPolicy = self.assignmentPolicy; return copy; } diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m index 0504747f..f458c40e 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m @@ -1709,10 +1709,182 @@ [mockUser verify]; } -- (void)testAppendingLoadedObjectsOntoRelationshipInsteadOfReplacing +#pragma mark Assignment Policies + +- (void)testThatAttemptingToUnionOneToOneRelationshipGeneratesMappingError { - // Behaviors: Replace, Append, Nullify. AssignmentPolicy - // Load the + RKTestUser *user = [RKTestUser new]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + RKObjectMapping *addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"address" toKeyPath:@"address" withMapping:addressMapping]; + relationshipMapping.assignmentPolicy = RKUnionAssignmentPolicy; + [mapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"address": @{ @"city": @"NYC" } }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect(error).notTo.beNil(); + expect(error.code).to.equal(RKMappingErrorInvalidAssignmentPolicy); + expect([error localizedDescription]).to.equal(@"Invalid assignment policy: cannot union a one-to-one relationship."); +} + +- (void)testUnionAssignmentPolicyWithSet +{ + RKTestUser *user = [RKTestUser new]; + RKTestUser *friend = [RKTestUser new]; + friend.name = @"Jeff"; + user.friendsSet = [NSSet setWithObject:friend]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"friendsSet" toKeyPath:@"friendsSet" withMapping:mapping]; + relationshipMapping.assignmentPolicy = RKUnionAssignmentPolicy; + [mapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"friendsSet": @[ @{ @"name": @"Zach" } ] }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect([user.friendsSet count]).to.equal(2); + NSArray *names = [user.friendsSet valueForKey:@"name"]; + assertThat(names, hasItems(@"Jeff", @"Zach", nil)); +} + +- (void)testUnionAssignmentPolicyWithOrderedSet +{ + RKTestUser *user = [RKTestUser new]; + RKTestUser *friend = [RKTestUser new]; + friend.name = @"Jeff"; + user.friendsOrderedSet = [NSOrderedSet orderedSetWithObject:friend]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"friendsOrderedSet" toKeyPath:@"friendsOrderedSet" withMapping:mapping]; + relationshipMapping.assignmentPolicy = RKUnionAssignmentPolicy; + [mapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"friendsOrderedSet": @[ @{ @"name": @"Zach" } ] }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect([user.friendsOrderedSet count]).to.equal(2); + NSArray *names = [user.friendsOrderedSet valueForKey:@"name"]; + assertThat(names, hasItems(@"Jeff", @"Zach", nil)); +} + +- (void)testUnionAssignmentPolicyWithArray +{ + RKTestUser *user = [RKTestUser new]; + RKTestUser *friend = [RKTestUser new]; + friend.name = @"Jeff"; + user.friends = [NSArray arrayWithObject:friend]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:mapping]; + relationshipMapping.assignmentPolicy = RKUnionAssignmentPolicy; + [mapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"friends": @[ @{ @"name": @"Zach" } ] }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect([user.friends count]).to.equal(2); + NSArray *names = [user.friends valueForKey:@"name"]; + assertThat(names, hasItems(@"Jeff", @"Zach", nil)); +} + +- (void)testReplacementPolicyForUnmanagedRelationship +{ + RKTestUser *user = [RKTestUser new]; + RKTestUser *friend = [RKTestUser new]; + friend.name = @"Jeff"; + user.friends = [NSArray arrayWithObject:friend]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:mapping]; + relationshipMapping.assignmentPolicy = RKReplaceAssignmentPolicy; + [mapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"friends": @[ @{ @"name": @"Zach" } ] }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect([user.friends count]).to.equal(1); + NSArray *names = [user.friends valueForKey:@"name"]; + assertThat(names, hasItems(@"Zach", nil)); +} + +- (void)testReplacmentPolicyForToManyCoreDataRelationshipDeletesExistingValues +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext]; + RKCat *existingCat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext]; + existingCat.name = @"Lola"; + human.cats = [NSSet setWithObject:existingCat]; + + RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + [entityMapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + [catMapping addAttributeMappingsFromArray:@[ @"name" ]]; + catMapping.identificationAttributes = @[ @"name" ]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"cats" toKeyPath:@"cats" withMapping:catMapping]; + relationshipMapping.assignmentPolicy = RKReplaceAssignmentPolicy; + [entityMapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"name": @"Blake", @"cats": @[ @{ @"name": @"Roy" } ] }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:human mapping:entityMapping]; + RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext cache:nil]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect([human.cats count]).to.equal(1); + NSArray *names = [human.cats valueForKey:@"name"]; + assertThat(names, hasItems(@"Roy", nil)); + expect([existingCat isDeleted]).to.equal(YES); +} + +- (void)testReplacmentPolicyForToOneCoreDataRelationshipDeletesExistingValues +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext]; + RKCat *existingCat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext]; + existingCat.name = @"Lola"; + human.favoriteCat = existingCat; + + RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + [entityMapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + [catMapping addAttributeMappingsFromArray:@[ @"name" ]]; + catMapping.identificationAttributes = @[ @"name" ]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"favoriteCat" toKeyPath:@"favoriteCat" withMapping:catMapping]; + relationshipMapping.assignmentPolicy = RKReplaceAssignmentPolicy; + [entityMapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"name": @"Blake", @"favoriteCat": @{ @"name": @"Roy" } }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:human mapping:entityMapping]; + RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext cache:nil]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect(human.favoriteCat.name).to.equal(@"Roy"); + expect([existingCat isDeleted]).to.equal(YES); } #pragma mark - RKDynamicMapping