Skip to content

Commit

Permalink
Add support for transforming source to destination key paths using a …
Browse files Browse the repository at this point in the history
…block. This enables one to DRY up mapping configuration.
  • Loading branch information
blakewatters committed Nov 19, 2012
1 parent 0aeb5f6 commit 36c6060
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 6 deletions.
60 changes: 58 additions & 2 deletions Code/ObjectMapping/RKObjectMapping.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,37 @@
/**
An `RKObjectMapping` object describes a transformation between object represenations using key-value coding and run-time type introspection. The mapping is defined in terms of a source object class and a collection of `RKPropertyMapping` objects describing how key paths in the source representation should be transformed into attributes and relationships on the target object. Object mappings are provided to instances of `RKMapperOperation` and `RKMappingOperation` to perform the transformations they describe.
Object mappings are containers of property mappings that describe the actual key path transformations. There are two types of property mappings:
Object mappings are containers of property mappings that describe the actual key path transformations. There are three types of property mappings:
1. `RKAttributeMapping`: An attribute mapping describes a transformation between a single value from a source key path to a destination key path. The value to be mapped is read from the source object representation using `valueForKeyPath:` and then set to the destination key path using `setValueForKeyPath:`. Before the value is set, the `RKObjecMappingOperation` performing the mapping performs runtime introspection on the destination property to determine what, if any, type transformation is to be performed. Typical type transformations include reading an `NSString` value representation and mapping it to an `NSDecimalNumber` destination key path or reading an `NSString` and transforming it into an `NSDate` value before assigning to the destination.
1. `RKRelationshipMapping`: A relationship mapping describes a transformation between a nested child object or objects from a source key path to a destination key path using another `RKObjectMapping`. The child objects to be mapped are read from the source object representation using `valueForKeyPath:`, then mapped recursively using the object mapping associated with the relationship mapping, and then finally assigned to the destination key path. Before assignment to the destination key path runtime type introspection is performed to determine if any type transformation is necessary. For relationship mappings, common type transformations include transforming a single object value in an `NSArray` or transforming an `NSArray` of object values into an `NSSet`.
1. `RKConnectionMapping`: A connection mapping describes how to establish a relationship within a Core Data data model using foreign keys transmitted within the mapped response body.
All type transformations available are discussed in detail in the documentation for `RKObjectMappingOperation`.
All type transformations available are discussed in detail in the documentation for `RKMappingOperation`.
## Transforming Representation to Property Keys
Configuring object mappings can become quite repetitive if the keys in your serialized object representations follow a different convention than their local domain counterparts. For example, consider a typical JSON document in the "snake case" format:
{"user": {"first_name": "Blake", "last_name": "Watters", "email_address": "[email protected]"}}
Typically when configuring a mapping for the object represented in this document we would transform the destination properties into the Objective-C idiomatic "llama case" variation. This can produce lengthy, error-prone mapping configurations in which the transformations are specified manually:
RKObjectMapping *userMapping = [RKObjectMapping mappingForClass:[RKUser class]];
[userMapping addAttributeMappingsFromDictionary:@{ @"first_name": @"firstName", @"last_name": @"lastName", @"email_address", @"emailAddress" }];
To combat this repetition, a block can be designated to perform a transformation on source keys to produce corresponding destination keys:
[userMapping setDefaultSourceToDestinationKeyTransformationBlock:^NSString *(NSString *sourceKey) {
// Value transformer compliments of TransformerKit (See https://github.com/mattt/TransformerKit)
return [[NSValueTransformer valueTransformerForName:TKLlamaCaseStringTransformerName] transformedValue:key];
}];
With the block configured, the original configuration can be changed into a simpler array based invocation:
[userMapping addAttributeMappingsFromArray:@[ @"first_name", @"last_name", @"email_address" ]];
Transformation blocks can be configured on a per-mapping basis or globally via `[RKObjectMapping setDefaultSourceToDestinationKeyTransformationBlock:]`.
@see `RKAttributeMapping`
@see `RKRelationshipMapping`
Expand Down Expand Up @@ -142,6 +167,37 @@
*/
- (void)addAttributeMappingsFromArray:(NSArray *)arrayOfAttributeNamesOrMappings;

/**
Adds a relationship mapping to the receiver with the given source key path and mapping.
The
*/
- (void)addRelationshipMappingWithSourceKeyPath:(NSString *)sourceKeyPath mapping:(RKMapping *)mapping;

///-------------------------------------
/// @name Configuring Key Transformation
///-------------------------------------

/**
Sets an application-wide default transformation block to be used when attribute or relationship mappings are added to an object mapping by source key path.
@param block The block to be set as the default source to destination key transformer for all object mappings in the application.
@see [RKObjectMapping setPropertyNameTransformationBlock:]
*/
+ (void)setDefaultSourceToDestinationKeyTransformationBlock:(NSString * (^)(NSString *sourceKey))block;

