diff --git a/Code/ObjectMapping/RKObjectMapping.h b/Code/ObjectMapping/RKObjectMapping.h index d4b735e9..e6f0cefe 100644 --- a/Code/ObjectMapping/RKObjectMapping.h +++ b/Code/ObjectMapping/RKObjectMapping.h @@ -294,9 +294,11 @@ @property (nonatomic, strong) NSFormatter *preferredDateFormatter; /** - Generates an inverse mapping for the rules specified within this object mapping. This can be used to - quickly generate a corresponding serialization mapping from a configured object mapping. The inverse - mapping will have the source and destination keyPaths swapped for all attribute and relationship mappings. + Generates an inverse mapping for the rules specified within this object mapping. + + This can be used to quickly generate a corresponding serialization mapping from a configured object mapping. The inverse mapping will have the source and destination keyPaths swapped for all attribute and relationship mappings. All mapping configuration and date formatters are copied from the parent to the inverse mapping. + + @return A new mapping that will map the inverse of the receiver. */ - (instancetype)inverseMapping; diff --git a/Code/ObjectMapping/RKObjectMapping.m b/Code/ObjectMapping/RKObjectMapping.m index 8eb74ed6..15e8a0ac 100644 --- a/Code/ObjectMapping/RKObjectMapping.m +++ b/Code/ObjectMapping/RKObjectMapping.m @@ -18,6 +18,7 @@ // limitations under the License. // +#import #import "RKObjectMapping.h" #import "RKRelationshipMapping.h" #import "RKPropertyInspector.h" @@ -30,13 +31,71 @@ typedef NSString * (^RKSourceToDesinationKeyTransformationBlock)(RKObjectMapping // Constants NSString * const RKObjectMappingNestingAttributeKeyName = @""; -static NSUInteger RKObjectMappingMaximumInverseMappingRecursionDepth = 100; // Private declaration NSDate *RKDateFromStringWithFormatters(NSString *dateString, NSArray *formatters); static RKSourceToDesinationKeyTransformationBlock defaultSourceToDestinationKeyTransformationBlock = nil; +@interface RKObjectMapping (Copying) +- (void)copyPropertiesFromMapping:(RKObjectMapping *)mapping; +@end + +@interface RKMappingInverter : NSObject +@property (nonatomic, strong) RKObjectMapping *mapping; +@property (nonatomic, strong) NSMutableDictionary *invertedMappings; + +- (id)initWithMapping:(RKObjectMapping *)mapping; +- (RKObjectMapping *)inverseMapping; +@end + +@implementation RKMappingInverter + +- (id)initWithMapping:(RKObjectMapping *)mapping +{ + self = [self init]; + if (self) { + self.mapping = mapping; + self.invertedMappings = [NSMutableDictionary dictionary]; + } + return self; +} + +- (RKObjectMapping *)invertMapping:(RKObjectMapping *)mapping +{ + // Use an NSValue to obtain a non-copied key into our inversed mappings dictionary + NSValue *dictionaryKey = [NSValue valueWithNonretainedObject:mapping]; + RKObjectMapping *inverseMapping = [self.invertedMappings objectForKey:dictionaryKey]; + if (inverseMapping) return inverseMapping; + + inverseMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + [self.invertedMappings setObject:inverseMapping forKey:dictionaryKey]; + [inverseMapping copyPropertiesFromMapping:mapping]; + + for (RKAttributeMapping *attributeMapping in mapping.attributeMappings) { + [inverseMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:attributeMapping.destinationKeyPath toKeyPath:attributeMapping.sourceKeyPath]]; + } + + for (RKRelationshipMapping *relationshipMapping in mapping.relationshipMappings) { + RKObjectMapping *mapping = (RKObjectMapping *) 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; + } + RKMapping *inverseRelationshipMapping = [self invertMapping:mapping]; + if (inverseRelationshipMapping) [inverseMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relationshipMapping.destinationKeyPath toKeyPath:relationshipMapping.sourceKeyPath withMapping:inverseRelationshipMapping]]; + } + + return inverseMapping; +} + +- (RKObjectMapping *)inverseMapping +{ + return [self invertMapping:self.mapping]; +} + +@end + @interface RKPropertyMapping () @property (nonatomic, weak, readwrite) RKObjectMapping *objectMapping; @end @@ -77,21 +136,26 @@ static RKSourceToDesinationKeyTransformationBlock defaultSourceToDestinationKeyT return self; } +- (void)copyPropertiesFromMapping:(RKObjectMapping *)mapping +{ + self.setDefaultValueForMissingAttributes = mapping.setDefaultValueForMissingAttributes; + self.setNilForMissingRelationships = mapping.setNilForMissingRelationships; + self.forceCollectionMapping = mapping.forceCollectionMapping; + self.performKeyValueValidation = mapping.performKeyValueValidation; + self.dateFormatters = mapping.dateFormatters; + self.preferredDateFormatter = mapping.preferredDateFormatter; + self.sourceToDestinationKeyTransformationBlock = self.sourceToDestinationKeyTransformationBlock; +} + - (id)copyWithZone:(NSZone *)zone { RKObjectMapping *copy = [[[self class] allocWithZone:zone] init]; copy.objectClass = self.objectClass; - copy.setDefaultValueForMissingAttributes = self.setDefaultValueForMissingAttributes; - copy.setNilForMissingRelationships = self.setNilForMissingRelationships; - copy.forceCollectionMapping = self.forceCollectionMapping; - copy.performKeyValueValidation = self.performKeyValueValidation; - copy.dateFormatters = self.dateFormatters; - copy.preferredDateFormatter = self.preferredDateFormatter; + [copy copyPropertiesFromMapping:self]; copy.mutablePropertyMappings = [NSMutableArray new]; - copy.sourceToDestinationKeyTransformationBlock = self.sourceToDestinationKeyTransformationBlock; for (RKPropertyMapping *propertyMapping in self.propertyMappings) { - [copy addPropertyMapping:propertyMapping]; + [copy addPropertyMapping:[propertyMapping copy]]; } return copy; @@ -264,29 +328,10 @@ static RKSourceToDesinationKeyTransformationBlock defaultSourceToDestinationKeyT } } -- (instancetype)inverseMappingAtDepth:(NSInteger)depth -{ - NSAssert(depth < RKObjectMappingMaximumInverseMappingRecursionDepth, @"Exceeded max recursion level in inverseMapping. This is likely due to a loop in the serialization graph. To break this loop, specify one-way relationships by setting serialize to NO in mapKeyPath:toRelationship:withObjectMapping:serialize:"); - RKObjectMapping *inverseMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; - for (RKAttributeMapping *attributeMapping in self.attributeMappings) { - [inverseMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:attributeMapping.destinationKeyPath toKeyPath:attributeMapping.sourceKeyPath]]; - } - - for (RKRelationshipMapping *relationshipMapping in self.relationshipMappings) { - 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; -} - - (instancetype)inverseMapping { - return [self inverseMappingAtDepth:0]; + RKMappingInverter *mappingInverter = [[RKMappingInverter alloc] initWithMapping:self]; + return [mappingInverter inverseMapping]; } - (void)addAttributeMappingFromKeyOfRepresentationToAttribute:(NSString *)attributeName diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingTest.m index 41d818f4..6f97a52d 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingTest.m @@ -219,4 +219,18 @@ expect([mapping.propertyMappingsByDestinationKeyPath allKeys]).to.equal(expectedNames); } +- (void)testBreakageOfRecursiveInverseCyclicGraphs +{ + RKObjectMapping *parentMapping = [RKObjectMapping mappingForClass:[NSObject class]]; + [parentMapping addAttributeMappingsFromDictionary:@{ @"first_name": @"firstName", @"last_name": @"lastName" }]; + RKObjectMapping *childMapping = [RKObjectMapping mappingForClass:[NSObject class]]; + [childMapping addAttributeMappingsFromDictionary:@{ @"first_name": @"firstName", @"last_name": @"lastName" }]; + [parentMapping addRelationshipMappingWithSourceKeyPath:@"children" mapping:childMapping]; + [childMapping addRelationshipMappingWithSourceKeyPath:@"parents" mapping:parentMapping]; + RKObjectMapping *inverseMapping = [parentMapping inverseMapping]; + expect([inverseMapping propertyMappingsBySourceKeyPath][@"firstName"]).notTo.beNil(); + expect([inverseMapping propertyMappingsBySourceKeyPath][@"lastName"]).notTo.beNil(); + expect([inverseMapping propertyMappingsBySourceKeyPath][@"children"]).notTo.beNil(); +} + @end