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

This commit is contained in:
Blake Watters
2012-12-26 17:56:39 -05:00
parent b858f2753f
commit 92458a1e88
7 changed files with 294 additions and 5 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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];

View File

@@ -20,7 +20,7 @@
#import <Foundation/Foundation.h>
@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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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