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..1f58c29443 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 @@ -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/RKEntityMapping.m b/Code/CoreData/RKEntityMapping.m index 1c52de3c2d..aaf714fe91 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" @@ -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 3339c0ba7e..21595a38fd 100644 --- a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m +++ b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m @@ -18,22 +18,26 @@ // limitations under the License. // +#import #import "RKManagedObjectMappingOperationDataSource.h" #import "RKObjectMapping.h" #import "RKEntityMapping.h" #import "RKLog.h" #import "RKManagedObjectStore.h" #import "RKMappingOperation.h" -#import "RKDynamicMappingMatcher.h" +#import "RKObjectMappingMatcher.h" #import "RKManagedObjectCaching.h" #import "RKRelationshipConnectionOperation.h" #import "RKMappingErrors.h" #import "RKValueTransformers.h" #import "RKRelationshipMapping.h" #import "RKObjectUtilities.h" +#import "NSManagedObject+RKAdditions.h" 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 +121,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,11 +182,12 @@ 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 -- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id)managedObjectCache +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id)managedObjectCache { NSParameterAssert(managedObjectContext); @@ -169,7 +226,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]) { @@ -187,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:)]) { @@ -220,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." }; @@ -246,6 +312,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/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 a6df90bc5f..6ea0148d41 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" @@ -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/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]; } } diff --git a/Code/Network/RKManagedObjectRequestOperation.m b/Code/Network/RKManagedObjectRequestOperation.m index 979d994ee0..345b903682 100644 --- a/Code/Network/RKManagedObjectRequestOperation.m +++ b/Code/Network/RKManagedObjectRequestOperation.m @@ -35,16 +35,45 @@ #undef RKLogComponent #define RKLogComponent RKlcl_cRestKitCoreData -@interface RKNestedManagedObjectKeyPathMappingGraphVisitor : NSObject +@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 -@property (nonatomic, readonly) NSSet *keyPaths; +@implementation RKMappingGraphVisitation -- (id)initWithResponseDescriptors:(NSArray *)responseDescriptors; +- (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. + + 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/tarjan.py + + */ +@interface RKNestedManagedObjectKeyPathMappingGraphVisitor : NSObject +@property (nonatomic, readonly, strong) NSMutableArray *visitations; +- (id)initWithResponseDescriptors:(NSArray *)responseDescriptors; +@end + @interface RKNestedManagedObjectKeyPathMappingGraphVisitor () -@property (nonatomic, strong) NSMutableSet *mutableKeyPaths; +@property (nonatomic, assign) NSUInteger indexCounter; +@property (nonatomic, strong) NSMutableArray *visitationStack; +@property (nonatomic, strong) NSMutableDictionary *index; +@property (nonatomic, strong) NSMutableDictionary *lowLinks; +@property (nonatomic, strong, readwrite) NSMutableArray *visitations; @end @implementation RKNestedManagedObjectKeyPathMappingGraphVisitor @@ -53,34 +82,108 @@ - (id)initWithResponseDescriptors:(NSArray *)responseDescriptors { self = [self init]; if (self) { - self.mutableKeyPaths = [NSMutableSet set]; + self.indexCounter = 0; + self.visitationStack = [NSMutableArray array]; + self.index = [NSMutableDictionary dictionary]; + self.lowLinks = [NSMutableDictionary dictionary]; + self.visitations = [NSMutableArray array]; + for (RKResponseDescriptor *responseDescriptor in responseDescriptors) { + self.indexCounter = 0; + [self.visitationStack removeAllObjects]; + [self.index removeAllObjects]; + [self.lowLinks removeAllObjects]; [self visitMapping:responseDescriptor.mapping atKeyPath:responseDescriptor.keyPath]; } } + return self; } -- (NSSet *)keyPaths +- (RKMappingGraphVisitation *)visitationForMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath { - return self.mutableKeyPaths; + 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 { - 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]]) { + // 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++; + + 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]; + // 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 + [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]; + } + + // 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]]) { + // 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]; + } + } + + // 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]; + 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]; } } } @@ -98,6 +201,49 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath 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' @@ -120,18 +266,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]; -} - -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 @@ -150,44 +295,56 @@ 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; + 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]; + NSSet *rootKeys = [NSSet setWithArray:[visitations valueForKey:@"rootKey"]]; + for (id rootKey in rootKeys) { + 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) { + 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:[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]; + + if (value) { + RKSetMappedValueForKeyPathInDictionary(value, rootKey, keyPath, newDictionary); } - 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, keyPath, newDictionary); } }]; @@ -366,7 +523,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"); @@ -386,20 +543,26 @@ - (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 = 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) { + NSSet *cyclicKeyPaths = [NSSet setWithArray:[visitation valueForKeyPath:@"mapping.relationshipMappings.destinationKeyPath"]]; + [managedObjectsInMappingResult unionSet:RKFlattenCollectionToSet(managedObjects)]; + RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(managedObjects, cyclicKeyPaths, managedObjectsInMappingResult); } } @@ -469,8 +632,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]; - NSSet *managedObjectMappingResultKeyPaths = visitor.keyPaths; + RKNestedManagedObjectKeyPathMappingGraphVisitor *visitor = [[RKNestedManagedObjectKeyPathMappingGraphVisitor alloc] initWithResponseDescriptors:self.responseMapperOperation.matchingResponseDescriptors]; // Handle any cleanup success = [self deleteTargetObjectIfAppropriate:&error]; @@ -479,7 +641,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; @@ -492,12 +654,14 @@ - (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) { - 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/Code/Network/RKObjectManager.h b/Code/Network/RKObjectManager.h index 217496cbf1..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. @@ -262,7 +271,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 @@ -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 4ec15a4907..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; } @@ -441,7 +499,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]; @@ -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,6 +615,7 @@ - (RKPaginator *)paginatorWithPathPattern:(NSString *)pathPattern paginator.managedObjectCache = self.managedObjectStore.managedObjectCache; paginator.fetchRequestBlocks = self.fetchRequestBlocks; paginator.operationQueue = self.operationQueue; + if (self.HTTPOperationClass) [paginator setHTTPOperationClass:self.HTTPOperationClass]; return paginator; } 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/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.h b/Code/Network/RKPaginator.h index 99cde3e70c..3b0cc80c6e 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 @@ -48,6 +48,10 @@ */ @interface RKPaginator : NSObject +///------------------------------------- +/// @name Initializing Paginator Objects +///------------------------------------- + /** Initializes a RKPaginator object with the a provided patternURL and mappingProvider. @@ -56,9 +60,13 @@ @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 - paginationMapping:(RKObjectMapping *)paginationMapping - responseDescriptors:(NSArray *)responseDescriptors; +- (id)initWithRequest:(NSURLRequest *)request + 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; +/** + Sets the `RKHTTPRequestOperation` subclass to be used when constructing HTTP request operations for requests dispatched by the paginator. + + **Default**: `[RKHTTPRequestOperation class]` + */ +- (void)setHTTPOperationClass:(Class)operationClass; + ///----------------------------------- /// @name Setting the Completion Block ///----------------------------------- diff --git a/Code/Network/RKPaginator.m b/Code/Network/RKPaginator.m index 8894baae85..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; @@ -63,6 +64,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 +95,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 { @@ -123,12 +131,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."); @@ -165,7 +167,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; @@ -178,20 +181,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 +203,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/Code/Network/RKResponseMapperOperation.h b/Code/Network/RKResponseMapperOperation.h index bd518f3892..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; @@ -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; 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/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/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/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/RKMappingOperation.m b/Code/ObjectMapping/RKMappingOperation.m index fb78e79465..5c7e621a1d 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,24 @@ 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]; + } +} + +// 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; @@ -369,7 +389,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]; } @@ -394,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; } @@ -579,13 +603,30 @@ - (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; - // 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]; @@ -660,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 @@ -672,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/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..6a6ee56671 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`. @@ -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 @@ -376,7 +408,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/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/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/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 5ad9a60a7e..bed5e2a7e1 100644 --- a/Code/Testing/RKConnectionTestExpectation.h +++ b/Code/Testing/RKConnectionTestExpectation.h @@ -18,6 +18,8 @@ // limitations under the License. // +#ifdef _COREDATADEFINES_H + #import /** @@ -50,7 +52,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 @@ -81,3 +83,5 @@ - (NSString *)summary; @end + +#endif diff --git a/Code/Testing/RKConnectionTestExpectation.m b/Code/Testing/RKConnectionTestExpectation.m index 4f2a3af9f2..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" @@ -35,7 +36,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 || @@ -64,3 +65,5 @@ - (NSString *)description } @end + +#endif diff --git a/Code/Testing/RKMappingTest.h b/Code/Testing/RKMappingTest.h index 9c6d0f0df1..a7758fca64 100644 --- a/Code/Testing/RKMappingTest.h +++ b/Code/Testing/RKMappingTest.h @@ -19,7 +19,6 @@ // #import -#import #import "RKMappingOperation.h" #import "RKPropertyMappingTestExpectation.h" @@ -105,7 +104,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 @@ -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 54da48540f..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 @@ -233,7 +235,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 +259,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]; diff --git a/RestKit.podspec b/RestKit.podspec index 16091c9c43..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', :tag => 'v0.20.0-pre4' } + s.source = { :git => 'https://github.com/RestKit/RestKit.git', :tag => 'v0.20.0-pre6' } s.license = 'Apache License, Version 2.0' # Platform setup @@ -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/RestKit.xcodeproj/project.pbxproj b/RestKit.xcodeproj/project.pbxproj index fb94a83ded..77db625f39 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 */; }; @@ -495,14 +497,14 @@ 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 */; }; 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 +762,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 +840,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 = ""; }; @@ -892,10 +895,10 @@ 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 /* 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 +1154,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 */, @@ -1292,6 +1295,7 @@ 2516101B1456F2330060A5C5 /* ObjectMapping */, 251610511456F2330060A5C5 /* Support */, 25104F9B15C33E3A00829135 /* Search */, + 25B639C816961EC70065EB7B /* Testing */, 251610471456F2330060A5C5 /* Server */, ); path = Tests; @@ -1466,7 +1470,7 @@ 2516101B1456F2330060A5C5 /* ObjectMapping */ = { isa = PBXGroup; children = ( - 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingTest.m */, + 2516101C1456F2330060A5C5 /* RKDynamicMappingTest.m */, 2516101F1456F2330060A5C5 /* RKObjectManagerTest.m */, 251610211456F2330060A5C5 /* RKObjectMappingNextGenTest.m */, 251610221456F2330060A5C5 /* RKMappingOperationTest.m */, @@ -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 = ( @@ -1723,7 +1736,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 +1839,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 +2251,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 +2306,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 +2327,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 */, @@ -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; }; @@ -2387,7 +2401,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 +2457,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 +2478,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 */, @@ -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/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/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/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/CoreData/RKManagedObjectMappingOperationDataSourceTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m index a33f8a42e4..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]; @@ -1064,6 +1065,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 //{ diff --git a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m index 5a84363672..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 @@ -300,7 +316,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]]]; @@ -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]; @@ -539,6 +556,164 @@ - (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; + 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:parentMapping]]; + [dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"name" expectedValue:@"Blake" objectMapping:humanMapping]]; + + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping pathPattern:nil keyPath:nil statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + 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"]; + }; + 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]; @@ -580,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/Logic/Network/RKObjectRequestOperationTest.m b/Tests/Logic/Network/RKObjectRequestOperationTest.m index 2a0aab2b02..1ee0562152 100644 --- a/Tests/Logic/Network/RKObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKObjectRequestOperationTest.m @@ -91,6 +91,27 @@ - (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); +} +- (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 - (void)testShouldLoadAComplexUserObjectWithTargetObject 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..a70527d65c 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 @@ -515,7 +516,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]; @@ -625,217 +626,232 @@ - (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 +{ + 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/Logic/ObjectMapping/RKObjectMappingNextGenTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m index f458c40e06..8d3a11b1b5 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]; @@ -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 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 diff --git a/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m b/Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m index b67bfa2b80..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 @@ -486,8 +501,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 +540,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; diff --git a/Tests/Logic/ObjectMapping/RKPaginatorTest.m b/Tests/Logic/ObjectMapping/RKPaginatorTest.m index 87fe2df8e4..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]; @@ -220,9 +236,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 +249,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 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 ' 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 @@ -304,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 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 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