diff --git a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m index 6c1c994d..866d7005 100644 --- a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m +++ b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m @@ -27,6 +27,7 @@ #import "RKDynamicMappingMatcher.h" #import "RKManagedObjectCaching.h" #import "RKRelationshipConnectionOperation.h" +#import "RKMappingErrors.h" // Set Logging Component #undef RKLogComponent @@ -144,12 +145,20 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName; } } -- (void)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation +- (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation error:(NSError **)error { if ([mappingOperation.objectMapping isKindOfClass:[RKEntityMapping class]]) { [self emitDeadlockWarningIfNecessary]; + + NSArray *connectionMappings = [(RKEntityMapping *)mappingOperation.objectMapping connectionMappings]; + if ([connectionMappings count] > 0) { + 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; + } - for (RKConnectionMapping *connectionMapping in [(RKEntityMapping *)mappingOperation.objectMapping connectionMappings]) { + for (RKConnectionMapping *connectionMapping in connectionMappings) { RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:mappingOperation.destinationObject connectionMapping:connectionMapping managedObjectCache:self.managedObjectCache]; @@ -164,6 +173,8 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName; RKLogTrace(@"Enqueued %@ dependent upon parent operation %@ to operation queue %@", operation, self.parentOperation, operationQueue); } } + + return YES; } @end diff --git a/Code/Network/RKObjectManager.h b/Code/Network/RKObjectManager.h index f9a98972..0f762451 100644 --- a/Code/Network/RKObjectManager.h +++ b/Code/Network/RKObjectManager.h @@ -68,7 +68,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; // Now GET our article object... sending a GET to '/categories/RestKit/articles/12345' [manager getObject:article path:nil parameters:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *result) { - NSLog(@"G"); + NSLog(@"Loading mapping result: %@", result); } failure:nil]; Once a path pattern has been registered via the routing system, the manager can automatically build full request URL's when given nothing but the object to be sent. diff --git a/Code/ObjectMapping/RKMappingErrors.h b/Code/ObjectMapping/RKMappingErrors.h index ab91305d..988e6a5d 100644 --- a/Code/ObjectMapping/RKMappingErrors.h +++ b/Code/ObjectMapping/RKMappingErrors.h @@ -28,7 +28,8 @@ enum { RKMappingErrorFromMappingResult = 1004, // The error was returned from the mapping result RKMappingErrorValidationFailure = 1005, // Generic error code for use when constructing validation errors 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. + 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. }; 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 61e19291..ba371779 100644 --- a/Code/ObjectMapping/RKMappingOperation.m +++ b/Code/ObjectMapping/RKMappingOperation.m @@ -41,7 +41,6 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName; @property (nonatomic, strong, readwrite) id sourceObject; @property (nonatomic, strong, readwrite) id destinationObject; @property (nonatomic, strong) NSDictionary *nestedAttributeSubstitution; -@property (nonatomic, strong) NSError *validationError; @property (nonatomic, strong, readwrite) NSError *error; @property (nonatomic, strong, readwrite) RKObjectMapping *objectMapping; // The concrete mapping @end @@ -210,10 +209,10 @@ static BOOL RKIsManagedObject(id object) NSError *validationError; success = [self.destinationObject validateValue:value forKeyPath:keyPath error:&validationError]; if (!success) { - _validationError = validationError; - if (_validationError) { - RKLogError(@"Validation failed while mapping attribute at key path '%@' to value %@. Error: %@", keyPath, *value, [_validationError localizedDescription]); - RKLogValidationError(_validationError); + self.error = validationError; + if (validationError) { + RKLogError(@"Validation failed while mapping attribute at key path '%@' to value %@. Error: %@", keyPath, *value, [validationError localizedDescription]); + RKLogValidationError(validationError); } else { RKLogWarning(@"Destination object %@ rejected attribute value %@ for keyPath %@. Skipping...", self.destinationObject, *value, keyPath); } @@ -346,7 +345,7 @@ static BOOL RKIsManagedObject(id object) // Return YES if we mapped any attributes - (BOOL)applyAttributeMappings:(NSArray *)attributeMappings { - // If we have a nesting substitution value, we have alread + // If we have a nesting substitution value, we have already succeeded BOOL appliedMappings = (_nestedAttributeSubstitution != nil); if (!self.objectMapping.performKeyValueValidation) { @@ -402,9 +401,7 @@ static BOOL RKIsManagedObject(id object) } // Fail out if an error has occurred - if (_validationError) { - return NO; - } + if (self.error) break; } return appliedMappings; @@ -624,9 +621,7 @@ static BOOL RKIsManagedObject(id object) } // Fail out if a validation error has occurred - if (_validationError) { - return NO; - } + if (self.error) break; } return [mappingsApplied count] > 0; @@ -694,29 +689,35 @@ static BOOL RKIsManagedObject(id object) if ([self isCancelled]) return; BOOL mappedRelationships = [[self relationshipMappings] count] ? [self applyRelationshipMappings] : NO; if ([self isCancelled]) return; + // NOTE: We map key path attributes last to allow you to map across the object graphs for objects created/updated by the relationship mappings BOOL mappedKeyPathAttributes = [self applyAttributeMappings:[self keyPathAttributeMappings]]; - if ((mappedSimpleAttributes || mappedKeyPathAttributes || mappedRelationships) && _validationError == nil) { - RKLogDebug(@"Finished mapping operation successfully..."); - + + if (!mappedSimpleAttributes && !mappedRelationships && !mappedKeyPathAttributes) { + // We did not find anything to do + RKLogDebug(@"Mapping operation did not find any mappable values for the attribute and relationship mappings in the given object representation"); + NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"No mappable values found for any of the attributes or relationship mappings" }; + self.error = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorUnmappableRepresentation userInfo:userInfo]; + } + + // We did some mapping work, if there's no error let's commit our changes to the data source + if (self.error == nil) { if ([self.dataSource respondsToSelector:@selector(commitChangesForMappingOperation:)]) { - [self.dataSource commitChangesForMappingOperation:self]; + NSError *error = nil; + BOOL success = [self.dataSource commitChangesForMappingOperation:self error:&error]; + if (! success) { + self.error = error; + } } - return; } - if (self.validationError) { - // We failed out due to validation - self.error = _validationError; + if (self.error) { if ([self.delegate respondsToSelector:@selector(mappingOperation:didFailWithError:)]) { [self.delegate mappingOperation:self didFailWithError:self.error]; } RKLogError(@"Failed mapping operation: %@", [self.error localizedDescription]); } else { - // We did not find anything to do - RKLogDebug(@"Mapping operation did not find any mappable values for the attribute and relationship mappings in the given object representation"); - NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"No mappable values found for any of the attributes or relationship mappings" }; - self.error = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorUnmappableRepresentation userInfo:userInfo]; + RKLogDebug(@"Finished mapping operation successfully..."); } } diff --git a/Code/ObjectMapping/RKMappingOperationDataSource.h b/Code/ObjectMapping/RKMappingOperationDataSource.h index 7ac35ad0..de2d4267 100644 --- a/Code/ObjectMapping/RKMappingOperationDataSource.h +++ b/Code/ObjectMapping/RKMappingOperationDataSource.h @@ -51,8 +51,9 @@ Tells the data source to commit any changes to the underlying data store. @param mappingOperation The mapping operation that has completed its work. + @param error A pointer to an error to be set in the event that the mapping operation could not be committed. + @return A Boolean value indicating if the changes for the mapping operation were committed successfully. */ -// TODO: better name? -- (void)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation; +- (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation error:(NSError **)error; @end diff --git a/Code/Testing/RKMappingTest.h b/Code/Testing/RKMappingTest.h index e004d571..3b447ce9 100644 --- a/Code/Testing/RKMappingTest.h +++ b/Code/Testing/RKMappingTest.h @@ -186,9 +186,9 @@ extern NSString * const RKMappingTestExpectationErrorKey; ///------------------------- /** - The object mapping under test. + The mapping under test. Can be either an `RKObjectMapping` or `RKDynamicMapping` object. */ -@property (nonatomic, strong, readonly) RKObjectMapping *mapping; +@property (nonatomic, strong, readonly) RKMapping *mapping; /** A data source for the mapping operation. diff --git a/Code/Testing/RKMappingTest.m b/Code/Testing/RKMappingTest.m index 1ceaf837..7e7ecd9e 100644 --- a/Code/Testing/RKMappingTest.m +++ b/Code/Testing/RKMappingTest.m @@ -90,7 +90,7 @@ NSString * const RKMappingTestVerificationFailureException = @"RKMappingTestVeri ///----------------------------------------------------------------------------- @interface RKMappingTest () -@property (nonatomic, strong, readwrite) RKObjectMapping *mapping; +@property (nonatomic, strong, readwrite) RKMapping *mapping; @property (nonatomic, strong, readwrite) id sourceObject; @property (nonatomic, strong, readwrite) id destinationObject; @property (nonatomic, strong) NSMutableArray *expectations; @@ -105,12 +105,12 @@ NSString * const RKMappingTestVerificationFailureException = @"RKMappingTestVeri @implementation RKMappingTest -+ (RKMappingTest *)testForMapping:(RKObjectMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject ++ (RKMappingTest *)testForMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject { return [[self alloc] initWithMapping:mapping sourceObject:sourceObject destinationObject:destinationObject]; } -- (id)initWithMapping:(RKObjectMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject +- (id)initWithMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject { NSAssert(sourceObject != nil, @"Cannot perform a mapping operation without a sourceObject object"); NSAssert(mapping != nil, @"Cannot perform a mapping operation without a mapping"); diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m index 766a10a4..fbb7d3e5 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m @@ -13,6 +13,7 @@ #import "RKHuman.h" #import "RKManagedObjectMappingOperationDataSource.h" #import "RKMappableObject.h" +#import "RKMappingErrors.h" @interface RKManagedObjectMappingOperationDataSourceTest : RKTestCase @@ -291,4 +292,23 @@ assertThatInteger([object.railsID integerValue], is(equalToInteger(12345))); } +- (void)testThatMappingAnEntityMappingContainingAConnectionMappingWithANilManagedObjectCacheTriggersError +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHuman" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKEntityMapping *mapping = [[RKEntityMapping alloc] initWithEntity:entity]; + RKConnectionMapping *connectionMapping = [[RKConnectionMapping alloc] initWithRelationship:entity.relationshipsByName[@"favoriteCat"] sourceKeyPath:@"test" destinationKeyPath:@"test" matcher:nil]; + [mapping addConnectionMapping:connectionMapping]; + + RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:nil]; + id mockOperation = [OCMockObject mockForClass:[RKMappingOperation class]]; + [[[mockOperation stub] andReturn:mapping] objectMapping]; + NSError *error = nil; + BOOL success = [dataSource commitChangesForMappingOperation:mockOperation error:&error]; + expect(success).to.beFalsy(); + expect([error code]).to.equal(RKMappingErrorNilManagedObjectCache); + expect([error localizedDescription]).to.equal(@"Cannot map an entity mapping that contains connection mappings with a data source whose managed object cache is nil."); +} + @end diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m index 810efafb..afe560f7 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m @@ -1623,13 +1623,13 @@ RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; NSArray *objects = [mapper.mappingResult array]; - assertThat(objects, hasCountOf(2)); - assertThat([objects objectAtIndex:0], is(instanceOf([Boy class]))); - assertThat([objects objectAtIndex:1], is(instanceOf([Girl class]))); + expect(objects).to.haveCountOf(2); + expect(objects[0]).to.beInstanceOf([Boy class]); + expect(objects[1]).to.beInstanceOf([Girl class]); Boy *boy = [objects objectAtIndex:0]; Girl *girl = [objects objectAtIndex:1]; - assertThat(boy.name, is(equalTo(@"Blake Watters"))); - assertThat(girl.name, is(equalTo(@"Sarah"))); + expect(boy.name).to.equal(@"Blake Watters"); + expect(girl.name).to.equal(@"Sarah"); } - (void)testShouldMapARelationshipDynamically