Implemented nested mapping for structures similar to the BuildBot JSON structure. fixes #112

This commit is contained in:
Blake Watters
2011-06-17 11:55:57 -04:00
parent e9e4c83630
commit 3bf4b7bc0f
18 changed files with 3342 additions and 77 deletions

View File

@@ -9,7 +9,7 @@
#import <Foundation/Foundation.h>
// Defines the rules for mapping a particular element
@interface RKObjectAttributeMapping : NSObject {
@interface RKObjectAttributeMapping : NSObject <NSCopying> {
NSString* _sourceKeyPath;
NSString* _destinationKeyPath;
}

View File

@@ -28,6 +28,11 @@
return self;
}
- (id)copyWithZone:(NSZone *)zone {
RKObjectAttributeMapping* copy = [[[self class] allocWithZone:zone] initWithSourceKeyPath:self.sourceKeyPath andDestinationKeyPath:self.destinationKeyPath];
return copy;
}
- (void)dealloc {
[_sourceKeyPath release];
[_destinationKeyPath release];

View File

@@ -134,6 +134,19 @@
NSAssert(mappableObjects != nil, @"Cannot map without an collection of mappable objects");
NSAssert(mapping != nil, @"Cannot map without a mapping to consult");
NSArray* objectsToMap = mappableObjects;
// If we have forced mapping of a dictionary, map each subdictionary
if (mapping.forceCollectionMapping && [mappableObjects isKindOfClass:[NSDictionary class]]) {
RKLogDebug(@"Collection mapping forced for NSDictionary, mapping each key/value independently...");
objectsToMap = [NSMutableArray arrayWithCapacity:[mappableObjects count]];
for (id key in mappableObjects) {
NSDictionary* dictionaryToMap = [NSDictionary dictionaryWithObject:[mappableObjects valueForKey:key] forKey:key];
[(NSMutableArray*)objectsToMap addObject:dictionaryToMap];
}
} else {
RKLogWarning(@"Collection mapping forced but mappable objects is of type '%@' rather than NSDictionary", NSStringFromClass([mappableObjects class]));
}
// Ensure we are mapping onto a mutable collection if there is a target
NSMutableArray* mappedObjects = self.targetObject ? self.targetObject : [NSMutableArray arrayWithCapacity:[mappableObjects count]];
if (NO == [mappedObjects respondsToSelector:@selector(addObject:)]) {
@@ -143,7 +156,7 @@
[self addErrorWithCode:RKObjectMapperErrorObjectMappingTypeMismatch message:errorMessage keyPath:keyPath userInfo:nil];
return nil;
}
for (id mappableObject in mappableObjects) {
for (id mappableObject in objectsToMap) {
id destinationObject = [self objectWithMapping:mapping andData:mappableObject];
BOOL success = [self mapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping];
if (success) {
@@ -226,11 +239,12 @@
RKObjectMapping* objectMapping = [keyPathsAndObjectMappings objectForKey:keyPath];
if ([self.delegate respondsToSelector:@selector(objectMapper:didFindMappableObject:atKeyPath:withMapping:)]) {
[self.delegate objectMapper:self didFindMappableObject:mappableValue atKeyPath:keyPath withMapping:objectMapping];
}
RKLogDebug(@"Found mappable data at keyPath '%@': %@", keyPath, mappableValue);
if ([mappableValue isKindOfClass:[NSArray class]] || [mappableValue isKindOfClass:[NSSet class]]) {
}
if (objectMapping.forceCollectionMapping || [mappableValue isKindOfClass:[NSArray class]] || [mappableValue isKindOfClass:[NSSet class]]) {
RKLogDebug(@"Found mappable collection at keyPath '%@': %@", keyPath, mappableValue);
mappingResult = [self mapCollection:mappableValue atKeyPath:keyPath usingMapping:objectMapping];
} else {
RKLogDebug(@"Found mappable data at keyPath '%@': %@", keyPath, mappableValue);
mappingResult = [self mapObject:mappableValue atKeyPath:keyPath usingMapping:objectMapping];
}

View File

@@ -37,6 +37,7 @@ relationship. Relationships are processed using an object mapping as well.
NSString* _rootKeyPath;
BOOL _setNilForMissingAttributes;
BOOL _setNilForMissingRelationships;
BOOL _forceCollectionMapping;
}
/**
@@ -86,6 +87,30 @@ relationship. Relationships are processed using an object mapping as well.
*/
@property (nonatomic, assign) BOOL setNilForMissingRelationships;
/**
Forces the mapper to treat the mapped keyPath as a collection even if it does not
return an array or a set of objects. This permits mapping where a dictionary identifies
a collection of objects.
When enabled, each key/value pair in the resolved dictionary will be mapped as a separate
entity. This is useful when you have a JSON structure similar to:
{ "users":
{
"blake": { "id": 1234, "email": "blake@restkit.org" },
"rachit": { "id": 5678", "email": "rachit@restkit.org" }
}
}
By enabling forceCollectionMapping, RestKit will map "blake" => attributes and
"rachit" => attributes as independent objects. This can be combined with
mapKeyOfNestedDictionaryToAttribute: to properly map these sorts of structures.
@default NO
@see mapKeyOfNestedDictionaryToAttribute
*/
@property (nonatomic, assign) BOOL forceCollectionMapping;
/**
An array of date format strings to apply when mapping a
String attribute to a NSDate property. Each format string will be applied
@@ -240,6 +265,31 @@ relationship. Relationships are processed using an object mapping as well.
*/
- (void)mapKeyPathsToAttributes:(NSString*)sourceKeyPath, ... NS_REQUIRES_NIL_TERMINATION;
/**
Configures a sub-key mapping for cases where JSON has been nested underneath a key named after an attribute.
For example, consider the following JSON:
{ "users":
{
"blake": { "id": 1234, "email": "blake@restkit.org" },
"rachit": { "id": 5678", "email": "rachit@restkit.org" }
}
}
We can configure our mappings to handle this in the following form:
RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[User class]];
mapping.forceCollectionMapping = YES; // RestKit cannot infer this is a collection, so we force it
[mapping mapKeyOfNestedDictionaryToAttribute:@"firstName"];
[mapping mapFromKeyPath:@"(firstName).id" toAttribute:"userID"];
[mapping mapFromKeyPath:@"(firstName).email" toAttribute:"email"];
[[RKObjectManager sharedManager].mappingProvider setMapping:mapping forKeyPath:@"users"];
*/
- (void)mapKeyOfNestedDictionaryToAttribute:(NSString*)attributeName;
/**
Removes all currently configured attribute and relationship mappings from the object mapping
*/

View File

@@ -9,6 +9,9 @@
#import "RKObjectMapping.h"
#import "RKObjectRelationshipMapping.h"
// Constants
NSString* const RKObjectMappingNestingAttributeKeyName = @"<RK_NESTING_ATTRIBUTE>";
@implementation RKObjectMapping
@synthesize objectClass = _objectClass;
@@ -17,6 +20,7 @@
@synthesize rootKeyPath = _rootKeyPath;
@synthesize setNilForMissingAttributes = _setNilForMissingAttributes;
@synthesize setNilForMissingRelationships = _setNilForMissingRelationships;
@synthesize forceCollectionMapping = _forceCollectionMapping;
+ (id)mappingForClass:(Class)objectClass {
RKObjectMapping* mapping = [self new];
@@ -31,6 +35,7 @@
_dateFormatStrings = [[NSMutableArray alloc] initWithObjects:@"yyyy-MM-dd'T'HH:mm:ss'Z'", @"MM/dd/yyyy", nil];
self.setNilForMissingAttributes = NO;
self.setNilForMissingRelationships = NO;
self.forceCollectionMapping = NO;
}
return self;
@@ -185,4 +190,8 @@
va_end(args);
}
- (void)mapKeyOfNestedDictionaryToAttribute:(NSString*)attributeName {
[self mapKeyPath:RKObjectMappingNestingAttributeKeyName toAttribute:attributeName];
}
@end

View File

@@ -32,6 +32,7 @@
RKObjectMapping* _objectMapping;
id<RKObjectMappingOperationDelegate> _delegate;
id<RKObjectFactory> _objectFactory;
NSDictionary* _nestedAttributeSubstitution;
}
/**

View File

@@ -19,6 +19,8 @@
#undef RKLogComponent
#define RKLogComponent lcl_cRestKitObjectMapping
extern NSString* const RKObjectMappingNestingAttributeKeyName;
@implementation RKObjectMappingOperation
@synthesize sourceObject = _sourceObject;
@@ -50,6 +52,7 @@
[_sourceObject release];
[_destinationObject release];
[_objectMapping release];
[_nestedAttributeSubstitution release];
[super dealloc];
}
@@ -115,7 +118,7 @@
// Number -> Date
if ([destinationType isSubclassOfClass:[NSDate class]]) {
return [NSDate dateWithTimeIntervalSince1970:[(NSNumber*)value intValue]];
} else if ([sourceType isSubclassOfClass:NSClassFromString(@"NSCFBoolean")] && [destinationType isSubclassOfClass:[NSString class]]) {
} else if ([sourceType isSubclassOfClass:NSClassFromString(@"__NSCFBoolean")] && [destinationType isSubclassOfClass:[NSString class]]) {
return ([value boolValue] ? @"true" : @"false");
}
} else if ([destinationType isSubclassOfClass:[NSString class]] && [value respondsToSelector:@selector(stringValue)]) {
@@ -173,11 +176,67 @@
return !isEqual;
}
- (NSArray*)applyNestingToMappings:(NSArray*)mappings {
if (_nestedAttributeSubstitution) {
NSString* searchString = [NSString stringWithFormat:@"(%@)", [[_nestedAttributeSubstitution allKeys] lastObject]];
NSString* replacementString = [[_nestedAttributeSubstitution allValues] lastObject];
NSMutableArray* array = [NSMutableArray arrayWithCapacity:[self.objectMapping.attributeMappings count]];
for (RKObjectAttributeMapping* mapping in mappings) {
RKObjectAttributeMapping* nestedMapping = [mapping copy];
nestedMapping.sourceKeyPath = [nestedMapping.sourceKeyPath stringByReplacingOccurrencesOfString:searchString withString:replacementString];
nestedMapping.destinationKeyPath = [nestedMapping.destinationKeyPath stringByReplacingOccurrencesOfString:searchString withString:replacementString];
[array addObject:nestedMapping];
}
return array;
}
return mappings;
}
- (NSArray*)attributeMappings {
return [self applyNestingToMappings:self.objectMapping.attributeMappings];
}
- (NSArray*)relationshipMappings {
return [self applyNestingToMappings:self.objectMapping.relationshipMappings];
}
- (void)applyAttributeMapping:(RKObjectAttributeMapping*)attributeMapping withValue:(id)value {
if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didFindMapping:forKeyPath:)]) {
[self.delegate objectMappingOperation:self didFindMapping:attributeMapping forKeyPath:attributeMapping.sourceKeyPath];
}
RKLogTrace(@"Mapping attribute value keyPath '%@' to '%@'", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath);
// Inspect the property type to handle any value transformations
Class type = [[RKObjectPropertyInspector sharedInspector] typeForProperty:attributeMapping.destinationKeyPath ofClass:[self.destinationObject class]];
if (type && NO == [[value class] isSubclassOfClass:type]) {
value = [self transformValue:value atKeyPath:attributeMapping.sourceKeyPath toType:type];
}
// Ensure that the value is different
if ([self shouldSetValue:value atKeyPath:attributeMapping.destinationKeyPath]) {
RKLogTrace(@"Mapped attribute value from keyPath '%@' to '%@'. Value: %@", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath, value);
[self.destinationObject setValue:value forKey:attributeMapping.destinationKeyPath];
if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didSetValue:forKeyPath:usingMapping:)]) {
[self.delegate objectMappingOperation:self didSetValue:value forKeyPath:attributeMapping.destinationKeyPath usingMapping:attributeMapping];
}
} else {
RKLogTrace(@"Skipped mapping of attribute value from keyPath '%@ to keyPath '%@' -- value is unchanged (%@)", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath, value);
}
}
// Return YES if we mapped any attributes
- (BOOL)applyAttributeMappings {
BOOL appliedMappings = NO;
// If we have a nesting substitution value, we have alread
BOOL appliedMappings = (_nestedAttributeSubstitution != nil);
for (RKObjectAttributeMapping* attributeMapping in self.objectMapping.attributeMappings) {
for (RKObjectAttributeMapping* attributeMapping in [self attributeMappings]) {
if ([attributeMapping.sourceKeyPath isEqualToString:RKObjectMappingNestingAttributeKeyName]) {
RKLogTrace(@"Skipping attribute mapping for special keyPath '%@'", RKObjectMappingNestingAttributeKeyName);
continue;
}
id value = nil;
if ([attributeMapping.sourceKeyPath isEqualToString:@""]) {
value = self.sourceObject;
@@ -186,27 +245,7 @@
}
if (value) {
appliedMappings = YES;
if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didFindMapping:forKeyPath:)]) {
[self.delegate objectMappingOperation:self didFindMapping:attributeMapping forKeyPath:attributeMapping.sourceKeyPath];
}
RKLogTrace(@"Mapping attribute value keyPath '%@' to '%@'", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath);
// Inspect the property type to handle any value transformations
Class type = [[RKObjectPropertyInspector sharedInspector] typeForProperty:attributeMapping.destinationKeyPath ofClass:[self.destinationObject class]];
if (type && NO == [[value class] isSubclassOfClass:type]) {
value = [self transformValue:value atKeyPath:attributeMapping.sourceKeyPath toType:type];
}
// Ensure that the value is different
if ([self shouldSetValue:value atKeyPath:attributeMapping.destinationKeyPath]) {
[self.destinationObject setValue:value forKey:attributeMapping.destinationKeyPath];
if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didSetValue:forKeyPath:usingMapping:)]) {
[self.delegate objectMappingOperation:self didSetValue:value forKeyPath:attributeMapping.destinationKeyPath usingMapping:attributeMapping];
}
RKLogTrace(@"Mapped attribute value from keyPath '%@' to '%@'. Value: %@", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath, value);
} else {
RKLogTrace(@"Skipped mapping of attribute value from keyPath '%@ to keyPath '%@' -- value is unchanged (%@)", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath, value);
}
[self applyAttributeMapping:attributeMapping withValue:value];
} else {
if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didNotFindMappingForKeyPath:)]) {
[self.delegate objectMappingOperation:self didNotFindMappingForKeyPath:attributeMapping.sourceKeyPath];
@@ -245,7 +284,7 @@
BOOL appliedMappings = NO;
id destinationObject = nil;
for (RKObjectRelationshipMapping* mapping in self.objectMapping.relationshipMappings) {
for (RKObjectRelationshipMapping* mapping in [self relationshipMappings]) {
id value = [self.sourceObject valueForKeyPath:mapping.sourceKeyPath];
if (value == nil || value == [NSNull null] || [value isEqual:[NSNull null]]) {
@@ -253,9 +292,8 @@
// Optionally nil out the property
if ([self.objectMapping setNilForMissingRelationships] && [self shouldSetValue:nil atKeyPath:mapping.destinationKeyPath]) {
[self.destinationObject setValue:nil forKey:mapping.destinationKeyPath];
RKLogTrace(@"Setting nil for missing relationship value at keyPath '%@'", mapping.sourceKeyPath);
[self.destinationObject setValue:nil forKey:mapping.destinationKeyPath];
}
continue;
@@ -284,6 +322,7 @@
RKLogDebug(@"Mapping one to one relationship value at keyPath '%@' to '%@'", mapping.sourceKeyPath, mapping.destinationKeyPath);
destinationObject = [self.objectFactory objectWithMapping:mapping.objectMapping andData:value];
NSAssert(destinationObject, @"Cannot map a relationship without an object factory to create it...");
if ([self mapNestedObject:value toObject:destinationObject withMapping:mapping]) {
appliedMappings = YES;
}
@@ -291,16 +330,32 @@
// If the relationship has changed, set it
if ([self shouldSetValue:destinationObject atKeyPath:mapping.destinationKeyPath]) {
[self.destinationObject setValue:destinationObject forKey:mapping.destinationKeyPath];
RKLogTrace(@"Mapped relationship object from keyPath '%@' to '%@'. Value: %@", mapping.sourceKeyPath, mapping.destinationKeyPath, destinationObject);
[self.destinationObject setValue:destinationObject forKey:mapping.destinationKeyPath];
}
}
return appliedMappings;
}
- (void)applyNestedMappings {
RKObjectAttributeMapping* attributeMapping = [self.objectMapping mappingForKeyPath:RKObjectMappingNestingAttributeKeyName];
if (attributeMapping) {
RKLogDebug(@"Found nested mapping definition to attribute '%@'", attributeMapping.destinationKeyPath);
id attributeValue = [[self.sourceObject allKeys] lastObject];
if (attributeValue) {
RKLogDebug(@"Found nesting value of '%@' for attribute '%@'", attributeValue, attributeMapping.destinationKeyPath);
_nestedAttributeSubstitution = [[NSDictionary alloc] initWithObjectsAndKeys:attributeValue, attributeMapping.destinationKeyPath, nil];
[self applyAttributeMapping:attributeMapping withValue:attributeValue];
} else {
RKLogWarning(@"Unable to find nesting value for attribute '%@'", attributeMapping.destinationKeyPath);
}
}
}
- (BOOL)performMapping:(NSError**)error {
RKLogDebug(@"Starting mapping operation...");
RKLogDebug(@"Starting mapping operation...");
[self applyNestedMappings];
BOOL mappedAttributes = [self applyAttributeMappings];
BOOL mappedRelationships = [self applyRelationshipMappings];
if (mappedAttributes || mappedRelationships) {

View File

@@ -25,6 +25,13 @@
return mapping;
}
- (id)copyWithZone:(NSZone *)zone {
RKObjectRelationshipMapping* copy = [super copyWithZone:zone];
copy.objectMapping = self.objectMapping;
copy.reversible = self.reversible;
return copy;
}
- (void)dealloc {
[_objectMapping release];
[super dealloc];