From fb6db948e195d2a14aff34189a7ff93ee354a004 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 18 Oct 2012 12:14:16 -0400 Subject: [PATCH] Implement support for using dynamic mapping during parameterization with a Request Descriptor. closes #684 --- Code/ObjectMapping/RKDynamicMapping.h | 11 ++- Code/ObjectMapping/RKDynamicMapping.m | 17 ++--- Code/ObjectMapping/RKMappingErrors.h | 3 +- Code/ObjectMapping/RKMappingOperation.m | 6 +- .../RKDynamicObjectMappingTest.m | 23 ++---- .../RKObjectMappingNextGenTest.m | 38 +++++----- .../RKObjectParameterizationTest.m | 72 +++++++++++++++++++ 7 files changed, 114 insertions(+), 56 deletions(-) diff --git a/Code/ObjectMapping/RKDynamicMapping.h b/Code/ObjectMapping/RKDynamicMapping.h index c089cc96..48d3f5cb 100644 --- a/Code/ObjectMapping/RKDynamicMapping.h +++ b/Code/ObjectMapping/RKDynamicMapping.h @@ -21,7 +21,7 @@ #import "RKMapping.h" #import "RKObjectMapping.h" -typedef RKObjectMapping *(^RKDynamicMappingDelegateBlock)(id); +typedef RKObjectMapping *(^RKDynamicMappingDelegateBlock)(id representation); /** Defines a dynamic object mapping that determines the appropriate concrete object mapping to apply at mapping time. This allows you to map very similar payloads differently depending on the type of data contained therein. @@ -37,8 +37,7 @@ typedef RKObjectMapping *(^RKDynamicMappingDelegateBlock)(id); @param block The block object to invoke to select the object mapping with which to map the given object representation. */ -- (void)setObjectMappingForDataBlock:(RKDynamicMappingDelegateBlock)block; -// TODO: Better method signature... +- (void)setObjectMappingForRepresentationBlock:(RKDynamicMappingDelegateBlock)block; /** Defines a dynamic mapping rule stating that when the value of the key property matches the specified value, the given mapping should be used to map the representation. @@ -67,11 +66,11 @@ typedef RKObjectMapping *(^RKDynamicMappingDelegateBlock)(id); ///----------------------------------------------------------------- /** - Invoked by the `RKMapperOperation` and `RKMappingOperation` to determine the appropriate `RKObjectMapping` to use when mapping the specified dictionary of mappable data. + Invoked by the `RKMapperOperation` and `RKMappingOperation` to determine the appropriate `RKObjectMapping` to use when mapping the given object representation. - @param representation A dictionary representation of the object that is being mapped. + @param representation The object representation that being mapped dynamically for which to determine the appropriate concrete mapping. @return The object mapping to be used to map the given object representation. */ -- (RKObjectMapping *)objectMappingForRepresentation:(NSDictionary *)representation; +- (RKObjectMapping *)objectMappingForRepresentation:(id)representation; @end diff --git a/Code/ObjectMapping/RKDynamicMapping.m b/Code/ObjectMapping/RKDynamicMapping.m index ea91ddfd..76f99bd6 100644 --- a/Code/ObjectMapping/RKDynamicMapping.m +++ b/Code/ObjectMapping/RKDynamicMapping.m @@ -28,7 +28,7 @@ @interface RKDynamicMapping () @property (nonatomic, strong) NSMutableArray *matchers; -@property (nonatomic, copy) RKDynamicMappingDelegateBlock objectMappingForDataBlock; +@property (nonatomic, copy) RKDynamicMappingDelegateBlock objectMappingForRepresentationBlock; @end @implementation RKDynamicMapping @@ -55,27 +55,24 @@ [_matchers addObject:matcher]; } -- (RKObjectMapping *)objectMappingForRepresentation:(NSDictionary *)data +- (RKObjectMapping *)objectMappingForRepresentation:(id)representation { - NSAssert([data isKindOfClass:[NSDictionary class]], @"Dynamic object mapping can only be performed on NSDictionary mappables, got %@", NSStringFromClass([data class])); RKObjectMapping *mapping = nil; - RKLogTrace(@"Performing dynamic object mapping for mappable data: %@", data); + RKLogTrace(@"Performing dynamic object mapping for object representation: %@", representation); // Consult the declarative matchers first for (RKDynamicMappingMatcher *matcher in _matchers) { - if ([matcher matches:data]) { + if ([matcher matches:representation]) { RKLogTrace(@"Found declarative match for matcher: %@.", matcher); return matcher.objectMapping; } } // Otherwise consult the block - if (self.objectMappingForDataBlock) { - mapping = self.objectMappingForDataBlock(data); - if (mapping) { - RKLogTrace(@"Found dynamic delegateBlock match. objectMappingForDataBlock = %@", self.objectMappingForDataBlock); - } + if (self.objectMappingForRepresentationBlock) { + mapping = self.objectMappingForRepresentationBlock(representation); + if (mapping) RKLogTrace(@"Determined concrete `RKObjectMapping` using object mapping for representation block"); } return mapping; diff --git a/Code/ObjectMapping/RKMappingErrors.h b/Code/ObjectMapping/RKMappingErrors.h index d852d374..011bcd18 100644 --- a/Code/ObjectMapping/RKMappingErrors.h +++ b/Code/ObjectMapping/RKMappingErrors.h @@ -26,7 +26,8 @@ enum { RKMappingErrorTypeMismatch = 1002, // Target class and object mapping are in disagreement RKMappingErrorUnmappableRepresentation = 1003, // No values were found at the key paths of any attribute or relationship mappings in the given representation RKMappingErrorFromMappingResult = 1004, // The error was returned from the mapping result - RKMappingErrorValidationFailure = 1005 // Generic error code for use when constructing validation errors + 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 }; 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 63c6f94f..80658923 100644 --- a/Code/ObjectMapping/RKMappingOperation.m +++ b/Code/ObjectMapping/RKMappingOperation.m @@ -623,6 +623,11 @@ static void RKSetIntermediateDictionaryValuesOnObjectForKeyPath(id object, NSStr // Determine the concrete mapping if we were initialized with a dynamic mapping if ([self.mapping isKindOfClass:[RKDynamicMapping class]]) { self.objectMapping = [(RKDynamicMapping *)self.mapping objectMappingForRepresentation:self.sourceObject]; + if (! self.objectMapping) { + NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"A dynamic mapping failed to return a concrete object mapping matching the representation being mapped." }; + self.error = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorUnableToDetermineMapping userInfo:userInfo]; + return; + } RKLogDebug(@"RKObjectMappingOperation was initialized with a dynamic mapping. Determined concrete mapping = %@", self.objectMapping); if ([self.delegate respondsToSelector:@selector(mappingOperation:didSelectObjectMapping:forDynamicMapping:)]) { @@ -631,7 +636,6 @@ static void RKSetIntermediateDictionaryValuesOnObjectForKeyPath(id object, NSStr } else if ([self.mapping isKindOfClass:[RKObjectMapping class]]) { self.objectMapping = (RKObjectMapping *)self.mapping; } - NSAssert(self.objectMapping, @"Cannot perform a mapping operation without an object mapping"); [self applyNestedMappings]; BOOL mappedSimpleAttributes = [self applyAttributeMappings:[self simpleAttributeMappings]]; diff --git a/Tests/Logic/ObjectMapping/RKDynamicObjectMappingTest.m b/Tests/Logic/ObjectMapping/RKDynamicObjectMappingTest.m index a0b08f47..7121f64b 100644 --- a/Tests/Logic/ObjectMapping/RKDynamicObjectMappingTest.m +++ b/Tests/Logic/ObjectMapping/RKDynamicObjectMappingTest.m @@ -65,19 +65,19 @@ - (void)testShouldPickTheAppropriateMappingBasedOnBlockDelegateCallback { RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - dynamicMapping.objectMappingForDataBlock = ^ RKObjectMapping *(id data) { - if ([[data valueForKey:@"type"] isEqualToString:@"Girl"]) { + [dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) { + if ([[representation valueForKey:@"type"] isEqualToString:@"Girl"]) { RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[Girl class]]; [mapping addAttributeMappingsFromArray:@[@"name"]]; return mapping; - } else if ([[data valueForKey:@"type"] isEqualToString:@"Boy"]) { + } else if ([[representation valueForKey:@"type"] isEqualToString:@"Boy"]) { RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[Boy class]]; [mapping addAttributeMappingsFromArray:@[@"name"]]; return mapping; } return nil; - }; + }]; RKObjectMapping *mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]]; assertThat(mapping, is(notNilValue())); assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Girl"))); @@ -86,21 +86,6 @@ assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); } -- (void)testShouldFailAnAssertionWhenInvokedWithSomethingOtherThanADictionary -{ - NSException *exception = nil; - RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - @try { - [dynamicMapping objectMappingForRepresentation:(NSDictionary *)[NSArray array]]; - } - @catch (NSException *e) { - exception = e; - } - @finally { - assertThat(exception, is(notNilValue())); - } -} - #pragma mark - RKDynamicMappingDelegate - (RKObjectMapping *)objectMappingForData:(id)data diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m index 5be099bd..810efafb 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m @@ -1564,10 +1564,10 @@ RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; [girlMapping addAttributeMappingsFromArray:@[@"name"]]; RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - [dynamicMapping setObjectMappingForDataBlock:^RKObjectMapping *(id mappableData) { - if ([[mappableData valueForKey:@"type"] isEqualToString:@"Boy"]) { + [dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) { + if ([[representation valueForKey:@"type"] isEqualToString:@"Boy"]) { return boyMapping; - } else if ([[mappableData valueForKey:@"type"] isEqualToString:@"Girl"]) { + } else if ([[representation valueForKey:@"type"] isEqualToString:@"Girl"]) { return girlMapping; } @@ -1668,16 +1668,16 @@ RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; [girlMapping addAttributeMappingsFromArray:@[@"name"]]; RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - dynamicMapping.objectMappingForDataBlock = ^ RKObjectMapping *(id mappableData) { - if ([[mappableData valueForKey:@"type"] isEqualToString:@"Boy"]) { + [dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) { + if ([[representation valueForKey:@"type"] isEqualToString:@"Boy"]) { return boyMapping; - } else if ([[mappableData valueForKey:@"type"] isEqualToString:@"Girl"]) { + } else if ([[representation valueForKey:@"type"] isEqualToString:@"Girl"]) { // NO GIRLS ALLOWED(*$!)(* return nil; } return nil; - }; + }]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; [mappingsDictionary setObject:dynamicMapping forKey:@""]; @@ -1699,16 +1699,16 @@ RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; [girlMapping addAttributeMappingsFromArray:@[@"name"]]; RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - dynamicMapping.objectMappingForDataBlock = ^ RKObjectMapping *(id mappableData) { - if ([[mappableData valueForKey:@"type"] isEqualToString:@"Boy"]) { + [dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) { + if ([[representation valueForKey:@"type"] isEqualToString:@"Boy"]) { return boyMapping; - } else if ([[mappableData valueForKey:@"type"] isEqualToString:@"Girl"]) { + } else if ([[representation valueForKey:@"type"] isEqualToString:@"Girl"]) { // NO GIRLS ALLOWED(*$!)(* return nil; } return nil; - }; + }]; [boyMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:dynamicMapping]];; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; @@ -1734,13 +1734,13 @@ RKObjectMapping *boyMapping = [RKObjectMapping mappingForClass:[Boy class]]; [boyMapping addAttributeMappingsFromArray:@[@"name"]]; RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - dynamicMapping.objectMappingForDataBlock = ^ RKObjectMapping *(id mappableData) { - if ([[mappableData valueForKey:@"type"] isEqualToString:@"Boy"]) { + [dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) { + if ([[representation valueForKey:@"type"] isEqualToString:@"Boy"]) { return boyMapping; } return nil; - }; + }]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; [mappingsDictionary setObject:dynamicMapping forKey:@""]; @@ -1760,9 +1760,9 @@ RKObjectMapping *boyMapping = [RKObjectMapping mappingForClass:[Boy class]]; [boyMapping addAttributeMappingsFromArray:@[@"name"]]; RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - dynamicMapping.objectMappingForDataBlock = ^ RKObjectMapping *(id mappableData) { + [dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) { return nil; - }; + }]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; [mappingsDictionary setObject:dynamicMapping forKey:@""]; @@ -1782,13 +1782,13 @@ RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; [girlMapping addAttributeMappingsFromArray:@[@"name"]]; RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - dynamicMapping.objectMappingForDataBlock = ^ RKObjectMapping *(id mappableData) { - if ([[mappableData valueForKey:@"type"] isEqualToString:@"Girl"]) { + [dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) { + if ([[representation valueForKey:@"type"] isEqualToString:@"Girl"]) { return girlMapping; } return nil; - }; + }]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; [mappingsDictionary setObject:dynamicMapping forKey:@""]; diff --git a/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m b/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m index 064ccfcf..1dd8f50f 100644 --- a/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m @@ -22,6 +22,8 @@ #import "RKObjectParameterization.h" #import "RKMIMETypeSerialization.h" #import "RKMappableObject.h" +#import "RKDynamicMapping.h" +#import "RKMappingErrors.h" @interface RKMIMETypeSerialization () @property (nonatomic, strong) NSMutableArray *registrations; @@ -338,3 +340,73 @@ } @end + +#pragma mark - Dynamic Request Paramterization + +typedef enum { + RKSearchByFlightNumberMode = 1, + RKSearchByRouteMode = 2, + RKSearcyByOtherMode +} RKFlightSearchMode; + +@interface RKDynamicParameterizationFlightSearch : NSObject +@property (nonatomic, assign) RKFlightSearchMode mode; +@property (nonatomic, copy) NSNumber *airlineID; +@property (nonatomic, copy) NSNumber *flightNumber; +@property (nonatomic, copy) NSNumber *departureAirportID; +@property (nonatomic, copy) NSNumber *arrivalAirportID; +@end + +@implementation RKDynamicParameterizationFlightSearch +@end + +@interface RKDynamicParameterizationTest : RKTestCase +@end + +@implementation RKDynamicParameterizationTest + +- (void)testParameterizationUsingDynamicMapping +{ + NSDictionary *expectedFlightNumberParameters = @{ @"flight_search": @{ @"flight_number": @1234, @"airline_id": @5678 } }; + NSDictionary *expectedRouteParameters = @{ @"flight_search": @{ @"departure_airport_id": @25, @"arrival_airport_id": @66, @"airline_id": @5678 } }; + + RKObjectMapping *flightNumberMapping = [RKObjectMapping requestMapping]; + [flightNumberMapping addAttributeMappingsFromDictionary:@{ @"flightNumber": @"flight_number", @"airlineID": @"airline_id" }]; + RKObjectMapping *routeMapping = [RKObjectMapping requestMapping]; + [routeMapping addAttributeMappingsFromDictionary:@{ @"airlineID": @"airline_id", @"departureAirportID": @"departure_airport_id", @"arrivalAirportID": @"arrival_airport_id" }]; + + RKDynamicMapping *flightSearchMapping = [RKDynamicMapping new]; + [flightSearchMapping setObjectMapping:flightNumberMapping whenValueOfKeyPath:@"mode" isEqualTo:@(RKSearchByFlightNumberMode)]; + [flightSearchMapping setObjectMapping:routeMapping whenValueOfKeyPath:@"mode" isEqualTo:@(RKSearchByRouteMode)]; + + RKDynamicParameterizationFlightSearch *flightSearch = [RKDynamicParameterizationFlightSearch new]; + flightSearch.airlineID = @5678; + flightSearch.flightNumber = @1234; + flightSearch.departureAirportID = @25; + flightSearch.arrivalAirportID = @66; + + RKRequestDescriptor *requestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:flightSearchMapping + objectClass:[RKDynamicParameterizationFlightSearch class] + rootKeyPath:@"flight_search"]; + NSError *error = nil; + NSDictionary *parameters = nil; + + // Test generation of Flight Number parameters + flightSearch.mode = RKSearchByFlightNumberMode; + parameters = [RKObjectParameterization parametersWithObject:flightSearch requestDescriptor:requestDescriptor error:&error]; + expect(parameters).to.equal(expectedFlightNumberParameters); + + // Test generation of Route paramters + flightSearch.mode = RKSearchByRouteMode; + parameters = [RKObjectParameterization parametersWithObject:flightSearch requestDescriptor:requestDescriptor error:&error]; + expect(parameters).to.equal(expectedRouteParameters); + + // Test non-match + flightSearch.mode = RKSearcyByOtherMode; + parameters = [RKObjectParameterization parametersWithObject:flightSearch requestDescriptor:requestDescriptor error:&error]; + expect(parameters).to.beNil(); + expect(error).notTo.beNil(); + expect(error.code).to.equal(RKMappingErrorUnableToDetermineMapping); +} + +@end