From 7745f17d9065f2688e1a9b4b92accb7854a1048c Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 27 Dec 2012 15:11:51 -0500 Subject: [PATCH 01/27] Add temporary workaround for crashes related to ambiguous key paths in the deletion support. refs #1111 --- Code/Network/RKManagedObjectRequestOperation.m | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Code/Network/RKManagedObjectRequestOperation.m b/Code/Network/RKManagedObjectRequestOperation.m index 979d994ee0..fe2166a493 100644 --- a/Code/Network/RKManagedObjectRequestOperation.m +++ b/Code/Network/RKManagedObjectRequestOperation.m @@ -123,7 +123,17 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath // When we map the root object, it is returned under the key `[NSNull null]` static id RKMappedValueForKeyPathInDictionary(NSString *keyPath, NSDictionary *dictionary) { - return ([keyPath isEqual:[NSNull null]]) ? [dictionary objectForKey:[NSNull null]] : [dictionary valueForKeyPath:keyPath]; + @try { + return ([keyPath isEqual:[NSNull null]]) ? [dictionary objectForKey:[NSNull null]] : [dictionary valueForKeyPath:keyPath]; + } + @catch (NSException *exception) { + if ([[exception name] isEqualToString:NSUndefinedKeyException]) { + RKLogWarning(@"Caught undefined key exception for keyPath '%@' in mapping result: This likely indicates an ambiguous keyPath is used across response descriptor or dynamic mappings.", keyPath); + return nil; + } + + [exception raise]; + } } static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath, NSMutableDictionary *dictionary) From bbfec220ed63b0aab018732970f277a791ee4599 Mon Sep 17 00:00:00 2001 From: Jeff Arena Date: Thu, 27 Dec 2012 16:47:24 -0500 Subject: [PATCH 02/27] Fix logging issues with error method --- Code/Network/RKHTTPRequestOperation.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Code/Network/RKHTTPRequestOperation.m b/Code/Network/RKHTTPRequestOperation.m index 3cbd0081b2..b747a30f22 100644 --- a/Code/Network/RKHTTPRequestOperation.m +++ b/Code/Network/RKHTTPRequestOperation.m @@ -210,14 +210,14 @@ - (NSError *)error if (![self hasAcceptableStatusCode] || ![self hasAcceptableContentType]) { NSMutableDictionary *userInfo = [error.userInfo mutableCopy]; - if (error.code == NSURLErrorBadServerResponse) { + if (error.code == NSURLErrorBadServerResponse && ![self hasAcceptableStatusCode]) { // Replace the NSLocalizedDescriptionKey NSUInteger statusCode = ([self.response isKindOfClass:[NSHTTPURLResponse class]]) ? (NSUInteger)[self.response statusCode] : 200; - [userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected status code in (%@), got %d", nil), RKStringFromIndexSet([self acceptableStatusCodes]), statusCode] forKey:NSLocalizedDescriptionKey]; + [userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected status code in (%@), got %d", nil), RKStringFromIndexSet(self.acceptableStatusCodes ?: [NSMutableIndexSet indexSet]), statusCode] forKey:NSLocalizedDescriptionKey]; self.HTTPError = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:NSURLErrorBadServerResponse userInfo:userInfo]; - } else if (error.code == NSURLErrorCannotDecodeContentData) { + } else if (error.code == NSURLErrorCannotDecodeContentData && ![self hasAcceptableContentType]) { // Because we have shifted the Acceptable Content Types and Status Codes - [userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected content type %@, got %@", nil), [self acceptableContentTypes], [self.response MIMEType]] forKey:NSLocalizedDescriptionKey]; + [userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected content type %@, got %@", nil), self.acceptableContentTypes, [self.response MIMEType]] forKey:NSLocalizedDescriptionKey]; self.HTTPError = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo]; } } From 5c21e52829a7bc3aecc2a9f3c7f54dd5223d5a1d Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 27 Dec 2012 22:12:40 -0500 Subject: [PATCH 03/27] Update Dynamic Mapping API's to match the rest of the 0.20.x style. Introduce support for predicate based dynamic matching. * Rename RKDynamicMappingMatcher to RKObjectMappingMatcher since it is not strictly coupled to dynamic mapping and works with object mappings. * Rework matchers into using a class cluster style to enable flexible subclassing to introduce additional matchers. --- Code/CoreData/RKEntityMapping.m | 2 +- ...KManagedObjectMappingOperationDataSource.m | 2 +- .../RKRelationshipConnectionOperation.m | 2 +- Code/Network/RKObjectManager.m | 2 +- Code/ObjectMapping/RKDynamicMapping.h | 75 ++++++--- Code/ObjectMapping/RKDynamicMapping.m | 35 +++-- Code/ObjectMapping/RKDynamicMappingMatcher.h | 67 -------- Code/ObjectMapping/RKDynamicMappingMatcher.m | 49 ------ Code/ObjectMapping/RKObjectMappingMatcher.h | 75 +++++++++ Code/ObjectMapping/RKObjectMappingMatcher.m | 123 +++++++++++++++ RestKit.xcodeproj/project.pbxproj | 36 ++--- Tests/Logic/CoreData/RKEntityMappingTest.m | 4 +- .../RKManagedObjectRequestOperationTest.m | 2 +- Tests/Logic/Network/RKRequestDescriptorTest.m | 2 +- .../ObjectMapping/RKDynamicMappingTest.m | 146 ++++++++++++++++++ .../RKDynamicObjectMappingTest.m | 89 ----------- .../Logic/ObjectMapping/RKObjectManagerTest.m | 2 +- .../RKObjectMappingNextGenTest.m | 12 +- .../RKObjectParameterizationTest.m | 6 +- 19 files changed, 462 insertions(+), 269 deletions(-) delete mode 100644 Code/ObjectMapping/RKDynamicMappingMatcher.h delete mode 100644 Code/ObjectMapping/RKDynamicMappingMatcher.m create mode 100644 Code/ObjectMapping/RKObjectMappingMatcher.h create mode 100644 Code/ObjectMapping/RKObjectMappingMatcher.m create mode 100644 Tests/Logic/ObjectMapping/RKDynamicMappingTest.m delete mode 100644 Tests/Logic/ObjectMapping/RKDynamicObjectMappingTest.m diff --git a/Code/CoreData/RKEntityMapping.m b/Code/CoreData/RKEntityMapping.m index 1c52de3c2d..a8e111e7fb 100644 --- a/Code/CoreData/RKEntityMapping.m +++ b/Code/CoreData/RKEntityMapping.m @@ -20,7 +20,7 @@ #import "RKEntityMapping.h" #import "RKManagedObjectStore.h" -#import "RKDynamicMappingMatcher.h" +#import "RKObjectMappingMatcher.h" #import "RKPropertyInspector+CoreData.h" #import "RKLog.h" #import "RKRelationshipMapping.h" diff --git a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m index 3339c0ba7e..b2579c5e95 100644 --- a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m +++ b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m @@ -24,7 +24,7 @@ #import "RKLog.h" #import "RKManagedObjectStore.h" #import "RKMappingOperation.h" -#import "RKDynamicMappingMatcher.h" +#import "RKObjectMappingMatcher.h" #import "RKManagedObjectCaching.h" #import "RKRelationshipConnectionOperation.h" #import "RKMappingErrors.h" diff --git a/Code/CoreData/RKRelationshipConnectionOperation.m b/Code/CoreData/RKRelationshipConnectionOperation.m index a6df90bc5f..1120d8cdb7 100644 --- a/Code/CoreData/RKRelationshipConnectionOperation.m +++ b/Code/CoreData/RKRelationshipConnectionOperation.m @@ -24,7 +24,7 @@ #import "RKEntityMapping.h" #import "RKLog.h" #import "RKManagedObjectCaching.h" -#import "RKDynamicMappingMatcher.h" +#import "RKObjectMappingMatcher.h" #import "RKErrors.h" #import "RKObjectUtilities.h" diff --git a/Code/Network/RKObjectManager.m b/Code/Network/RKObjectManager.m index 4ec15a4907..f0f2fce145 100644 --- a/Code/Network/RKObjectManager.m +++ b/Code/Network/RKObjectManager.m @@ -441,7 +441,7 @@ - (id)appropriateObjectRequestOperationWithObject:(id)object _blockSuccess = [[object managedObjectContext] obtainPermanentIDsForObjects:@[object] error:&_blockError]; }]; if (! _blockSuccess) RKLogWarning(@"Failed to obtain permanent ID for object %@: %@", object, _blockError); - } + } } else { // Non-Core Data operation operation = [self objectRequestOperationWithRequest:request success:nil failure:nil]; diff --git a/Code/ObjectMapping/RKDynamicMapping.h b/Code/ObjectMapping/RKDynamicMapping.h index 455e358a3f..ba60aca730 100644 --- a/Code/ObjectMapping/RKDynamicMapping.h +++ b/Code/ObjectMapping/RKDynamicMapping.h @@ -19,40 +19,77 @@ // #import "RKMapping.h" -#import "RKObjectMapping.h" - -typedef RKObjectMapping *(^RKDynamicMappingDelegateBlock)(id representation); +#import "RKObjectMappingMatcher.h" /** - 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. + The `RKDynamicMapping` class is an `RKMapping` subclass that provides an interface for deferring the decision about how a given object representation is to be mapped until run time. This enables many interesting mapping strategies, such as mapping similarly structured data differently and constructing object mappings at run time by examining the data being mapped. + + ## Configuring Mapping Selection + + Dynamic mappings support the selection of the concrete object mapping in one of two ways: + + 1. Through the use of a mapping selection block configured by `setObjectMappingForRepresentationBlock:`. When configured, the block is called with a reference to the current object representation being mapped and is expected to return an `RKObjectMapping` object. Returning `nil` declines the mapping of the representation. + 1. Through the configuration of one of more `RKObjectMappingMatcher` objects. The matchers are consulted in registration order and the first matcher to return an object mapping is used to map the matched representation. + + When both a mapping selection block and matchers are configured on a `RKDynamicMapping` object, the matcher objects are consulted first and if none match, the selection block is invoked. + + ## Using Matcher Objects + + The `RKObjectMappingMatcher` class provides an interface for evaluating a key path or predicate based match and returning an appropriate object mapping. Matchers can be added to the `RKDynamicMapping` objects to declaratively describe a particular mapping strategy. + + For example, suppose that we have a JSON fragment for a person that we want to map differently based on the gender of the person. When the gender is 'male', we want to use the Boy class and when then the gender is 'female' we want to use the Girl class. The JSON might look something like this: + + [ { "name": "Blake", "gender": "male" }, { "name": "Sarah", "gender": "female" } ] + + We might define configure the dynamic mapping like so: + + RKDynamicMapping *mapping = [RKDynamicMapping new]; + RKObjectMapping *boyMapping = [RKObjectMapping mappingForClass:[Boy class]]; + RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; + [mapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"gender" expectedValue:@"male" objectMapping:boyMapping]]; + [mapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"gender" expectedValue:@"female" objectMapping:girlMapping]]; + + When evaluated, the matchers will invoke `valueForKeyPath:@"gender"` against each dictionary in the array of object representations and apply the appropriate object mapping for each representation. This would return a mapping result containing an array of two objects, one an instance of the `Boy` class and the other an instance of the `Girl` class. + + ## HTTP Integration + + Dynamic mappings can be used to map HTTP requests and responses by adding them to an `RKRequestDescriptor` or `RKResponseDescriptor` objects. */ @interface RKDynamicMapping : RKMapping -///------------------------------------ -/// @name Configuring Mapping Selection -///------------------------------------ +///------------------------------------------ +/// @name Configuring Block Mapping Selection +///------------------------------------------ /** Sets a block to be invoked to determine the appropriate concrete object mapping with which to map an object representation. - @param block The block object to invoke to select the object mapping with which to map the given object representation. + @param block The block object to invoke to select the object mapping with which to map the given object representation. The block returns an object mapping and accepts a single parameter: the object representation being mapped. */ -- (void)setObjectMappingForRepresentationBlock:(RKDynamicMappingDelegateBlock)block; +- (void)setObjectMappingForRepresentationBlock:(RKObjectMapping *(^)(id representation))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. + Returns the array of matchers objects added to the receiver. + */ +@property (nonatomic, strong, readonly) NSArray *matchers; - For example, suppose that we have a JSON fragment for a person that we want to map differently based on the gender of the person. When the gender is 'male', we want to use the Boy class and when then the gender is 'female' we want to use the Girl class. We might define our dynamic mapping like so: +/** + Adds a matcher to the receiver. - RKDynamicMapping *mapping = [RKDynamicMapping new]; - [mapping setObjectMapping:boyMapping whenValueOfKeyPath:@"gender" isEqualTo:@"male"]; - [mapping setObjectMapping:girlMapping whenValueOfKeyPath:@"gender" isEqualTo:@"female"]; + If the matcher has already been added to the receiver, then adding it again moves it to the top of the matcher stack. - @param objectMapping The mapping to be used when the value at the given key path is equal to the given value. - @param keyPath The key path to retrieve the comparison value from in the object representation being mapped. - @param value The value to be compared with the value at `keyPath`. If they are equal, the `objectMapping` will be used to map the representation. + @param matcher The matcher to add to the receiver. */ -- (void)setObjectMapping:(RKObjectMapping *)objectMapping whenValueOfKeyPath:(NSString *)keyPath isEqualTo:(id)value; +- (void)addMatcher:(RKObjectMappingMatcher *)matcher; + +/** + Removes a matcher from the receiver. + + If the matcher has already been added to the receiver, then adding it again moves it to the top of the matcher stack. + + @param matcher The matcher to remove from the receiver. + */ +- (void)removeMatcher:(RKObjectMappingMatcher *)matcher; /** Returns an array of object mappings that have been registered with the receiver. @@ -68,6 +105,8 @@ typedef RKObjectMapping *(^RKDynamicMappingDelegateBlock)(id representation); /** Invoked by the `RKMapperOperation` and `RKMappingOperation` to determine the appropriate `RKObjectMapping` to use when mapping the given object representation. + This method searches the stack of registered matchers and then executes the block, if any, set by `setObjectMappingForRepresentationBlock:`. If `nil` is returned, then mapping for the representation is declined and it will not be 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. */ diff --git a/Code/ObjectMapping/RKDynamicMapping.m b/Code/ObjectMapping/RKDynamicMapping.m index 76f99bd6ab..c34b0fd3c7 100644 --- a/Code/ObjectMapping/RKDynamicMapping.m +++ b/Code/ObjectMapping/RKDynamicMapping.m @@ -19,7 +19,7 @@ // #import "RKDynamicMapping.h" -#import "RKDynamicMappingMatcher.h" +#import "RKObjectMappingMatcher.h" #import "RKLog.h" // Set Logging Component @@ -27,8 +27,8 @@ #define RKLogComponent RKlcl_cRestKitObjectMapping @interface RKDynamicMapping () -@property (nonatomic, strong) NSMutableArray *matchers; -@property (nonatomic, copy) RKDynamicMappingDelegateBlock objectMappingForRepresentationBlock; +@property (nonatomic, strong) NSMutableArray *mutableMatchers; +@property (nonatomic, copy) RKObjectMapping *(^objectMappingForRepresentationBlock)(id representation); @end @implementation RKDynamicMapping @@ -37,22 +37,37 @@ - (id)init { self = [super init]; if (self) { - self.matchers = [NSMutableArray new]; + self.mutableMatchers = [NSMutableArray new]; } return self; } +- (NSArray *)matchers +{ + return [self.mutableMatchers copy]; +} + - (NSArray *)objectMappings { - return [self.matchers valueForKey:@"objectMapping"]; + return [self.mutableMatchers valueForKey:@"objectMapping"]; +} + +- (void)addMatcher:(RKObjectMappingMatcher *)matcher +{ + NSParameterAssert(matcher); + if ([self.mutableMatchers containsObject:matcher]) { + [self.mutableMatchers removeObject:matcher]; + [self.mutableMatchers insertObject:matcher atIndex:0]; + } else { + [self.mutableMatchers addObject:matcher]; + } } -- (void)setObjectMapping:(RKObjectMapping *)objectMapping whenValueOfKeyPath:(NSString *)keyPath isEqualTo:(id)expectedValue +- (void)removeMatcher:(RKObjectMappingMatcher *)matcher { - RKLogDebug(@"Adding dynamic object mapping for key '%@' with value '%@' to destination class: %@", keyPath, expectedValue, NSStringFromClass(objectMapping.objectClass)); - RKDynamicMappingMatcher *matcher = [[RKDynamicMappingMatcher alloc] initWithKeyPath:keyPath expectedValue:expectedValue objectMapping:objectMapping]; - [_matchers addObject:matcher]; + NSParameterAssert(matcher); + [self.mutableMatchers removeObject:matcher]; } - (RKObjectMapping *)objectMappingForRepresentation:(id)representation @@ -62,7 +77,7 @@ - (RKObjectMapping *)objectMappingForRepresentation:(id)representation RKLogTrace(@"Performing dynamic object mapping for object representation: %@", representation); // Consult the declarative matchers first - for (RKDynamicMappingMatcher *matcher in _matchers) { + for (RKObjectMappingMatcher *matcher in self.mutableMatchers) { if ([matcher matches:representation]) { RKLogTrace(@"Found declarative match for matcher: %@.", matcher); return matcher.objectMapping; diff --git a/Code/ObjectMapping/RKDynamicMappingMatcher.h b/Code/ObjectMapping/RKDynamicMappingMatcher.h deleted file mode 100644 index cd9367ef2a..0000000000 --- a/Code/ObjectMapping/RKDynamicMappingMatcher.h +++ /dev/null @@ -1,67 +0,0 @@ -// -// RKDynamicMappingMatcher.h -// RestKit -// -// Created by Jeff Arena on 8/2/11. -// Copyright (c) 2009-2012 RestKit. All rights reserved. -// - -#import -#import "RKObjectMapping.h" - -/** - The `RKDynamicMappingMatcher` class provides an interface for encapsulating the selection of an object mapping based on the runtime value of a property at a given key path. A matcher object is initialized with a key path, an expected value to be read from the key path, and an object mapping that is to be applied if the match evaluates to `YES`. When evaluating the match, the matcher invokes `valueForKeyPath:` on the object being matched and compares the value returned with the `expectedValue` via the `RKObjectIsEqualToObject` function. - - @see `RKObjectIsEqualToObject()` - */ -// TODO: better name? RKKeyPathMappingMatcher | RKMappingMatcher | RKKeyPathMatcher | RKMatcher | RKValueMatcher | RKPropertyMatcher -@interface RKDynamicMappingMatcher : NSObject - -///----------------------------- -/// @name Initializing a Matcher -///----------------------------- - -/** - Initializes the receiver with a given key path, expected value, and an object mapping that applies in the event of a positive match. - - @param keyPath The key path to obtain the comparison value from the object being matched via `valueForKeyPath:`. - @param expectedValue The value that is expected to be read from `keyPath` if there is a match. - @param objectMapping The object mapping object that applies if the comparison value is equal to the expected value. - @return The receiver, initialized with the given key path, expected value, and object mapping. - */ -- (id)initWithKeyPath:(NSString *)keyPath expectedValue:(id)expectedValue objectMapping:(RKObjectMapping *)objectMapping; - -///----------------------------- -/// @name Initializing a Matcher -///----------------------------- - -/** - The key path to obtain the comparison value from the object being matched via `valueForKeyPath:`. - */ -@property (nonatomic, copy, readonly) NSString *keyPath; - -/** - The value that is expected to be read from `keyPath` if there is a match. - */ -@property (nonatomic, strong, readonly) id expectedValue; - -/** - The object mapping object that applies if the comparison value read from `keyPath` is equal to the `expectedValue`. - */ -@property (nonatomic, strong, readonly) RKObjectMapping *objectMapping; - -///------------------------- -/// @name Evaluating a Match -///------------------------- - -/** - Returns a Boolean value that indicates if the given object matches the expectations of the receiver. - - The match is evaluated by invoking `valueForKeyPath:` on the give object with the value of the `keyPath` property and comparing the returned value with the `expectedValue` using the `RKObjectIsEqualToObject` function. - - @param object The object to be evaluated. - @return `YES` if the object matches the expectations of the receiver, else `NO`. - */ -- (BOOL)matches:(id)object; - -@end diff --git a/Code/ObjectMapping/RKDynamicMappingMatcher.m b/Code/ObjectMapping/RKDynamicMappingMatcher.m deleted file mode 100644 index 6629795cfb..0000000000 --- a/Code/ObjectMapping/RKDynamicMappingMatcher.m +++ /dev/null @@ -1,49 +0,0 @@ -// -// RKDynamicMappingMatcher.m -// RestKit -// -// Created by Jeff Arena on 8/2/11. -// Copyright (c) 2009-2012 RestKit. All rights reserved. -// - -#import "RKDynamicMappingMatcher.h" -#import "RKObjectUtilities.h" - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -@interface RKDynamicMappingMatcher () -@property (nonatomic, copy) NSString *keyPath; -@property (nonatomic, strong, readwrite) id expectedValue; -@property (nonatomic, strong, readwrite) RKObjectMapping *objectMapping; -@end - -@implementation RKDynamicMappingMatcher - -- (id)initWithKeyPath:(NSString *)keyPath expectedValue:(id)expectedValue objectMapping:(RKObjectMapping *)objectMapping -{ - NSParameterAssert(keyPath); - NSParameterAssert(expectedValue); - NSParameterAssert(objectMapping); - self = [super init]; - if (self) { - self.keyPath = keyPath; - self.expectedValue = expectedValue; - self.objectMapping = objectMapping; - } - - return self; -} - -- (BOOL)matches:(id)object -{ - id value = [object valueForKeyPath:self.keyPath]; - if (value == nil) return NO; - return RKObjectIsEqualToObject(value, self.expectedValue); -} - -- (NSString *)description -{ - return [NSString stringWithFormat:@"<%@: %p when `%@` == '%@' objectMapping: %@>", NSStringFromClass([self class]), self, self.keyPath, self.expectedValue, self.objectMapping]; -} - -@end diff --git a/Code/ObjectMapping/RKObjectMappingMatcher.h b/Code/ObjectMapping/RKObjectMappingMatcher.h new file mode 100644 index 0000000000..ee43f721a4 --- /dev/null +++ b/Code/ObjectMapping/RKObjectMappingMatcher.h @@ -0,0 +1,75 @@ +// +// RKDynamicMappingMatcher.h +// RestKit +// +// Created by Jeff Arena on 8/2/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import +#import "RKObjectMapping.h" + +/** + The `RKObjectMappingMatcher` class provides an interface for encapsulating the selection of an object mapping based on runtime values. Matcher objects may be configured by key path and expected value or with a predicate object. + + ## Key Path Matching + + A key path matcher object is initialized with a key path, an expected value to be read from the key path, and an object mapping that is to be applied if the match evaluates to `YES`. When evaluating the match, the matcher invokes `valueForKeyPath:` on the object being matched and compares the value returned with the `expectedValue` via the `RKObjectIsEqualToObject` function. This provides a flexible, semantic match of the property value. + + ## Predicate Matching + + A predicate matcher object is initialized with a predicate object and an object mapping that is to be applied if the predicate evaluates to `YES` for the object being matched. + */ +@interface RKObjectMappingMatcher : NSObject + +///------------------------------------- +/// @name Constructing Key Path Matchers +///------------------------------------- + +/** + Creates and returns a key path matcher object with a given key path, expected value, and an object mapping that applies in the event of a positive match. + + @param keyPath The key path to obtain the comparison value from the object being matched via `valueForKeyPath:`. + @param expectedValue The value that is expected to be read from `keyPath` if there is a match. + @param objectMapping The object mapping object that applies if the comparison value is equal to the expected value. + @return The receiver, initialized with the given key path, expected value, and object mapping. + */ ++ (instancetype)matcherWithKeyPath:(NSString *)keyPath expectedValue:(id)expectedValue objectMapping:(RKObjectMapping *)objectMapping; + +///-------------------------------------- +/// @name Constructing Predicate Matchers +///-------------------------------------- + +/** + Creates and returns a predicate matcher object with a given predicate and an object mapping that applies in the predicate evaluates positively. + + @param predicate The predicate with which to evaluate the matched object. + @param objectMapping The object mapping object that applies if the predicate evaluates positively for the matched object. + @return The receiver, initialized with the given key path, expected value, and object mapping. + */ ++ (instancetype)matcherWithPredicate:(NSPredicate *)predicate objectMapping:(RKObjectMapping *)objectMapping; + +///----------------------------------- +/// @name Accessing the Object Mapping +///----------------------------------- + +/** + The object mapping object that applies when the receiver matches a given object. + + @see `matches:` + */ +@property (nonatomic, strong, readonly) RKObjectMapping *objectMapping; + +///------------------------- +/// @name Evaluating a Match +///------------------------- + +/** + Returns a Boolean value that indicates if the given object matches the expectations of the receiver. + + @param object The object to be evaluated. + @return `YES` if the object matches the expectations of the receiver, else `NO`. + */ +- (BOOL)matches:(id)object; + +@end diff --git a/Code/ObjectMapping/RKObjectMappingMatcher.m b/Code/ObjectMapping/RKObjectMappingMatcher.m new file mode 100644 index 0000000000..d2019d8119 --- /dev/null +++ b/Code/ObjectMapping/RKObjectMappingMatcher.m @@ -0,0 +1,123 @@ +// +// RKDynamicMappingMatcher.m +// RestKit +// +// Created by Jeff Arena on 8/2/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKObjectMappingMatcher.h" +#import "RKObjectUtilities.h" + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface RKObjectMappingMatcher () +@property (nonatomic, strong, readwrite) RKObjectMapping *objectMapping; +@end + +@interface RKKeyPathObjectMappingMatcher : RKObjectMappingMatcher +@property (nonatomic, copy) NSString *keyPath; +@property (nonatomic, strong, readwrite) id expectedValue; + +- (id)initWithKeyPath:(NSString *)keyPath expectedValue:(id)expectedValue objectMapping:(RKObjectMapping *)objectMapping; +@end + +@interface RKPredicateObjectMappingMatcher : RKObjectMappingMatcher +@property (nonatomic, strong) NSPredicate *predicate; + +- (id)initWithPredicate:(NSPredicate *)predicate objectMapping:(RKObjectMapping *)objectMapping; +@end + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation RKObjectMappingMatcher + ++ (instancetype)matcherWithKeyPath:(NSString *)keyPath expectedValue:(id)expectedValue objectMapping:(RKObjectMapping *)objectMapping +{ + return [[RKKeyPathObjectMappingMatcher alloc] initWithKeyPath:keyPath expectedValue:expectedValue objectMapping:objectMapping]; +} + ++ (instancetype)matcherWithPredicate:(NSPredicate *)predicate objectMapping:(RKObjectMapping *)objectMapping +{ + return [[RKPredicateObjectMappingMatcher alloc] initWithPredicate:predicate objectMapping:objectMapping]; +} + +- (id)init +{ + self = [super init]; + if (self) { + if ([self isMemberOfClass:[RKObjectMappingMatcher class]]) { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"%@ is not meant to be directly instantiated. Use one of the initializer methods instead.", + NSStringFromClass([self class])] + userInfo:nil]; + } + } + + return self; +} + +- (BOOL)matches:(id)object +{ + return NO; +} + +@end + +@implementation RKKeyPathObjectMappingMatcher + +- (id)initWithKeyPath:(NSString *)keyPath expectedValue:(id)expectedValue objectMapping:(RKObjectMapping *)objectMapping +{ + NSParameterAssert(keyPath); + NSParameterAssert(expectedValue); + NSParameterAssert(objectMapping); + self = [super init]; + if (self) { + self.keyPath = keyPath; + self.expectedValue = expectedValue; + self.objectMapping = objectMapping; + } + + return self; +} + +- (BOOL)matches:(id)object +{ + id value = [object valueForKeyPath:self.keyPath]; + if (value == nil) return NO; + return RKObjectIsEqualToObject(value, self.expectedValue); +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p when `%@` == '%@' objectMapping: %@>", NSStringFromClass([self class]), self, self.keyPath, self.expectedValue, self.objectMapping]; +} + +@end + +@implementation RKPredicateObjectMappingMatcher + +- (id)initWithPredicate:(NSPredicate *)predicate objectMapping:(RKObjectMapping *)objectMapping +{ + NSParameterAssert(predicate); + NSParameterAssert(objectMapping); + self = [super init]; + if (self) { + self.predicate = predicate; + self.objectMapping = objectMapping; + } + + return self; +} + +- (BOOL)matches:(id)object +{ + return [self.predicate evaluateWithObject:object]; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p when '%@' objectMapping: %@>", NSStringFromClass([self class]), self, self.predicate, self.objectMapping]; +} + +@end diff --git a/RestKit.xcodeproj/project.pbxproj b/RestKit.xcodeproj/project.pbxproj index fb94a83ded..15c964543a 100644 --- a/RestKit.xcodeproj/project.pbxproj +++ b/RestKit.xcodeproj/project.pbxproj @@ -254,8 +254,8 @@ 251610B71456F2330060A5C5 /* RKParent.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610081456F2330060A5C5 /* RKParent.m */; }; 251610B81456F2330060A5C5 /* RKResident.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516100A1456F2330060A5C5 /* RKResident.m */; }; 251610B91456F2330060A5C5 /* RKResident.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516100A1456F2330060A5C5 /* RKResident.m */; }; - 251610D21456F2330060A5C5 /* RKDynamicObjectMappingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingTest.m */; }; - 251610D31456F2330060A5C5 /* RKDynamicObjectMappingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingTest.m */; }; + 251610D21456F2330060A5C5 /* RKDynamicMappingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101C1456F2330060A5C5 /* RKDynamicMappingTest.m */; }; + 251610D31456F2330060A5C5 /* RKDynamicMappingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101C1456F2330060A5C5 /* RKDynamicMappingTest.m */; }; 251610DC1456F2330060A5C5 /* RKObjectMappingNextGenTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610211456F2330060A5C5 /* RKObjectMappingNextGenTest.m */; }; 251610DD1456F2330060A5C5 /* RKObjectMappingNextGenTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610211456F2330060A5C5 /* RKObjectMappingNextGenTest.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 251610DE1456F2330060A5C5 /* RKMappingOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610221456F2330060A5C5 /* RKMappingOperationTest.m */; }; @@ -388,6 +388,8 @@ 257ABAB71511371E00CCAA76 /* NSManagedObject+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 257ABAB41511371C00CCAA76 /* NSManagedObject+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 257ABAB81511371E00CCAA76 /* NSManagedObject+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 257ABAB51511371D00CCAA76 /* NSManagedObject+RKAdditions.m */; }; 257ABAB91511371E00CCAA76 /* NSManagedObject+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 257ABAB51511371D00CCAA76 /* NSManagedObject+RKAdditions.m */; }; + 258BEA02168D058300C74C8C /* RKObjectMappingMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 258BEA01168D058300C74C8C /* RKObjectMappingMatcher.m */; }; + 258BEA03168D058300C74C8C /* RKObjectMappingMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 258BEA01168D058300C74C8C /* RKObjectMappingMatcher.m */; }; 258EA4A815A38BC0007E07A6 /* RKObjectMappingOperationDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 258EA4A615A38BBF007E07A6 /* RKObjectMappingOperationDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 258EA4A915A38BC0007E07A6 /* RKObjectMappingOperationDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 258EA4A615A38BBF007E07A6 /* RKObjectMappingOperationDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 258EA4AA15A38BC0007E07A6 /* RKObjectMappingOperationDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 258EA4A715A38BBF007E07A6 /* RKObjectMappingOperationDataSource.m */; }; @@ -499,10 +501,8 @@ 25B6E95614CF795D00B1E881 /* RKErrors.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E95414CF795D00B1E881 /* RKErrors.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25B6E95814CF7A1C00B1E881 /* RKErrors.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E95714CF7A1C00B1E881 /* RKErrors.m */; }; 25B6E95914CF7A1C00B1E881 /* RKErrors.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E95714CF7A1C00B1E881 /* RKErrors.m */; }; - 25B6E95C14CF7E3C00B1E881 /* RKDynamicMappingMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E95A14CF7E3C00B1E881 /* RKDynamicMappingMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25B6E95D14CF7E3C00B1E881 /* RKDynamicMappingMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E95A14CF7E3C00B1E881 /* RKDynamicMappingMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25B6E95E14CF7E3C00B1E881 /* RKDynamicMappingMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E95B14CF7E3C00B1E881 /* RKDynamicMappingMatcher.m */; }; - 25B6E95F14CF7E3C00B1E881 /* RKDynamicMappingMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E95B14CF7E3C00B1E881 /* RKDynamicMappingMatcher.m */; }; + 25B6E95C14CF7E3C00B1E881 /* RKObjectMappingMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E95A14CF7E3C00B1E881 /* RKObjectMappingMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E95D14CF7E3C00B1E881 /* RKObjectMappingMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E95A14CF7E3C00B1E881 /* RKObjectMappingMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25B6E9DB14CF912500B1E881 /* RKSearchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9D614CF912500B1E881 /* RKSearchable.m */; }; 25B6E9DC14CF912500B1E881 /* RKSearchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9D614CF912500B1E881 /* RKSearchable.m */; }; 25B6E9DD14CF912500B1E881 /* RKTestAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9D814CF912500B1E881 /* RKTestAddress.m */; }; @@ -760,7 +760,7 @@ 251610081456F2330060A5C5 /* RKParent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKParent.m; sourceTree = ""; }; 251610091456F2330060A5C5 /* RKResident.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKResident.h; sourceTree = ""; }; 2516100A1456F2330060A5C5 /* RKResident.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKResident.m; sourceTree = ""; }; - 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKDynamicObjectMappingTest.m; sourceTree = ""; }; + 2516101C1456F2330060A5C5 /* RKDynamicMappingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKDynamicMappingTest.m; sourceTree = ""; }; 2516101F1456F2330060A5C5 /* RKObjectManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectManagerTest.m; sourceTree = ""; }; 251610211456F2330060A5C5 /* RKObjectMappingNextGenTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = RKObjectMappingNextGenTest.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 251610221456F2330060A5C5 /* RKMappingOperationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = RKMappingOperationTest.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; @@ -838,6 +838,7 @@ 257ABAAF15112DD400CCAA76 /* NSManagedObjectContext+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObjectContext+RKAdditions.m"; sourceTree = ""; }; 257ABAB41511371C00CCAA76 /* NSManagedObject+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObject+RKAdditions.h"; sourceTree = ""; }; 257ABAB51511371D00CCAA76 /* NSManagedObject+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+RKAdditions.m"; sourceTree = ""; }; + 258BEA01168D058300C74C8C /* RKObjectMappingMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappingMatcher.m; sourceTree = ""; }; 258EA4A615A38BBF007E07A6 /* RKObjectMappingOperationDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectMappingOperationDataSource.h; sourceTree = ""; }; 258EA4A715A38BBF007E07A6 /* RKObjectMappingOperationDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappingOperationDataSource.m; sourceTree = ""; }; 258EA4AD15A38E7D007E07A6 /* RKMappingOperationDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKMappingOperationDataSource.h; sourceTree = ""; }; @@ -894,8 +895,7 @@ 25B408251491CDDB00F21111 /* RKPathUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKPathUtilities.m; sourceTree = ""; }; 25B6E95414CF795D00B1E881 /* RKErrors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKErrors.h; sourceTree = ""; }; 25B6E95714CF7A1C00B1E881 /* RKErrors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKErrors.m; sourceTree = ""; }; - 25B6E95A14CF7E3C00B1E881 /* RKDynamicMappingMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKDynamicMappingMatcher.h; sourceTree = ""; }; - 25B6E95B14CF7E3C00B1E881 /* RKDynamicMappingMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKDynamicMappingMatcher.m; sourceTree = ""; }; + 25B6E95A14CF7E3C00B1E881 /* RKObjectMappingMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectMappingMatcher.h; sourceTree = ""; }; 25B6E9D514CF912500B1E881 /* RKSearchable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKSearchable.h; sourceTree = ""; }; 25B6E9D614CF912500B1E881 /* RKSearchable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKSearchable.m; sourceTree = ""; }; 25B6E9D714CF912500B1E881 /* RKTestAddress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTestAddress.h; sourceTree = ""; }; @@ -1151,8 +1151,8 @@ 25160D7A145650490060A5C5 /* ObjectMapping */ = { isa = PBXGroup; children = ( - 25B6E95A14CF7E3C00B1E881 /* RKDynamicMappingMatcher.h */, - 25B6E95B14CF7E3C00B1E881 /* RKDynamicMappingMatcher.m */, + 258BEA01168D058300C74C8C /* RKObjectMappingMatcher.m */, + 25B6E95A14CF7E3C00B1E881 /* RKObjectMappingMatcher.h */, 25160D7C145650490060A5C5 /* RKDynamicMapping.h */, 25160D7D145650490060A5C5 /* RKDynamicMapping.m */, 25160D7E145650490060A5C5 /* RKErrorMessage.h */, @@ -1466,7 +1466,7 @@ 2516101B1456F2330060A5C5 /* ObjectMapping */ = { isa = PBXGroup; children = ( - 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingTest.m */, + 2516101C1456F2330060A5C5 /* RKDynamicMappingTest.m */, 2516101F1456F2330060A5C5 /* RKObjectManagerTest.m */, 251610211456F2330060A5C5 /* RKObjectMappingNextGenTest.m */, 251610221456F2330060A5C5 /* RKMappingOperationTest.m */, @@ -1723,7 +1723,7 @@ 25160E2E145650490060A5C5 /* RestKit.h in Headers */, 25B408261491CDDC00F21111 /* RKPathUtilities.h in Headers */, 25B6E95514CF795D00B1E881 /* RKErrors.h in Headers */, - 25B6E95C14CF7E3C00B1E881 /* RKDynamicMappingMatcher.h in Headers */, + 25B6E95C14CF7E3C00B1E881 /* RKObjectMappingMatcher.h in Headers */, 253B495214E35D1A00B0483F /* RKTestFixture.h in Headers */, 25FABED214E3796B00E609E7 /* RKTestNotificationObserver.h in Headers */, 25055B8414EEF32A00B9C4DD /* RKMappingTest.h in Headers */, @@ -1826,7 +1826,7 @@ 25160F25145655AF0060A5C5 /* RestKit.h in Headers */, 25B408271491CDDC00F21111 /* RKPathUtilities.h in Headers */, 25B6E95614CF795D00B1E881 /* RKErrors.h in Headers */, - 25B6E95D14CF7E3C00B1E881 /* RKDynamicMappingMatcher.h in Headers */, + 25B6E95D14CF7E3C00B1E881 /* RKObjectMappingMatcher.h in Headers */, 25FABED314E3796C00E609E7 /* RKTestNotificationObserver.h in Headers */, 25055B8514EEF32A00B9C4DD /* RKMappingTest.h in Headers */, 25055B8914EEF32A00B9C4DD /* RKTestFactory.h in Headers */, @@ -2238,7 +2238,6 @@ 25160F0A1456532C0060A5C5 /* SOCKit.m in Sources */, 25B408281491CDDC00F21111 /* RKPathUtilities.m in Sources */, 25B6E95814CF7A1C00B1E881 /* RKErrors.m in Sources */, - 25B6E95E14CF7E3C00B1E881 /* RKDynamicMappingMatcher.m in Sources */, 25FABED114E3796400E609E7 /* RKTestNotificationObserver.m in Sources */, 25CA7A8F14EC570200888FF8 /* RKMapping.m in Sources */, 25055B8614EEF32A00B9C4DD /* RKMappingTest.m in Sources */, @@ -2294,6 +2293,7 @@ 25E88C8A165C5CC30042ABD0 /* RKConnectionDescription.m in Sources */, 25019497166406E30081D68A /* RKValueTransformers.m in Sources */, 25A8C2361673BD480014D9A6 /* RKConnectionTestExpectation.m in Sources */, + 258BEA02168D058300C74C8C /* RKObjectMappingMatcher.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2314,7 +2314,7 @@ 251610B41456F2330060A5C5 /* RKObjectMapperTestModel.m in Sources */, 251610B61456F2330060A5C5 /* RKParent.m in Sources */, 251610B81456F2330060A5C5 /* RKResident.m in Sources */, - 251610D21456F2330060A5C5 /* RKDynamicObjectMappingTest.m in Sources */, + 251610D21456F2330060A5C5 /* RKDynamicMappingTest.m in Sources */, 251610DC1456F2330060A5C5 /* RKObjectMappingNextGenTest.m in Sources */, 251610DE1456F2330060A5C5 /* RKMappingOperationTest.m in Sources */, 251610E21456F2330060A5C5 /* RKMappingResultTest.m in Sources */, @@ -2387,7 +2387,6 @@ 25160F991456576C0060A5C5 /* RKPathMatcher.m in Sources */, 25B408291491CDDC00F21111 /* RKPathUtilities.m in Sources */, 25B6E95914CF7A1C00B1E881 /* RKErrors.m in Sources */, - 25B6E95F14CF7E3C00B1E881 /* RKDynamicMappingMatcher.m in Sources */, 25FABED014E3796400E609E7 /* RKTestNotificationObserver.m in Sources */, 25CA7A9014EC570200888FF8 /* RKMapping.m in Sources */, 25CA7A9114EC5C2D00888FF8 /* RKTestFixture.m in Sources */, @@ -2444,6 +2443,7 @@ 25E88C8B165C5CC30042ABD0 /* RKConnectionDescription.m in Sources */, 25019498166406E30081D68A /* RKValueTransformers.m in Sources */, 25A8C2371673BD480014D9A6 /* RKConnectionTestExpectation.m in Sources */, + 258BEA03168D058300C74C8C /* RKObjectMappingMatcher.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2464,7 +2464,7 @@ 251610B51456F2330060A5C5 /* RKObjectMapperTestModel.m in Sources */, 251610B71456F2330060A5C5 /* RKParent.m in Sources */, 251610B91456F2330060A5C5 /* RKResident.m in Sources */, - 251610D31456F2330060A5C5 /* RKDynamicObjectMappingTest.m in Sources */, + 251610D31456F2330060A5C5 /* RKDynamicMappingTest.m in Sources */, 251610DD1456F2330060A5C5 /* RKObjectMappingNextGenTest.m in Sources */, 251610DF1456F2330060A5C5 /* RKMappingOperationTest.m in Sources */, 251610E31456F2330060A5C5 /* RKMappingResultTest.m in Sources */, diff --git a/Tests/Logic/CoreData/RKEntityMappingTest.m b/Tests/Logic/CoreData/RKEntityMappingTest.m index 46c674f762..dd4b9dfad5 100644 --- a/Tests/Logic/CoreData/RKEntityMappingTest.m +++ b/Tests/Logic/CoreData/RKEntityMappingTest.m @@ -96,8 +96,8 @@ - (void)testShouldPickTheAppropriateMappingBasedOnAnAttributeValue parentMapping.identificationAttributes = @[ @"railsID" ]; [parentMapping addAttributeMappingsFromArray:@[@"name", @"age"]]; - [dynamicMapping setObjectMapping:parentMapping whenValueOfKeyPath:@"type" isEqualTo:@"Parent"]; - [dynamicMapping setObjectMapping:childMapping whenValueOfKeyPath:@"type" isEqualTo:@"Child"]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"type" expectedValue:@"Parent" objectMapping:parentMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"type" expectedValue:@"Child" objectMapping:childMapping]]; RKObjectMapping *mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"parent.json"]]; expect(mapping).notTo.beNil(); diff --git a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m index 5a84363672..30ee3ea487 100644 --- a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m @@ -300,7 +300,7 @@ - (void)testThatManagedObjectMappedAsTheRelationshipOfNonManagedObjectsWithADyna [entityMapping addAttributeMappingsFromArray:@[ @"name" ]]; [userMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"favorite_cat" toKeyPath:@"friends" withMapping:entityMapping]]; RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - [dynamicMapping setObjectMapping:userMapping whenValueOfKeyPath:@"name" isEqualTo:@"Blake Watters"]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"name" expectedValue:@"Blake Watters" objectMapping:userMapping]]; RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping pathPattern:nil keyPath:@"human" statusCodes:[NSIndexSet indexSetWithIndex:200]]; NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/with_to_one_relationship.json" relativeToURL:[RKTestFactory baseURL]]]; diff --git a/Tests/Logic/Network/RKRequestDescriptorTest.m b/Tests/Logic/Network/RKRequestDescriptorTest.m index 3705d1a9c0..87143908a0 100644 --- a/Tests/Logic/Network/RKRequestDescriptorTest.m +++ b/Tests/Logic/Network/RKRequestDescriptorTest.m @@ -51,7 +51,7 @@ - (void)testInvalidArgumentExceptionIsRaisedInInitializedWithDynamicMappingConta { RKObjectMapping *invalidMapping = [RKObjectMapping mappingForClass:[RKRequestDescriptorTest class]]; RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - [dynamicMapping setObjectMapping:invalidMapping whenValueOfKeyPath:@"whatever" isEqualTo:@"whatever"]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"whatever" expectedValue:@"whatever" objectMapping:invalidMapping]]; NSException *exception = nil; @try { diff --git a/Tests/Logic/ObjectMapping/RKDynamicMappingTest.m b/Tests/Logic/ObjectMapping/RKDynamicMappingTest.m new file mode 100644 index 0000000000..80528bcd83 --- /dev/null +++ b/Tests/Logic/ObjectMapping/RKDynamicMappingTest.m @@ -0,0 +1,146 @@ +// +// RKDynamicMappingTest.m +// RestKit +// +// Created by Blake Watters on 7/28/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestEnvironment.h" +#import "RKDynamicMapping.h" +#import "RKDynamicMappingModels.h" + +@interface RKDynamicMappingTest : RKTestCase + +@end + +@implementation RKDynamicMappingTest + +- (void)testShouldPickTheAppropriateMappingBasedOnAnAttributeValue +{ + RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; + RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; + [girlMapping addAttributeMappingsFromArray:@[@"name"]]; + RKObjectMapping *boyMapping = [RKObjectMapping mappingForClass:[Boy class]]; + [boyMapping addAttributeMappingsFromArray:@[@"name"]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"type" expectedValue:@"Girl" objectMapping:girlMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"type" expectedValue:@"Boy" objectMapping:boyMapping]]; + RKObjectMapping *mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]]; + assertThat(mapping, is(notNilValue())); + assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Girl"))); + mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]]; + assertThat(mapping, is(notNilValue())); + assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); +} + +- (void)testShouldMatchOnAnNSNumberAttributeValue +{ + RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; + RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; + [girlMapping addAttributeMappingsFromArray:@[@"name"]]; + RKObjectMapping *boyMapping = [RKObjectMapping mappingForClass:[Boy class]]; + [boyMapping addAttributeMappingsFromArray:@[@"name"]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"numeric_type" expectedValue:@(0) objectMapping:girlMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"numeric_type" expectedValue:@(1) objectMapping:boyMapping]]; + RKObjectMapping *mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]]; + assertThat(mapping, is(notNilValue())); + assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Girl"))); + mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]]; + assertThat(mapping, is(notNilValue())); + assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); +} + +- (void)testMappingSelectionUsingPredicateMatchers +{ + RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; + RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; + [girlMapping addAttributeMappingsFromArray:@[@"name"]]; + RKObjectMapping *boyMapping = [RKObjectMapping mappingForClass:[Boy class]]; + [boyMapping addAttributeMappingsFromArray:@[@"name"]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithPredicate:[NSPredicate predicateWithFormat:@"numeric_type = 0"] objectMapping:girlMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithPredicate:[NSPredicate predicateWithFormat:@"numeric_type = 1"] objectMapping:boyMapping]]; + RKObjectMapping *mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]]; + assertThat(mapping, is(notNilValue())); + assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Girl"))); + mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]]; + assertThat(mapping, is(notNilValue())); + assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); +} + +- (void)testThatRegistrationOfMatcherASecondTimeMovesToTopOfTheStack +{ + RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; + RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; + [girlMapping addAttributeMappingsFromArray:@[@"name"]]; + RKObjectMapping *boyMapping = [RKObjectMapping mappingForClass:[Boy class]]; + [boyMapping addAttributeMappingsFromArray:@[@"name"]]; + RKObjectMappingMatcher *girlMatcher = [RKObjectMappingMatcher matcherWithPredicate:[NSPredicate predicateWithFormat:@"numeric_type = 0"] objectMapping:girlMapping]; + RKObjectMappingMatcher *boyMatcher = [RKObjectMappingMatcher matcherWithPredicate:[NSPredicate predicateWithFormat:@"numeric_type = 0"] objectMapping:boyMapping]; + [dynamicMapping addMatcher:girlMatcher]; + [dynamicMapping addMatcher:boyMatcher]; + + // Frst time you get a Girl + RKObjectMapping *mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]]; + assertThat(mapping, is(notNilValue())); + assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Girl"))); + + // Reregister boyMatcher + [dynamicMapping addMatcher:boyMatcher]; + mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]]; + assertThat(mapping, is(notNilValue())); + assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); +} + +- (void)testIteratingAndRemovingAllMatchers +{ + RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; + RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; + [girlMapping addAttributeMappingsFromArray:@[@"name"]]; + RKObjectMapping *boyMapping = [RKObjectMapping mappingForClass:[Boy class]]; + [boyMapping addAttributeMappingsFromArray:@[@"name"]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithPredicate:[NSPredicate predicateWithFormat:@"numeric_type = 0"] objectMapping:girlMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithPredicate:[NSPredicate predicateWithFormat:@"numeric_type = 1"] objectMapping:boyMapping]]; + + for (RKObjectMappingMatcher *matcher in dynamicMapping.matchers) { + [dynamicMapping removeMatcher:matcher]; + } + expect(dynamicMapping.matchers).to.beEmpty(); +} + +- (void)testShouldPickTheAppropriateMappingBasedOnBlockDelegateCallback +{ + RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; + [dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) { + if ([[representation valueForKey:@"type"] isEqualToString:@"Girl"]) { + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[Girl class]]; + [mapping addAttributeMappingsFromArray:@[@"name"]]; + return mapping; + } 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"))); + mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]]; + assertThat(mapping, is(notNilValue())); + assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); +} + +@end diff --git a/Tests/Logic/ObjectMapping/RKDynamicObjectMappingTest.m b/Tests/Logic/ObjectMapping/RKDynamicObjectMappingTest.m deleted file mode 100644 index 19f9eb5f17..0000000000 --- a/Tests/Logic/ObjectMapping/RKDynamicObjectMappingTest.m +++ /dev/null @@ -1,89 +0,0 @@ -// -// RKDynamicMappingTest.m -// RestKit -// -// Created by Blake Watters on 7/28/11. -// Copyright (c) 2009-2012 RestKit. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import "RKTestEnvironment.h" -#import "RKDynamicMapping.h" -#import "RKDynamicMappingModels.h" - -@interface RKDynamicMappingTest : RKTestCase - -@end - -@implementation RKDynamicMappingTest - -- (void)testShouldPickTheAppropriateMappingBasedOnAnAttributeValue -{ - RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; - [girlMapping addAttributeMappingsFromArray:@[@"name"]]; - RKObjectMapping *boyMapping = [RKObjectMapping mappingForClass:[Boy class]]; - [boyMapping addAttributeMappingsFromArray:@[@"name"]]; - [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"type" isEqualTo:@"Girl"]; - [dynamicMapping setObjectMapping:boyMapping whenValueOfKeyPath:@"type" isEqualTo:@"Boy"]; - RKObjectMapping *mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]]; - assertThat(mapping, is(notNilValue())); - assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Girl"))); - mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]]; - assertThat(mapping, is(notNilValue())); - assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); -} - -- (void)testShouldMatchOnAnNSNumberAttributeValue -{ - RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; - [girlMapping addAttributeMappingsFromArray:@[@"name"]]; - RKObjectMapping *boyMapping = [RKObjectMapping mappingForClass:[Boy class]]; - [boyMapping addAttributeMappingsFromArray:@[@"name"]]; - [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"numeric_type" isEqualTo:[NSNumber numberWithInt:0]]; - [dynamicMapping setObjectMapping:boyMapping whenValueOfKeyPath:@"numeric_type" isEqualTo:[NSNumber numberWithInt:1]]; - RKObjectMapping *mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]]; - assertThat(mapping, is(notNilValue())); - assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Girl"))); - mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]]; - assertThat(mapping, is(notNilValue())); - assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); -} - -- (void)testShouldPickTheAppropriateMappingBasedOnBlockDelegateCallback -{ - RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - [dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) { - if ([[representation valueForKey:@"type"] isEqualToString:@"Girl"]) { - RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[Girl class]]; - [mapping addAttributeMappingsFromArray:@[@"name"]]; - return mapping; - } 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"))); - mapping = [dynamicMapping objectMappingForRepresentation:[RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]]; - assertThat(mapping, is(notNilValue())); - assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); -} - -@end diff --git a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m index 4438020d50..6b8d604a61 100644 --- a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m @@ -515,7 +515,7 @@ - (void)testThatResponseDescriptorWithDynamicMappingContainingEntityMappingsTrig { RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:_objectManager.managedObjectStore]; RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - [dynamicMapping setObjectMapping:humanMapping whenValueOfKeyPath:@"whatever" isEqualTo:@"whatever"]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"whatever" expectedValue:@"whatever" objectMapping:humanMapping]]; RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping pathPattern:nil keyPath:nil statusCodes:nil]; RKObjectManager *manager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:@"http://restkit.org"]]; manager.managedObjectStore = [RKTestFactory managedObjectStore]; diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m index f458c40e06..582ae516e8 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m @@ -1924,8 +1924,8 @@ - (void)testShouldMapASingleObjectDynamicallyWithADeclarativeMatcher RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; [girlMapping addAttributeMappingsFromArray:@[@"name"]]; RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - [dynamicMapping setObjectMapping:boyMapping whenValueOfKeyPath:@"type" isEqualTo:@"Boy"]; - [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"type" isEqualTo:@"Girl"]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"type" expectedValue:@"Boy" objectMapping:boyMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"type" expectedValue:@"Girl" objectMapping:girlMapping]]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; [mappingsDictionary setObject:dynamicMapping forKey:[NSNull null]]; @@ -1945,8 +1945,8 @@ - (void)testShouldACollectionOfObjectsDynamically RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; [girlMapping addAttributeMappingsFromArray:@[@"name"]]; RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - [dynamicMapping setObjectMapping:boyMapping whenValueOfKeyPath:@"type" isEqualTo:@"Boy"]; - [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"type" isEqualTo:@"Girl"]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"type" expectedValue:@"Boy" objectMapping:boyMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"type" expectedValue:@"Girl" objectMapping:girlMapping]]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; [mappingsDictionary setObject:dynamicMapping forKey:[NSNull null]]; @@ -1971,8 +1971,8 @@ - (void)testShouldMapARelationshipDynamically RKObjectMapping *girlMapping = [RKObjectMapping mappingForClass:[Girl class]]; [girlMapping addAttributeMappingsFromArray:@[@"name"]]; RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; - [dynamicMapping setObjectMapping:boyMapping whenValueOfKeyPath:@"type" isEqualTo:@"Boy"]; - [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"type" isEqualTo:@"Girl"]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"type" expectedValue:@"Boy" objectMapping:boyMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"type" expectedValue:@"Girl" objectMapping:girlMapping]]; [boyMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:dynamicMapping]];; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; diff --git a/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m b/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m index b67bfa2b80..dcdcf330ba 100644 --- a/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m @@ -486,8 +486,8 @@ - (void)testParameterizationUsingDynamicMapping [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)]; + [flightSearchMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"mode" expectedValue:@(RKSearchByFlightNumberMode) objectMapping:flightNumberMapping]]; + [flightSearchMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"mode" expectedValue:@(RKSearchByRouteMode) objectMapping:routeMapping]]; RKDynamicParameterizationFlightSearch *flightSearch = [RKDynamicParameterizationFlightSearch new]; flightSearch.airlineID = @5678; @@ -525,7 +525,7 @@ - (void)testDynamicParameterizationIncludingADate [concreteMapping addAttributeMappingsFromDictionary:@{ @"departureDate": @"departure_date" }]; RKDynamicMapping *flightSearchMapping = [RKDynamicMapping new]; - [flightSearchMapping setObjectMapping:concreteMapping whenValueOfKeyPath:@"mode" isEqualTo:@(RKSearchByFlightNumberMode)]; + [flightSearchMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"mode" expectedValue:@(RKSearchByFlightNumberMode) objectMapping:concreteMapping]]; RKDynamicParameterizationFlightSearch *flightSearch = [RKDynamicParameterizationFlightSearch new]; flightSearch.airlineID = @5678; From 84c6822d25ca2fa5f68c18eb40cdb2b63ec8d297 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 28 Dec 2012 00:29:27 -0500 Subject: [PATCH 04/27] Implement Tarjan's algorithm to efficiently traverse the `RKResponseDescriptors` within the `RKManagedObjectRequestOperation` and compute the key paths to all managed objects. refs #1113, refs #1112 --- .../Network/RKManagedObjectRequestOperation.m | 84 +++++++++++++++---- .../RKManagedObjectRequestOperationTest.m | 22 +++++ 2 files changed, 89 insertions(+), 17 deletions(-) diff --git a/Code/Network/RKManagedObjectRequestOperation.m b/Code/Network/RKManagedObjectRequestOperation.m index fe2166a493..2668123563 100644 --- a/Code/Network/RKManagedObjectRequestOperation.m +++ b/Code/Network/RKManagedObjectRequestOperation.m @@ -35,16 +35,27 @@ #undef RKLogComponent #define RKLogComponent RKlcl_cRestKitCoreData +/** + This class implements Tarjan's algorithm to efficiently visit all nodes within the mapping graph and detect cycles in the graph. + + For more details on the algorithm, refer to the Wikipedia page: http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm + + The following reference implementations were used when building out an Objective-C implementation: + + 1. http://algowiki.net/wiki/index.php?title=Tarjan%27s_algorithm + 1. http://www.logarithmic.net/pfh-files/blog/01208083168/sort.py + + */ @interface RKNestedManagedObjectKeyPathMappingGraphVisitor : NSObject - @property (nonatomic, readonly) NSSet *keyPaths; - - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors; - @end @interface RKNestedManagedObjectKeyPathMappingGraphVisitor () @property (nonatomic, strong) NSMutableSet *mutableKeyPaths; +@property (nonatomic, strong) NSMutableArray *visitationStack; +@property (nonatomic, strong) NSMutableDictionary *lowValues; +@property (nonatomic, strong) NSNumber *numberOfDecriptors; @end @implementation RKNestedManagedObjectKeyPathMappingGraphVisitor @@ -53,7 +64,11 @@ - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors { self = [self init]; if (self) { + self.numberOfDecriptors = @([responseDescriptors count]); self.mutableKeyPaths = [NSMutableSet set]; + self.visitationStack = [NSMutableArray array]; + self.lowValues = [NSMutableDictionary dictionary]; + for (RKResponseDescriptor *responseDescriptor in responseDescriptors) { [self visitMapping:responseDescriptor.mapping atKeyPath:responseDescriptor.keyPath]; } @@ -61,30 +76,65 @@ - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors return self; } -- (NSSet *)keyPaths -{ - return self.mutableKeyPaths; -} - +// Traverse the mappings graph using Tarjan's algorithm - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath { - id actualKeyPath = keyPath ?: [NSNull null]; - if ([self.keyPaths containsObject:actualKeyPath]) return; - if ([mapping isKindOfClass:[RKEntityMapping class]]) [self.mutableKeyPaths addObject:actualKeyPath]; - if ([mapping isKindOfClass:[RKDynamicMapping class]]) { - RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)mapping; - for (RKMapping *nestedMapping in dynamicMapping.objectMappings) { - [self visitMapping:nestedMapping atKeyPath:keyPath]; - } - } else if ([mapping isKindOfClass:[RKObjectMapping class]]) { + NSValue *dictionaryKey = [NSValue valueWithNonretainedObject:mapping]; + if ([self.lowValues objectForKey:dictionaryKey]) { + // This key path points to a cycle back into the graph + if ([mapping isKindOfClass:[RKEntityMapping class]]) [self.mutableKeyPaths addObject:keyPath]; + return; + } + + NSNumber *lowValue = @([self.lowValues count]); + [self.lowValues setObject:lowValue forKey:dictionaryKey]; + NSUInteger stackPosition = [self.visitationStack count]; + [self.visitationStack addObject:@{ @"mapping": mapping, @"keyPath": keyPath ?: [NSNull null] }]; + + if ([mapping isKindOfClass:[RKObjectMapping class]]) { RKObjectMapping *objectMapping = (RKObjectMapping *)mapping; for (RKRelationshipMapping *relationshipMapping in objectMapping.relationshipMappings) { NSString *nestedKeyPath = keyPath ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath; [self visitMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath]; + + // We want the minimum value + NSValue *relationshipKey = [NSValue valueWithNonretainedObject:relationshipMapping.mapping]; + NSNumber *relationshipLowValue = [self.lowValues objectForKey:relationshipKey]; + if ([lowValue compare:relationshipLowValue] == NSOrderedDescending) { + [self.lowValues setObject:relationshipLowValue forKey:dictionaryKey]; + } + } + } else if ([mapping isKindOfClass:[RKDynamicMapping class]]) { + RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)mapping; + for (RKMapping *nestedMapping in dynamicMapping.objectMappings) { + [self visitMapping:nestedMapping atKeyPath:keyPath]; + } + } + + if ([[self.lowValues objectForKey:dictionaryKey] isEqualToNumber:lowValue]) { + NSRange range = NSMakeRange(stackPosition, [self.visitationStack count] - stackPosition); + NSArray *mappingDetails = [self.visitationStack subarrayWithRange:range]; + [self.visitationStack removeObjectsInRange:range]; + + NSArray *mappings = [mappingDetails valueForKey:@"mapping"]; + for (NSDictionary *dictionary in mappingDetails) { + NSString *keyPath = [dictionary objectForKey:@"keyPath"]; + NSString *mapping = [dictionary objectForKey:@"mapping"]; + if ([mapping isKindOfClass:[RKEntityMapping class]]) [self.mutableKeyPaths addObject:keyPath]; + } + + for (RKMapping *mapping in mappings) { + NSValue *relationshipKey = [NSValue valueWithNonretainedObject:mapping]; + [self.lowValues setObject:self.numberOfDecriptors forKey:relationshipKey]; } } } +- (NSSet *)keyPaths +{ + return self.mutableKeyPaths; +} + @end NSArray *RKArrayOfFetchRequestFromBlocksWithURL(NSArray *fetchRequestBlocks, NSURL *URL) diff --git a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m index 30ee3ea487..60462bcc8e 100644 --- a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m @@ -539,6 +539,28 @@ - (void)testPruningOfSubkeypathsFromSet expect(prunedSet).to.equal(expectedSet); } +- (void)testPathVisitationDoesNotRecurseInfinitelyForSelfReferentialMappings +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKHuman *orphanedHuman = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + [entityMapping addAttributeMappingsFromArray:@[ @"name" ]]; + [entityMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:entityMapping]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:entityMapping pathPattern:nil keyPath:@"human" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/with_to_one_relationship.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Human"]; + }; + managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).to.beNil(); + expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); + expect(orphanedHuman.managedObjectContext).to.beNil(); +} + - (void)testThatMappingObjectsWithTheSameIdentificationAttributesAcrossTwoObjectRequestOperationConcurrentlyDoesNotCreateDuplicateObjects { RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; From 6493282772919c9342199fac84ff2c69cefee947 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 28 Dec 2012 13:47:13 -0500 Subject: [PATCH 05/27] Rework implementation of managed object deletion and refetching to use the refreshed visitor implementation. refs #1111, #1113 --- .../Network/RKManagedObjectRequestOperation.m | 266 +++++++++++------- .../JSON/humans/nested_self_referential.json | 61 ++++ .../JSON/humans/self_referential.json | 26 ++ .../RKManagedObjectRequestOperationTest.m | 136 +++++++++ 4 files changed, 395 insertions(+), 94 deletions(-) create mode 100644 Tests/Fixtures/JSON/humans/nested_self_referential.json create mode 100644 Tests/Fixtures/JSON/humans/self_referential.json diff --git a/Code/Network/RKManagedObjectRequestOperation.m b/Code/Network/RKManagedObjectRequestOperation.m index 2668123563..393f157434 100644 --- a/Code/Network/RKManagedObjectRequestOperation.m +++ b/Code/Network/RKManagedObjectRequestOperation.m @@ -35,6 +35,23 @@ #undef RKLogComponent #define RKLogComponent RKlcl_cRestKitCoreData +@interface RKMappingGraphVisitation : NSObject +@property (nonatomic, strong) id rootKey; // Will be [NSNull null] or a string value +@property (nonatomic, strong) NSString *keyPath; +@property (nonatomic, assign, getter = isCyclic) BOOL cyclic; +@property (nonatomic, strong) RKMapping *mapping; +@end + +@implementation RKMappingGraphVisitation + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p rootKey=%@ keyPath=%@ isCylic=%@ mapping=%@>", + [self class], self, self.rootKey, self.keyPath, self.isCyclic ? @"YES" : @"NO", self.mapping]; +} + +@end + /** This class implements Tarjan's algorithm to efficiently visit all nodes within the mapping graph and detect cycles in the graph. @@ -47,15 +64,15 @@ */ @interface RKNestedManagedObjectKeyPathMappingGraphVisitor : NSObject -@property (nonatomic, readonly) NSSet *keyPaths; +@property (nonatomic, readonly, strong) NSMutableArray *visitations; - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors; @end @interface RKNestedManagedObjectKeyPathMappingGraphVisitor () -@property (nonatomic, strong) NSMutableSet *mutableKeyPaths; @property (nonatomic, strong) NSMutableArray *visitationStack; @property (nonatomic, strong) NSMutableDictionary *lowValues; @property (nonatomic, strong) NSNumber *numberOfDecriptors; +@property (nonatomic, strong, readwrite) NSMutableArray *visitations; @end @implementation RKNestedManagedObjectKeyPathMappingGraphVisitor @@ -65,37 +82,62 @@ - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors self = [self init]; if (self) { self.numberOfDecriptors = @([responseDescriptors count]); - self.mutableKeyPaths = [NSMutableSet set]; self.visitationStack = [NSMutableArray array]; self.lowValues = [NSMutableDictionary dictionary]; + self.visitations = [NSMutableArray array]; for (RKResponseDescriptor *responseDescriptor in responseDescriptors) { [self visitMapping:responseDescriptor.mapping atKeyPath:responseDescriptor.keyPath]; } } + return self; } +- (RKMappingGraphVisitation *)visitationForMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath +{ + RKMappingGraphVisitation *visitation = [RKMappingGraphVisitation new]; + visitation.mapping = mapping; + if ([self.visitationStack count] == 0) { + // If we are the first item in the stack, we are visiting the rootKey + visitation.rootKey = keyPath ?: [NSNull null]; + } else { + // Take the root key from the visitation stack + visitation.rootKey = [[self.visitationStack objectAtIndex:0] rootKey]; + visitation.keyPath = keyPath; + } + + return visitation; +} + // Traverse the mappings graph using Tarjan's algorithm - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath { NSValue *dictionaryKey = [NSValue valueWithNonretainedObject:mapping]; if ([self.lowValues objectForKey:dictionaryKey]) { - // This key path points to a cycle back into the graph - if ([mapping isKindOfClass:[RKEntityMapping class]]) [self.mutableKeyPaths addObject:keyPath]; + if (![ mapping isKindOfClass:[RKEntityMapping class]]) return; + + NSArray *keyPathComponents = [[self.visitationStack valueForKey:@"keyPath"] arrayByAddingObject:keyPath]; + NSString *keyPath = [[keyPathComponents subarrayWithRange:NSMakeRange(1, [keyPathComponents count] - 1)] componentsJoinedByString:@"."]; + + RKMappingGraphVisitation *cyclicVisitation = [self visitationForMapping:mapping atKeyPath:keyPath]; + cyclicVisitation.cyclic = YES; + [self.visitations addObject:cyclicVisitation]; + return; } NSNumber *lowValue = @([self.lowValues count]); [self.lowValues setObject:lowValue forKey:dictionaryKey]; + NSUInteger stackPosition = [self.visitationStack count]; - [self.visitationStack addObject:@{ @"mapping": mapping, @"keyPath": keyPath ?: [NSNull null] }]; + RKMappingGraphVisitation *visitation = [self visitationForMapping:mapping atKeyPath:keyPath]; + [self.visitationStack addObject:visitation]; if ([mapping isKindOfClass:[RKObjectMapping class]]) { RKObjectMapping *objectMapping = (RKObjectMapping *)mapping; for (RKRelationshipMapping *relationshipMapping in objectMapping.relationshipMappings) { - NSString *nestedKeyPath = keyPath ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath; - [self visitMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath]; + [self visitMapping:relationshipMapping.mapping atKeyPath:relationshipMapping.destinationKeyPath]; // We want the minimum value NSValue *relationshipKey = [NSValue valueWithNonretainedObject:relationshipMapping.mapping]; @@ -105,36 +147,38 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath } } } else if ([mapping isKindOfClass:[RKDynamicMapping class]]) { - RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)mapping; - for (RKMapping *nestedMapping in dynamicMapping.objectMappings) { + // Pop the visitation stack to remove the dynamic mapping, since each mapping within the dynamic mapping + // is rooted at the same point in the graph + [self.visitationStack removeLastObject]; + + for (RKMapping *nestedMapping in [(RKDynamicMapping *)mapping objectMappings]) { [self visitMapping:nestedMapping atKeyPath:keyPath]; } } if ([[self.lowValues objectForKey:dictionaryKey] isEqualToNumber:lowValue]) { NSRange range = NSMakeRange(stackPosition, [self.visitationStack count] - stackPosition); - NSArray *mappingDetails = [self.visitationStack subarrayWithRange:range]; + NSArray *visitations = [self.visitationStack subarrayWithRange:range]; [self.visitationStack removeObjectsInRange:range]; - NSArray *mappings = [mappingDetails valueForKey:@"mapping"]; - for (NSDictionary *dictionary in mappingDetails) { - NSString *keyPath = [dictionary objectForKey:@"keyPath"]; - NSString *mapping = [dictionary objectForKey:@"mapping"]; - if ([mapping isKindOfClass:[RKEntityMapping class]]) [self.mutableKeyPaths addObject:keyPath]; - } + // Take everything left on the stack + NSArray *keyPathComponents = [self.visitationStack valueForKey:@"keyPath"]; + NSString *nestingKeyPath = ([keyPathComponents count] > 1) ? [[keyPathComponents subarrayWithRange:NSMakeRange(1, [keyPathComponents count] - 1)] componentsJoinedByString:@"."] : nil; - for (RKMapping *mapping in mappings) { + [visitations enumerateObjectsUsingBlock:^(RKMappingGraphVisitation *visitation, NSUInteger idx, BOOL *stop) { + // If this is an entity mapping, collect the complete key path + if ([visitation.mapping isKindOfClass:[RKEntityMapping class]]) { + visitation.keyPath = nestingKeyPath ? [@[ nestingKeyPath, visitation.keyPath ] componentsJoinedByString:@"."] : visitation.keyPath; + [self.visitations addObject:visitation]; + } + + // Update the low value NSValue *relationshipKey = [NSValue valueWithNonretainedObject:mapping]; [self.lowValues setObject:self.numberOfDecriptors forKey:relationshipKey]; - } + }]; } } -- (NSSet *)keyPaths -{ - return self.mutableKeyPaths; -} - @end NSArray *RKArrayOfFetchRequestFromBlocksWithURL(NSArray *fetchRequestBlocks, NSURL *URL) @@ -148,6 +192,49 @@ - (NSSet *)keyPaths return fetchRequests; } +static NSSet *RKFlattenCollectionToSet(id collection) +{ + NSMutableSet *mutableSet = [NSMutableSet set]; + if ([collection conformsToProtocol:@protocol(NSFastEnumeration)]) { + for (id nestedObject in collection) { + if ([nestedObject conformsToProtocol:@protocol(NSFastEnumeration)]) { + if ([nestedObject isKindOfClass:[NSArray class]]) { + [mutableSet unionSet:RKFlattenCollectionToSet([NSSet setWithArray:nestedObject])]; + } else if ([nestedObject isKindOfClass:[NSSet class]]) { + [mutableSet unionSet:RKFlattenCollectionToSet(nestedObject)]; + } else if ([nestedObject isKindOfClass:[NSOrderedSet class]]) { + [mutableSet unionSet:RKFlattenCollectionToSet([(NSOrderedSet *)nestedObject set])]; + } + } else { + [mutableSet addObject:nestedObject]; + } + } + } else if (collection) { + [mutableSet addObject:collection]; + } + + return mutableSet; +} + +/** + Traverses a set of cyclic key paths within the mapping result. Because these relationships are cyclic, we continue collecting managed objects and traversing until the values returned by the key path are a complete subset of all objects already in the set. + */ +static void RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(id graph, NSSet *cyclicKeyPaths, NSMutableSet *mutableSet) +{ + if ([graph respondsToSelector:@selector(count)] && [graph count] == 0) return; + + for (NSString *cyclicKeyPath in cyclicKeyPaths) { + NSSet *objectsAtCyclicKeyPath = RKFlattenCollectionToSet([graph valueForKeyPath:cyclicKeyPath]); + if ([objectsAtCyclicKeyPath count] == 0 || [objectsAtCyclicKeyPath isEqualToSet:[NSSet setWithObject:[NSNull null]]]) continue; + if (! [objectsAtCyclicKeyPath isSubsetOfSet:mutableSet]) { + [mutableSet unionSet:objectsAtCyclicKeyPath]; + for (id nestedValue in objectsAtCyclicKeyPath) { + RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(nestedValue, cyclicKeyPaths, mutableSet); + } + } + } +} + /** Returns the set of keys containing the outermost nesting keypath for all children. For example, given a set containing: 'this', 'this.that', 'another.one.test', 'another.two.test', 'another.one.test.nested' @@ -170,28 +257,17 @@ - (NSSet *)keyPaths }]; } -// When we map the root object, it is returned under the key `[NSNull null]` -static id RKMappedValueForKeyPathInDictionary(NSString *keyPath, NSDictionary *dictionary) -{ - @try { - return ([keyPath isEqual:[NSNull null]]) ? [dictionary objectForKey:[NSNull null]] : [dictionary valueForKeyPath:keyPath]; - } - @catch (NSException *exception) { - if ([[exception name] isEqualToString:NSUndefinedKeyException]) { - RKLogWarning(@"Caught undefined key exception for keyPath '%@' in mapping result: This likely indicates an ambiguous keyPath is used across response descriptor or dynamic mappings.", keyPath); - return nil; - } - - [exception raise]; - } -} - -static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath, NSMutableDictionary *dictionary) +static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSString *keyPath, NSMutableDictionary *dictionary) { NSCParameterAssert(value); - NSCParameterAssert(keyPath); + NSCParameterAssert(rootKey); NSCParameterAssert(dictionary); - [keyPath isEqual:[NSNull null]] ? [dictionary setObject:value forKey:keyPath] : [dictionary setValue:value forKeyPath:keyPath]; + if (keyPath && ![keyPath isEqual:[NSNull null]]) { + id valueAtRootKey = [dictionary objectForKey:rootKey]; + [valueAtRootKey setValue:value forKeyPath:keyPath]; + } else { + [dictionary setObject:value forKey:rootKey]; + } } // Precondition: Must be called from within the correct context @@ -210,44 +286,52 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath, } // Finds the key paths for all entity mappings in the graph whose parent objects are not other managed objects -static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSSet *keyPaths, NSManagedObjectContext *managedObjectContext) +static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsInVisitationsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSArray *visitations, NSManagedObjectContext *managedObjectContext) { if (! [dictionaryOfManagedObjects count]) return dictionaryOfManagedObjects; NSMutableDictionary *newDictionary = [dictionaryOfManagedObjects mutableCopy]; [managedObjectContext performBlockAndWait:^{ - for (NSString *keyPath in keyPaths) { - id value = RKMappedValueForKeyPathInDictionary(keyPath, dictionaryOfManagedObjects); - if (! value) { - continue; - } else if ([value isKindOfClass:[NSArray class]]) { - BOOL isMutable = [value isKindOfClass:[NSMutableArray class]]; - NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]]; - for (__strong id object in value) { - if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); - if (object) [newValue addObject:object]; - } - value = (isMutable) ? newValue : [newValue copy]; - } else if ([value isKindOfClass:[NSSet class]]) { - BOOL isMutable = [value isKindOfClass:[NSMutableSet class]]; - NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]]; - for (__strong id object in value) { - if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); - if (object) [newValue addObject:object]; + NSArray *rootKeys = [visitations valueForKey:@"rootKey"]; + for (id rootKey in rootKeys) { + NSSet *keyPaths = [visitations valueForKey:@"keyPath"]; + // If keyPaths contains null, then the root object is a managed object and we only need to refetch it + NSSet *nonNestedKeyPaths = ([keyPaths containsObject:[NSNull null]]) ? [NSSet setWithObject:[NSNull null]] : RKSetByRemovingSubkeypathsFromSet(keyPaths); + + NSDictionary *mappingResultsAtRootKey = [dictionaryOfManagedObjects objectForKey:rootKey]; + for (NSString *keyPath in nonNestedKeyPaths) { + id value = [keyPath isEqual:[NSNull null]] ? mappingResultsAtRootKey : [mappingResultsAtRootKey valueForKeyPath:keyPath]; + if (! value) { + continue; + } else if ([value isKindOfClass:[NSArray class]]) { + BOOL isMutable = [value isKindOfClass:[NSMutableArray class]]; + NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]]; + for (__strong id object in value) { + if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); + if (object) [newValue addObject:object]; + } + value = (isMutable) ? newValue : [newValue copy]; + } else if ([value isKindOfClass:[NSSet class]]) { + BOOL isMutable = [value isKindOfClass:[NSMutableSet class]]; + NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]]; + for (__strong id object in value) { + if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); + if (object) [newValue addObject:object]; + } + value = (isMutable) ? newValue : [newValue copy]; + } else if ([value isKindOfClass:[NSOrderedSet class]]) { + BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]]; + NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet]; + [(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) { + if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); + if (object) [newValue setObject:object atIndex:index]; + }]; + value = (isMutable) ? newValue : [newValue copy]; + } else if ([value isKindOfClass:[NSManagedObject class]]) { + value = RKRefetchManagedObjectInContext(value, managedObjectContext); } - value = (isMutable) ? newValue : [newValue copy]; - } else if ([value isKindOfClass:[NSOrderedSet class]]) { - BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]]; - NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet]; - [(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) { - if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); - if (object) [newValue setObject:object atIndex:index]; - }]; - value = (isMutable) ? newValue : [newValue copy]; - } else if ([value isKindOfClass:[NSManagedObject class]]) { - value = RKRefetchManagedObjectInContext(value, managedObjectContext); + + if (value) RKSetMappedValueForKeyPathInDictionary(value, rootKey, keyPath, newDictionary); } - - if (value) RKSetMappedValueForKeyPathInDictionary(value, keyPath, newDictionary); } }]; @@ -426,7 +510,7 @@ - (NSSet *)localObjectsFromFetchRequestsMatchingRequestURL:(NSError **)error return localObjects; } -- (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result atKeyPaths:(NSSet *)keyPaths error:(NSError **)error +- (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result withVisitor:(RKNestedManagedObjectKeyPathMappingGraphVisitor *)visitor error:(NSError **)error { if (! self.deletesOrphanedObjects) { RKLogDebug(@"Skipping deletion of orphaned objects: disabled as deletesOrphanedObjects=NO"); @@ -446,20 +530,16 @@ - (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result atK // Build an aggregate collection of all the managed objects in the mapping result NSMutableSet *managedObjectsInMappingResult = [NSMutableSet set]; NSDictionary *mappingResultDictionary = result.dictionary; - for (NSString *keyPath in keyPaths) { - id managedObjects = RKMappedValueForKeyPathInDictionary(keyPath, mappingResultDictionary); - if (! managedObjects) { - continue; - } else if ([managedObjects isKindOfClass:[NSManagedObject class]]) { - [managedObjectsInMappingResult addObject:managedObjects]; - } else if ([managedObjects isKindOfClass:[NSSet class]]) { - [managedObjectsInMappingResult unionSet:managedObjects]; - } else if ([managedObjects isKindOfClass:[NSArray class]]) { - [managedObjectsInMappingResult addObjectsFromArray:managedObjects]; - } else if ([managedObjects isKindOfClass:[NSOrderedSet class]]) { - [managedObjectsInMappingResult addObjectsFromArray:[managedObjects array]]; - } else { - [NSException raise:NSInternalInconsistencyException format:@"Unexpected object type '%@' encountered at keyPath '%@': Expected an `NSManagedObject`, `NSArray`, or `NSSet`.", [managedObjects class], keyPath]; + + for (RKMappingGraphVisitation *visitation in visitor.visitations) { + id objectsAtRoot = [mappingResultDictionary objectForKey:visitation.rootKey]; + id managedObjects = visitation.keyPath ? [objectsAtRoot valueForKeyPath:visitation.keyPath] : objectsAtRoot; + [managedObjectsInMappingResult unionSet:RKFlattenCollectionToSet(managedObjects)]; + + if (visitation.isCyclic) { + NSSet *cyclicKeyPaths = [NSSet setWithArray:[visitation valueForKeyPath:@"mapping.relationshipMappings.destinationKeyPath"]]; + [managedObjectsInMappingResult unionSet:RKFlattenCollectionToSet(managedObjects)]; + RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(managedObjects, cyclicKeyPaths, managedObjectsInMappingResult); } } @@ -530,7 +610,6 @@ - (void)willFinish // Construct a set of key paths to all of the managed objects in the mapping result RKNestedManagedObjectKeyPathMappingGraphVisitor *visitor = [[RKNestedManagedObjectKeyPathMappingGraphVisitor alloc] initWithResponseDescriptors:self.responseDescriptors]; - NSSet *managedObjectMappingResultKeyPaths = visitor.keyPaths; // Handle any cleanup success = [self deleteTargetObjectIfAppropriate:&error]; @@ -539,7 +618,7 @@ - (void)willFinish return; } - success = [self deleteLocalObjectsMissingFromMappingResult:self.mappingResult atKeyPaths:managedObjectMappingResultKeyPaths error:&error]; + success = [self deleteLocalObjectsMissingFromMappingResult:self.mappingResult withVisitor:visitor error:&error]; if (! success) { self.error = error; return; @@ -556,8 +635,7 @@ - (void)willFinish // Refetch all managed objects nested at key paths within the results dictionary before returning if (self.mappingResult) { - NSSet *nonNestedKeyPaths = RKSetByRemovingSubkeypathsFromSet(managedObjectMappingResultKeyPaths); - NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext([self.mappingResult dictionary], nonNestedKeyPaths, self.managedObjectContext); + NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsInVisitationsRefetchedInContext([self.mappingResult dictionary], visitor.visitations, self.managedObjectContext); self.mappingResult = [[RKMappingResult alloc] initWithDictionary:resultsDictionaryFromOriginalContext]; } } diff --git a/Tests/Fixtures/JSON/humans/nested_self_referential.json b/Tests/Fixtures/JSON/humans/nested_self_referential.json new file mode 100644 index 0000000000..d5f9258d5b --- /dev/null +++ b/Tests/Fixtures/JSON/humans/nested_self_referential.json @@ -0,0 +1,61 @@ +{ + "houses": [ + { + "houseID": 1, + "city": "New York City", + "state": "New York", + "owner": { + "humanID": 1, + "name": "Blake" + }, + "occupants": [ + { + "humanID": 2, + "name": "John", + "landlord": { + "humanID": 1, + "name": "Blake" + }, + "roommates": [ + { + "humanID": 3, + "name": "Mary", + "landlord": { + "humanID": 1, + "name": "Blake" + }, + "roommates": [ + { + "humanID": 2, + "name": "John" + }, + { + "humanID": 4, + "name": "Edward" + } + ] + }, + { + "humanID": 4, + "name": "Edward", + "landlord": { + "humanID": 1, + "name": "Blake" + }, + "roommates": [ + { + "humanID": 2, + "name": "John" + }, + { + "humanID": 3, + "name": "Mary" + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Tests/Fixtures/JSON/humans/self_referential.json b/Tests/Fixtures/JSON/humans/self_referential.json new file mode 100644 index 0000000000..a9188c3fb6 --- /dev/null +++ b/Tests/Fixtures/JSON/humans/self_referential.json @@ -0,0 +1,26 @@ +{ + "name": "Blake", + "id": 1, + "friends": [ + { + "name": "Sarah", + "id": 2, + "friends": [ + { + "name": "Monkey", + "id": 3 + } + ] + }, + { + "name": "Colin", + "id": 4, + "friends": [ + { + "id": "3", + "name": "Monkey" + } + ] + } + ] +} \ No newline at end of file diff --git a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m index 60462bcc8e..bd89100315 100644 --- a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m @@ -555,12 +555,148 @@ - (void)testPathVisitationDoesNotRecurseInfinitelyForSelfReferentialMappings }; managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ]; managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + managedObjectRequestOperation.managedObjectCache = managedObjectCache; [managedObjectRequestOperation start]; expect(managedObjectRequestOperation.error).to.beNil(); expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); expect(orphanedHuman.managedObjectContext).to.beNil(); } +- (void)testDeletionOfObjectsMappedFindsObjectsMappedBySelfReferentialMappings +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + entityMapping.identificationAttributes = @[ @"railsID" ]; + [entityMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"id": @"railsID" }]; + [entityMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:entityMapping]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:entityMapping pathPattern:nil keyPath:nil statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + // Create Blake, Sarah, Colin, Monkey & Orphan + NSManagedObjectContext *context = managedObjectStore.persistentStoreManagedObjectContext; + NSUInteger count = [context countForEntityForName:@"Human" predicate:nil error:nil]; + expect(count).to.equal(0); + RKHuman *blake = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(1), @"name": @"Blake" }]; + RKHuman *sarah = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(2), @"name": @"Sarah" }]; + RKHuman *monkey = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(3), @"name": @"Monkey" }]; + RKHuman *colin = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(4), @"name": @"Colin" }]; + RKHuman *orphan = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(5), @"name": @"Orphan" }]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/self_referential.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Human"]; + }; + managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + managedObjectRequestOperation.managedObjectCache = managedObjectCache; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).to.beNil(); + expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); + + // Verify that orphan was deleted + count = [context countForEntityForName:@"Human" predicate:nil error:nil]; + expect(count).to.equal(4); + + expect(blake.managedObjectContext).notTo.beNil(); + expect(sarah.managedObjectContext).notTo.beNil(); + expect(monkey.managedObjectContext).notTo.beNil(); + expect(colin.managedObjectContext).notTo.beNil(); + expect(orphan.managedObjectContext).to.beNil(); +} + +- (void)testDeletionOfObjectsMappedFindsObjectsMappedByNestedSelfReferentialMappings +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *houseMapping = [RKEntityMapping mappingForEntityForName:@"House" inManagedObjectStore:managedObjectStore]; + [houseMapping addAttributeMappingsFromDictionary:@{ @"houseID": @"railsID" }]; + [houseMapping addAttributeMappingsFromArray:@[ @"city", @"state" ]]; + houseMapping.identificationAttributes = @[ @"railsID" ]; + + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"humanID": @"railsID" }]; + [humanMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"roommates" toKeyPath:@"friends" withMapping:humanMapping]]; + [humanMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"landlord" toKeyPath:@"landlord" withMapping:humanMapping]]; + + [houseMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"owner" toKeyPath:@"owner" withMapping:humanMapping]]; + [houseMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"occupants" toKeyPath:@"occupants" withMapping:humanMapping]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:houseMapping pathPattern:nil keyPath:@"houses" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + // Create Blake, Sarah, Colin, Monkey & Orphan + NSManagedObjectContext *context = managedObjectStore.persistentStoreManagedObjectContext; + RKHuman *orphan = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(5), @"name": @"Orphan" }]; + RKHuman *edward = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(4), @"name": @"Edward" }]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/nested_self_referential.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + RKFetchRequestBlock humanFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Human"]; + }; + RKFetchRequestBlock houseFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"House"]; + }; + managedObjectRequestOperation.fetchRequestBlocks = @[ humanFetchRequestBlock, houseFetchRequestBlock ]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + managedObjectRequestOperation.deletesOrphanedObjects = YES; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + managedObjectRequestOperation.managedObjectCache = managedObjectCache; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).to.beNil(); + expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); + + NSUInteger count = [context countForEntityForName:@"Human" predicate:nil error:nil]; + expect(count).to.equal(4); + + count = [context countForEntityForName:@"House" predicate:nil error:nil]; + expect(count).to.equal(1); + + expect(edward.managedObjectContext).notTo.beNil(); + expect(orphan.managedObjectContext).to.beNil(); +} + +- (void)testMappingWithDynamicMappingContainingIncompatibleEntityMappingsAtSameKeyPath +{ + RKDynamicMapping *dynamicMapping = [RKDynamicMapping new]; + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + humanMapping.identificationAttributes = @[ @"railsID" ]; + [humanMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"humanID": @"railsID" }]; + [humanMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"roommates" toKeyPath:@"friends" withMapping:humanMapping]]; + + RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore]; + RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore]; + parentMapping.identificationAttributes = @[ @"railsID" ]; + [parentMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"humanID": @"railsID" }]; + [parentMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"children" toKeyPath:@"children" withMapping:childMapping]]; + + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"invalid" expectedValue:@"whatever" objectMapping:humanMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"name" expectedValue:@"Blake" objectMapping:parentMapping]]; + + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping pathPattern:nil keyPath:@"houses.owner" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/nested_self_referential.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + RKFetchRequestBlock humanFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Human"]; + }; + RKFetchRequestBlock parentFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Parent"]; + }; + RKFetchRequestBlock childFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Child"]; + }; + managedObjectRequestOperation.fetchRequestBlocks = @[ humanFetchRequestBlock, parentFetchRequestBlock, childFetchRequestBlock ]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + managedObjectRequestOperation.deletesOrphanedObjects = YES; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + managedObjectRequestOperation.managedObjectCache = managedObjectCache; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).to.beNil(); + expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); +} + - (void)testThatMappingObjectsWithTheSameIdentificationAttributesAcrossTwoObjectRequestOperationConcurrentlyDoesNotCreateDuplicateObjects { RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; From 5a20e54de7dfdebfb0f9789ca953e1a151acdc0f Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 29 Dec 2012 20:32:12 -0500 Subject: [PATCH 06/27] Replace `[NSNull null]` in examples with nil to match current API --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 54da48540f..cd940003dc 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ operation.managedObjectCache = managedObjectStore.managedObjectCache; // You can map errors to any class, but `RKErrorMessage` is included for free RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[RKErrorMessage class]]; // The entire value at the source key path containing the errors maps to the message -[errorMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:[NSNull null] toKeyPath:@"errorMessage"]]; +[errorMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:nil toKeyPath:@"errorMessage"]]; NSIndexSet *statusCodes = RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError); // Any response in the 4xx status code range with an "errors" key path uses this mapping @@ -257,7 +257,7 @@ RKResponseDescriptor *articleDescriptor = [RKResponseDescriptor responseDescript RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[RKErrorMessage class]]; // The entire value at the source key path containing the errors maps to the message -[errorMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:[NSNull null] toKeyPath:@"message"]]; +[errorMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:nil toKeyPath:@"message"]]; NSIndexSet *statusCodes = RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError); // Any response in the 4xx status code range with an "errors" key path uses this mapping RKResponseDescriptor *errorDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:mapping pathPattern:nil keyPath:@"errors" statusCodes:statusCodes]; From b23ea410a8789e48c61faed3aecb07d7f3142651 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 29 Dec 2012 22:13:12 -0500 Subject: [PATCH 07/27] Add support and test for inversing a mapping containing attributes mapped with nil destination key paths. fixes #1116 --- Code/ObjectMapping/RKAttributeMapping.m | 2 +- Code/ObjectMapping/RKMappingOperation.m | 17 ++++++++++++++- .../Logic/ObjectMapping/RKObjectMappingTest.m | 21 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Code/ObjectMapping/RKAttributeMapping.m b/Code/ObjectMapping/RKAttributeMapping.m index a09064507a..ff93e29261 100644 --- a/Code/ObjectMapping/RKAttributeMapping.m +++ b/Code/ObjectMapping/RKAttributeMapping.m @@ -29,7 +29,7 @@ @implementation RKAttributeMapping + (instancetype)attributeMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath { - NSParameterAssert(destinationKeyPath); + NSAssert(sourceKeyPath || destinationKeyPath, @"Both the source and destination key paths cannot be nil"); RKAttributeMapping *attributeMapping = [self new]; attributeMapping.sourceKeyPath = sourceKeyPath; attributeMapping.destinationKeyPath = destinationKeyPath; diff --git a/Code/ObjectMapping/RKMappingOperation.m b/Code/ObjectMapping/RKMappingOperation.m index fb78e79465..d2114f939c 100644 --- a/Code/ObjectMapping/RKMappingOperation.m +++ b/Code/ObjectMapping/RKMappingOperation.m @@ -71,6 +71,8 @@ id RKTransformedValueWithClass(id value, Class destinationType, NSValueTransform if ([value isKindOfClass:destinationType]) { // No transformation necessary return value; + } else if ([destinationType isSubclassOfClass:[NSDictionary class]]) { + return [NSMutableDictionary dictionaryWithObject:[NSMutableDictionary dictionary] forKey:value]; } else if (RKClassIsCollection(destinationType) && !RKObjectIsCollection(value)) { // Call ourself recursively with an array value to transform as appropriate return RKTransformedValueWithClass(@[ value ], destinationType, dateToStringValueTransformer); @@ -187,6 +189,15 @@ static id RKPrimitiveValueForNilValueOfClass(Class keyValueCodingClass) return nestedMappings; } +static void RKSetValueForObject(id value, id destinationObject) +{ + if ([destinationObject isKindOfClass:[NSMutableDictionary class]] && [value isKindOfClass:[NSDictionary class]]) { + [destinationObject setDictionary:value]; + } else { + [NSException raise:NSInvalidArgumentException format:@"Unable to set value for destination object of type '%@': no strategy available. %@ to %@", [destinationObject class], value, destinationObject]; + } +} + @interface RKMappingOperation () @property (nonatomic, strong, readwrite) RKMapping *mapping; @property (nonatomic, strong, readwrite) id sourceObject; @@ -369,7 +380,11 @@ - (void)applyAttributeMapping:(RKAttributeMapping *)attributeMapping withValue:( if ([self shouldSetValue:&value atKeyPath:attributeMapping.destinationKeyPath]) { RKLogTrace(@"Mapped attribute value from keyPath '%@' to '%@'. Value: %@", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath, value); - [self.destinationObject setValue:value forKeyPath:attributeMapping.destinationKeyPath]; + if (attributeMapping.destinationKeyPath) { + [self.destinationObject setValue:value forKeyPath:attributeMapping.destinationKeyPath]; + } else { + RKSetValueForObject(value, self.destinationObject); + } if ([self.delegate respondsToSelector:@selector(mappingOperation:didSetValue:forKeyPath:usingMapping:)]) { [self.delegate mappingOperation:self didSetValue:value forKeyPath:attributeMapping.destinationKeyPath usingMapping:attributeMapping]; } diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingTest.m index 6f97a52df9..19b67d3ee0 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingTest.m @@ -7,6 +7,8 @@ // #import "RKTestEnvironment.h" +#import "RKTestUser.h" +#import "RKObjectMappingOperationDataSource.h" @interface RKObjectMappingTest : RKTestCase @@ -233,4 +235,23 @@ - (void)testBreakageOfRecursiveInverseCyclicGraphs expect([inverseMapping propertyMappingsBySourceKeyPath][@"children"]).notTo.beNil(); } +- (void)testInverseMappingWithNilDestinationKeyPathForAttributeMapping +{ + // Map @"Blake" to RKTestUser with name = @"Blake" + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:nil toKeyPath:@"name"]]; + + RKObjectMapping *inverseMapping = [mapping inverseMapping]; + + RKTestUser *user = [RKTestUser new]; + user.name = @"Blake"; + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:user destinationObject:dictionary mapping:inverseMapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + [operation start]; + + expect(operation.destinationObject).to.equal(@{ @"Blake": @{} }); +} + @end From 312b382f258e54c2c3ffb3924665c24a98769039 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 30 Dec 2012 00:01:50 -0500 Subject: [PATCH 08/27] Add test and fix for invalid key path exceptions with dynamic mappings. refs #1111 --- Code/Network/RKManagedObjectRequestOperation.m | 12 +++++++++++- .../Network/RKManagedObjectRequestOperationTest.m | 8 ++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Code/Network/RKManagedObjectRequestOperation.m b/Code/Network/RKManagedObjectRequestOperation.m index 393f157434..7aefa1f2aa 100644 --- a/Code/Network/RKManagedObjectRequestOperation.m +++ b/Code/Network/RKManagedObjectRequestOperation.m @@ -533,7 +533,17 @@ - (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result wit for (RKMappingGraphVisitation *visitation in visitor.visitations) { id objectsAtRoot = [mappingResultDictionary objectForKey:visitation.rootKey]; - id managedObjects = visitation.keyPath ? [objectsAtRoot valueForKeyPath:visitation.keyPath] : objectsAtRoot; + id managedObjects = nil; + @try { + managedObjects = visitation.keyPath ? [objectsAtRoot valueForKeyPath:visitation.keyPath] : objectsAtRoot; + } + @catch (NSException *exception) { + if ([exception.name isEqualToString:NSUndefinedKeyException]) { + RKLogWarning(@"Caught undefined key exception for keyPath '%@' in mapping result: This likely indicates an ambiguous keyPath is used across response descriptor or dynamic mappings.", visitation.keyPath); + continue; + } + [exception raise]; + } [managedObjectsInMappingResult unionSet:RKFlattenCollectionToSet(managedObjects)]; if (visitation.isCyclic) { diff --git a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m index bd89100315..8ada240d4c 100644 --- a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m @@ -671,12 +671,12 @@ - (void)testMappingWithDynamicMappingContainingIncompatibleEntityMappingsAtSameK [parentMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"humanID": @"railsID" }]; [parentMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"children" toKeyPath:@"children" withMapping:childMapping]]; - [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"invalid" expectedValue:@"whatever" objectMapping:humanMapping]]; - [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"name" expectedValue:@"Blake" objectMapping:parentMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"invalid" expectedValue:@"whatever" objectMapping:parentMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"name" expectedValue:@"Blake" objectMapping:humanMapping]]; - RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping pathPattern:nil keyPath:@"houses.owner" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping pathPattern:nil keyPath:nil statusCodes:[NSIndexSet indexSetWithIndex:200]]; - NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/nested_self_referential.json" relativeToURL:[RKTestFactory baseURL]]]; + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/self_referential.json" relativeToURL:[RKTestFactory baseURL]]]; RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; RKFetchRequestBlock humanFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { return [NSFetchRequest fetchRequestWithEntityName:@"Human"]; From 9a128b678cad23c5d302d1110bf4ccd72ce3e9ac Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 30 Dec 2012 22:54:09 -0500 Subject: [PATCH 09/27] Fix issues with refetching results using visitation --- .../Network/RKManagedObjectRequestOperation.m | 65 +++++++++++++++---- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/Code/Network/RKManagedObjectRequestOperation.m b/Code/Network/RKManagedObjectRequestOperation.m index 7aefa1f2aa..fa5c314584 100644 --- a/Code/Network/RKManagedObjectRequestOperation.m +++ b/Code/Network/RKManagedObjectRequestOperation.m @@ -87,6 +87,8 @@ - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors self.visitations = [NSMutableArray array]; for (RKResponseDescriptor *responseDescriptor in responseDescriptors) { + [self.visitationStack removeAllObjects]; + [self.lowValues removeAllObjects]; [self visitMapping:responseDescriptor.mapping atKeyPath:responseDescriptor.keyPath]; } } @@ -240,10 +242,10 @@ static void RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(id graph, NSSet *c For example, given a set containing: 'this', 'this.that', 'another.one.test', 'another.two.test', 'another.one.test.nested' would return: 'this, 'another.one', 'another.two' */ -NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths); -NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths) +NSOrderedSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths); +NSOrderedSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths) { - return [setOfKeyPaths objectsPassingTest:^BOOL(NSString *keyPath, BOOL *stop) { + NSSet *prunedSet = [setOfKeyPaths objectsPassingTest:^BOOL(NSString *keyPath, BOOL *stop) { if ([keyPath isEqual:[NSNull null]]) return YES; // Special case the root key path NSArray *keyPathComponents = [keyPath componentsSeparatedByString:@"."]; NSMutableSet *parentKeyPaths = [NSMutableSet set]; @@ -255,6 +257,12 @@ static void RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(id graph, NSSet *c } return YES; }]; + NSMutableOrderedSet *orderedSet = [NSMutableOrderedSet orderedSetWithSet:prunedSet]; + if ([orderedSet containsObject:[NSNull null]]) { + [orderedSet removeObject:[NSNull null]]; + [orderedSet setObject:[NSNull null] atIndex:0]; + } + return orderedSet; } static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSString *keyPath, NSMutableDictionary *dictionary) @@ -288,17 +296,33 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSStrin // Finds the key paths for all entity mappings in the graph whose parent objects are not other managed objects static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsInVisitationsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSArray *visitations, NSManagedObjectContext *managedObjectContext) { - if (! [dictionaryOfManagedObjects count]) return dictionaryOfManagedObjects; + if (! [dictionaryOfManagedObjects count]) return dictionaryOfManagedObjects; NSMutableDictionary *newDictionary = [dictionaryOfManagedObjects mutableCopy]; [managedObjectContext performBlockAndWait:^{ - NSArray *rootKeys = [visitations valueForKey:@"rootKey"]; + NSSet *rootKeys = [NSSet setWithArray:[visitations valueForKey:@"rootKey"]]; for (id rootKey in rootKeys) { - NSSet *keyPaths = [visitations valueForKey:@"keyPath"]; - // If keyPaths contains null, then the root object is a managed object and we only need to refetch it - NSSet *nonNestedKeyPaths = ([keyPaths containsObject:[NSNull null]]) ? [NSSet setWithObject:[NSNull null]] : RKSetByRemovingSubkeypathsFromSet(keyPaths); + NSSet *keyPaths = [NSSet setWithArray:[visitations valueForKey:@"keyPath"]]; + NSOrderedSet *nonNestedKeyPaths = RKSetByRemovingSubkeypathsFromSet(keyPaths); NSDictionary *mappingResultsAtRootKey = [dictionaryOfManagedObjects objectForKey:rootKey]; for (NSString *keyPath in nonNestedKeyPaths) { + __block BOOL refetched = NO; + + if (! [keyPath isEqual:[NSNull null]]) { + if ([mappingResultsAtRootKey conformsToProtocol:@protocol(NSFastEnumeration)]) { + // This is a collection + BOOL respondsToSelector = YES; + for (id object in mappingResultsAtRootKey) { + if (! [object respondsToSelector:NSSelectorFromString(keyPath)]) respondsToSelector = NO; + } + + if (! respondsToSelector) continue; + } else { + if (! [mappingResultsAtRootKey respondsToSelector:NSSelectorFromString(keyPath)]) { + continue; + } + } + } id value = [keyPath isEqual:[NSNull null]] ? mappingResultsAtRootKey : [mappingResultsAtRootKey valueForKeyPath:keyPath]; if (! value) { continue; @@ -306,7 +330,10 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSStrin BOOL isMutable = [value isKindOfClass:[NSMutableArray class]]; NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]]; for (__strong id object in value) { - if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); + if ([object isKindOfClass:[NSManagedObject class]]) { + object = RKRefetchManagedObjectInContext(object, managedObjectContext); + refetched = YES; + } if (object) [newValue addObject:object]; } value = (isMutable) ? newValue : [newValue copy]; @@ -314,7 +341,10 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSStrin BOOL isMutable = [value isKindOfClass:[NSMutableSet class]]; NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]]; for (__strong id object in value) { - if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); + if ([object isKindOfClass:[NSManagedObject class]]) { + object = RKRefetchManagedObjectInContext(object, managedObjectContext); + refetched = YES; + } if (object) [newValue addObject:object]; } value = (isMutable) ? newValue : [newValue copy]; @@ -322,15 +352,26 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSStrin BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]]; NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet]; [(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) { - if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); + if ([object isKindOfClass:[NSManagedObject class]]) { + object = RKRefetchManagedObjectInContext(object, managedObjectContext); + refetched = YES; + } if (object) [newValue setObject:object atIndex:index]; }]; value = (isMutable) ? newValue : [newValue copy]; } else if ([value isKindOfClass:[NSManagedObject class]]) { value = RKRefetchManagedObjectInContext(value, managedObjectContext); + refetched = YES; } - if (value) RKSetMappedValueForKeyPathInDictionary(value, rootKey, keyPath, newDictionary); + if (value) { + RKSetMappedValueForKeyPathInDictionary(value, rootKey, keyPath, newDictionary); + + // If we have refetched the root object, then we are done + if (keyPath == [NSNull null] && refetched) { + break; + } + } } } }]; From 1d27679ee368603695a99cc29577abbdf692079d Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Mon, 31 Dec 2012 14:39:37 -0500 Subject: [PATCH 10/27] Clean up the nasty mess in the visitor code and fix flaws in implementation of algorithm. Still some test breakage and additional coverage to put down --- .../Network/RKManagedObjectRequestOperation.m | 151 +++++++----------- Code/Network/RKResponseMapperOperation.h | 8 + Code/Network/RKResponseMapperOperation.m | 17 +- 3 files changed, 81 insertions(+), 95 deletions(-) diff --git a/Code/Network/RKManagedObjectRequestOperation.m b/Code/Network/RKManagedObjectRequestOperation.m index fa5c314584..445225804d 100644 --- a/Code/Network/RKManagedObjectRequestOperation.m +++ b/Code/Network/RKManagedObjectRequestOperation.m @@ -60,7 +60,7 @@ - (NSString *)description The following reference implementations were used when building out an Objective-C implementation: 1. http://algowiki.net/wiki/index.php?title=Tarjan%27s_algorithm - 1. http://www.logarithmic.net/pfh-files/blog/01208083168/sort.py + 1. http://www.logarithmic.net/pfh-files/blog/01208083168/tarjan.py */ @interface RKNestedManagedObjectKeyPathMappingGraphVisitor : NSObject @@ -69,9 +69,10 @@ - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors; @end @interface RKNestedManagedObjectKeyPathMappingGraphVisitor () +@property (nonatomic, assign) NSUInteger indexCounter; @property (nonatomic, strong) NSMutableArray *visitationStack; -@property (nonatomic, strong) NSMutableDictionary *lowValues; -@property (nonatomic, strong) NSNumber *numberOfDecriptors; +@property (nonatomic, strong) NSMutableDictionary *index; +@property (nonatomic, strong) NSMutableDictionary *lowLinks; @property (nonatomic, strong, readwrite) NSMutableArray *visitations; @end @@ -81,14 +82,17 @@ - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors { self = [self init]; if (self) { - self.numberOfDecriptors = @([responseDescriptors count]); + self.indexCounter = 0; self.visitationStack = [NSMutableArray array]; - self.lowValues = [NSMutableDictionary dictionary]; + self.index = [NSMutableDictionary dictionary]; + self.lowLinks = [NSMutableDictionary dictionary]; self.visitations = [NSMutableArray array]; for (RKResponseDescriptor *responseDescriptor in responseDescriptors) { + self.indexCounter = 0; [self.visitationStack removeAllObjects]; - [self.lowValues removeAllObjects]; + [self.index removeAllObjects]; + [self.lowLinks removeAllObjects]; [self visitMapping:responseDescriptor.mapping atKeyPath:responseDescriptor.keyPath]; } } @@ -116,11 +120,9 @@ - (RKMappingGraphVisitation *)visitationForMapping:(RKMapping *)mapping atKeyPat - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath { NSValue *dictionaryKey = [NSValue valueWithNonretainedObject:mapping]; - if ([self.lowValues objectForKey:dictionaryKey]) { - if (![ mapping isKindOfClass:[RKEntityMapping class]]) return; - - NSArray *keyPathComponents = [[self.visitationStack valueForKey:@"keyPath"] arrayByAddingObject:keyPath]; - NSString *keyPath = [[keyPathComponents subarrayWithRange:NSMakeRange(1, [keyPathComponents count] - 1)] componentsJoinedByString:@"."]; + // If a given mapping already appears within the lowValues, then we have a cycle in the graph + if ([self.lowLinks objectForKey:dictionaryKey]) { + if (![mapping isKindOfClass:[RKEntityMapping class]]) return; RKMappingGraphVisitation *cyclicVisitation = [self visitationForMapping:mapping atKeyPath:keyPath]; cyclicVisitation.cyclic = YES; @@ -129,55 +131,59 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath return; } - NSNumber *lowValue = @([self.lowValues count]); - [self.lowValues setObject:lowValue forKey:dictionaryKey]; + // Track the visit to each node in the graph. Note that we do not pop the stack as we traverse back up + [self.index setObject:@(self.indexCounter) forKey:dictionaryKey]; + [self.lowLinks setObject:@(self.indexCounter) forKey:dictionaryKey]; + self.indexCounter++; - NSUInteger stackPosition = [self.visitationStack count]; RKMappingGraphVisitation *visitation = [self visitationForMapping:mapping atKeyPath:keyPath]; [self.visitationStack addObject:visitation]; if ([mapping isKindOfClass:[RKObjectMapping class]]) { RKObjectMapping *objectMapping = (RKObjectMapping *)mapping; for (RKRelationshipMapping *relationshipMapping in objectMapping.relationshipMappings) { - [self visitMapping:relationshipMapping.mapping atKeyPath:relationshipMapping.destinationKeyPath]; - - // We want the minimum value + // Check if the successor relationship appears in the lowlinks NSValue *relationshipKey = [NSValue valueWithNonretainedObject:relationshipMapping.mapping]; - NSNumber *relationshipLowValue = [self.lowValues objectForKey:relationshipKey]; - if ([lowValue compare:relationshipLowValue] == NSOrderedDescending) { - [self.lowValues setObject:relationshipLowValue forKey:dictionaryKey]; + NSNumber *relationshipLowValue = [self.lowLinks objectForKey:relationshipKey]; + if (relationshipLowValue == nil) { + // The relationship has not yet been visited, recurse + NSString *nestedKeyPath = ([self.visitationStack count] > 1 && keyPath) ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath; + [self visitMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath]; + + // Set the lowlink value for parent mapping to the lower value for us or the child mapping we just recursed on + NSNumber *lowLinkForMapping = [self.lowLinks objectForKey:dictionaryKey]; + NSNumber *lowLinkForSuccessor = [self.lowLinks objectForKey:relationshipKey]; + + if ([lowLinkForMapping compare:lowLinkForSuccessor] == NSOrderedDescending) { + [self.lowLinks setObject:lowLinkForSuccessor forKey:dictionaryKey]; + } + } else { + // The child mapping is already in the stack, so it is part of a strongly connected component + NSNumber *lowLinkForMapping = [self.lowLinks objectForKey:dictionaryKey]; + NSNumber *indexValueForSuccessor = [self.index objectForKey:relationshipKey]; + if ([lowLinkForMapping compare:indexValueForSuccessor] == NSOrderedDescending) { + [self.lowLinks setObject:indexValueForSuccessor forKey:dictionaryKey]; + } } } } else if ([mapping isKindOfClass:[RKDynamicMapping class]]) { - // Pop the visitation stack to remove the dynamic mapping, since each mapping within the dynamic mapping - // is rooted at the same point in the graph - [self.visitationStack removeLastObject]; - + // Dynamic mappings appear at the same point in the graph, so we recurse with the same keyPath for (RKMapping *nestedMapping in [(RKDynamicMapping *)mapping objectMappings]) { [self visitMapping:nestedMapping atKeyPath:keyPath]; } } - if ([[self.lowValues objectForKey:dictionaryKey] isEqualToNumber:lowValue]) { - NSRange range = NSMakeRange(stackPosition, [self.visitationStack count] - stackPosition); - NSArray *visitations = [self.visitationStack subarrayWithRange:range]; - [self.visitationStack removeObjectsInRange:range]; + // If the current mapping is a root node, then pop the stack to create an SCC + NSNumber *lowLinkValueForMapping = [self.lowLinks objectForKey:dictionaryKey]; + NSNumber *indexValueForMapping = [self.index objectForKey:dictionaryKey]; + if ([lowLinkValueForMapping isEqualToNumber:indexValueForMapping]) { + NSUInteger index = [self.visitationStack indexOfObject:visitation]; + NSRange removalRange = NSMakeRange(index, [self.visitationStack count] - index); + [self.visitationStack removeObjectsInRange:removalRange]; - // Take everything left on the stack - NSArray *keyPathComponents = [self.visitationStack valueForKey:@"keyPath"]; - NSString *nestingKeyPath = ([keyPathComponents count] > 1) ? [[keyPathComponents subarrayWithRange:NSMakeRange(1, [keyPathComponents count] - 1)] componentsJoinedByString:@"."] : nil; - - [visitations enumerateObjectsUsingBlock:^(RKMappingGraphVisitation *visitation, NSUInteger idx, BOOL *stop) { - // If this is an entity mapping, collect the complete key path - if ([visitation.mapping isKindOfClass:[RKEntityMapping class]]) { - visitation.keyPath = nestingKeyPath ? [@[ nestingKeyPath, visitation.keyPath ] componentsJoinedByString:@"."] : visitation.keyPath; - [self.visitations addObject:visitation]; - } - - // Update the low value - NSValue *relationshipKey = [NSValue valueWithNonretainedObject:mapping]; - [self.lowValues setObject:self.numberOfDecriptors forKey:relationshipKey]; - }]; + if ([visitation.mapping isKindOfClass:[RKEntityMapping class]]) { + [self.visitations addObject:visitation]; + } } } @@ -242,10 +248,10 @@ static void RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(id graph, NSSet *c For example, given a set containing: 'this', 'this.that', 'another.one.test', 'another.two.test', 'another.one.test.nested' would return: 'this, 'another.one', 'another.two' */ -NSOrderedSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths); -NSOrderedSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths) +NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths); +NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths) { - NSSet *prunedSet = [setOfKeyPaths objectsPassingTest:^BOOL(NSString *keyPath, BOOL *stop) { + return [setOfKeyPaths objectsPassingTest:^BOOL(NSString *keyPath, BOOL *stop) { if ([keyPath isEqual:[NSNull null]]) return YES; // Special case the root key path NSArray *keyPathComponents = [keyPath componentsSeparatedByString:@"."]; NSMutableSet *parentKeyPaths = [NSMutableSet set]; @@ -257,12 +263,6 @@ static void RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(id graph, NSSet *c } return YES; }]; - NSMutableOrderedSet *orderedSet = [NSMutableOrderedSet orderedSetWithSet:prunedSet]; - if ([orderedSet containsObject:[NSNull null]]) { - [orderedSet removeObject:[NSNull null]]; - [orderedSet setObject:[NSNull null] atIndex:0]; - } - return orderedSet; } static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSString *keyPath, NSMutableDictionary *dictionary) @@ -297,32 +297,18 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSStrin static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsInVisitationsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSArray *visitations, NSManagedObjectContext *managedObjectContext) { if (! [dictionaryOfManagedObjects count]) return dictionaryOfManagedObjects; + NSMutableDictionary *newDictionary = [dictionaryOfManagedObjects mutableCopy]; [managedObjectContext performBlockAndWait:^{ NSSet *rootKeys = [NSSet setWithArray:[visitations valueForKey:@"rootKey"]]; for (id rootKey in rootKeys) { - NSSet *keyPaths = [NSSet setWithArray:[visitations valueForKey:@"keyPath"]]; - NSOrderedSet *nonNestedKeyPaths = RKSetByRemovingSubkeypathsFromSet(keyPaths); + NSArray *visitationsForRootKey = [visitations filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"rootKey = %@", rootKey]]; + NSSet *keyPaths = [visitationsForRootKey valueForKey:@"keyPath"]; + // If keyPaths contains null, then the root object is a managed object and we only need to refetch it + NSSet *nonNestedKeyPaths = ([keyPaths containsObject:[NSNull null]]) ? [NSSet setWithObject:[NSNull null]] : RKSetByRemovingSubkeypathsFromSet(keyPaths); NSDictionary *mappingResultsAtRootKey = [dictionaryOfManagedObjects objectForKey:rootKey]; for (NSString *keyPath in nonNestedKeyPaths) { - __block BOOL refetched = NO; - - if (! [keyPath isEqual:[NSNull null]]) { - if ([mappingResultsAtRootKey conformsToProtocol:@protocol(NSFastEnumeration)]) { - // This is a collection - BOOL respondsToSelector = YES; - for (id object in mappingResultsAtRootKey) { - if (! [object respondsToSelector:NSSelectorFromString(keyPath)]) respondsToSelector = NO; - } - - if (! respondsToSelector) continue; - } else { - if (! [mappingResultsAtRootKey respondsToSelector:NSSelectorFromString(keyPath)]) { - continue; - } - } - } id value = [keyPath isEqual:[NSNull null]] ? mappingResultsAtRootKey : [mappingResultsAtRootKey valueForKeyPath:keyPath]; if (! value) { continue; @@ -330,10 +316,7 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSStrin BOOL isMutable = [value isKindOfClass:[NSMutableArray class]]; NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]]; for (__strong id object in value) { - if ([object isKindOfClass:[NSManagedObject class]]) { - object = RKRefetchManagedObjectInContext(object, managedObjectContext); - refetched = YES; - } + if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); if (object) [newValue addObject:object]; } value = (isMutable) ? newValue : [newValue copy]; @@ -341,10 +324,7 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSStrin BOOL isMutable = [value isKindOfClass:[NSMutableSet class]]; NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]]; for (__strong id object in value) { - if ([object isKindOfClass:[NSManagedObject class]]) { - object = RKRefetchManagedObjectInContext(object, managedObjectContext); - refetched = YES; - } + if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); if (object) [newValue addObject:object]; } value = (isMutable) ? newValue : [newValue copy]; @@ -352,25 +332,16 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSStrin BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]]; NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet]; [(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) { - if ([object isKindOfClass:[NSManagedObject class]]) { - object = RKRefetchManagedObjectInContext(object, managedObjectContext); - refetched = YES; - } + if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext); if (object) [newValue setObject:object atIndex:index]; }]; value = (isMutable) ? newValue : [newValue copy]; } else if ([value isKindOfClass:[NSManagedObject class]]) { value = RKRefetchManagedObjectInContext(value, managedObjectContext); - refetched = YES; } if (value) { RKSetMappedValueForKeyPathInDictionary(value, rootKey, keyPath, newDictionary); - - // If we have refetched the root object, then we are done - if (keyPath == [NSNull null] && refetched) { - break; - } } } } @@ -660,7 +631,7 @@ - (void)willFinish NSError *error = nil; // Construct a set of key paths to all of the managed objects in the mapping result - RKNestedManagedObjectKeyPathMappingGraphVisitor *visitor = [[RKNestedManagedObjectKeyPathMappingGraphVisitor alloc] initWithResponseDescriptors:self.responseDescriptors]; + RKNestedManagedObjectKeyPathMappingGraphVisitor *visitor = [[RKNestedManagedObjectKeyPathMappingGraphVisitor alloc] initWithResponseDescriptors:self.responseMapperOperation.matchingResponseDescriptors]; // Handle any cleanup success = [self deleteTargetObjectIfAppropriate:&error]; diff --git a/Code/Network/RKResponseMapperOperation.h b/Code/Network/RKResponseMapperOperation.h index bd518f3892..1559fa430b 100644 --- a/Code/Network/RKResponseMapperOperation.h +++ b/Code/Network/RKResponseMapperOperation.h @@ -114,6 +114,14 @@ */ @property (nonatomic, strong, readonly) NSDictionary *responseMappingsDictionary; +/** + Returns an array containing all `RKResponseDescriptor` objects in the configured `responseDescriptors` array that were found to match response. + + @see `responseDescriptors` + @see `RKResponseDescriptor` + */ +@property (nonatomic, strong, readonly) NSArray *matchingResponseDescriptors; + ///-------------------------------- /// @name Accessing Mapping Results ///-------------------------------- diff --git a/Code/Network/RKResponseMapperOperation.m b/Code/Network/RKResponseMapperOperation.m index 93d50a1f2b..eb5207bed4 100644 --- a/Code/Network/RKResponseMapperOperation.m +++ b/Code/Network/RKResponseMapperOperation.m @@ -110,6 +110,7 @@ @interface RKResponseMapperOperation () @property (nonatomic, strong, readwrite) NSArray *responseDescriptors; @property (nonatomic, strong, readwrite) RKMappingResult *mappingResult; @property (nonatomic, strong, readwrite) NSError *error; +@property (nonatomic, strong, readwrite) NSArray *matchingResponseDescriptors; @property (nonatomic, strong, readwrite) NSDictionary *responseMappingsDictionary; @property (nonatomic, strong) RKMapperOperation *mapperOperation; @property (nonatomic, copy) id (^willMapDeserializedResponseBlock)(id deserializedResponseBody); @@ -133,6 +134,7 @@ - (id)initWithResponse:(NSHTTPURLResponse *)response data:(NSData *)data respons self.response = response; self.data = data; self.responseDescriptors = responseDescriptors; + self.matchingResponseDescriptors = [self buildMatchingResponseDescriptors]; self.responseMappingsDictionary = [self buildResponseMappingsDictionary]; self.treatsEmptyResponseAsSuccess = YES; } @@ -163,14 +165,19 @@ - (id)parseResponseData:(NSError **)error return object; } +- (NSArray *)buildMatchingResponseDescriptors +{ + NSIndexSet *indexSet = [self.responseDescriptors indexesOfObjectsPassingTest:^BOOL(RKResponseDescriptor *responseDescriptor, NSUInteger idx, BOOL *stop) { + return [responseDescriptor matchesResponse:self.response]; + }]; + return [self.responseDescriptors objectsAtIndexes:indexSet]; +} + - (NSDictionary *)buildResponseMappingsDictionary { NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; - for (RKResponseDescriptor *responseDescriptor in self.responseDescriptors) { - if ([responseDescriptor matchesResponse:self.response]) { - id key = responseDescriptor.keyPath ? responseDescriptor.keyPath : [NSNull null]; - [dictionary setObject:responseDescriptor.mapping forKey:key]; - } + for (RKResponseDescriptor *responseDescriptor in self.matchingResponseDescriptors) { + [dictionary setObject:responseDescriptor.mapping forKey:(responseDescriptor.keyPath ?: [NSNull null])]; } return dictionary; From b93fe1f7c9808189a79a78ef8990d5ae96bf5a0a Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Mon, 31 Dec 2012 14:50:32 -0500 Subject: [PATCH 11/27] Restore cycle detection and cleanup visitor codebase further --- .../Network/RKManagedObjectRequestOperation.m | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/Code/Network/RKManagedObjectRequestOperation.m b/Code/Network/RKManagedObjectRequestOperation.m index 445225804d..f5c5702716 100644 --- a/Code/Network/RKManagedObjectRequestOperation.m +++ b/Code/Network/RKManagedObjectRequestOperation.m @@ -119,19 +119,8 @@ - (RKMappingGraphVisitation *)visitationForMapping:(RKMapping *)mapping atKeyPat // Traverse the mappings graph using Tarjan's algorithm - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath { - NSValue *dictionaryKey = [NSValue valueWithNonretainedObject:mapping]; - // If a given mapping already appears within the lowValues, then we have a cycle in the graph - if ([self.lowLinks objectForKey:dictionaryKey]) { - if (![mapping isKindOfClass:[RKEntityMapping class]]) return; - - RKMappingGraphVisitation *cyclicVisitation = [self visitationForMapping:mapping atKeyPath:keyPath]; - cyclicVisitation.cyclic = YES; - [self.visitations addObject:cyclicVisitation]; - - return; - } - // Track the visit to each node in the graph. Note that we do not pop the stack as we traverse back up + NSValue *dictionaryKey = [NSValue valueWithNonretainedObject:mapping]; [self.index setObject:@(self.indexCounter) forKey:dictionaryKey]; [self.lowLinks setObject:@(self.indexCounter) forKey:dictionaryKey]; self.indexCounter++; @@ -145,9 +134,9 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath // Check if the successor relationship appears in the lowlinks NSValue *relationshipKey = [NSValue valueWithNonretainedObject:relationshipMapping.mapping]; NSNumber *relationshipLowValue = [self.lowLinks objectForKey:relationshipKey]; + NSString *nestedKeyPath = ([self.visitationStack count] > 1 && keyPath) ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath; if (relationshipLowValue == nil) { // The relationship has not yet been visited, recurse - NSString *nestedKeyPath = ([self.visitationStack count] > 1 && keyPath) ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath; [self visitMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath]; // Set the lowlink value for parent mapping to the lower value for us or the child mapping we just recursed on @@ -164,6 +153,13 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath if ([lowLinkForMapping compare:indexValueForSuccessor] == NSOrderedDescending) { [self.lowLinks setObject:indexValueForSuccessor forKey:dictionaryKey]; } + + // Since this mapping already appears in lowLinks, we have a cycle at this point in the graph + if ([relationshipMapping.mapping isKindOfClass:[RKEntityMapping class]]) { + RKMappingGraphVisitation *cyclicVisitation = [self visitationForMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath]; + cyclicVisitation.cyclic = YES; + [self.visitations addObject:cyclicVisitation]; + } } } } else if ([mapping isKindOfClass:[RKDynamicMapping class]]) { From d024a518a4d8594edb25c82bad07cda974ce8961 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Mon, 31 Dec 2012 15:26:44 -0500 Subject: [PATCH 12/27] Fix broken dynamic mapping test --- Code/Network/RKManagedObjectRequestOperation.m | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Code/Network/RKManagedObjectRequestOperation.m b/Code/Network/RKManagedObjectRequestOperation.m index f5c5702716..3bba6d844b 100644 --- a/Code/Network/RKManagedObjectRequestOperation.m +++ b/Code/Network/RKManagedObjectRequestOperation.m @@ -163,6 +163,9 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath } } } else if ([mapping isKindOfClass:[RKDynamicMapping class]]) { + // Pop the dynamic mapping off of the stack so that our children are rooted at the same level + [self.visitationStack removeLastObject]; + // Dynamic mappings appear at the same point in the graph, so we recurse with the same keyPath for (RKMapping *nestedMapping in [(RKDynamicMapping *)mapping objectMappings]) { [self visitMapping:nestedMapping atKeyPath:keyPath]; @@ -174,8 +177,10 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath NSNumber *indexValueForMapping = [self.index objectForKey:dictionaryKey]; if ([lowLinkValueForMapping isEqualToNumber:indexValueForMapping]) { NSUInteger index = [self.visitationStack indexOfObject:visitation]; - NSRange removalRange = NSMakeRange(index, [self.visitationStack count] - index); - [self.visitationStack removeObjectsInRange:removalRange]; + if (index != NSNotFound) { + NSRange removalRange = NSMakeRange(index, [self.visitationStack count] - index); + [self.visitationStack removeObjectsInRange:removalRange]; + } if ([visitation.mapping isKindOfClass:[RKEntityMapping class]]) { [self.visitations addObject:visitation]; From a03191c29165a79a785a17ac8ea498f6659b3263 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Mon, 31 Dec 2012 17:33:41 -0500 Subject: [PATCH 13/27] Add support for skipping an aggregate relationship mapping in the event that the parent representation does not contain any values for the property mappings of the concrete `RKObjectMapping` configured for the relationship. fixes #1114, closes #1115 --- Code/ObjectMapping/RKMappingOperation.m | 31 +++++++++++++++++-- .../RKObjectMappingNextGenTest.m | 17 ++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Code/ObjectMapping/RKMappingOperation.m b/Code/ObjectMapping/RKMappingOperation.m index d2114f939c..a68fcc48ce 100644 --- a/Code/ObjectMapping/RKMappingOperation.m +++ b/Code/ObjectMapping/RKMappingOperation.m @@ -198,6 +198,15 @@ static void RKSetValueForObject(id value, id destinationObject) } } +// Returns YES if there is a value present for at least one key path in the given collection +static BOOL RKObjectContainsValueForKeyPaths(id representation, NSArray *keyPaths) +{ + for (NSString *keyPath in keyPaths) { + if ([representation valueForKeyPath:keyPath]) return YES; + } + return NO; +} + @interface RKMappingOperation () @property (nonatomic, strong, readwrite) RKMapping *mapping; @property (nonatomic, strong, readwrite) id sourceObject; @@ -599,8 +608,26 @@ - (BOOL)applyRelationshipMappings for (RKRelationshipMapping *relationshipMapping in [self relationshipMappings]) { if ([self isCancelled]) return NO; - // The nil source keyPath indicates that we want to map directly from the parent representation - id value = (relationshipMapping.sourceKeyPath == nil) ? self.sourceObject : [self.sourceObject valueForKeyPath:relationshipMapping.sourceKeyPath]; + id value = nil; + if (relationshipMapping.sourceKeyPath) { + value = [self.sourceObject valueForKeyPath:relationshipMapping.sourceKeyPath]; + } else { + // The nil source keyPath indicates that we want to map directly from the parent representation + value = self.sourceObject; + RKObjectMapping *objectMapping = nil; + + if ([relationshipMapping.mapping isKindOfClass:[RKObjectMapping class]]) { + objectMapping = (RKObjectMapping *)relationshipMapping.mapping; + } else if ([relationshipMapping.mapping isKindOfClass:[RKDynamicMapping class]]) { + objectMapping = [(RKDynamicMapping *)relationshipMapping.mapping objectMappingForRepresentation:value]; + } + + if (! objectMapping) continue; // Mapping declined + NSArray *propertyKeyPaths = [relationshipMapping valueForKeyPath:@"mapping.propertyMappings.sourceKeyPath"]; + if (! RKObjectContainsValueForKeyPaths(value, propertyKeyPaths)) { + continue; + } + } // Track that we applied this mapping [mappingsApplied addObject:relationshipMapping]; diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m index 582ae516e8..8d3a11b1b5 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m @@ -2414,4 +2414,21 @@ - (void)testAggregatingPropertyMappingUsingNilKeyPath expect(user.coordinate.longitude).to.equal(200.5); } +- (void)testThatAggregatedRelationshipMappingsAreOnlyAppliedIfThereIsAtLeastOneValueInTheRepresentation +{ + NSDictionary *objectRepresentation = @{ @"name": @"Blake" }; + RKObjectMapping *userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [userMapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKObjectMapping *coordinateMapping = [RKObjectMapping mappingForClass:[RKTestCoordinate class]]; + [coordinateMapping addAttributeMappingsFromArray:@[ @"latitude", @"longitude" ]]; + [userMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:nil toKeyPath:@"coordinate" withMapping:coordinateMapping]]; + RKTestUser *user = [RKTestUser new]; + RKMappingOperation *mappingOperation = [[RKMappingOperation alloc] initWithSourceObject:objectRepresentation destinationObject:user mapping:userMapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + mappingOperation.dataSource = dataSource; + [mappingOperation start]; + expect(mappingOperation.error).to.beNil(); + expect(user.coordinate).to.beNil(); +} + @end From 9dc08ca27b1523b864b6a81c021335e38369e06a Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Tue, 1 Jan 2013 13:12:21 -0500 Subject: [PATCH 14/27] Drop instancetype from `init` methods since the compiler will infer it --- Code/CoreData/RKConnectionDescription.h | 4 ++-- Code/CoreData/RKConnectionDescription.m | 4 ++-- Code/CoreData/RKEntityByAttributeCache.h | 2 +- Code/CoreData/RKEntityCache.h | 2 +- Code/CoreData/RKEntityMapping.h | 2 +- Code/CoreData/RKEntityMapping.m | 4 ++-- Code/CoreData/RKInMemoryManagedObjectCache.h | 2 +- Code/CoreData/RKManagedObjectImporter.h | 4 ++-- Code/CoreData/RKManagedObjectMappingOperationDataSource.h | 2 +- Code/CoreData/RKManagedObjectMappingOperationDataSource.m | 2 +- Code/CoreData/RKManagedObjectStore.h | 6 +++--- Code/CoreData/RKManagedObjectStore.m | 4 ++-- Code/CoreData/RKRelationshipConnectionOperation.h | 2 +- Code/CoreData/RKRelationshipConnectionOperation.m | 2 +- Code/Network/RKObjectManager.h | 2 +- Code/Network/RKObjectRequestOperation.h | 4 ++-- Code/Network/RKPaginator.h | 2 +- Code/Network/RKResponseMapperOperation.h | 2 +- Code/Network/RKRouter.h | 2 +- Code/ObjectMapping/RKMapperOperation.h | 2 +- Code/ObjectMapping/RKMappingOperation.h | 2 +- Code/ObjectMapping/RKMappingResult.h | 2 +- Code/ObjectMapping/RKObjectMapping.h | 2 +- Code/Testing/RKConnectionTestExpectation.h | 2 +- Code/Testing/RKConnectionTestExpectation.m | 2 +- Code/Testing/RKMappingTest.h | 2 +- 26 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Code/CoreData/RKConnectionDescription.h b/Code/CoreData/RKConnectionDescription.h index d564ccfb80..de3f8d3bea 100644 --- a/Code/CoreData/RKConnectionDescription.h +++ b/Code/CoreData/RKConnectionDescription.h @@ -88,7 +88,7 @@ @param sourceToDestinationEntityAttributes A dictionary specifying how attributes on the source entity correspond to attributes on the destination entity. @return The receiver, initialized with the given relationship and attributes. */ -- (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship attributes:(NSDictionary *)sourceToDestinationEntityAttributes; +- (id)initWithRelationship:(NSRelationshipDescription *)relationship attributes:(NSDictionary *)sourceToDestinationEntityAttributes; /** The dictionary of attributes specifying how attributes on the source entity for the relationship correspond to attributes on the destination entity. @@ -115,7 +115,7 @@ @param keyPath The key path from which to read the value that is to be set for the relationship. @return The receiver, initialized with the given relationship and key path. */ -- (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship keyPath:(NSString *)keyPath; +- (id)initWithRelationship:(NSRelationshipDescription *)relationship keyPath:(NSString *)keyPath; /** The key path that is to be evaluated to obtain the value for the relationship. diff --git a/Code/CoreData/RKConnectionDescription.m b/Code/CoreData/RKConnectionDescription.m index 538f329cee..ac651c84ce 100644 --- a/Code/CoreData/RKConnectionDescription.m +++ b/Code/CoreData/RKConnectionDescription.m @@ -44,7 +44,7 @@ @interface RKConnectionDescription () @implementation RKConnectionDescription -- (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship attributes:(NSDictionary *)attributes +- (id)initWithRelationship:(NSRelationshipDescription *)relationship attributes:(NSDictionary *)attributes { NSParameterAssert(relationship); NSParameterAssert(attributes); @@ -63,7 +63,7 @@ - (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship a return self; } -- (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship keyPath:(NSString *)keyPath +- (id)initWithRelationship:(NSRelationshipDescription *)relationship keyPath:(NSString *)keyPath { NSParameterAssert(relationship); NSParameterAssert(keyPath); diff --git a/Code/CoreData/RKEntityByAttributeCache.h b/Code/CoreData/RKEntityByAttributeCache.h index 8f4d57c312..06f3faf7bc 100644 --- a/Code/CoreData/RKEntityByAttributeCache.h +++ b/Code/CoreData/RKEntityByAttributeCache.h @@ -46,7 +46,7 @@ @return The receiver, initialized with the given entity, attribute, and managed object context. */ -- (instancetype)initWithEntity:(NSEntityDescription *)entity attributes:(NSArray *)attributeNames managedObjectContext:(NSManagedObjectContext *)context; +- (id)initWithEntity:(NSEntityDescription *)entity attributes:(NSArray *)attributeNames managedObjectContext:(NSManagedObjectContext *)context; ///----------------------------- /// @name Getting Cache Identity diff --git a/Code/CoreData/RKEntityCache.h b/Code/CoreData/RKEntityCache.h index 2a5938fa7c..d791b86dba 100644 --- a/Code/CoreData/RKEntityCache.h +++ b/Code/CoreData/RKEntityCache.h @@ -43,7 +43,7 @@ @param context The managed object context containing objects to be cached. @returns self, initialized with context. */ -- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)context; +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)context; /** The managed object context with which the receiver is associated. diff --git a/Code/CoreData/RKEntityMapping.h b/Code/CoreData/RKEntityMapping.h index 248a11b7f1..a8191b302c 100644 --- a/Code/CoreData/RKEntityMapping.h +++ b/Code/CoreData/RKEntityMapping.h @@ -68,7 +68,7 @@ @param entity An entity with which to initialize the receiver. @returns The receiver, initialized with the given entity. */ -- (instancetype)initWithEntity:(NSEntityDescription *)entity; +- (id)initWithEntity:(NSEntityDescription *)entity; /** A convenience initializer that creates and returns an entity mapping for the entity with the given name in diff --git a/Code/CoreData/RKEntityMapping.m b/Code/CoreData/RKEntityMapping.m index a8e111e7fb..aaf714fe91 100644 --- a/Code/CoreData/RKEntityMapping.m +++ b/Code/CoreData/RKEntityMapping.m @@ -153,7 +153,7 @@ + (instancetype)mappingForEntityForName:(NSString *)entityName inManagedObjectSt return [[self alloc] initWithEntity:entity]; } -- (instancetype)initWithEntity:(NSEntityDescription *)entity +- (id)initWithEntity:(NSEntityDescription *)entity { NSAssert(entity, @"Cannot initialize an RKEntityMapping without an entity. Maybe you want RKObjectMapping instead?"); Class objectClass = NSClassFromString([entity managedObjectClassName]); @@ -167,7 +167,7 @@ - (instancetype)initWithEntity:(NSEntityDescription *)entity return self; } -- (instancetype)initWithClass:(Class)objectClass +- (id)initWithClass:(Class)objectClass { self = [super initWithClass:objectClass]; if (self) { diff --git a/Code/CoreData/RKInMemoryManagedObjectCache.h b/Code/CoreData/RKInMemoryManagedObjectCache.h index 73c4e167f1..6e725752f2 100644 --- a/Code/CoreData/RKInMemoryManagedObjectCache.h +++ b/Code/CoreData/RKInMemoryManagedObjectCache.h @@ -35,6 +35,6 @@ @param managedObjectContext The managed object context with which to initialize the receiver. @return The receiver, initialized with the given managed object context. */ -- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; @end diff --git a/Code/CoreData/RKManagedObjectImporter.h b/Code/CoreData/RKManagedObjectImporter.h index c855d5bc36..38e22276c8 100644 --- a/Code/CoreData/RKManagedObjectImporter.h +++ b/Code/CoreData/RKManagedObjectImporter.h @@ -55,7 +55,7 @@ @warning As this initialization code path is typical for generating seed databases, the value of `resetsStoreBeforeImporting` is initialized to **YES**. */ -- (instancetype)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel storePath:(NSString *)storePath; +- (id)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel storePath:(NSString *)storePath; /** Initializes the receiver with a given persistent store in which to persist imported managed objects. @@ -69,7 +69,7 @@ managed object model are determined from the given persistent store and a new managed object context with the private queue concurrency type is constructed. */ -- (instancetype)initWithPersistentStore:(NSPersistentStore *)persistentStore; +- (id)initWithPersistentStore:(NSPersistentStore *)persistentStore; /** A Boolean value indicating whether existing managed objects in the persistent store should diff --git a/Code/CoreData/RKManagedObjectMappingOperationDataSource.h b/Code/CoreData/RKManagedObjectMappingOperationDataSource.h index 9f8c19d9fa..c785ccba9f 100644 --- a/Code/CoreData/RKManagedObjectMappingOperationDataSource.h +++ b/Code/CoreData/RKManagedObjectMappingOperationDataSource.h @@ -42,7 +42,7 @@ @param managedObjectCache The managed object cache used by the receiver to find existing object instances by their identification attributes. @return The receiver, initialized with the given managed object context and managed objet cache. */ -- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id)managedObjectCache; +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id)managedObjectCache; ///----------------------------------------------------- /// @name Accessing the Managed Object Context and Cache diff --git a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m index b2579c5e95..b627edea2c 100644 --- a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m +++ b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m @@ -130,7 +130,7 @@ @interface RKManagedObjectMappingOperationDataSource () @implementation RKManagedObjectMappingOperationDataSource -- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id)managedObjectCache +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id)managedObjectCache { NSParameterAssert(managedObjectContext); diff --git a/Code/CoreData/RKManagedObjectStore.h b/Code/CoreData/RKManagedObjectStore.h index d50fd4969c..4a22dd5b11 100644 --- a/Code/CoreData/RKManagedObjectStore.h +++ b/Code/CoreData/RKManagedObjectStore.h @@ -76,7 +76,7 @@ RKManagedObjectStore *managedObjectStore = [[RKManagedObjectStore alloc] initWithManagedObjectModel:managedObjectModel]; */ -- (instancetype)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel; +- (id)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel; /** Initializes the receiver with an existing persistent store coordinator. @@ -88,7 +88,7 @@ @param persistentStoreCoordinator The persistent store coordinator with which to initialize the receiver. @return The receiver, initialized with the managed object model of the given persistent store coordinator and the persistent store coordinator. */ -- (instancetype)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator; +- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator; /** Initializes the receiver with a managed object model obtained by merging the models from all of the application's non-framework bundles. @@ -98,7 +98,7 @@ @warning Obtaining a managed object model by merging all bundles may result in an application error if versioned object models are in use. */ -- (instancetype)init; +- (id)init; ///----------------------------------------------------------------------------- /// @name Configuring Persistent Stores diff --git a/Code/CoreData/RKManagedObjectStore.m b/Code/CoreData/RKManagedObjectStore.m index c945718270..c7f49ab754 100644 --- a/Code/CoreData/RKManagedObjectStore.m +++ b/Code/CoreData/RKManagedObjectStore.m @@ -57,7 +57,7 @@ + (void)setDefaultStore:(RKManagedObjectStore *)managedObjectStore } } -- (instancetype)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel +- (id)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel { self = [super init]; if (self) { @@ -73,7 +73,7 @@ - (instancetype)initWithManagedObjectModel:(NSManagedObjectModel *)managedObject return self; } -- (instancetype)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator +- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator { self = [self initWithManagedObjectModel:persistentStoreCoordinator.managedObjectModel]; if (self) { diff --git a/Code/CoreData/RKRelationshipConnectionOperation.h b/Code/CoreData/RKRelationshipConnectionOperation.h index 6be6081afb..1265cbbc85 100644 --- a/Code/CoreData/RKRelationshipConnectionOperation.h +++ b/Code/CoreData/RKRelationshipConnectionOperation.h @@ -44,7 +44,7 @@ @param managedObjectCache The managed object cache from which to attempt to fetch a matching object to satisfy the connection. @return The receiver, initialized with the given managed object, connection mapping, and managed object cache. */ -- (instancetype)initWithManagedObject:(NSManagedObject *)managedObject +- (id)initWithManagedObject:(NSManagedObject *)managedObject connection:(RKConnectionDescription *)connection managedObjectCache:(id)managedObjectCache; diff --git a/Code/CoreData/RKRelationshipConnectionOperation.m b/Code/CoreData/RKRelationshipConnectionOperation.m index 1120d8cdb7..6ea0148d41 100644 --- a/Code/CoreData/RKRelationshipConnectionOperation.m +++ b/Code/CoreData/RKRelationshipConnectionOperation.m @@ -70,7 +70,7 @@ @interface RKRelationshipConnectionOperation () @implementation RKRelationshipConnectionOperation -- (instancetype)initWithManagedObject:(NSManagedObject *)managedObject +- (id)initWithManagedObject:(NSManagedObject *)managedObject connection:(RKConnectionDescription *)connection managedObjectCache:(id)managedObjectCache; { diff --git a/Code/Network/RKObjectManager.h b/Code/Network/RKObjectManager.h index 217496cbf1..b38ec3d255 100644 --- a/Code/Network/RKObjectManager.h +++ b/Code/Network/RKObjectManager.h @@ -262,7 +262,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; @param client The AFNetworking HTTP client with which to initialize the receiver. @return The receiver, initialized with the given client. */ -- (instancetype)initWithHTTPClient:(AFHTTPClient *)client; +- (id)initWithHTTPClient:(AFHTTPClient *)client; ///------------------------------------------ /// @name Accessing Object Manager Properties diff --git a/Code/Network/RKObjectRequestOperation.h b/Code/Network/RKObjectRequestOperation.h index 6f11e4da9f..e1e725e2d8 100644 --- a/Code/Network/RKObjectRequestOperation.h +++ b/Code/Network/RKObjectRequestOperation.h @@ -73,7 +73,7 @@ @param responseDescriptors An array of `RKResponseDescriptor` objects specifying how object mapping is to be performed on the response loaded by the network operation. @return The receiver, initialized with the given request and response descriptors. */ -- (instancetype)initWithHTTPRequestOperation:(RKHTTPRequestOperation *)requestOperation responseDescriptors:(NSArray *)responseDescriptors; +- (id)initWithHTTPRequestOperation:(RKHTTPRequestOperation *)requestOperation responseDescriptors:(NSArray *)responseDescriptors; /** Initializes an object request operation with a request object and a set of response descriptors. @@ -87,7 +87,7 @@ @param responseDescriptors An array of `RKResponseDescriptor` objects specifying how object mapping is to be performed on the response loaded by the network operation. @return The receiver, initialized with the given request and response descriptors. */ -- (instancetype)initWithRequest:(NSURLRequest *)request responseDescriptors:(NSArray *)responseDescriptors; +- (id)initWithRequest:(NSURLRequest *)request responseDescriptors:(NSArray *)responseDescriptors; ///--------------------------------- /// @name Configuring Object Mapping diff --git a/Code/Network/RKPaginator.h b/Code/Network/RKPaginator.h index 99cde3e70c..6eb7468919 100644 --- a/Code/Network/RKPaginator.h +++ b/Code/Network/RKPaginator.h @@ -56,7 +56,7 @@ @param responseDescriptors An array of response descriptors describing how to map object representations loaded by object request operations dispatched by the paginator. @return The receiver, initialized with the request, pagination mapping, and response descriptors. */ -- (instancetype)initWithRequest:(NSURLRequest *)request +- (id)initWithRequest:(NSURLRequest *)request paginationMapping:(RKObjectMapping *)paginationMapping responseDescriptors:(NSArray *)responseDescriptors; diff --git a/Code/Network/RKResponseMapperOperation.h b/Code/Network/RKResponseMapperOperation.h index 1559fa430b..abb12d00e0 100644 --- a/Code/Network/RKResponseMapperOperation.h +++ b/Code/Network/RKResponseMapperOperation.h @@ -53,7 +53,7 @@ @param responseDescriptors An array whose elements are `RKResponseDescriptor` objects specifying object mapping configurations that may be applied to the response. @return The receiver, initialized with the response, data, and response descriptor objects. */ -- (instancetype)initWithResponse:(NSHTTPURLResponse *)response +- (id)initWithResponse:(NSHTTPURLResponse *)response data:(NSData *)data responseDescriptors:(NSArray *)responseDescriptors; diff --git a/Code/Network/RKRouter.h b/Code/Network/RKRouter.h index 3a8c322e2c..4bb3cdb4a4 100644 --- a/Code/Network/RKRouter.h +++ b/Code/Network/RKRouter.h @@ -49,7 +49,7 @@ @param baseURL The base URL with which to initialize the receiver. @return The receiver, initialized with the given base URL. */ -- (instancetype)initWithBaseURL:(NSURL *)baseURL; +- (id)initWithBaseURL:(NSURL *)baseURL; ///---------------------- /// @name Generating URLs diff --git a/Code/ObjectMapping/RKMapperOperation.h b/Code/ObjectMapping/RKMapperOperation.h index cd5ac81ef5..7dd5da8353 100644 --- a/Code/ObjectMapping/RKMapperOperation.h +++ b/Code/ObjectMapping/RKMapperOperation.h @@ -81,7 +81,7 @@ @param mappingsDictionary An `NSDictionary` wherein the keys are mappable key paths in `object` and the values are `RKMapping` objects specifying how the representations at its key path are to be mapped. @return The receiver, initialized with the given object and and dictionary of key paths to mappings. */ -- (instancetype)initWithRepresentation:(id)representation mappingsDictionary:(NSDictionary *)mappingsDictionary; +- (id)initWithRepresentation:(id)representation mappingsDictionary:(NSDictionary *)mappingsDictionary; ///------------------------------------------ /// @name Accessing Mapping Result and Errors diff --git a/Code/ObjectMapping/RKMappingOperation.h b/Code/ObjectMapping/RKMappingOperation.h index df60662ebf..344980d458 100644 --- a/Code/ObjectMapping/RKMappingOperation.h +++ b/Code/ObjectMapping/RKMappingOperation.h @@ -147,7 +147,7 @@ @param objectOrDynamicMapping An instance of `RKObjectMapping` or `RKDynamicMapping` defining how the mapping is to be performed. @return The receiver, initialized with a source object, a destination object, and a mapping. */ -- (instancetype)initWithSourceObject:(id)sourceObject destinationObject:(id)destinationObject mapping:(RKMapping *)objectOrDynamicMapping; +- (id)initWithSourceObject:(id)sourceObject destinationObject:(id)destinationObject mapping:(RKMapping *)objectOrDynamicMapping; ///-------------------------------------- /// @name Accessing Mapping Configuration diff --git a/Code/ObjectMapping/RKMappingResult.h b/Code/ObjectMapping/RKMappingResult.h index b4d7bce9a8..de53e4db47 100644 --- a/Code/ObjectMapping/RKMappingResult.h +++ b/Code/ObjectMapping/RKMappingResult.h @@ -35,7 +35,7 @@ @param dictionary A dictionary wherein the keys represent mapped key paths and the values represent the objects mapped at those key paths. Cannot be nil. @return The receiver, initialized with the given dictionary. */ -- (instancetype)initWithDictionary:(NSDictionary *)dictionary; +- (id)initWithDictionary:(NSDictionary *)dictionary; ///---------------------------------------- /// @name Retrieving Result Representations diff --git a/Code/ObjectMapping/RKObjectMapping.h b/Code/ObjectMapping/RKObjectMapping.h index e6f0cefe49..5574d79d13 100644 --- a/Code/ObjectMapping/RKObjectMapping.h +++ b/Code/ObjectMapping/RKObjectMapping.h @@ -83,7 +83,7 @@ @param objectClass The class that the mapping targets. Cannot be `nil`. @return The receiver, initialized with the given class. */ -- (instancetype)initWithClass:(Class)objectClass; +- (id)initWithClass:(Class)objectClass; /** Returns an object mapping with an `objectClass` of `NSMutableDictionary`. diff --git a/Code/Testing/RKConnectionTestExpectation.h b/Code/Testing/RKConnectionTestExpectation.h index 5ad9a60a7e..c665e1fcbd 100644 --- a/Code/Testing/RKConnectionTestExpectation.h +++ b/Code/Testing/RKConnectionTestExpectation.h @@ -50,7 +50,7 @@ @param value The value that is expected to be set for the relationship when the connection is established. @return The receiver, initialized with the given relationship name, attributes dictionary, and expected value. */ -- (instancetype)initWithRelationshipName:(NSString *)relationshipName attributes:(NSDictionary *)attributes value:(id)value; +- (id)initWithRelationshipName:(NSString *)relationshipName attributes:(NSDictionary *)attributes value:(id)value; ///------------------------------------ /// @name Accessing Expectation Details diff --git a/Code/Testing/RKConnectionTestExpectation.m b/Code/Testing/RKConnectionTestExpectation.m index 4f2a3af9f2..c4e86cdd4f 100644 --- a/Code/Testing/RKConnectionTestExpectation.m +++ b/Code/Testing/RKConnectionTestExpectation.m @@ -35,7 +35,7 @@ + (instancetype)expectationWithRelationshipName:(NSString *)relationshipName att return [[self alloc] initWithRelationshipName:relationshipName attributes:attributes value:value]; } -- (instancetype)initWithRelationshipName:(NSString *)relationshipName attributes:(NSDictionary *)attributes value:(id)value +- (id)initWithRelationshipName:(NSString *)relationshipName attributes:(NSDictionary *)attributes value:(id)value { NSParameterAssert(relationshipName); NSAssert(value == nil || diff --git a/Code/Testing/RKMappingTest.h b/Code/Testing/RKMappingTest.h index 9c6d0f0df1..01d75a58bb 100644 --- a/Code/Testing/RKMappingTest.h +++ b/Code/Testing/RKMappingTest.h @@ -105,7 +105,7 @@ extern NSString * const RKMappingTestExpectationErrorKey; @param destinationObject The destionation object being to. @return The receiver, initialized with mapping, sourceObject and destinationObject. */ -- (instancetype)initWithMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject; +- (id)initWithMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject; ///---------------------------- /// @name Managing Expectations From 28887d3384ed8fda390fe5c9fd2db8ed490a7a22 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Tue, 1 Jan 2013 15:26:36 -0500 Subject: [PATCH 15/27] Add support for deletion of mapped objects by predicate. closes #1109 --- Code/CoreData/RKEntityMapping.h | 11 +++ ...KManagedObjectMappingOperationDataSource.m | 79 ++++++++++++++++++- ...agedObjectMappingOperationDataSourceTest.m | 44 +++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/Code/CoreData/RKEntityMapping.h b/Code/CoreData/RKEntityMapping.h index a8191b302c..1f58c29443 100644 --- a/Code/CoreData/RKEntityMapping.h +++ b/Code/CoreData/RKEntityMapping.h @@ -177,6 +177,17 @@ */ - (RKConnectionDescription *)connectionForRelationship:(id)relationshipOrName; +///------------------------------------ +/// @name Flagging Objects for Deletion +///------------------------------------ + +/** + A predicate that identifies objects for the receiver's entity that are to be deleted from the local store. + + This property provides support for local deletion of managed objects mapped as a 'tombstone' record from the source representation. + */ +@property (nonatomic, copy) NSPredicate *deletionPredicate; + ///------------------------------------------ /// @name Retrieving Default Attribute Values ///------------------------------------------ diff --git a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m index b627edea2c..3a0aa6af4b 100644 --- a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m +++ b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m @@ -18,6 +18,7 @@ // limitations under the License. // +#import #import "RKManagedObjectMappingOperationDataSource.h" #import "RKObjectMapping.h" #import "RKEntityMapping.h" @@ -34,6 +35,8 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName; +static char kRKManagedObjectMappingOperationDataSourceAssociatedObjectKey; + id RKTransformedValueWithClass(id value, Class destinationType, NSValueTransformer *dateToStringValueTransformer); NSArray *RKApplyNestingAttributeValueToMappings(NSString *attributeName, id value, NSArray *propertyMappings); @@ -117,6 +120,58 @@ static id RKValueForAttributeMappingInRepresentation(RKAttributeMapping *attribu return entityIdentifierAttributes; } +@interface RKManagedObjectDeletionOperation : NSOperation + +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; +- (void)addEntityMapping:(RKEntityMapping *)entityMapping; +@end + +@interface RKManagedObjectDeletionOperation () +@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext; +@property (nonatomic, strong) NSMutableSet *entityMappings; +@end + +@implementation RKManagedObjectDeletionOperation + +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + self = [self init]; + if (self) { + self.managedObjectContext = managedObjectContext; + self.entityMappings = [NSMutableSet new]; + } + return self; +} + +- (void)addEntityMapping:(RKEntityMapping *)entityMapping +{ + if (! entityMapping.deletionPredicate) return; + [self.entityMappings addObject:entityMapping]; +} + +- (void)main +{ + [self.managedObjectContext performBlockAndWait:^{ + NSMutableSet *objectsToDelete = [NSMutableSet set]; + for (RKEntityMapping *entityMapping in self.entityMappings) { + NSFetchRequest *fetchRequest = [NSFetchRequest alloc]; + [fetchRequest setEntity:entityMapping.entity]; + [fetchRequest setPredicate:entityMapping.deletionPredicate]; + NSError *error = nil; + NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; + if (fetchedObjects) { + [objectsToDelete addObjectsFromArray:fetchedObjects]; + } + } + + for (NSManagedObject *managedObject in objectsToDelete) { + [self.managedObjectContext deleteObject:managedObject]; + } + }]; +} + +@end + // Set Logging Component #undef RKLogComponent #define RKLogComponent RKlcl_cRestKitCoreData @@ -126,6 +181,7 @@ static id RKValueForAttributeMappingInRepresentation(RKAttributeMapping *attribu @interface RKManagedObjectMappingOperationDataSource () @property (nonatomic, strong, readwrite) NSManagedObjectContext *managedObjectContext; @property (nonatomic, strong, readwrite) id managedObjectCache; +@property (nonatomic, strong) NSMutableArray *deletionPredicates; @end @implementation RKManagedObjectMappingOperationDataSource @@ -169,7 +225,7 @@ - (id)mappingOperation:(RKMappingOperation *)mappingOperation targetObjectForRep "Unable to update existing object instances by identification attributes. Duplicate objects may be created."); } - // If we have found the entity identifier attributes, try to find an existing instance to update + // If we have found the entity identification attributes, try to find an existing instance to update NSEntityDescription *entity = [entityMapping entity]; NSManagedObject *managedObject = nil; if ([entityIdentifierAttributes count]) { @@ -246,6 +302,27 @@ - (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation [operationQueue addOperation:operation]; RKLogTrace(@"Enqueued %@ dependent upon parent operation %@ to operation queue %@", operation, self.parentOperation, operationQueue); } + + // Handle tombstone deletion by predicate + if ([(RKEntityMapping *)mappingOperation.objectMapping deletionPredicate]) { + RKManagedObjectDeletionOperation *deletionOperation = nil; + if (self.parentOperation) { + // Attach a deletion operation for execution after the parent operation completes + deletionOperation = (RKManagedObjectDeletionOperation *)objc_getAssociatedObject(self.parentOperation, &kRKManagedObjectMappingOperationDataSourceAssociatedObjectKey); + if (! deletionOperation) { + deletionOperation = [[RKManagedObjectDeletionOperation alloc] initWithManagedObjectContext:self.managedObjectContext]; + objc_setAssociatedObject(self.parentOperation, &kRKManagedObjectMappingOperationDataSourceAssociatedObjectKey, deletionOperation, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [deletionOperation addDependency:self.parentOperation]; + NSOperationQueue *operationQueue = self.operationQueue ?: [NSOperationQueue currentQueue]; + [operationQueue addOperation:deletionOperation]; + } + [deletionOperation addEntityMapping:(RKEntityMapping *)mappingOperation.objectMapping]; + } else { + deletionOperation = [[RKManagedObjectDeletionOperation alloc] initWithManagedObjectContext:self.managedObjectContext]; + [deletionOperation addEntityMapping:(RKEntityMapping *)mappingOperation.objectMapping]; + [deletionOperation start]; + } + } } return YES; diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m index a33f8a42e4..30efed76a6 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m @@ -1064,6 +1064,50 @@ - (void)testConnectingToSubentitiesByInMemoryCache } +- (void)testDeletionOfTombstoneRecords +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKEntityMapping *mapping = [[RKEntityMapping alloc] initWithEntity:entity]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + mapping.deletionPredicate = [NSPredicate predicateWithFormat:@"sex = %@", @"female"]; + + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + human.sex = @"female"; + + RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:nil]; + NSDictionary *representation = @{ @"name": @"Whatever" }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:representation destinationObject:human mapping:mapping]; + operation.dataSource = dataSource; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + expect([human isDeleted]).to.equal(YES); +} + +- (void)testDeletionOfTombstoneRecordsInMapperOperation +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKEntityMapping *mapping = [[RKEntityMapping alloc] initWithEntity:entity]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + mapping.deletionPredicate = [NSPredicate predicateWithFormat:@"sex = %@", @"female"]; + + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + human.sex = @"female"; + + RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:nil]; + NSDictionary *representation = @{ @"name": @"Whatever" }; + NSError *error = nil; + RKMapperOperation *mapperOperation = [[RKMapperOperation alloc] initWithRepresentation:representation mappingsDictionary:@{ [NSNull null]: mapping }]; + mapperOperation.mappingOperationDataSource = dataSource; + BOOL success = [mapperOperation execute:&error]; + assertThatBool(success, is(equalToBool(YES))); + expect([human isDeleted]).to.equal(YES); +} + // TODO: Import bencharmk utility somehow... //- (void)testMappingAPayloadContainingRepeatedObjectsPerformsAcceptablyWithFetchRequestMappingCache //{ From c08909761eaefe0a40e36c38ada93ddc0d015503 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Tue, 1 Jan 2013 22:21:47 -0500 Subject: [PATCH 16/27] Adjust completion block implementation for `RKPaginator` to enable completion block to be invoked when used without a strong reference. fixes #1119 fixes #1093 --- Code/Network/RKObjectRequestOperation.m | 14 +++++++++++ Code/Network/RKPaginator.m | 28 +++++++++++---------- Tests/Logic/ObjectMapping/RKPaginatorTest.m | 20 ++++++++++----- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/Code/Network/RKObjectRequestOperation.m b/Code/Network/RKObjectRequestOperation.m index 1b412b26cf..4f58348c03 100644 --- a/Code/Network/RKObjectRequestOperation.m +++ b/Code/Network/RKObjectRequestOperation.m @@ -201,6 +201,20 @@ - (void)setCompletionBlock:(void (^)(void))block } } +- (void)setWillMapDeserializedResponseBlock:(id (^)(id))block +{ + if (!block) { + _willMapDeserializedResponseBlock = nil; + } else { + __unsafe_unretained id weakSelf = self; + _willMapDeserializedResponseBlock = ^id (id deserializedResponse) { + id result = block(deserializedResponse); + [weakSelf setWillMapDeserializedResponseBlock:nil]; + return result; + }; + } +} + - (void)setCompletionBlockWithSuccess:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure { diff --git a/Code/Network/RKPaginator.m b/Code/Network/RKPaginator.m index 8894baae85..d59980f52c 100644 --- a/Code/Network/RKPaginator.m +++ b/Code/Network/RKPaginator.m @@ -178,20 +178,21 @@ - (void)loadPage:(NSUInteger)pageNumber // Add KVO to ensure notification of loaded state prior to execution of completion block [self.objectRequestOperation addObserver:self forKeyPath:@"isFinished" options:0 context:nil]; - - __weak RKPaginator *weakSelf = self; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" [self.objectRequestOperation setWillMapDeserializedResponseBlock:^id(id deserializedResponseBody) { NSError *error = nil; - RKMappingOperation *mappingOperation = [[RKMappingOperation alloc] initWithSourceObject:deserializedResponseBody destinationObject:weakSelf mapping:weakSelf.paginationMapping]; + RKMappingOperation *mappingOperation = [[RKMappingOperation alloc] initWithSourceObject:deserializedResponseBody destinationObject:self mapping:self.paginationMapping]; BOOL success = [mappingOperation performMapping:&error]; if (!success) { - weakSelf.pageCount = 0; - weakSelf.currentPage = 0; + self.pageCount = 0; + self.currentPage = 0; RKLogError(@"Paginator didn't map info to compute page count. Assuming no pages."); - } else if (weakSelf.perPage && [weakSelf hasObjectCount]) { - float objectCountFloat = weakSelf.objectCount; - weakSelf.pageCount = ceilf(objectCountFloat / weakSelf.perPage); - RKLogInfo(@"Paginator objectCount: %ld pageCount: %ld", (long)weakSelf.objectCount, (long)weakSelf.pageCount); + } else if (self.perPage && [self hasObjectCount]) { + float objectCountFloat = self.objectCount; + self.pageCount = ceilf(objectCountFloat / self.perPage); + RKLogInfo(@"Paginator objectCount: %ld pageCount: %ld", (long)self.objectCount, (long)self.pageCount); } else { RKLogError(@"Paginator perPage set is 0."); } @@ -199,14 +200,15 @@ - (void)loadPage:(NSUInteger)pageNumber return deserializedResponseBody; }]; [self.objectRequestOperation setCompletionBlockWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) { - if (weakSelf.successBlock) { - weakSelf.successBlock(weakSelf, [mappingResult array], weakSelf.currentPage); + if (self.successBlock) { + self.successBlock(self, [mappingResult array], self.currentPage); } } failure:^(RKObjectRequestOperation *operation, NSError *error) { - if (weakSelf.failureBlock) { - weakSelf.failureBlock(weakSelf, error); + if (self.failureBlock) { + self.failureBlock(self, error); } }]; +#pragma clang diagnostic pop if (self.operationQueue) { [self.operationQueue addOperation:self.objectRequestOperation]; diff --git a/Tests/Logic/ObjectMapping/RKPaginatorTest.m b/Tests/Logic/ObjectMapping/RKPaginatorTest.m index 87fe2df8e4..890faf5cf9 100644 --- a/Tests/Logic/ObjectMapping/RKPaginatorTest.m +++ b/Tests/Logic/ObjectMapping/RKPaginatorTest.m @@ -220,9 +220,7 @@ - (void)testInvocationOfCompletionBlockWithSuccess } failure:nil]; [paginator loadPage:1]; [paginator waitUntilFinished]; - dispatch_async(dispatch_get_main_queue(), ^{ - expect(blockObjects).notTo.beNil(); - }); + expect(blockObjects).willNot.beNil(); } - (void)testOnDidFailWithErrorBlockIsInvokedOnError @@ -235,9 +233,19 @@ - (void)testOnDidFailWithErrorBlockIsInvokedOnError }]; [paginator loadPage:999]; [paginator waitUntilFinished]; - dispatch_async(dispatch_get_main_queue(), ^{ - expect(expectedError).notTo.beNil(); - }); + expect(expectedError).willNot.beNil(); +} + +- (void)testInvocationOfCompletionBlockWithoutWaiting +{ + NSURLRequest *request = [NSURLRequest requestWithURL:self.paginationURL]; + RKPaginator *paginator = [[RKPaginator alloc] initWithRequest:request paginationMapping:self.paginationMapping responseDescriptors:@[ self.responseDescriptor ]]; + __block NSArray *blockObjects = nil; + [paginator setCompletionBlockWithSuccess:^(RKPaginator *paginator, NSArray *objects, NSUInteger page) { + blockObjects = objects; + } failure:nil]; + [paginator loadPage:1]; + expect(blockObjects).willNot.beNil(); } - (void)testLoadingNextPageOfObjects From a30263b19609bb04439027f94269516d6bcaf4e1 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Tue, 1 Jan 2013 22:38:18 -0500 Subject: [PATCH 17/27] Fix issues with using the Paginator as documented in the headers. fixes #1117 --- Code/Network/RKPaginator.h | 6 +++--- Code/Network/RKPaginator.m | 6 ------ Tests/Logic/ObjectMapping/RKPaginatorTest.m | 16 ++++++++++++++++ Tests/Server/server.rb | 4 +--- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Code/Network/RKPaginator.h b/Code/Network/RKPaginator.h index 6eb7468919..db8071e025 100644 --- a/Code/Network/RKPaginator.h +++ b/Code/Network/RKPaginator.h @@ -36,9 +36,9 @@ RKObjectMapping *paginationMapping = [RKObjectMapping mappingForClass:[RKPaginator class]]; [paginationMapping addAttributeMappingsFromDictionary:@{ - @"pagination.per_page", @"perPage", - @"pagination.total_pages", @"pageCount", - @"pagination.total_objects", @"objectCount", + @"pagination.per_page": @"perPage", + @"pagination.total_pages": @"pageCount", + @"pagination.total_objects": @"objectCount", }]; ## iOS 5 Compatibility Caveats diff --git a/Code/Network/RKPaginator.m b/Code/Network/RKPaginator.m index d59980f52c..8c223274af 100644 --- a/Code/Network/RKPaginator.m +++ b/Code/Network/RKPaginator.m @@ -123,12 +123,6 @@ - (NSUInteger)currentPage return _currentPage; } -- (NSUInteger)pageCount -{ - NSAssert([self hasPageCount], @"Page count not available."); - return _pageCount; -} - - (BOOL)hasNextPage { NSAssert(self.isLoaded, @"Cannot determine hasNextPage: paginator is not loaded."); diff --git a/Tests/Logic/ObjectMapping/RKPaginatorTest.m b/Tests/Logic/ObjectMapping/RKPaginatorTest.m index 890faf5cf9..6d1ddfefb1 100644 --- a/Tests/Logic/ObjectMapping/RKPaginatorTest.m +++ b/Tests/Logic/ObjectMapping/RKPaginatorTest.m @@ -154,6 +154,22 @@ - (void)testLoadingAPageOfObjects expect(paginator.isLoaded).to.equal(YES); } +- (void)testLoadingAPageOfObjectsWithDocumentedPaginationMapping +{ + RKObjectMapping *paginationMapping = [RKObjectMapping mappingForClass:[RKPaginator class]]; + [paginationMapping addAttributeMappingsFromDictionary:@{ + @"per_page": @"perPage", + @"total_pages": @"pageCount", + @"total_objects": @"objectCount", + }]; + + NSURLRequest *request = [NSURLRequest requestWithURL:self.paginationURL]; + RKPaginator *paginator = [[RKPaginator alloc] initWithRequest:request paginationMapping:paginationMapping responseDescriptors:@[ self.responseDescriptor ]]; + [paginator loadPage:1]; + [paginator waitUntilFinished]; + expect(paginator.isLoaded).to.equal(YES); +} + - (void)testLoadingPageOfObjectMapsPerPage { NSURLRequest *request = [NSURLRequest requestWithURL:self.paginationURL]; diff --git a/Tests/Server/server.rb b/Tests/Server/server.rb index 3fca541098..6236253f3a 100755 --- a/Tests/Server/server.rb +++ b/Tests/Server/server.rb @@ -234,8 +234,6 @@ def render_fixture(path, options = {}) current_page = params[:page].to_i entries = [] - puts "Params are: #{params.inspect}. CurrentPage = #{current_page}" - case current_page when 1 entries << Person.new('Blake', 29) @@ -255,7 +253,7 @@ def render_fixture(path, options = {}) end {:per_page => per_page, :total_entries => total_entries, - :current_page => current_page, :entries => entries}.to_json + :current_page => current_page, :entries => entries, :total_pages => 3}.to_json end get '/coredata/etag' do From 879ffd73e6bafb9856b11347562a036d9a9d9c87 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Tue, 1 Jan 2013 23:56:58 -0500 Subject: [PATCH 18/27] Add support for deleting Core Data managed objects that fail validation out of the mapping context. This enables you to silently drop mapping for managed objects that fail validation. fixes #691 closes #694 --- ...KManagedObjectMappingOperationDataSource.m | 14 ++++++-- .../Network/RKManagedObjectRequestOperation.m | 5 ++- ...agedObjectMappingOperationDataSourceTest.m | 1 + .../RKManagedObjectRequestOperationTest.m | 32 ++++++++++++++++++ Tests/Models/Data Model.xcdatamodel/elements | Bin 322018 -> 322007 bytes Tests/Models/Data Model.xcdatamodel/layout | Bin 46897 -> 46897 bytes Tests/Server/server.rb | 5 +++ 7 files changed, 54 insertions(+), 3 deletions(-) diff --git a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m index 3a0aa6af4b..21595a38fd 100644 --- a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m +++ b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m @@ -32,6 +32,7 @@ #import "RKValueTransformers.h" #import "RKRelationshipMapping.h" #import "RKObjectUtilities.h" +#import "NSManagedObject+RKAdditions.h" extern NSString * const RKObjectMappingNestingAttributeKeyName; @@ -243,8 +244,7 @@ - (id)mappingOperation:(RKMappingOperation *)mappingOperation targetObjectForRep } if (managedObject == nil) { - managedObject = [[NSManagedObject alloc] initWithEntity:entity - insertIntoManagedObjectContext:self.managedObjectContext]; + managedObject = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext]; [managedObject setValuesForKeysWithDictionary:entityIdentifierAttributes]; if ([self.managedObjectCache respondsToSelector:@selector(didCreateObject:)]) { @@ -276,6 +276,16 @@ - (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation if ([mappingOperation.objectMapping isKindOfClass:[RKEntityMapping class]]) { [self emitDeadlockWarningIfNecessary]; + // Validate unsaved objects + if ([mappingOperation.destinationObject isKindOfClass:[NSManagedObject class]] && [(NSManagedObject *)mappingOperation.destinationObject isNew]) { + NSError *validationError = nil; + if (! [(NSManagedObject *)mappingOperation.destinationObject validateForInsert:&validationError]) { + RKLogDebug(@"Unsaved NSManagedObject failed `validateForInsert:` - Deleting object from context: %@", validationError); + [self.managedObjectContext deleteObject:mappingOperation.destinationObject]; + return YES; + } + } + NSArray *connections = [(RKEntityMapping *)mappingOperation.objectMapping connections]; if ([connections count] > 0 && self.managedObjectCache == nil) { NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Cannot map an entity mapping that contains connection mappings with a data source whose managed object cache is nil." }; diff --git a/Code/Network/RKManagedObjectRequestOperation.m b/Code/Network/RKManagedObjectRequestOperation.m index 3bba6d844b..345b903682 100644 --- a/Code/Network/RKManagedObjectRequestOperation.m +++ b/Code/Network/RKManagedObjectRequestOperation.m @@ -654,7 +654,10 @@ - (void)willFinish return; } success = [self saveContext:&error]; - if (! success) self.error = error; + if (! success) { + self.error = error; + return; + } // Refetch all managed objects nested at key paths within the results dictionary before returning if (self.mappingResult) { diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m index 30efed76a6..9b76ddcca2 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m @@ -296,6 +296,7 @@ - (void)testThatMappingAnEntityMappingContainingAConnectionMappingWithANilManage RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext cache:nil]; id mockOperation = [OCMockObject mockForClass:[RKMappingOperation class]]; + [[mockOperation stub] destinationObject]; [[[mockOperation stub] andReturn:mapping] objectMapping]; NSError *error = nil; BOOL success = [dataSource commitChangesForMappingOperation:mockOperation error:&error]; diff --git a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m index 8ada240d4c..a1c9e6c753 100644 --- a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m @@ -13,6 +13,22 @@ #import "RKTestUser.h" #import "RKMappingErrors.h" +@interface RKPost : NSManagedObject +@end + +@implementation RKPost + +- (BOOL)validateTitle:(id *)ioValue error:(NSError **)outError { + // Don't allow blank titles + if ((*ioValue == nil) || ([[(NSString*)*ioValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] isEqualToString:@""])) { + return NO; + } + + return YES; +} + +@end + @interface RKManagedObjectRequestOperation () - (NSSet *)localObjectsFromFetchRequestsMatchingRequestURL:(NSError **)error; @end @@ -447,6 +463,7 @@ - (void)testThatDeletionOfOrphanedObjectsCanBeSuppressedByPredicate [tagOnDiferentObject setValue:@"orphaned" forKey:@"name"]; NSManagedObject *otherPost = [NSEntityDescription insertNewObjectForEntityForName:@"Post" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + [otherPost setValue:@"Title" forKey:@"title"]; [otherPost setValue:[NSSet setWithObject:tagOnDiferentObject] forKey:@"tags"]; RKEntityMapping *postMapping = [RKEntityMapping mappingForEntityForName:@"Post" inManagedObjectStore:managedObjectStore]; @@ -738,4 +755,19 @@ - (void)testThatMappingObjectsWithTheSameIdentificationAttributesAcrossTwoObject expect(fetchedObjects).to.haveCountOf(1); } +- (void)testManagedObjectRequestOperationCompletesAndIgnoresInvalidObjects +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *postMapping = [RKEntityMapping mappingForEntityForName:@"Post" inManagedObjectStore:managedObjectStore]; + [postMapping addAttributeMappingsFromArray:@[ @"title", @"body" ]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:postMapping pathPattern:nil keyPath:@"posts" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/posts_with_invalid.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).to.beNil(); + expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); +} + @end diff --git a/Tests/Models/Data Model.xcdatamodel/elements b/Tests/Models/Data Model.xcdatamodel/elements index 4a2cdf976b6caa00867d2b40ea7cb641ecc02615..531946e1c7a758da4d0b8bb9eab9059165132bbe 100644 GIT binary patch delta 21190 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 delta 21183 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 diff --git a/Tests/Models/Data Model.xcdatamodel/layout b/Tests/Models/Data Model.xcdatamodel/layout index 6d4db1b7848bb070316b8ad650b6e53d59df8e09..1188bb2b9e9897d88f5a7408496bb0c93ebe2ab8 100644 GIT binary patch delta 669 zcmW-bZAepL7>3XLo^8iviRUpBLZ~3LrA3(vLZzvsM3VWD;s>apsM!ipmgu(g#)48O zwC8=3l+emR$zrA%QekCbS&%=(D1_2}sAW<}Vt$p|^Ygy0oBQdRr#*RmB<{@NeU~LW-*)F0{ZO`1hzlZ?vrZp`a>tv#{k@ z+W?p!HB~?dboYAU_|>2%@OHFoYQwN9;78h4J?^3xuD=`1hr*~-0wMRFJ3aX~$QySI zL%I2eFKkX;a|B3|M08gBtlL|dXkJ#qIegayaeJIE;7n(IEvSa63W!U0ta^=>?xa{y zeQ&E^`)fG`KaZI9f@}R?Ae2Q8{DSNlwFFEVj^m!KH=dWad0UIRRtMx*Y*moe|D_HZ z(w0n_kh)H+?Q!o|NFcY&e z2lKH2i?A3=@hqOh3s{NOcoA#Sigvt&m+=ZVU=vaD zH6P(0_!$4hr}#9V(nOZX6NRE&oEKH1 KM%ZwrKl(ouFB?q& delta 669 zcmW-bZAg<*6vub(^K6gHl0C;Ph^U}YXGSPmD2PjY5i-(ztEQxpq85wF5^=ic%7QW% z<+)cW39S^eq$ZjmA(jT~#SaEyP(o>eS}BoaX47&zpMHn){~i9FGqiJtx`*l=x%DrD(W82Rz~9( zS(l~NG9ojnoRK;vbui*()U9PQlBE@CKT2-Br_v#8JW;-G18}*umvX_1+i%a>X)KIU^>AZ0jgr3gH^kldm(qk=~-T@6i^N0 zr+lObWNK@Gg4f+fT0g_4-wAK>6$V?&_vog)WwdOq6y#?tHlJm23J!8)Ce$W>Eru!Osw z`ELu#FY1Rmxyk8=k;`kJ_|j9bGG);Tp%0$ApxPN61GdLk{k7M?;W)Jjf=IL1FWl_d zmk5@yd=nfkuQrrH_PyH5t~zmiQ+hZ?U?gtGXpF&~7>@~P!6e*+DYzdGU>as%HXgxT zJcb2$0*kO1PofRa;5od26=+8X*5F0Fgth3xYlzr@i9ID5*LeeP;w`+5-{+5bH}B&E z+{1_X8$QB6@GpFt|Kz{9R}f(kfx;w0#0IfhM2aX8FOo&7NEg|{D)L34C>5tgnWz#q I__t@rf6Jd6M*si- diff --git a/Tests/Server/server.rb b/Tests/Server/server.rb index 6236253f3a..bc93fb9b74 100755 --- a/Tests/Server/server.rb +++ b/Tests/Server/server.rb @@ -302,6 +302,11 @@ def render_fixture(path, options = {}) content_type 'application/json' { :posts => [{:title => 'Post Title', :body => 'Some body.', :tags => [{ :name => 'development' }, { :name => 'restkit' }] }] }.to_json end + + get '/posts_with_invalid.json' do + content_type 'application/json' + { :posts => [{:title => 'Post Title', :body => 'Some body.'}, {:title => '', :body => 'Some body.'} ] }.to_json + end # start the server if ruby file executed directly run! if app_file == $0 From c06347d5c51d7b0ace6906ab80a10bbd723373d1 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Wed, 2 Jan 2013 00:27:40 -0500 Subject: [PATCH 19/27] Add support for customizing the HTTP request operation class used by `RKPaginator`. closes #1067 --- Code/Network/RKObjectManager.m | 1 + Code/Network/RKPaginator.h | 19 +++++++++++++++++-- Code/Network/RKPaginator.m | 10 +++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Code/Network/RKObjectManager.m b/Code/Network/RKObjectManager.m index f0f2fce145..2914c4b67a 100644 --- a/Code/Network/RKObjectManager.m +++ b/Code/Network/RKObjectManager.m @@ -555,6 +555,7 @@ - (RKPaginator *)paginatorWithPathPattern:(NSString *)pathPattern paginator.managedObjectCache = self.managedObjectStore.managedObjectCache; paginator.fetchRequestBlocks = self.fetchRequestBlocks; paginator.operationQueue = self.operationQueue; + if (self.HTTPOperationClass) paginator.HTTPOperationClass = self.HTTPOperationClass; return paginator; } diff --git a/Code/Network/RKPaginator.h b/Code/Network/RKPaginator.h index db8071e025..a02c409e51 100644 --- a/Code/Network/RKPaginator.h +++ b/Code/Network/RKPaginator.h @@ -48,6 +48,10 @@ */ @interface RKPaginator : NSObject +///------------------------------------- +/// @name Initializing Paginator Objects +///------------------------------------- + /** Initializes a RKPaginator object with the a provided patternURL and mappingProvider. @@ -57,8 +61,12 @@ @return The receiver, initialized with the request, pagination mapping, and response descriptors. */ - (id)initWithRequest:(NSURLRequest *)request - paginationMapping:(RKObjectMapping *)paginationMapping - responseDescriptors:(NSArray *)responseDescriptors; + paginationMapping:(RKObjectMapping *)paginationMapping + responseDescriptors:(NSArray *)responseDescriptors; + +///----------------------------- +/// @name Configuring Networking +///----------------------------- /** A URL with a path pattern for building a complete URL from @@ -89,6 +97,13 @@ */ @property (nonatomic, strong) NSOperationQueue *operationQueue; +/** + The `RKHTTPRequestOperation` subclass to be used for HTTP request operations made by the paginator. + + **Default**: `[RKHTTPRequestOperation class]` + */ +@property (nonatomic, strong) Class HTTPOperationClass; + ///----------------------------------- /// @name Setting the Completion Block ///----------------------------------- diff --git a/Code/Network/RKPaginator.m b/Code/Network/RKPaginator.m index 8c223274af..4e51296d4c 100644 --- a/Code/Network/RKPaginator.m +++ b/Code/Network/RKPaginator.m @@ -63,6 +63,7 @@ - (id)initWithRequest:(NSURLRequest *)request NSAssert([paginationMapping.objectClass isSubclassOfClass:[RKPaginator class]], @"The paginationMapping must have a target object class of `RKPaginator`"); self = [super init]; if (self) { + self.HTTPOperationClass = [RKHTTPRequestOperation class]; self.request = request; self.paginationMapping = paginationMapping; self.responseDescriptors = responseDescriptors; @@ -93,6 +94,12 @@ - (NSURL *)URL return [NSURL URLWithString:interpolatedString relativeToURL:self.request.URL]; } +- (void)setHTTPOperationClass:(Class)operationClass +{ + NSAssert(operationClass == nil || [operationClass isSubclassOfClass:[RKHTTPRequestOperation class]], @"The HTTP operation class must be a subclass of `RKHTTPRequestOperation`"); + _HTTPOperationClass = operationClass; +} + - (void)setCompletionBlockWithSuccess:(void (^)(RKPaginator *paginator, NSArray *objects, NSUInteger page))success failure:(void (^)(RKPaginator *paginator, NSError *error))failure { @@ -159,7 +166,8 @@ - (void)loadPage:(NSUInteger)pageNumber mutableRequest.URL = self.URL; if (self.managedObjectContext) { - RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:mutableRequest responseDescriptors:self.responseDescriptors]; + RKHTTPRequestOperation *requestOperation = [[self.HTTPOperationClass alloc] initWithRequest:mutableRequest]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithHTTPRequestOperation:requestOperation responseDescriptors:self.responseDescriptors]; managedObjectRequestOperation.managedObjectContext = self.managedObjectContext; managedObjectRequestOperation.managedObjectCache = self.managedObjectCache; managedObjectRequestOperation.fetchRequestBlocks = self.fetchRequestBlocks; From 688ef760f4b047b12cdf2e04da1c0ba6f2b426d1 Mon Sep 17 00:00:00 2001 From: Jean Regisser Date: Wed, 2 Jan 2013 19:49:02 +0100 Subject: [PATCH 20/27] Bumped AFNetworking to 1.1.0 --- RestKit.podspec | 2 +- Vendor/AFNetworking | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RestKit.podspec b/RestKit.podspec index 845e7e1208..7117888071 100644 --- a/RestKit.podspec +++ b/RestKit.podspec @@ -37,7 +37,7 @@ Pod::Spec.new do |s| ns.ios.frameworks = 'CFNetwork', 'Security', 'MobileCoreServices', 'SystemConfiguration' ns.osx.frameworks = 'CoreServices', 'Security', 'SystemConfiguration' ns.dependency 'SOCKit' - ns.dependency 'AFNetworking', '1.0.1' + ns.dependency 'AFNetworking', '1.1.0' ns.dependency 'RestKit/ObjectMapping' ns.dependency 'RestKit/Support' end diff --git a/Vendor/AFNetworking b/Vendor/AFNetworking index ff8e70a49c..121ef7afa8 160000 --- a/Vendor/AFNetworking +++ b/Vendor/AFNetworking @@ -1 +1 @@ -Subproject commit ff8e70a49ca8070f5b528794eb61cd9be4de0f9f +Subproject commit 121ef7afa8ce1b1cefdc5312f7e9d75f5af65b8d From bf63a77bc1d197470c1b71388815078bf95bd45e Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 3 Jan 2013 11:10:43 -0500 Subject: [PATCH 21/27] Add support for parameterizing an array of objects. closes #398 --- Code/Network/RKObjectManager.h | 19 +- Code/Network/RKObjectManager.m | 134 ++++++++--- Code/Network/RKPaginator.h | 4 +- Code/Network/RKPaginator.m | 1 + .../Logic/ObjectMapping/RKObjectManagerTest.m | 214 ++++++++++++++++++ Tests/Models/RKTestUser.h | 1 + 6 files changed, 329 insertions(+), 44 deletions(-) diff --git a/Code/Network/RKObjectManager.h b/Code/Network/RKObjectManager.h index b38ec3d255..6c42a48582 100644 --- a/Code/Network/RKObjectManager.h +++ b/Code/Network/RKObjectManager.h @@ -146,6 +146,15 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; In the above example, request and response mapping configurations were described for a simple data model and then used to perform a basic POST operation and map the results. An arbitrary number of request and response descriptors may be added to the manager to accommodate your application's needs. + ## Multi-object Parameterization + + The object manager provides support for the parameterization of multiple objects provided as an array. The `requestWithObject:method:path:parameters:` and `multipartFormRequestWithObject:method:path:parameters:constructingBodyWithBlock:` methods can parameterize an array of objects for you provided that the `RKRequestDescriptor` objects are configured in a compatible way. The rules for multi-object parameterization are simple: + + 1. If a `nil` root key path is used, then it must be used for all objects in the array. This is because the objects will be parameterized into a dictionary and then each dictionary will be added to an array. This array is then serialized for transport, so objects parameterized to a non-nil key path cannot be merged with the array. + 1. If a `nil` root key path is used to parameterize the array of objects, then you cannot provide additional parameters to be merged with the request. This is again because you cannot merge a dictionary with an array. + + If non-nil key paths are used, then each object will be set in the parameters dictionary at the specified key-path. If more than one object uses the same root key path, then the parameters will be combined into an array for transport. + ## MIME Types MIME Types serve an important function to the object manager. They are used to identify how content is to be serialized when constructing request bodies and also used to set the 'Accept' header for content negotiation. RestKit aspires to be content type agnostic by leveraging the pluggable `RKMIMESerialization` class to handle content serialization and deserialization. @@ -613,7 +622,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; The type of object request operation created is determined by invoking `appropriateObjectRequestOperationWithObject:method:path:parameters:`. - @param object The object with which to construct the object request operation. Cannot be nil. + @param object The object with which to construct the object request operation. If `nil`, then the path must be provided. @param path The path to be appended to the HTTP client's base URL and used as the request URL. If nil, the request URL will be obtained by consulting the router for a route registered for the given object's class and the `RKRequestMethodGET` request method. @param parameters The parameters to be encoded and appended as the query string for the request URL. @param success A block object to be executed when the object request operation finishes successfully. This block has no return value and takes two arguments: the created object request operation and the `RKMappingResult` object created by object mapping the response data of request. @@ -631,7 +640,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; /** Creates an `RKObjectRequestOperation` with a `POST` request for the given object, and enqueues it to the manager's operation queue. - @param object The object with which to construct the object request operation. Cannot be nil. + @param object The object with which to construct the object request operation. If `nil`, then the path must be provided. @param path The path to be appended to the HTTP client's base URL and used as the request URL. If nil, the request URL will be obtained by consulting the router for a route registered for the given object's class and the `RKRequestMethodPOST` method. @param parameters The parameters to be reverse merged with the parameterization of the given object and set as the request body. @param success A block object to be executed when the object request operation finishes successfully. This block has no return value and takes two arguments: the created object request operation and the `RKMappingResult` object created by object mapping the response data of request. @@ -649,7 +658,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; /** Creates an `RKObjectRequestOperation` with a `PUT` request for the given object, and enqueues it to the manager's operation queue. - @param object The object with which to construct the object request operation. Cannot be nil. + @param object The object with which to construct the object request operation. If `nil`, then the path must be provided. @param path The path to be appended to the HTTP client's base URL and used as the request URL. If nil, the request URL will be obtained by consulting the router for a route registered for the given object's class and the `RKRequestMethodPUT` method. @param parameters The parameters to be reverse merged with the parameterization of the given object and set as the request body. @param success A block object to be executed when the object request operation finishes successfully. This block has no return value and takes two arguments: the created object request operation and the `RKMappingResult` object created by object mapping the response data of request. @@ -667,7 +676,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; /** Creates an `RKObjectRequestOperation` with a `PATCH` request for the given object, and enqueues it to the manager's operation queue. - @param object The object with which to construct the object request operation. Cannot be nil. + @param object The object with which to construct the object request operation. If `nil`, then the path must be provided. @param path The path to be appended to the HTTP client's base URL and used as the request URL. If nil, the request URL will be obtained by consulting the router for a route registered for the given object's class and the `RKRequestMethodPATCH` method. @param parameters The parameters to be reverse merged with the parameterization of the given object and set as the request body. @param success A block object to be executed when the object request operation finishes successfully. This block has no return value and takes two arguments: the created object request operation and the `RKMappingResult` object created by object mapping the response data of request. @@ -687,7 +696,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; The type of object request operation created is determined by invoking `appropriateObjectRequestOperationWithObject:method:path:parameters:`. - @param object The object with which to construct the object request operation. Cannot be nil. + @param object The object with which to construct the object request operation. If `nil`, then the path must be provided. @param path The path to be appended to the HTTP client's base URL and used as the request URL. If nil, the request URL will be obtained by consulting the router for a route registered for the given object's class and the `RKRequestMethodDELETE` request method. @param parameters The parameters to be encoded and appended as the query string for the request URL. @param success A block object to be executed when the object request operation finishes successfully. This block has no return value and takes two arguments: the created object request operation and the `RKMappingResult` object created by object mapping the response data of request. diff --git a/Code/Network/RKObjectManager.m b/Code/Network/RKObjectManager.m index 2914c4b67a..634d6bced6 100644 --- a/Code/Network/RKObjectManager.m +++ b/Code/Network/RKObjectManager.m @@ -82,6 +82,58 @@ return nil; } +@interface RKObjectParameters : NSObject + +@property (nonatomic, strong) NSMutableDictionary *parameters; +- (void)addParameters:(NSDictionary *)serialization atRootKeyPath:(NSString *)rootKeyPath; + +@end + +@implementation RKObjectParameters + +- (id)init +{ + self = [super init]; + if (self) { + self.parameters = [NSMutableDictionary new]; + } + return self; +} + +- (void)addParameters:(NSDictionary *)parameters atRootKeyPath:(NSString *)rootKeyPath +{ + id rootKey = rootKeyPath ?: [NSNull null]; + id nonNestedParameters = rootKeyPath ? [parameters objectForKey:rootKeyPath] : parameters; + id value = [self.parameters objectForKey:rootKey]; + if (value) { + if ([value isKindOfClass:[NSMutableArray class]]) { + [value addObject:nonNestedParameters]; + } else if ([value isKindOfClass:[NSDictionary class]]) { + NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:value, nonNestedParameters, nil]; + [self.parameters setObject:mutableArray forKey:rootKey]; + } else { + [NSException raise:NSInvalidArgumentException format:@"Unexpected argument of type '%@': expected an NSDictionary or NSArray.", [value class]]; + } + } else { + [self.parameters setObject:nonNestedParameters forKey:rootKey]; + } +} + +- (id)requestParameters +{ + if ([self.parameters count] == 0) return nil; + id valueAtNullKey = [self.parameters objectForKey:[NSNull null]]; + if (valueAtNullKey) { + if ([self.parameters count] == 1) return valueAtNullKey; + + // If we have values at `[NSNull null]` and other key paths, we have an invalid configuration + [NSException raise:NSInvalidArgumentException format:@"Invalid request descriptor configuration: The request descriptors specify that multiple objects be serialized at incompatible key paths. Cannot serialize objects at the `nil` root key path in the same request as objects with a non-nil root key path. Please check your request descriptors and try again."]; + } + return self.parameters; +} + +@end + /** Visits all mappings accessible via relationships or dynamic mapping in an object graph starting from a given mapping. */ @@ -330,30 +382,44 @@ - (NSMutableURLRequest *)requestWithPathForRelationship:(NSString *)relationship return [self requestWithMethod:RKStringFromRequestMethod(method) path:[URL relativeString] parameters:parameters]; } +- (id)mergedParametersWithObject:(id)object method:(RKRequestMethod)method parameters:(NSDictionary *)parameters +{ + NSArray *objectsToParameterize = ([object isKindOfClass:[NSArray class]] || object == nil) ? object : @[ object ]; + RKObjectParameters *objectParameters = [RKObjectParameters new]; + for (id object in objectsToParameterize) { + RKRequestDescriptor *requestDescriptor = RKRequestDescriptorFromArrayMatchingObject(self.requestDescriptors, object); + if ((method != RKRequestMethodGET && method != RKRequestMethodDELETE) && requestDescriptor) { + NSError *error = nil; + NSDictionary *parametersForObject = [RKObjectParameterization parametersWithObject:object requestDescriptor:requestDescriptor error:&error]; + if (error) { + RKLogError(@"Object parameterization failed while building %@ request for object '%@': %@", RKStringFromRequestMethod(method), object, error); + return nil; + } + [objectParameters addParameters:parametersForObject atRootKeyPath:requestDescriptor.rootKeyPath]; + } + } + id requestParameters = [objectParameters requestParameters]; + + // Merge the extra parameters if possible + if ([requestParameters isKindOfClass:[NSArray class]] && parameters) { + [NSException raise:NSInvalidArgumentException format:@"Cannot merge parameters with array of object representations serialized with a nil root key path."]; + } else if (requestParameters && parameters) { + requestParameters = RKDictionaryByMergingDictionaryWithDictionary(requestParameters, parameters); + } else if (parameters && !requestParameters) { + requestParameters = parameters; + } + + return requestParameters; +} + - (NSMutableURLRequest *)requestWithObject:(id)object method:(RKRequestMethod)method path:(NSString *)path parameters:(NSDictionary *)parameters; { NSString *requestPath = (path) ? path : [[self.router URLForObject:object method:method] relativeString]; - NSString *stringMethod = RKStringFromRequestMethod(method); - NSDictionary *requestParameters = nil; - RKRequestDescriptor *requestDescriptor = RKRequestDescriptorFromArrayMatchingObject(self.requestDescriptors, object); - if ((method != RKRequestMethodGET && method != RKRequestMethodDELETE) && requestDescriptor) { - NSError *error = nil; - requestParameters = [[RKObjectParameterization parametersWithObject:object requestDescriptor:requestDescriptor error:&error] mutableCopy]; - if (error) { - RKLogError(@"Object parameterization failed while building %@ request to '%@': %@", stringMethod, requestPath, error); - return nil; - } - if (parameters) { - requestParameters = RKDictionaryByMergingDictionaryWithDictionary(requestParameters, parameters); - } - } else { - requestParameters = parameters; - } - - return [self requestWithMethod:stringMethod path:requestPath parameters:requestParameters]; + id requestParameters = [self mergedParametersWithObject:object method:method parameters:parameters]; + return [self requestWithMethod:RKStringFromRequestMethod(method) path:requestPath parameters:requestParameters]; } - (NSMutableURLRequest *)multipartFormRequestWithObject:(id)object @@ -363,19 +429,11 @@ - (NSMutableURLRequest *)multipartFormRequestWithObject:(id)object constructingBodyWithBlock:(void (^)(id formData))block { NSString *requestPath = (path) ? path : [[self.router URLForObject:object method:method] relativeString]; - NSString *stringMethod = RKStringFromRequestMethod(method); - NSDictionary *requestParameters = nil; - RKRequestDescriptor *requestDescriptor = RKRequestDescriptorFromArrayMatchingObject(self.requestDescriptors, object); - if (requestDescriptor) { - NSError *error = nil; - requestParameters = [[RKObjectParameterization parametersWithObject:object requestDescriptor:requestDescriptor error:&error] mutableCopy]; - if (parameters) { - requestParameters = RKDictionaryByMergingDictionaryWithDictionary(requestParameters, parameters); - } - } else { - requestParameters = parameters; - } - NSMutableURLRequest *multipartRequest = [self.HTTPClient multipartFormRequestWithMethod:stringMethod path:requestPath parameters:requestParameters constructingBodyWithBlock:block]; + id requestParameters = [self mergedParametersWithObject:object method:method parameters:parameters]; + NSMutableURLRequest *multipartRequest = [self.HTTPClient multipartFormRequestWithMethod:RKStringFromRequestMethod(method) + path:requestPath + parameters:requestParameters + constructingBodyWithBlock:block]; return multipartRequest; } @@ -468,6 +526,7 @@ - (void)getObjectsAtPathForRouteNamed:(NSString *)routeName success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure { + NSParameterAssert(routeName); RKRequestMethod method; NSURL *URL = [self.router URLForRouteNamed:routeName method:&method object:object]; NSAssert(URL, @"No route found named '%@'", routeName); @@ -481,6 +540,7 @@ - (void)getObjectsAtPath:(NSString *)path success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure { + NSParameterAssert(path); RKObjectRequestOperation *operation = [self appropriateObjectRequestOperationWithObject:nil method:RKRequestMethodGET path:path parameters:parameters]; [operation setCompletionBlockWithSuccess:success failure:failure]; [self enqueueObjectRequestOperation:operation]; @@ -492,7 +552,7 @@ - (void)getObject:(id)object success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure { - NSParameterAssert(object); + NSAssert(object || path, @"Cannot make a request without an object or a path."); RKObjectRequestOperation *operation = [self appropriateObjectRequestOperationWithObject:object method:RKRequestMethodGET path:path parameters:parameters]; [operation setCompletionBlockWithSuccess:success failure:failure]; [self enqueueObjectRequestOperation:operation]; @@ -504,7 +564,7 @@ - (void)postObject:(id)object success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure { - NSParameterAssert(object); + NSAssert(object || path, @"Cannot make a request without an object or a path."); RKObjectRequestOperation *operation = [self appropriateObjectRequestOperationWithObject:object method:RKRequestMethodPOST path:path parameters:parameters]; [operation setCompletionBlockWithSuccess:success failure:failure]; [self enqueueObjectRequestOperation:operation]; @@ -516,7 +576,7 @@ - (void)putObject:(id)object success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure { - NSParameterAssert(object); + NSAssert(object || path, @"Cannot make a request without an object or a path."); RKObjectRequestOperation *operation = [self appropriateObjectRequestOperationWithObject:object method:RKRequestMethodPUT path:path parameters:parameters]; [operation setCompletionBlockWithSuccess:success failure:failure]; [self enqueueObjectRequestOperation:operation]; @@ -528,7 +588,7 @@ - (void)patchObject:(id)object success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure { - NSParameterAssert(object); + NSAssert(object || path, @"Cannot make a request without an object or a path."); RKObjectRequestOperation *operation = [self appropriateObjectRequestOperationWithObject:object method:RKRequestMethodPATCH path:path parameters:parameters]; [operation setCompletionBlockWithSuccess:success failure:failure]; [self enqueueObjectRequestOperation:operation]; @@ -540,7 +600,7 @@ - (void)deleteObject:(id)object success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure { - NSParameterAssert(object); + NSAssert(object || path, @"Cannot make a request without an object or a path."); RKObjectRequestOperation *operation = [self appropriateObjectRequestOperationWithObject:object method:RKRequestMethodDELETE path:path parameters:parameters]; [operation setCompletionBlockWithSuccess:success failure:failure]; [self enqueueObjectRequestOperation:operation]; @@ -555,7 +615,7 @@ - (RKPaginator *)paginatorWithPathPattern:(NSString *)pathPattern paginator.managedObjectCache = self.managedObjectStore.managedObjectCache; paginator.fetchRequestBlocks = self.fetchRequestBlocks; paginator.operationQueue = self.operationQueue; - if (self.HTTPOperationClass) paginator.HTTPOperationClass = self.HTTPOperationClass; + if (self.HTTPOperationClass) [paginator setHTTPOperationClass:self.HTTPOperationClass]; return paginator; } diff --git a/Code/Network/RKPaginator.h b/Code/Network/RKPaginator.h index a02c409e51..3b0cc80c6e 100644 --- a/Code/Network/RKPaginator.h +++ b/Code/Network/RKPaginator.h @@ -98,11 +98,11 @@ @property (nonatomic, strong) NSOperationQueue *operationQueue; /** - The `RKHTTPRequestOperation` subclass to be used for HTTP request operations made by the paginator. + Sets the `RKHTTPRequestOperation` subclass to be used when constructing HTTP request operations for requests dispatched by the paginator. **Default**: `[RKHTTPRequestOperation class]` */ -@property (nonatomic, strong) Class HTTPOperationClass; +- (void)setHTTPOperationClass:(Class)operationClass; ///----------------------------------- /// @name Setting the Completion Block diff --git a/Code/Network/RKPaginator.m b/Code/Network/RKPaginator.m index 4e51296d4c..b44b05a64b 100644 --- a/Code/Network/RKPaginator.m +++ b/Code/Network/RKPaginator.m @@ -32,6 +32,7 @@ // Private interface @interface RKPaginator () @property (nonatomic, copy) NSURLRequest *request; +@property (nonatomic, strong) Class HTTPOperationClass; @property (nonatomic, strong) RKObjectRequestOperation *objectRequestOperation; @property (nonatomic, copy) NSArray *responseDescriptors; @property (nonatomic, assign, readwrite) NSUInteger currentPage; diff --git a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m index 6b8d604a61..0b7e7e7ab7 100644 --- a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m @@ -27,6 +27,7 @@ #import "RKTestUser.h" #import "RKObjectMapperTestModel.h" #import "RKDynamicMapping.h" +#import "RKTestAddress.h" @interface RKSubclassedTestModel : RKObjectMapperTestModel @end @@ -838,4 +839,217 @@ - (void)testThatAppropriateObjectRequestOperationReturnsManagedObjectRequestOper //} // +- (void)testPostingAnArrayOfObjectsWhereNoneHaveARootKeyPath +{ + RKObjectMapping *firstRequestMapping = [RKObjectMapping requestMapping]; + [firstRequestMapping addAttributeMappingsFromArray:@[ @"name", @"emailAddress" ]]; + RKObjectMapping *secondRequestMapping = [RKObjectMapping requestMapping]; + [secondRequestMapping addAttributeMappingsFromArray:@[ @"city", @"state" ]]; + + RKRequestDescriptor *firstRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:firstRequestMapping objectClass:[RKTestUser class] rootKeyPath:nil]; + RKRequestDescriptor *secondRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:secondRequestMapping objectClass:[RKTestAddress class] rootKeyPath:nil]; + + RKTestUser *user = [RKTestUser new]; + user.name = @"Blake"; + user.emailAddress = @"blake@restkit.org"; + + RKTestAddress *address = [RKTestAddress new]; + address.city = @"New York City"; + address.state = @"New York"; + + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.requestSerializationMIMEType = RKMIMETypeJSON; + [objectManager addRequestDescriptor:firstRequestDescriptor]; + [objectManager addRequestDescriptor:secondRequestDescriptor]; + + NSArray *arrayOfObjects = @[ user, address ]; + NSURLRequest *request = [objectManager requestWithObject:arrayOfObjects method:RKRequestMethodPOST path:@"/path" parameters:nil]; + NSArray *array = [NSJSONSerialization JSONObjectWithData:request.HTTPBody options:0 error:nil]; + NSArray *expected = @[ @{ @"name": @"Blake", @"emailAddress": @"blake@restkit.org" }, @{ @"city": @"New York City", @"state": @"New York" } ]; + expect(array).to.equal(expected); +} + +- (void)testPostingAnArrayOfObjectsWhereAllObjectsHaveAnOverlappingRootKeyPath +{ + RKObjectMapping *firstRequestMapping = [RKObjectMapping requestMapping]; + [firstRequestMapping addAttributeMappingsFromArray:@[ @"name", @"emailAddress" ]]; + RKObjectMapping *secondRequestMapping = [RKObjectMapping requestMapping]; + [secondRequestMapping addAttributeMappingsFromArray:@[ @"city", @"state" ]]; + + RKRequestDescriptor *firstRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:firstRequestMapping objectClass:[RKTestUser class] rootKeyPath:@"whatever"]; + RKRequestDescriptor *secondRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:secondRequestMapping objectClass:[RKTestAddress class] rootKeyPath:@"whatever"]; + + RKTestUser *user = [RKTestUser new]; + user.name = @"Blake"; + user.emailAddress = @"blake@restkit.org"; + + RKTestAddress *address = [RKTestAddress new]; + address.city = @"New York City"; + address.state = @"New York"; + + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.requestSerializationMIMEType = RKMIMETypeJSON; + [objectManager addRequestDescriptor:firstRequestDescriptor]; + [objectManager addRequestDescriptor:secondRequestDescriptor]; + + NSArray *arrayOfObjects = @[ user, address ]; + NSURLRequest *request = [objectManager requestWithObject:arrayOfObjects method:RKRequestMethodPOST path:@"/path" parameters:nil]; + NSArray *array = [NSJSONSerialization JSONObjectWithData:request.HTTPBody options:0 error:nil]; + NSDictionary *expected = @{ @"whatever": @[ @{ @"name": @"Blake", @"emailAddress": @"blake@restkit.org" }, @{ @"city": @"New York City", @"state": @"New York" } ] }; + expect(array).to.equal(expected); +} + +- (void)testPostingAnArrayOfObjectsWithMixedRootKeyPath +{ + RKObjectMapping *firstRequestMapping = [RKObjectMapping requestMapping]; + [firstRequestMapping addAttributeMappingsFromArray:@[ @"name", @"emailAddress" ]]; + RKObjectMapping *secondRequestMapping = [RKObjectMapping requestMapping]; + [secondRequestMapping addAttributeMappingsFromArray:@[ @"city", @"state" ]]; + + RKRequestDescriptor *firstRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:firstRequestMapping objectClass:[RKTestUser class] rootKeyPath:@"this"]; + RKRequestDescriptor *secondRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:secondRequestMapping objectClass:[RKTestAddress class] rootKeyPath:@"that"]; + + RKTestUser *user = [RKTestUser new]; + user.name = @"Blake"; + user.emailAddress = @"blake@restkit.org"; + + RKTestAddress *address = [RKTestAddress new]; + address.city = @"New York City"; + address.state = @"New York"; + + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.requestSerializationMIMEType = RKMIMETypeJSON; + [objectManager addRequestDescriptor:firstRequestDescriptor]; + [objectManager addRequestDescriptor:secondRequestDescriptor]; + + NSArray *arrayOfObjects = @[ user, address ]; + NSURLRequest *request = [objectManager requestWithObject:arrayOfObjects method:RKRequestMethodPOST path:@"/path" parameters:nil]; + NSArray *array = [NSJSONSerialization JSONObjectWithData:request.HTTPBody options:0 error:nil]; + NSDictionary *expected = @{ @"this": @{ @"name": @"Blake", @"emailAddress": @"blake@restkit.org" }, @"that": @{ @"city": @"New York City", @"state": @"New York" } }; + expect(array).to.equal(expected); +} + +- (void)testPostingAnArrayOfObjectsWithNonNilRootKeyPathAndExtraParameters +{ + RKObjectMapping *firstRequestMapping = [RKObjectMapping requestMapping]; + [firstRequestMapping addAttributeMappingsFromArray:@[ @"name", @"emailAddress" ]]; + RKObjectMapping *secondRequestMapping = [RKObjectMapping requestMapping]; + [secondRequestMapping addAttributeMappingsFromArray:@[ @"city", @"state" ]]; + + RKRequestDescriptor *firstRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:firstRequestMapping objectClass:[RKTestUser class] rootKeyPath:@"this"]; + RKRequestDescriptor *secondRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:secondRequestMapping objectClass:[RKTestAddress class] rootKeyPath:@"that"]; + + RKTestUser *user = [RKTestUser new]; + user.name = @"Blake"; + user.emailAddress = @"blake@restkit.org"; + + RKTestAddress *address = [RKTestAddress new]; + address.city = @"New York City"; + address.state = @"New York"; + + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.requestSerializationMIMEType = RKMIMETypeJSON; + [objectManager addRequestDescriptor:firstRequestDescriptor]; + [objectManager addRequestDescriptor:secondRequestDescriptor]; + + NSArray *arrayOfObjects = @[ user, address ]; + NSURLRequest *request = [objectManager requestWithObject:arrayOfObjects method:RKRequestMethodPOST path:@"/path" parameters:@{ @"extra": @"info" }]; + NSArray *array = [NSJSONSerialization JSONObjectWithData:request.HTTPBody options:0 error:nil]; + NSDictionary *expected = @{ @"this": @{ @"name": @"Blake", @"emailAddress": @"blake@restkit.org" }, @"that": @{ @"city": @"New York City", @"state": @"New York" }, @"extra": @"info" }; + expect(array).to.equal(expected); +} + +- (void)testPostingNilObjectWithExtraParameters +{ + RKObjectMapping *firstRequestMapping = [RKObjectMapping requestMapping]; + [firstRequestMapping addAttributeMappingsFromArray:@[ @"name", @"emailAddress" ]]; + RKObjectMapping *secondRequestMapping = [RKObjectMapping requestMapping]; + [secondRequestMapping addAttributeMappingsFromArray:@[ @"city", @"state" ]]; + + RKRequestDescriptor *firstRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:firstRequestMapping objectClass:[RKTestUser class] rootKeyPath:@"this"]; + RKRequestDescriptor *secondRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:secondRequestMapping objectClass:[RKTestAddress class] rootKeyPath:@"that"]; + + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.requestSerializationMIMEType = RKMIMETypeJSON; + [objectManager addRequestDescriptor:firstRequestDescriptor]; + [objectManager addRequestDescriptor:secondRequestDescriptor]; + + NSDictionary *parameters = @{ @"this": @"that" }; + NSURLRequest *request = [objectManager requestWithObject:nil method:RKRequestMethodPOST path:@"/path" parameters:parameters]; + NSArray *array = [NSJSONSerialization JSONObjectWithData:request.HTTPBody options:0 error:nil]; + expect(array).to.equal(parameters); +} + +- (void)testAttemptingToPostAnArrayOfObjectsWithMixtureOfNilAndNonNilRootKeyPathsRaisesError +{ + RKObjectMapping *firstRequestMapping = [RKObjectMapping requestMapping]; + [firstRequestMapping addAttributeMappingsFromArray:@[ @"name", @"emailAddress" ]]; + RKObjectMapping *secondRequestMapping = [RKObjectMapping requestMapping]; + [secondRequestMapping addAttributeMappingsFromArray:@[ @"city", @"state" ]]; + + RKRequestDescriptor *firstRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:firstRequestMapping objectClass:[RKTestUser class] rootKeyPath:nil]; + RKRequestDescriptor *secondRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:secondRequestMapping objectClass:[RKTestAddress class] rootKeyPath:nil]; + + RKTestUser *user = [RKTestUser new]; + user.name = @"Blake"; + user.emailAddress = @"blake@restkit.org"; + + RKTestAddress *address = [RKTestAddress new]; + address.city = @"New York City"; + address.state = @"New York"; + + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.requestSerializationMIMEType = RKMIMETypeJSON; + [objectManager addRequestDescriptor:firstRequestDescriptor]; + [objectManager addRequestDescriptor:secondRequestDescriptor]; + + NSArray *arrayOfObjects = @[ user, address ]; + NSException *caughtException = nil; + @try { + NSURLRequest __unused *request = [objectManager requestWithObject:arrayOfObjects method:RKRequestMethodPOST path:@"/path" parameters:@{ @"name": @"Foo" }]; + } + @catch (NSException *exception) { + caughtException = exception; + expect([exception name]).to.equal(NSInvalidArgumentException); + expect([exception reason]).to.equal(@"Cannot merge parameters with array of object representations serialized with a nil root key path."); + } + expect(caughtException).notTo.beNil(); +} + +- (void)testThatAttemptingToPostObjectsWithAMixtureOfNilAndNonNilRootKeyPathsRaisesError +{ + RKObjectMapping *firstRequestMapping = [RKObjectMapping requestMapping]; + [firstRequestMapping addAttributeMappingsFromArray:@[ @"name", @"emailAddress" ]]; + RKObjectMapping *secondRequestMapping = [RKObjectMapping requestMapping]; + [secondRequestMapping addAttributeMappingsFromArray:@[ @"city", @"state" ]]; + + RKRequestDescriptor *firstRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:firstRequestMapping objectClass:[RKTestUser class] rootKeyPath:@"bang"]; + RKRequestDescriptor *secondRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:secondRequestMapping objectClass:[RKTestAddress class] rootKeyPath:nil]; + + RKTestUser *user = [RKTestUser new]; + user.name = @"Blake"; + user.emailAddress = @"blake@restkit.org"; + + RKTestAddress *address = [RKTestAddress new]; + address.city = @"New York City"; + address.state = @"New York"; + + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.requestSerializationMIMEType = RKMIMETypeJSON; + [objectManager addRequestDescriptor:firstRequestDescriptor]; + [objectManager addRequestDescriptor:secondRequestDescriptor]; + + NSArray *arrayOfObjects = @[ user, address ]; + NSException *caughtException = nil; + @try { + NSURLRequest __unused *request = [objectManager requestWithObject:arrayOfObjects method:RKRequestMethodPOST path:@"/path" parameters:nil]; + } + @catch (NSException *exception) { + caughtException = exception; + expect([exception name]).to.equal(NSInvalidArgumentException); + expect([exception reason]).to.equal(@"Invalid request descriptor configuration: The request descriptors specify that multiple objects be serialized at incompatible key paths. Cannot serialize objects at the `nil` root key path in the same request as objects with a non-nil root key path. Please check your request descriptors and try again."); + } + expect(caughtException).notTo.beNil(); +} + @end diff --git a/Tests/Models/RKTestUser.h b/Tests/Models/RKTestUser.h index 0d4a6c4b48..3ba24a45af 100644 --- a/Tests/Models/RKTestUser.h +++ b/Tests/Models/RKTestUser.h @@ -18,6 +18,7 @@ @property (nonatomic, strong) NSNumber *userID; @property (nonatomic, strong) NSString *name; +@property (nonatomic, strong) NSString *emailAddress; @property (nonatomic, strong) NSDate *birthDate; @property (nonatomic, strong) NSDate *favoriteDate; @property (nonatomic, strong) NSArray *favoriteColors; From 9005bd573cf24c3dfbc52a6050789ffbddecfee4 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 3 Jan 2013 18:19:50 -0500 Subject: [PATCH 22/27] Add test coverage and fixes for `RKMappingTest`. closes #1086 --- Code/ObjectMapping/RKMappingOperation.m | 4 +- Code/ObjectMapping/RKObjectMapping.h | 2 +- Code/Support/RestKit-Prefix.pch | 1 + Code/Testing.h | 4 + Code/Testing/RKConnectionTestExpectation.h | 4 + Code/Testing/RKConnectionTestExpectation.m | 5 +- Code/Testing/RKMappingTest.h | 5 +- Code/Testing/RKMappingTest.m | 14 +- README.md | 2 + RestKit.xcodeproj/project.pbxproj | 15 ++ .../JSON/humans/with_to_one_relationship.json | 13 +- Tests/Fixtures/JSON/user.json | 5 +- Tests/Logic/Testing/RKMappingTestTest.m | 224 ++++++++++++++++++ Tests/Models/RKTestUser.h | 1 + Tests/Models/RKTestUser.m | 7 + 15 files changed, 295 insertions(+), 11 deletions(-) create mode 100644 Tests/Logic/Testing/RKMappingTestTest.m diff --git a/Code/ObjectMapping/RKMappingOperation.m b/Code/ObjectMapping/RKMappingOperation.m index a68fcc48ce..a6dbd99124 100644 --- a/Code/ObjectMapping/RKMappingOperation.m +++ b/Code/ObjectMapping/RKMappingOperation.m @@ -603,7 +603,6 @@ - (BOOL)applyRelationshipMappings { NSAssert(self.dataSource, @"Cannot perform relationship mapping without a data source"); NSMutableArray *mappingsApplied = [NSMutableArray array]; - id destinationObject = nil; for (RKRelationshipMapping *relationshipMapping in [self relationshipMappings]) { if ([self isCancelled]) return NO; @@ -702,7 +701,8 @@ - (BOOL)applyRelationshipMappings // Notify the delegate if ([self.delegate respondsToSelector:@selector(mappingOperation:didSetValue:forKeyPath:usingMapping:)]) { - [self.delegate mappingOperation:self didSetValue:destinationObject forKeyPath:relationshipMapping.destinationKeyPath usingMapping:relationshipMapping]; + id setValue = [self.destinationObject valueForKeyPath:relationshipMapping.destinationKeyPath]; + [self.delegate mappingOperation:self didSetValue:setValue forKeyPath:relationshipMapping.destinationKeyPath usingMapping:relationshipMapping]; } // Fail out if a validation error has occurred diff --git a/Code/ObjectMapping/RKObjectMapping.h b/Code/ObjectMapping/RKObjectMapping.h index 5574d79d13..eaff572bf9 100644 --- a/Code/ObjectMapping/RKObjectMapping.h +++ b/Code/ObjectMapping/RKObjectMapping.h @@ -376,7 +376,7 @@ /** Returns the preferred date formatter to use when generating NSString representations from NSDate attributes. This type of transformation occurs when RestKit is mapping local objects into JSON or form encoded serializations that do not have a native time construct. - Defaults to an instance of the `RKISO8601DateFormatter` configured with the UTC time-zone. The format string is equal to "YYYY-MM-DDThh:mm:ssTZD" + Defaults to an instance of the `RKISO8601DateFormatter` configured with the UTC time-zone. The format string is equal to "yyyy-MM-DDThh:mm:ssTZD" For details about the ISO-8601 format, see http://www.w3.org/TR/NOTE-datetime diff --git a/Code/Support/RestKit-Prefix.pch b/Code/Support/RestKit-Prefix.pch index 86deeeaa1c..2297f18246 100644 --- a/Code/Support/RestKit-Prefix.pch +++ b/Code/Support/RestKit-Prefix.pch @@ -13,4 +13,5 @@ #import #import #endif + #import #endif diff --git a/Code/Testing.h b/Code/Testing.h index 871002d4b6..55c676017c 100644 --- a/Code/Testing.h +++ b/Code/Testing.h @@ -11,3 +11,7 @@ #import "RKTestFactory.h" #import "RKTestHelpers.h" #import "RKMappingTest.h" + +#ifdef _COREDATADEFINES_H +#import "RKConnectionTestExpectation.h" +#endif diff --git a/Code/Testing/RKConnectionTestExpectation.h b/Code/Testing/RKConnectionTestExpectation.h index c665e1fcbd..bed5e2a7e1 100644 --- a/Code/Testing/RKConnectionTestExpectation.h +++ b/Code/Testing/RKConnectionTestExpectation.h @@ -18,6 +18,8 @@ // limitations under the License. // +#ifdef _COREDATADEFINES_H + #import /** @@ -81,3 +83,5 @@ - (NSString *)summary; @end + +#endif diff --git a/Code/Testing/RKConnectionTestExpectation.m b/Code/Testing/RKConnectionTestExpectation.m index c4e86cdd4f..a6a688002c 100644 --- a/Code/Testing/RKConnectionTestExpectation.m +++ b/Code/Testing/RKConnectionTestExpectation.m @@ -18,7 +18,8 @@ // limitations under the License. // -#import +#ifdef _COREDATADEFINES_H + #import "RKConnectionTestExpectation.h" #import "RKObjectUtilities.h" @@ -64,3 +65,5 @@ - (NSString *)description } @end + +#endif diff --git a/Code/Testing/RKMappingTest.h b/Code/Testing/RKMappingTest.h index 01d75a58bb..a7758fca64 100644 --- a/Code/Testing/RKMappingTest.h +++ b/Code/Testing/RKMappingTest.h @@ -19,7 +19,6 @@ // #import -#import #import "RKMappingOperation.h" #import "RKPropertyMappingTestExpectation.h" @@ -195,6 +194,8 @@ extern NSString * const RKMappingTestExpectationErrorKey; */ @property (nonatomic, strong, readonly) id destinationObject; +#ifdef _COREDATADEFINES_H + ///---------------------------- /// @name Core Data Integration ///---------------------------- @@ -213,4 +214,6 @@ extern NSString * const RKMappingTestExpectationErrorKey; */ @property (nonatomic, strong) id managedObjectCache; +#endif // _COREDATADEFINES_H + @end diff --git a/Code/Testing/RKMappingTest.m b/Code/Testing/RKMappingTest.m index a6464bcbef..1810bcc8e0 100644 --- a/Code/Testing/RKMappingTest.m +++ b/Code/Testing/RKMappingTest.m @@ -242,7 +242,7 @@ - (BOOL)event:(RKMappingTestEvent *)event satisfiesExpectation:(id)expectation e NSString *reason = [NSString stringWithFormat:@"expected to %@, but instead got %@ '%@'", expectation, [event.value class], event.value]; if (error) *error = [self errorForExpectation:expectation - withCode:RKMappingTestEvaluationBlockError + withCode:RKMappingTestValueInequalityError userInfo:userInfo description:description reason:reason]; @@ -258,7 +258,7 @@ - (BOOL)event:(RKMappingTestEvent *)event satisfiesExpectation:(id)expectation e NSString *reason = [NSString stringWithFormat:@"expected to %@, but was instead mapped using: %@", expectation, relationshipMapping]; if (error) *error = [self errorForExpectation:expectation - withCode:RKMappingTestValueInequalityError + withCode:RKMappingTestMappingMismatchError userInfo:userInfo description:description reason:reason]; @@ -319,6 +319,7 @@ - (BOOL)event:(RKMappingTestEvent *)event satisfiesExpectation:(id)expectation e // If we have been given an explicit data source, use it if (self.mappingOperationDataSource) return self.mappingOperationDataSource; +#ifdef _COREDATADEFINES_H if ([self.mapping isKindOfClass:[RKEntityMapping class]]) { NSAssert(self.managedObjectContext, @"Cannot test an `RKEntityMapping` with a nil managed object context."); id managedObjectCache = self.managedObjectCache ?: [RKFetchRequestManagedObjectCache new]; @@ -332,6 +333,9 @@ - (BOOL)event:(RKMappingTestEvent *)event satisfiesExpectation:(id)expectation e } else { return [RKObjectMappingOperationDataSource new]; } +#else + return [RKObjectMappingOperationDataSource new]; +#endif } - (void)performMapping @@ -350,7 +354,8 @@ - (void)performMapping } // Let the connection operations execute to completion - if ([mappingOperation.dataSource isKindOfClass:[RKManagedObjectMappingOperationDataSource class]]) { + Class managedObjectMappingOperationDataSourceClass = NSClassFromString(@"RKManagedObjectMappingOperationDataSource"); + if ([mappingOperation.dataSource isKindOfClass:managedObjectMappingOperationDataSourceClass]) { NSOperationQueue *operationQueue = [(RKManagedObjectMappingOperationDataSource *)mappingOperation.dataSource operationQueue]; if (! [operationQueue isEqual:[NSOperationQueue mainQueue]]) { [operationQueue waitUntilAllOperationsAreFinished]; @@ -410,7 +415,8 @@ - (BOOL)evaluate - (BOOL)evaluateExpectation:(id)expectation error:(NSError **)error { NSParameterAssert(expectation); - NSAssert([expectation isKindOfClass:[RKPropertyMappingTestExpectation class]] || [expectation isKindOfClass:[RKConnectionTestExpectation class]], @"Must be an instance of `RKPropertyMappingTestExpectation` or `RKConnectionTestExpectation`"); + Class connectionTestExpectation = NSClassFromString(@"RKConnectionTestExpectation"); + NSAssert([expectation isKindOfClass:[RKPropertyMappingTestExpectation class]] || (connectionTestExpectation && [expectation isKindOfClass:connectionTestExpectation]), @"Must be an instance of `RKPropertyMappingTestExpectation` or `RKConnectionTestExpectation`"); [self performMapping]; RKMappingTestEvent *event = [self eventMatchingExpectation:expectation]; diff --git a/README.md b/README.md index cd940003dc..fdd8ed3f7c 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,8 @@ operation.managedObjectCache = managedObjectStore.managedObjectCache; } failure:^(RKObjectRequestOperation *operation, NSError *error) { NSLog(@"Failed with error: %@", [error localizedDescription]); }]; +NSOperationQueue *operationQueue = [NSOperationQueue new]; +[operationQueue addOperation:operation]; ``` ### Map a Client Error Response to an NSError diff --git a/RestKit.xcodeproj/project.pbxproj b/RestKit.xcodeproj/project.pbxproj index 15c964543a..77db625f39 100644 --- a/RestKit.xcodeproj/project.pbxproj +++ b/RestKit.xcodeproj/project.pbxproj @@ -497,6 +497,8 @@ 25B408271491CDDC00F21111 /* RKPathUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B408241491CDDB00F21111 /* RKPathUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25B408281491CDDC00F21111 /* RKPathUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B408251491CDDB00F21111 /* RKPathUtilities.m */; }; 25B408291491CDDC00F21111 /* RKPathUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B408251491CDDB00F21111 /* RKPathUtilities.m */; }; + 25B639CC16961EFA0065EB7B /* RKMappingTestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B639CB16961EFA0065EB7B /* RKMappingTestTest.m */; }; + 25B639CD16961EFA0065EB7B /* RKMappingTestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B639CB16961EFA0065EB7B /* RKMappingTestTest.m */; }; 25B6E95514CF795D00B1E881 /* RKErrors.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E95414CF795D00B1E881 /* RKErrors.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25B6E95614CF795D00B1E881 /* RKErrors.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E95414CF795D00B1E881 /* RKErrors.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25B6E95814CF7A1C00B1E881 /* RKErrors.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E95714CF7A1C00B1E881 /* RKErrors.m */; }; @@ -893,6 +895,7 @@ 25AFF8F015B4CF1F0051877F /* RKMappingErrors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RKMappingErrors.h; sourceTree = ""; }; 25B408241491CDDB00F21111 /* RKPathUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKPathUtilities.h; sourceTree = ""; }; 25B408251491CDDB00F21111 /* RKPathUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKPathUtilities.m; sourceTree = ""; }; + 25B639CB16961EFA0065EB7B /* RKMappingTestTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKMappingTestTest.m; sourceTree = ""; }; 25B6E95414CF795D00B1E881 /* RKErrors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKErrors.h; sourceTree = ""; }; 25B6E95714CF7A1C00B1E881 /* RKErrors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKErrors.m; sourceTree = ""; }; 25B6E95A14CF7E3C00B1E881 /* RKObjectMappingMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectMappingMatcher.h; sourceTree = ""; }; @@ -1292,6 +1295,7 @@ 2516101B1456F2330060A5C5 /* ObjectMapping */, 251610511456F2330060A5C5 /* Support */, 25104F9B15C33E3A00829135 /* Search */, + 25B639C816961EC70065EB7B /* Testing */, 251610471456F2330060A5C5 /* Server */, ); path = Tests; @@ -1561,6 +1565,15 @@ name = Testing; sourceTree = ""; }; + 25B639C816961EC70065EB7B /* Testing */ = { + isa = PBXGroup; + children = ( + 25B639CB16961EFA0065EB7B /* RKMappingTestTest.m */, + ); + name = Testing; + path = Logic/Testing; + sourceTree = ""; + }; 25BCB31715ED57D500EE84DD /* AFNetworking */ = { isa = PBXGroup; children = ( @@ -2358,6 +2371,7 @@ 2536D1FD167270F100DF9BB0 /* RKRouterTest.m in Sources */, 2551338F167838590017E4B6 /* RKHTTPRequestOperationTest.m in Sources */, 255133CF167AC7600017E4B6 /* RKManagedObjectRequestOperationTest.m in Sources */, + 25B639CC16961EFA0065EB7B /* RKMappingTestTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2508,6 +2522,7 @@ 2543A25E1664FD3200821D5B /* RKResponseDescriptorTest.m in Sources */, 2536D1FE167270F100DF9BB0 /* RKRouterTest.m in Sources */, 25513390167838590017E4B6 /* RKHTTPRequestOperationTest.m in Sources */, + 25B639CD16961EFA0065EB7B /* RKMappingTestTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Tests/Fixtures/JSON/humans/with_to_one_relationship.json b/Tests/Fixtures/JSON/humans/with_to_one_relationship.json index 3c2b500fac..14414f005a 100644 --- a/Tests/Fixtures/JSON/humans/with_to_one_relationship.json +++ b/Tests/Fixtures/JSON/humans/with_to_one_relationship.json @@ -1 +1,12 @@ -{"human":{"name":"Blake Watters","id":null,"age":28,"favorite_cat":{"name": "Asia"}}} +{ + "human": { + "name": "Blake Watters", + "id": null, + "age": 28, + "favorite_cat_id": 1234, + "favorite_cat": { + "name": "Asia", + "id": 1234 + } + } +} diff --git a/Tests/Fixtures/JSON/user.json b/Tests/Fixtures/JSON/user.json index b0ef7c16b0..1fb3b5a4bb 100644 --- a/Tests/Fixtures/JSON/user.json +++ b/Tests/Fixtures/JSON/user.json @@ -26,5 +26,8 @@ "id": 7, "name": "Rachit Shukla" } - ] + ], + "created_at": "2011-02-02 07:53:08", + "latitude": 12345, + "longitude": 56789 } diff --git a/Tests/Logic/Testing/RKMappingTestTest.m b/Tests/Logic/Testing/RKMappingTestTest.m new file mode 100644 index 0000000000..02deb7fcf8 --- /dev/null +++ b/Tests/Logic/Testing/RKMappingTestTest.m @@ -0,0 +1,224 @@ +// +// RKMappingTestTest.m +// RestKit +// +// Created by Blake Watters on 1/3/13. +// Copyright (c) 2013 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "RKMappingTest.h" +#import "RKTestUser.h" +#import "RKHuman.h" +#import "RKCat.h" + +@interface RKMappingTestTest : SenTestCase +@property (nonatomic, strong) id objectRepresentation; +@property (nonatomic, strong) RKMappingTest *mappingTest; +@end + +@implementation RKMappingTestTest + +- (void)setUp +{ + self.objectRepresentation = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromDictionary:@{ + @"id": @"userID", + @"name": @"name", + @"birthdate": @"birthDate", + @"created_at": @"createdAt" + }]; + RKObjectMapping *addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; + [addressMapping addAttributeMappingsFromDictionary:@{ @"id": @"addressID"}]; + [addressMapping addAttributeMappingsFromArray:@[ @"city", @"state", @"country" ]]; + [mapping addRelationshipMappingWithSourceKeyPath:@"address" mapping:addressMapping]; + RKObjectMapping *coordinateMapping = [RKObjectMapping mappingForClass:[RKTestCoordinate class]]; + [coordinateMapping addAttributeMappingsFromArray:@[ @"latitude", @"longitude" ]]; + [mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:nil toKeyPath:@"coordinate" withMapping:coordinateMapping]]; + + self.mappingTest = [[RKMappingTest alloc] initWithMapping:mapping sourceObject:self.objectRepresentation destinationObject:nil]; +} + +- (void)testMappingTestForAttribute +{ + [self.mappingTest addExpectation:[RKPropertyMappingTestExpectation expectationWithSourceKeyPath:@"name" + destinationKeyPath:@"name" + value:@"Blake Watters"]]; + expect([self.mappingTest evaluate]).to.equal(YES); +} + +- (void)testMappingTestFailureForAttribute +{ + [self.mappingTest addExpectation:[RKPropertyMappingTestExpectation expectationWithSourceKeyPath:@"name" + destinationKeyPath:@"name" + value:@"Invalid"]]; + expect([self.mappingTest evaluate]).to.equal(NO); +} + +- (void)testMappingTestForAttributeWithBlock +{ + [self.mappingTest addExpectation:[RKPropertyMappingTestExpectation expectationWithSourceKeyPath:@"name" destinationKeyPath:@"name" evaluationBlock:^BOOL(RKPropertyMappingTestExpectation *expectation, RKPropertyMapping *mapping, id mappedValue, NSError *__autoreleasing *error) { + return [mappedValue isEqualToString:@"Blake Watters"]; + }]]; + expect([self.mappingTest evaluate]).to.equal(YES); +} + +- (void)testMappingTestForRelationship +{ + RKTestCoordinate *coordinate = [RKTestCoordinate new]; + coordinate.latitude = 12345; + coordinate.longitude = 56789; + [self.mappingTest addExpectation:[RKPropertyMappingTestExpectation expectationWithSourceKeyPath:nil + destinationKeyPath:@"coordinate" + value:coordinate]]; + expect([self.mappingTest evaluate]).to.equal(NO); + +} + +- (void)testMappingTestForRelationshipWithBlock +{ + [self.mappingTest addExpectation:[RKPropertyMappingTestExpectation expectationWithSourceKeyPath:@"address" destinationKeyPath:@"address" evaluationBlock:^BOOL(RKPropertyMappingTestExpectation *expectation, RKPropertyMapping *mapping, id mappedValue, NSError *__autoreleasing *error) { + RKTestAddress *address = (RKTestAddress *)mappedValue; + return [address.addressID isEqualToNumber:@(1234)] && [address.city isEqualToString:@"Carrboro"] && [address.state isEqualToString:@"North Carolina"] && [address.country isEqualToString:@"USA"]; + return YES; + }]]; + expect([self.mappingTest evaluate]).to.equal(YES); +} + +- (void)testEvaluateExpectationReturnsUnsatisfiedExpectationErrorForUnmappedKeyPath +{ + NSError *error = nil; + BOOL success = [self.mappingTest evaluateExpectation:[RKPropertyMappingTestExpectation expectationWithSourceKeyPath:@"nonexistant" + destinationKeyPath:@"name" + value:@"Invalid"] error:&error]; + expect(success).to.equal(NO); + expect(error).notTo.beNil(); + expect(error.code).to.equal(RKMappingTestUnsatisfiedExpectationError); + expect([error localizedDescription]).to.equal(@"expected to map 'nonexistant' to 'name', but did not."); +} + +- (void)testEvaluateExpectationReturnsValueInequalityErrorErrorForValueMismatch +{ + NSError *error = nil; + BOOL success = [self.mappingTest evaluateExpectation:[RKPropertyMappingTestExpectation expectationWithSourceKeyPath:@"name" + destinationKeyPath:@"name" + value:@"Incorrect"] error:&error]; + expect(success).to.equal(NO); + expect(error).notTo.beNil(); + expect(error.code).to.equal(RKMappingTestValueInequalityError); + expect([error localizedDescription]).to.equal(@"mapped to unexpected __NSCFString value 'Blake Watters'"); +} + +- (void)testEvaluateExpectationReturnsEvaluationBlockErrorForBlockFailure +{ + NSError *error = nil; + BOOL success = [self.mappingTest evaluateExpectation:[RKPropertyMappingTestExpectation expectationWithSourceKeyPath:@"address" destinationKeyPath:@"address" evaluationBlock:^BOOL(RKPropertyMappingTestExpectation *expectation, RKPropertyMapping *mapping, id mappedValue, NSError *__autoreleasing *error) { + return NO; + }] error:&error]; + expect(success).to.equal(NO); + expect(error).notTo.beNil(); + expect(error.code).to.equal(RKMappingTestEvaluationBlockError); + assertThat([error localizedDescription], startsWith(@"evaluation block returned `NO` for RKTestAddress value ' Date: Thu, 3 Jan 2013 21:56:12 -0500 Subject: [PATCH 23/27] Add test verifying extra parameters are sent without a request descriptor. refs #1123 --- .../Logic/ObjectMapping/RKObjectManagerTest.m | 226 ++---------------- 1 file changed, 14 insertions(+), 212 deletions(-) diff --git a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m index 0b7e7e7ab7..a70527d65c 100644 --- a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m @@ -626,218 +626,20 @@ - (void)testThatAppropriateObjectRequestOperationReturnsManagedObjectRequestOper expect(objectRequestOperation).to.beInstanceOf([RKManagedObjectRequestOperation class]); } -//- (void)testShouldHandleConnectionFailures -//{ -// NSString *localBaseURL = [NSString stringWithFormat:@"http://127.0.0.1:3001"]; -// RKObjectManager *modelManager = [RKObjectManager managerWithBaseURLString:localBaseURL]; -// modelManager.client.requestQueue.suspended = NO; -// RKTestResponseLoader *loader = [RKTestResponseLoader responseLoader]; -// [modelManager loadObjectsAtResourcePath:@"/JSON/humans/1" delegate:loader]; -// [loader waitForResponse]; -// assertThatBool(loader.wasSuccessful, is(equalToBool(NO))); -//} -// -//- (void)testShouldPOSTAnObject -//{ -// RKObjectManager *manager = [RKTestFactory objectManager]; -// [manager.router.routeSet addRoute:[RKRoute routeWithClass:[RKObjectMapperTestModel class] pathPattern:@"/humans" method:RKRequestMethodPOST]]; -// -// RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; -// mapping.rootKeyPath = @"human"; -// [mapping addAttributeMappingsFromArray:@[@"name", @"age"]]; -// [manager.mappingProvider setMapping:mapping forKeyPath:@"human"]; -// [manager.mappingProvider setSerializationMapping:mapping forClass:[RKObjectMapperTestModel class]]; -// -// RKObjectMapperTestModel *human = [[RKObjectMapperTestModel new] autorelease]; -// human.name = @"Blake Watters"; -// human.age = [NSNumber numberWithInt:28]; -// -// RKTestResponseLoader *loader = [RKTestResponseLoader responseLoader]; -// [manager postObject:human delegate:loader]; -// [loader waitForResponse]; -// -// // NOTE: The /humans endpoint returns a canned response, we are testing the plumbing -// // of the object manager here. -// assertThat(human.name, is(equalTo(@"My Name"))); -//} -// -//- (void)testShouldNotSetAContentBodyOnAGET -//{ -// RKObjectManager *objectManager = [RKTestFactory objectManager]; -// [objectManager.router.routeSet addRoute:[RKRoute routeWithClass:[RKObjectMapperTestModel class] pathPattern:@"/humans/1" method:RKRequestMethodAny]]; -// -// RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; -// [mapping addAttributeMappingsFromArray:@[@"name", @"age"]]; -// [objectManager.mappingProvider registerMapping:mapping withRootKeyPath:@"human"]; -// -// RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; -// RKObjectMapperTestModel *human = [[RKObjectMapperTestModel new] autorelease]; -// human.name = @"Blake Watters"; -// human.age = [NSNumber numberWithInt:28]; -// __block RKObjectLoader *objectLoader = nil; -// [objectManager getObject:human usingBlock:^(RKObjectLoader *loader) { -// loader.delegate = responseLoader; -// objectLoader = loader; -// }]; -// [responseLoader waitForResponse]; -// RKLogCritical(@"%@", [objectLoader.URLRequest allHTTPHeaderFields]); -// assertThat([objectLoader.URLRequest valueForHTTPHeaderField:@"Content-Length"], is(equalTo(@"0"))); -//} -// -//- (void)testShouldNotSetAContentBodyOnADELETE -//{ -// RKObjectManager *objectManager = [RKTestFactory objectManager]; -// [objectManager.router.routeSet addRoute:[RKRoute routeWithClass:[RKObjectMapperTestModel class] pathPattern:@"/humans/1" method:RKRequestMethodAny]]; -// -// RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; -// [mapping addAttributeMappingsFromArray:@[@"name", @"age"]]; -// [objectManager.mappingProvider registerMapping:mapping withRootKeyPath:@"human"]; -// -// RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; -// RKObjectMapperTestModel *human = [[RKObjectMapperTestModel new] autorelease]; -// human.name = @"Blake Watters"; -// human.age = [NSNumber numberWithInt:28]; -// __block RKObjectLoader *objectLoader = nil; -// [objectManager deleteObject:human usingBlock:^(RKObjectLoader *loader) { -// loader.delegate = responseLoader; -// objectLoader = loader; -// }]; -// [responseLoader waitForResponse]; -// RKLogCritical(@"%@", [objectLoader.URLRequest allHTTPHeaderFields]); -// assertThat([objectLoader.URLRequest valueForHTTPHeaderField:@"Content-Length"], is(equalTo(@"0"))); -//} -// -//#pragma mark - Block Helpers -// -//- (void)testShouldLetYouLoadObjectsWithABlock -//{ -// RKObjectManager *objectManager = [RKTestFactory objectManager]; -// RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; -// [mapping addAttributeMappingsFromArray:@[@"name", @"age"]]; -// [objectManager.mappingProvider registerMapping:mapping withRootKeyPath:@"human"]; -// -// RKTestResponseLoader *responseLoader = [[RKTestResponseLoader responseLoader] retain]; -// [objectManager loadObjectsAtResourcePath:@"/JSON/humans/1.json" usingBlock:^(RKObjectLoader *loader) { -// loader.delegate = responseLoader; -// loader.objectMapping = mapping; -// }]; -// [responseLoader waitForResponse]; -// assertThatBool(responseLoader.wasSuccessful, is(equalToBool(YES))); -// assertThat(responseLoader.objects, hasCountOf(1)); -//} -// -//- (void)testShouldAllowYouToOverrideTheRoutedResourcePath -//{ -// RKObjectManager *objectManager = [RKTestFactory objectManager]; -// [objectManager.router.routeSet addRoute:[RKRoute routeWithClass:[RKObjectMapperTestModel class] pathPattern:@"/humans/2" method:RKRequestMethodAny]]; -// RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; -// [mapping addAttributeMappingsFromArray:@[@"name", @"age"]]; -// [objectManager.mappingProvider registerMapping:mapping withRootKeyPath:@"human"]; -// -// RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; -// RKObjectMapperTestModel *human = [[RKObjectMapperTestModel new] autorelease]; -// human.name = @"Blake Watters"; -// human.age = [NSNumber numberWithInt:28]; -// [objectManager deleteObject:human usingBlock:^(RKObjectLoader *loader) { -// loader.delegate = responseLoader; -// loader.resourcePath = @"/humans/1"; -// }]; -// responseLoader.timeout = 50; -// [responseLoader waitForResponse]; -// assertThat(responseLoader.response.request.resourcePath, is(equalTo(@"/humans/1"))); -//} -// -//- (void)testShouldAllowYouToUseObjectHelpersWithoutRouting -//{ -// RKObjectManager *objectManager = [RKTestFactory objectManager]; -// RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; -// [mapping addAttributeMappingsFromArray:@[@"name", @"age"]]; -// [objectManager.mappingProvider registerMapping:mapping withRootKeyPath:@"human"]; -// -// RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; -// RKObjectMapperTestModel *human = [[RKObjectMapperTestModel new] autorelease]; -// human.name = @"Blake Watters"; -// human.age = [NSNumber numberWithInt:28]; -// [objectManager sendObject:human toResourcePath:@"/humans/1" usingBlock:^(RKObjectLoader *loader) { -// loader.method = RKRequestMethodDELETE; -// loader.delegate = responseLoader; -// loader.resourcePath = @"/humans/1"; -// }]; -// [responseLoader waitForResponse]; -// assertThat(responseLoader.response.request.resourcePath, is(equalTo(@"/humans/1"))); -//} -// -//- (void)testShouldAllowYouToSkipTheMappingProvider -//{ -// RKObjectManager *objectManager = [RKTestFactory objectManager]; -// RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; -// mapping.rootKeyPath = @"human"; -// [mapping addAttributeMappingsFromArray:@[@"name", @"age"]]; -// -// RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; -// RKObjectMapperTestModel *human = [[RKObjectMapperTestModel new] autorelease]; -// human.name = @"Blake Watters"; -// human.age = [NSNumber numberWithInt:28]; -// [objectManager sendObject:human toResourcePath:@"/humans/1" usingBlock:^(RKObjectLoader *loader) { -// loader.method = RKRequestMethodDELETE; -// loader.delegate = responseLoader; -// loader.objectMapping = mapping; -// }]; -// [responseLoader waitForResponse]; -// assertThatBool(responseLoader.wasSuccessful, is(equalToBool(YES))); -// assertThat(responseLoader.response.request.resourcePath, is(equalTo(@"/humans/1"))); -//} -// -//- (void)testShouldLetYouOverloadTheParamsOnAnObjectLoaderRequest -//{ -// RKObjectManager *objectManager = [RKTestFactory objectManager]; -// RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; -// mapping.rootKeyPath = @"human"; -// [mapping addAttributeMappingsFromArray:@[@"name", @"age"]]; -// -// RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; -// RKObjectMapperTestModel *human = [[RKObjectMapperTestModel new] autorelease]; -// human.name = @"Blake Watters"; -// human.age = [NSNumber numberWithInt:28]; -// NSDictionary *myParams = [NSDictionary dictionaryWithObject:@"bar" forKey:@"foo"]; -// __block RKObjectLoader *objectLoader = nil; -// [objectManager sendObject:human toResourcePath:@"/humans/1" usingBlock:^(RKObjectLoader *loader) { -// loader.delegate = responseLoader; -// loader.method = RKRequestMethodPOST; -// loader.objectMapping = mapping; -// loader.params = myParams; -// objectLoader = loader; -// }]; -// [responseLoader waitForResponse]; -// assertThat(objectLoader.params, is(equalTo(myParams))); -//} -// -//- (void)testInitializationOfObjectLoaderViaManagerConfiguresSerializationMIMEType -//{ -// RKObjectManager *objectManager = [RKTestFactory objectManager]; -// objectManager.serializationMIMEType = RKMIMETypeJSON; -// RKObjectLoader *loader = [objectManager loaderWithResourcePath:@"/test"]; -// assertThat(loader.serializationMIMEType, isNot(nilValue())); -// assertThat(loader.serializationMIMEType, is(equalTo(RKMIMETypeJSON))); -//} -// -//- (void)testInitializationOfRoutedPathViaSendObjectMethodUsingBlock -//{ -// RKObjectManager *objectManager = [RKTestFactory objectManager]; -// RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; -// mapping.rootKeyPath = @"human"; -// [objectManager.mappingProvider registerObjectMapping:mapping withRootKeyPath:@"human"]; -// [objectManager.router.routeSet addRoute:[RKRoute routeWithClass:[RKObjectMapperTestModel class] pathPattern:@"/human/1" method:RKRequestMethodAny]]; -// objectManager.serializationMIMEType = RKMIMETypeJSON; -// RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; -// -// RKObjectMapperTestModel *object = [RKObjectMapperTestModel new]; -// [objectManager putObject:object usingBlock:^(RKObjectLoader *loader) { -// loader.delegate = responseLoader;sss -// }]; -// [responseLoader waitForResponse]; -//} -// +- (void)testCreatingAnObjectRequestWithoutARequestDescriptorButWithParametersSetsTheRequestBody +{ + RKTestUser *user = [RKTestUser new]; + user.name = @"Blake"; + user.emailAddress = @"blake@restkit.org"; + + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.requestSerializationMIMEType = RKMIMETypeJSON; + + NSURLRequest *request = [objectManager requestWithObject:user method:RKRequestMethodPOST path:@"/path" parameters:@{ @"this": @"that" }]; + id body = [NSJSONSerialization JSONObjectWithData:request.HTTPBody options:0 error:nil]; + NSDictionary *expected = @{ @"this": @"that" }; + expect(body).to.equal(expected); +} - (void)testPostingAnArrayOfObjectsWhereNoneHaveARootKeyPath { From 1423f77a51cd4bb7621fbc24ed8f612513516db0 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 3 Jan 2013 22:03:01 -0500 Subject: [PATCH 24/27] Add test case for failure to load due to invalid hostname. refs #1122 --- Tests/Logic/Network/RKObjectRequestOperationTest.m | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/Logic/Network/RKObjectRequestOperationTest.m b/Tests/Logic/Network/RKObjectRequestOperationTest.m index 2a0aab2b02..642e619c18 100644 --- a/Tests/Logic/Network/RKObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKObjectRequestOperationTest.m @@ -91,6 +91,17 @@ - (void)testShouldReturnSuccessWhenTheStatusCodeIs200AndTheResponseBodyOnlyConta expect(requestOperation.mappingResult).notTo.beNil(); } +- (void)testSendingAnObjectRequestOperationToAnInvalidHostname +{ + NSMutableURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://invalid.is"]]; + RKObjectRequestOperation *requestOperation = [[RKObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ [self responseDescriptorForComplexUser] ]]; + NSOperationQueue *operationQueue = [NSOperationQueue new]; + [operationQueue addOperation:requestOperation]; + [operationQueue waitUntilAllOperationsAreFinished]; + + expect([requestOperation.error code]).to.equal(NSURLErrorCannotFindHost); +} + #pragma mark - Complex JSON - (void)testShouldLoadAComplexUserObjectWithTargetObject From 896eef1a1b8e0c1dae01d6816c39df04ecd8672e Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 3 Jan 2013 22:06:26 -0500 Subject: [PATCH 25/27] Add test case for failure to load due to unsupported URL. refs #1122 --- Tests/Logic/Network/RKObjectRequestOperationTest.m | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/Logic/Network/RKObjectRequestOperationTest.m b/Tests/Logic/Network/RKObjectRequestOperationTest.m index 642e619c18..1ee0562152 100644 --- a/Tests/Logic/Network/RKObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKObjectRequestOperationTest.m @@ -101,6 +101,16 @@ - (void)testSendingAnObjectRequestOperationToAnInvalidHostname expect([requestOperation.error code]).to.equal(NSURLErrorCannotFindHost); } +- (void)testSendingAnObjectRequestOperationToAnBrokenURL +{ + NSMutableURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://invalid••™¡.is"]]; + RKObjectRequestOperation *requestOperation = [[RKObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ [self responseDescriptorForComplexUser] ]]; + NSOperationQueue *operationQueue = [NSOperationQueue new]; + [operationQueue addOperation:requestOperation]; + [operationQueue waitUntilAllOperationsAreFinished]; + + expect([requestOperation.error code]).to.equal(NSURLErrorBadURL); +} #pragma mark - Complex JSON From 422768f6b1ea7d12bad1937574e5c6c23b59c659 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 3 Jan 2013 23:02:49 -0500 Subject: [PATCH 26/27] Add support for dynamic nesting key serialization. closes #684 --- Code/ObjectMapping/RKMappingOperation.m | 19 +++++-- Code/ObjectMapping/RKObjectMapping.h | 50 +++++++++++++++---- Code/ObjectMapping/RKObjectMapping.m | 11 ++-- .../RKObjectParameterizationTest.m | 15 ++++++ 4 files changed, 76 insertions(+), 19 deletions(-) diff --git a/Code/ObjectMapping/RKMappingOperation.m b/Code/ObjectMapping/RKMappingOperation.m index a6dbd99124..5c7e621a1d 100644 --- a/Code/ObjectMapping/RKMappingOperation.m +++ b/Code/ObjectMapping/RKMappingOperation.m @@ -418,7 +418,7 @@ - (BOOL)applyAttributeMappings:(NSArray *)attributeMappings for (RKAttributeMapping *attributeMapping in attributeMappings) { if ([self isCancelled]) return NO; - if ([attributeMapping.sourceKeyPath isEqualToString:RKObjectMappingNestingAttributeKeyName]) { + if ([attributeMapping.sourceKeyPath isEqualToString:RKObjectMappingNestingAttributeKeyName] || [attributeMapping.destinationKeyPath isEqualToString:RKObjectMappingNestingAttributeKeyName]) { RKLogTrace(@"Skipping attribute mapping for special keyPath '%@'", attributeMapping.sourceKeyPath); continue; } @@ -714,18 +714,31 @@ - (BOOL)applyRelationshipMappings - (void)applyNestedMappings { - RKAttributeMapping *attributeMapping = [self.objectMapping attributeMappingForKeyOfRepresentation]; + RKAttributeMapping *attributeMapping = [self.objectMapping mappingForSourceKeyPath: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]; + _nestedAttributeSubstitution = @{ attributeMapping.destinationKeyPath: attributeValue }; [self applyAttributeMapping:attributeMapping withValue:attributeValue]; } else { RKLogWarning(@"Unable to find nesting value for attribute '%@'", attributeMapping.destinationKeyPath); } } + + // Serialization + attributeMapping = [self.objectMapping mappingForDestinationKeyPath:RKObjectMappingNestingAttributeKeyName]; + if (attributeMapping) { + RKLogDebug(@"Found nested mapping definition to attribute '%@'", attributeMapping.destinationKeyPath); + id attributeValue = [self.sourceObject valueForKeyPath:attributeMapping.sourceKeyPath]; + if (attributeValue) { + RKLogDebug(@"Found nesting value of '%@' for attribute '%@'", attributeValue, attributeMapping.sourceKeyPath); + _nestedAttributeSubstitution = @{ attributeMapping.sourceKeyPath: attributeValue }; + } else { + RKLogWarning(@"Unable to find nesting value for attribute '%@'", attributeMapping.destinationKeyPath); + } + } } - (void)cancel diff --git a/Code/ObjectMapping/RKObjectMapping.h b/Code/ObjectMapping/RKObjectMapping.h index eaff572bf9..6a6ee56671 100644 --- a/Code/ObjectMapping/RKObjectMapping.h +++ b/Code/ObjectMapping/RKObjectMapping.h @@ -96,9 +96,9 @@ */ + (instancetype)requestMapping; -///--------------------------------- -/// @name Managing Property Mappings -///--------------------------------- +///---------------------------------- +/// @name Accessing Property Mappings +///---------------------------------- /** The aggregate collection of attribute and relationship mappings within this object mapping. @@ -109,6 +109,7 @@ Returns the property mappings of the receiver in a dictionary, where the keys are the source key paths and the values are instances of `RKAttributeMapping` or `RKRelationshipMapping`. @return The property mappings of the receiver in a dictionary, where the keys are the source key paths and the values are instances of `RKAttributeMapping` or `RKRelationshipMapping`. + @warning Note this method does not return any property mappings with a `nil` value for the source key path in the dictionary returned. */ @property (nonatomic, readonly) NSDictionary *propertyMappingsBySourceKeyPath; @@ -116,6 +117,7 @@ Returns the property mappings of the receiver in a dictionary, where the keys are the destination key paths and the values are instances of `RKAttributeMapping` or `RKRelationshipMapping`. @return The property mappings of the receiver in a dictionary, where the keys are the destination key paths and the values are instances of `RKAttributeMapping` or `RKRelationshipMapping`. + @warning Note this method does not return any property mappings with a `nil` value for the source key path in the dictionary returned. */ @property (nonatomic, readonly) NSDictionary *propertyMappingsByDestinationKeyPath; @@ -129,6 +131,24 @@ */ @property (nonatomic, readonly) NSArray *relationshipMappings; +/** + Returns the property mapping registered with the receiver with the given source key path. + + @param sourceKeyPath The key path to retrieve. + */ +- (id)mappingForSourceKeyPath:(NSString *)sourceKeyPath; + +/** + Returns the property mapping registered with the receiver with the given destinationKeyPath key path. + + @param destinationKeyPath The key path to retrieve. + */ +- (id)mappingForDestinationKeyPath:(NSString *)destinationKeyPath; + +///--------------------------- +/// Managing Property Mappings +///--------------------------- + /** Adds a property mapping to the receiver. @@ -226,14 +246,26 @@ - (void)addAttributeMappingFromKeyOfRepresentationToAttribute:(NSString *)attributeName; /** - Returns the attribute mapping targeting the key of a nested dictionary in the source JSON. - - This attribute mapping corresponds to the attributeName configured via `mapKeyOfNestedDictionaryToAttribute:` + Adds an attribute mapping to a dynamic nesting key from an attribute. The mapped attribute name can then be referenced wthin other attribute mappings to map content under the nesting key path. + + For example, consider that we wish to map a local user object with the properties 'id', 'firstName' and 'email': + + RKUser *user = [RKUser new]; + user.firstName = @"blake"; + user.userID = @(1234); + user.email = @"blake@restkit.org"; - @return An attribute mapping for the key of a nested dictionary being mapped or nil - @see `addAttributeMappingFromKeyOfRepresentationToAttribute:` + And we wish to map it into JSON that looks like: + + { "blake": { "id": 1234, "email": "blake@restkit.org" } } + + We can configure our request mapping to handle this like so: + + RKObjectMapping *mapping = [RKObjectMapping requestMapping]; + [mapping addAttributeMappingToKeyOfRepresentationFromAttribute:@"firstName"]; + [mapping addAttributeMappingsFromDictionary:@{ @"(firstName).userID": @"id", @"(firstName).email": @"email" }]; */ -- (RKAttributeMapping *)attributeMappingForKeyOfRepresentation; +- (void)addAttributeMappingToKeyOfRepresentationFromAttribute:(NSString *)attributeName; ///---------------------------------- /// @name Configuring Mapping Options diff --git a/Code/ObjectMapping/RKObjectMapping.m b/Code/ObjectMapping/RKObjectMapping.m index 00ced82c2a..5028843cb6 100644 --- a/Code/ObjectMapping/RKObjectMapping.m +++ b/Code/ObjectMapping/RKObjectMapping.m @@ -175,6 +175,7 @@ - (NSDictionary *)propertyMappingsBySourceKeyPath { NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:[self.propertyMappings count]]; for (RKPropertyMapping *propertyMapping in self.propertyMappings) { + if (! propertyMapping.sourceKeyPath) continue; [dictionary setObject:propertyMapping forKey:propertyMapping.sourceKeyPath]; } @@ -185,6 +186,7 @@ - (NSDictionary *)propertyMappingsByDestinationKeyPath { NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:[self.propertyMappings count]]; for (RKPropertyMapping *propertyMapping in self.propertyMappings) { + if (! propertyMapping.destinationKeyPath) continue; [dictionary setObject:propertyMapping forKey:propertyMapping.destinationKeyPath]; } @@ -244,11 +246,6 @@ - (NSString *)description NSStringFromClass([self class]), self, NSStringFromClass(self.objectClass), self.propertyMappings]; } -- (id)mappingForKeyPath:(NSString *)keyPath -{ - return [self mappingForSourceKeyPath:keyPath]; -} - - (id)mappingForSourceKeyPath:(NSString *)sourceKeyPath { for (RKPropertyMapping *mapping in self.propertyMappings) { @@ -339,9 +336,9 @@ - (void)addAttributeMappingFromKeyOfRepresentationToAttribute:(NSString *)attrib [self addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:RKObjectMappingNestingAttributeKeyName toKeyPath:attributeName]]; } -- (RKAttributeMapping *)attributeMappingForKeyOfRepresentation +- (void)addAttributeMappingToKeyOfRepresentationFromAttribute:(NSString *)attributeName { - return [self mappingForKeyPath:RKObjectMappingNestingAttributeKeyName]; + [self addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:attributeName toKeyPath:RKObjectMappingNestingAttributeKeyName]]; } - (RKAttributeMapping *)mappingForAttribute:(NSString *)attributeKey diff --git a/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m b/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m index dcdcf330ba..267110807a 100644 --- a/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m @@ -448,6 +448,21 @@ - (void)testParameterizationofBooleanPropertiesFromManagedObjectPropertyWithFals expect(string).to.equal(@"{\"name\":\"Blake Watters\",\"happy\":false}"); } +- (void)testSerializingWithDynamicNestingAttribute +{ + NSDictionary *object = @{ @"name" : @"blake", @"occupation" : @"Hacker" }; + RKObjectMapping *mapping = [RKObjectMapping requestMapping]; + [mapping addAttributeMappingToKeyOfRepresentationFromAttribute:@"name"]; + [mapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:@"name" toKeyPath:@"(name).name"]]; + [mapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:@"occupation" toKeyPath:@"(name).job"]]; + + NSError *error = nil; + RKRequestDescriptor *requestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:mapping objectClass:[NSDictionary class] rootKeyPath:nil]; + NSDictionary *parameters = [RKObjectParameterization parametersWithObject:object requestDescriptor:requestDescriptor error:&error]; + NSDictionary *expected = @{@"blake": @{@"name": @"blake", @"job": @"Hacker"}}; + expect(parameters).to.equal(expected); +} + @end #pragma mark - Dynamic Request Paramterization From ee00e59854c0c70a5ed1e325802e642ff64fd6bf Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Thu, 3 Jan 2013 23:06:31 -0500 Subject: [PATCH 27/27] Bump VERSION to 0.20.0-pre6 --- RestKit.podspec | 4 ++-- VERSION | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RestKit.podspec b/RestKit.podspec index 7117888071..92f7a4c819 100644 --- a/RestKit.podspec +++ b/RestKit.podspec @@ -1,10 +1,10 @@ Pod::Spec.new do |s| s.name = 'RestKit' - s.version = '0.20.0pre5' + s.version = '0.20.0pre6' s.summary = 'RestKit is a framework for consuming and modeling RESTful web resources on iOS and OS X.' s.homepage = 'http://www.restkit.org' s.author = { 'Blake Watters' => 'blakewatters@gmail.com' } - s.source = { :git => 'https://github.com/RestKit/RestKit.git', :branch => 'development' } + s.source = { :git => 'https://github.com/RestKit/RestKit.git', :tag => 'v0.20.0-pre6' } s.license = 'Apache License, Version 2.0' # Platform setup diff --git a/VERSION b/VERSION index f2864e8013..43910b72db 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.20.0-pre5 \ No newline at end of file +0.20.0-pre6 \ No newline at end of file