/**
Sets a block to executed to transform a source key into a destination key.
The transformation block set with this method is used whenever an attribute or relationship mapping is added to the receiver via a method that accepts a string value for the source key. The block will be executed with the source key as the only argument and the value returned will be taken as the corresponding destination key. Methods on the `RKObjectMapping` class that will trigger the execution of the block configured via this method include:
* `addAttributeMappingsFromArray:` - Each string element contained in the given array is interpretted as a source key path and will be evaluated with the block to obtain a corresponding destination key path.
* `addRelationshipMappingWithSourceKeyPath:mapping:` - The source key path will be evaluated with the block to obtain a corresponding destination key path.
@param block The block to execute when the receiver needs to transform a source key into a destination key. The block has a string return value specifying the destination key and accepts a single string argument: the source key that is to be transformed.
@warning Please note that the block given accepts a **key** as opposed to a **key path**. When a key path is given to a method supporting key transformation it will be decomposed into its key components by splitting the key path at the '.' (period) character, then each key will be evaluated using the transformation block and the results will be joined together into a new key path with the period character delimiter.
*/
- (void)setSourceToDestinationKeyTransformationBlock:(NSString * (^)(NSString *sourceKey))block;

///----------------------------------
/// @name Mapping Nested Dictionaries
///----------------------------------
Expand Down
41 changes: 37 additions & 4 deletions Code/ObjectMapping/RKObjectMapping.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@
// Private declaration
NSDate *RKDateFromStringWithFormatters(NSString *dateString, NSArray *formatters);

typedef NSString * (^RKSourceToDesinationKeyTransformationBlock)(NSString *sourceKey);

static RKSourceToDesinationKeyTransformationBlock defaultSourceToDestinationKeyTransformationBlock = nil;

// Evaluate each component individually so that camelization, etc. considers each component individually
static NSString *RKDestinationKeyPathFromTransformationBlockWithSourceKeyPath(RKSourceToDesinationKeyTransformationBlock block, NSString *keyPath)
{
NSArray *components = [keyPath componentsSeparatedByString:@"."];
NSMutableArray *mutableComponents = [NSMutableArray arrayWithCapacity:[components count]];
[components enumerateObjectsUsingBlock:^(id component, NSUInteger idx, BOOL *stop) {
[mutableComponents addObject:block(component)];
}];

return [mutableComponents componentsJoinedByString:@"."];
}

@interface RKPropertyMapping ()
@property (nonatomic, weak, readwrite) RKObjectMapping *objectMapping;
@end
Expand All @@ -41,7 +57,8 @@ @interface RKObjectMapping ()
@property (nonatomic, weak, readwrite) Class objectClass;
@property (nonatomic, strong) NSMutableArray *mutablePropertyMappings;

@property (weak, nonatomic, readonly) NSArray *mappedKeyPaths;
@property (nonatomic, weak, readonly) NSArray *mappedKeyPaths;
@property (nonatomic, copy) RKSourceToDesinationKeyTransformationBlock sourceToDestinationKeyTransformationBlock;
@end

@implementation RKObjectMapping
Expand All @@ -67,6 +84,7 @@ - (id)initWithClass:(Class)objectClass
self.forceCollectionMapping = NO;
self.performKeyValueValidation = YES;
self.ignoreUnknownKeyPaths = NO;
self.sourceToDestinationKeyTransformationBlock = defaultSourceToDestinationKeyTransformationBlock;
}

return self;
Expand All @@ -83,6 +101,7 @@ - (id)copyWithZone:(NSZone *)zone
copy.dateFormatters = self.dateFormatters;
copy.preferredDateFormatter = self.preferredDateFormatter;
copy.mutablePropertyMappings = [NSMutableArray new];
copy.sourceToDestinationKeyTransformationBlock = self.sourceToDestinationKeyTransformationBlock;

for (RKPropertyMapping *propertyMapping in self.propertyMappings) {
[copy addPropertyMapping:propertyMapping];
Expand All @@ -91,6 +110,11 @@ - (id)copyWithZone:(NSZone *)zone
return copy;
}

+ (void)setDefaultSourceToDestinationKeyTransformationBlock:(NSString * (^)(NSString *sourceKey))block
{
defaultSourceToDestinationKeyTransformationBlock = block;
}

