mirror of
https://github.com/zhigang1992/RestKit.git
synced 2026-04-10 18:15:25 +08:00
Implemented nested mapping for structures similar to the BuildBot JSON structure. fixes #112
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
RKObjectMapping* _objectMapping;
|
||||
id<RKObjectMappingOperationDelegate> _delegate;
|
||||
id<RKObjectFactory> _objectFactory;
|
||||
NSDictionary* _nestedAttributeSubstitution;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user