- (NSArray *)propertyMappings
{
return [NSArray arrayWithArray:_mutablePropertyMappings];
Expand Down Expand Up @@ -208,7 +232,8 @@ - (void)addAttributeMappingsFromArray:(NSArray *)arrayOfAttributeNamesOrMappings
NSMutableArray *arrayOfAttributeMappings = [NSMutableArray arrayWithCapacity:[arrayOfAttributeNamesOrMappings count]];
for (id entry in arrayOfAttributeNamesOrMappings) {
if ([entry isKindOfClass:[NSString class]]) {
[arrayOfAttributeMappings addObject:[RKAttributeMapping attributeMappingFromKeyPath:entry toKeyPath:entry]];
NSString *destinationKeyPath = self.sourceToDestinationKeyTransformationBlock ? RKDestinationKeyPathFromTransformationBlockWithSourceKeyPath(self.sourceToDestinationKeyTransformationBlock, entry) : entry;
[arrayOfAttributeMappings addObject:[RKAttributeMapping attributeMappingFromKeyPath:entry toKeyPath:destinationKeyPath]];
} else if ([entry isKindOfClass:[RKAttributeMapping class]]) {
[arrayOfAttributeMappings addObject:entry];
} else {
Expand All @@ -220,6 +245,16 @@ - (void)addAttributeMappingsFromArray:(NSArray *)arrayOfAttributeNamesOrMappings
[self addPropertyMappingsFromArray:arrayOfAttributeMappings];
}

- (void)addRelationshipMappingWithSourceKeyPath:(NSString *)sourceKeyPath mapping:(RKMapping *)mapping
{
NSParameterAssert(sourceKeyPath);
NSParameterAssert(mapping);

NSString *destinationKeyPath = self.sourceToDestinationKeyTransformationBlock ? RKDestinationKeyPathFromTransformationBlockWithSourceKeyPath(self.sourceToDestinationKeyTransformationBlock, sourceKeyPath) : sourceKeyPath;
RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:sourceKeyPath toKeyPath:destinationKeyPath withMapping:mapping];
[self addPropertyMapping:relationshipMapping];
}

- (void)removePropertyMapping:(RKPropertyMapping *)attributeOrRelationshipMapping
{
if ([self.mutablePropertyMappings containsObject:attributeOrRelationshipMapping]) {
Expand All @@ -237,14 +272,12 @@ - (RKObjectMapping *)inverseMappingAtDepth:(NSInteger)depth
}

for (RKRelationshipMapping *relationshipMapping in self.relationshipMappings) {
// if (relationshipMapping.reversible) {
RKMapping *mapping = relationshipMapping.mapping;
if (! [mapping isKindOfClass:[RKObjectMapping class]]) {
RKLogWarning(@"Unable to generate inverse mapping for relationship '%@': %@ relationships cannot be inversed.", relationshipMapping.sourceKeyPath, NSStringFromClass([mapping class]));
continue;
}
[inverseMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relationshipMapping.destinationKeyPath toKeyPath:relationshipMapping.sourceKeyPath withMapping:[(RKObjectMapping *)mapping inverseMappingAtDepth:depth+1]]];
// }
}

return inverseMapping;
Expand Down
54 changes: 54 additions & 0 deletions Tests/Logic/ObjectMapping/RKObjectMappingTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ @interface RKObjectMappingTest : RKTestCase

@implementation RKObjectMappingTest

- (void)tearDown
{
[RKObjectMapping setDefaultSourceToDestinationKeyTransformationBlock:nil];
}

- (void)testThatTwoMappingsWithTheSameAttributeMappingsButDifferentObjectClassesAreNotConsideredEqual
{
RKObjectMapping *mapping1 = [RKObjectMapping mappingForClass:[NSString class]];
Expand Down Expand Up @@ -165,4 +170,53 @@ - (void)testThatAddingAnArrayOfAttributeMappingsThatExistInAnotherMappingTrigger
}
}

#pragma mark - Key Transformations

- (void)testPropertyNameTransformationBlockForAttributes
{
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]];
[mapping setSourceToDestinationKeyTransformationBlock:^NSString *(NSString *sourceKey) {
return [sourceKey uppercaseString];
}];
[mapping addAttributeMappingsFromArray:@[ @"name", @"rank" ]];
NSArray *expectedNames = @[ @"NAME", @"RANK" ];
expect([mapping.propertyMappingsByDestinationKeyPath allKeys]).to.equal(expectedNames);
}

- (void)testPropertyNameTransformationBlockForRelationships
{
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]];
[mapping setSourceToDestinationKeyTransformationBlock:^NSString *(NSString *sourceKey) {
return [sourceKey uppercaseString];
}];
RKObjectMapping *relatedMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]];
[mapping addRelationshipMappingWithSourceKeyPath:@"something" mapping:relatedMapping];
RKRelationshipMapping *relationshipMapping = [mapping propertyMappingsByDestinationKeyPath][@"SOMETHING"];
expect(relationshipMapping).notTo.beNil();
expect(relationshipMapping.sourceKeyPath).to.equal(@"something");
expect(relationshipMapping.destinationKeyPath).to.equal(@"SOMETHING");
}

- (void)testTransformationOfAttributeKeyPaths
{
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]];
[mapping setSourceToDestinationKeyTransformationBlock:^NSString *(NSString *sourceKey) {
return [sourceKey capitalizedString];
}];
[mapping addAttributeMappingsFromArray:@[ @"user.comments" ]];
NSArray *expectedNames = @[ @"User.Comments" ];
expect([mapping.propertyMappingsByDestinationKeyPath allKeys]).to.equal(expectedNames);
}

- (void)testDefaultSourceToDestinationKeyTransformationBlock
{
[RKObjectMapping setDefaultSourceToDestinationKeyTransformationBlock:^NSString *(NSString *sourceKey) {
return [sourceKey capitalizedString];
}];
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]];
[mapping addAttributeMappingsFromArray:@[ @"user.comments" ]];
NSArray *expectedNames = @[ @"User.Comments" ];
expect([mapping.propertyMappingsByDestinationKeyPath allKeys]).to.equal(expectedNames);
}

@end

0 comments on commit 36c6060

Please sign in to comment.