diff --git a/Code/CoreData/RKConnectionDescription.h b/Code/CoreData/RKConnectionDescription.h index 6445130cd6..d564ccfb80 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. */ -- (id)initWithRelationship:(NSRelationshipDescription *)relationship attributes:(NSDictionary *)sourceToDestinationEntityAttributes; +- (instancetype)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. */ -- (id)initWithRelationship:(NSRelationshipDescription *)relationship keyPath:(NSString *)keyPath; +- (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship keyPath:(NSString *)keyPath; /** The key path that is to be evaluated to obtain the value for the relationship. @@ -144,9 +144,21 @@ /// @name Setting the Predicate ///---------------------------- +/** + Returns a Boolean value that determines if the connection includes subentities. If `NO`, then the connection will only be established to objects of exactly the entity specified by the relationship's entity. If `YES`, then the connection will be established to all objects of the relationship's entity and all subentities. + + **Default**: `YES` + */ +@property (nonatomic, assign) BOOL includesSubentities; + +/** + An optional predicate for conditionally evaluating the connection based on the state of the source object. + */ +@property (nonatomic, strong) NSPredicate *sourcePredicate; + /** An optional predicate for filtering objects to be connected. */ -@property (nonatomic, copy) NSPredicate *predicate; +@property (nonatomic, copy) NSPredicate *destinationPredicate; @end diff --git a/Code/CoreData/RKConnectionDescription.m b/Code/CoreData/RKConnectionDescription.m index a714fc8f36..538f329cee 100644 --- a/Code/CoreData/RKConnectionDescription.m +++ b/Code/CoreData/RKConnectionDescription.m @@ -44,7 +44,7 @@ @interface RKConnectionDescription () @implementation RKConnectionDescription -- (id)initWithRelationship:(NSRelationshipDescription *)relationship attributes:(NSDictionary *)attributes +- (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship attributes:(NSDictionary *)attributes { NSParameterAssert(relationship); NSParameterAssert(attributes); @@ -58,11 +58,12 @@ - (id)initWithRelationship:(NSRelationshipDescription *)relationship attributes: if (self) { self.relationship = relationship; self.attributes = attributes; + self.includesSubentities = YES; } return self; } -- (id)initWithRelationship:(NSRelationshipDescription *)relationship keyPath:(NSString *)keyPath +- (instancetype)initWithRelationship:(NSRelationshipDescription *)relationship keyPath:(NSString *)keyPath { NSParameterAssert(relationship); NSParameterAssert(keyPath); @@ -79,7 +80,7 @@ - (id)init if ([self class] == [RKConnectionDescription class]) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ Failed to call designated initializer. " - "Invoke initWithRelationship:sourceKeyPath:destinationKeyPath:matcher: instead.", + "Invoke initWithRelationship:attributes: instead.", NSStringFromClass([self class])] userInfo:nil]; } diff --git a/Code/CoreData/RKEntityByAttributeCache.h b/Code/CoreData/RKEntityByAttributeCache.h index 06f3faf7bc..8f4d57c312 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. */ -- (id)initWithEntity:(NSEntityDescription *)entity attributes:(NSArray *)attributeNames managedObjectContext:(NSManagedObjectContext *)context; +- (instancetype)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 d791b86dba..2a5938fa7c 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. */ -- (id)initWithManagedObjectContext:(NSManagedObjectContext *)context; +- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)context; /** The managed object context with which the receiver is associated. diff --git a/Code/CoreData/RKEntityCache.m b/Code/CoreData/RKEntityCache.m index 90920277d5..e9c7cea099 100644 --- a/Code/CoreData/RKEntityCache.m +++ b/Code/CoreData/RKEntityCache.m @@ -27,7 +27,6 @@ @interface RKEntityCache () @implementation RKEntityCache - - (id)initWithManagedObjectContext:(NSManagedObjectContext *)context { NSAssert(context, @"Cannot initialize entity cache with a nil context"); @@ -45,7 +44,6 @@ - (id)init return [self initWithManagedObjectContext:nil]; } - - (void)cacheObjectsForEntity:(NSEntityDescription *)entity byAttributes:(NSArray *)attributeNames { NSParameterAssert(entity); diff --git a/Code/CoreData/RKEntityMapping.h b/Code/CoreData/RKEntityMapping.h index e7a2f57529..248a11b7f1 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. */ -- (id)initWithEntity:(NSEntityDescription *)entity; +- (instancetype)initWithEntity:(NSEntityDescription *)entity; /** A convenience initializer that creates and returns an entity mapping for the entity with the given name in @@ -83,7 +83,7 @@ @param managedObjectStore A managed object store containing the managed object model in which an entity with the given name is defined. @return A new entity mapping for the entity with the given name in the managed object model of the given managed object store. */ -+ (id)mappingForEntityForName:(NSString *)entityName inManagedObjectStore:(RKManagedObjectStore *)managedObjectStore; ++ (instancetype)mappingForEntityForName:(NSString *)entityName inManagedObjectStore:(RKManagedObjectStore *)managedObjectStore; ///--------------------------- /// @name Accessing the Entity diff --git a/Code/CoreData/RKEntityMapping.m b/Code/CoreData/RKEntityMapping.m index 5cf76003f3..1c52de3c2d 100644 --- a/Code/CoreData/RKEntityMapping.m +++ b/Code/CoreData/RKEntityMapping.m @@ -36,24 +36,27 @@ static NSArray *RKEntityIdentificationAttributesFromUserInfoOfEntity(NSEntityDescription *entity) { - id userInfoValue = [[entity userInfo] valueForKey:RKEntityIdentificationAttributesUserInfoKey]; - if (userInfoValue) { - NSArray *attributeNames = [userInfoValue isKindOfClass:[NSArray class]] ? userInfoValue : @[ userInfoValue ]; - NSMutableArray *attributes = [NSMutableArray arrayWithCapacity:[attributeNames count]]; - [attributeNames enumerateObjectsUsingBlock:^(NSString *attributeName, NSUInteger idx, BOOL *stop) { - if (! [attributeName isKindOfClass:[NSString class]]) { - [NSException raise:NSInvalidArgumentException format:@"Invalid value given in user info key '%@' of entity '%@': expected an `NSString` or `NSArray` of strings, instead got '%@' (%@)", RKEntityIdentificationAttributesUserInfoKey, [entity name], attributeName, [attributeName class]]; - } - - NSAttributeDescription *attribute = [[entity attributesByName] valueForKey:attributeName]; - if (! attribute) { - [NSException raise:NSInvalidArgumentException format:@"Invalid identifier attribute specified in user info key '%@' of entity '%@': no attribue was found with the name '%@'", RKEntityIdentificationAttributesUserInfoKey, [entity name], attributeName]; - } - - [attributes addObject:attribute]; - }]; - return attributes; - } + do { + id userInfoValue = [[entity userInfo] valueForKey:RKEntityIdentificationAttributesUserInfoKey]; + if (userInfoValue) { + NSArray *attributeNames = [userInfoValue isKindOfClass:[NSArray class]] ? userInfoValue : @[ userInfoValue ]; + NSMutableArray *attributes = [NSMutableArray arrayWithCapacity:[attributeNames count]]; + [attributeNames enumerateObjectsUsingBlock:^(NSString *attributeName, NSUInteger idx, BOOL *stop) { + if (! [attributeName isKindOfClass:[NSString class]]) { + [NSException raise:NSInvalidArgumentException format:@"Invalid value given in user info key '%@' of entity '%@': expected an `NSString` or `NSArray` of strings, instead got '%@' (%@)", RKEntityIdentificationAttributesUserInfoKey, [entity name], attributeName, [attributeName class]]; + } + + NSAttributeDescription *attribute = [[entity attributesByName] valueForKey:attributeName]; + if (! attribute) { + [NSException raise:NSInvalidArgumentException format:@"Invalid identifier attribute specified in user info key '%@' of entity '%@': no attribue was found with the name '%@'", RKEntityIdentificationAttributesUserInfoKey, [entity name], attributeName]; + } + + [attributes addObject:attribute]; + }]; + return attributes; + } + entity = [entity superentity]; + } while (entity); return nil; } @@ -137,20 +140,20 @@ @implementation RKEntityMapping @synthesize identificationAttributes = _identificationAttributes; -+ (id)mappingForClass:(Class)objectClass ++ (instancetype)mappingForClass:(Class)objectClass { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"You must provide a managedObjectStore. Invoke mappingForClass:inManagedObjectStore: instead."] userInfo:nil]; } -+ (id)mappingForEntityForName:(NSString *)entityName inManagedObjectStore:(RKManagedObjectStore *)managedObjectStore ++ (instancetype)mappingForEntityForName:(NSString *)entityName inManagedObjectStore:(RKManagedObjectStore *)managedObjectStore { NSEntityDescription *entity = [[managedObjectStore.managedObjectModel entitiesByName] objectForKey:entityName]; return [[self alloc] initWithEntity:entity]; } -- (id)initWithEntity:(NSEntityDescription *)entity +- (instancetype)initWithEntity:(NSEntityDescription *)entity { NSAssert(entity, @"Cannot initialize an RKEntityMapping without an entity. Maybe you want RKObjectMapping instead?"); Class objectClass = NSClassFromString([entity managedObjectClassName]); @@ -164,7 +167,7 @@ - (id)initWithEntity:(NSEntityDescription *)entity return self; } -- (id)initWithClass:(Class)objectClass +- (instancetype)initWithClass:(Class)objectClass { self = [super initWithClass:objectClass]; if (self) { @@ -275,7 +278,7 @@ - (Class)classForKeyPath:(NSString *)keyPath NSArray *components = [keyPath componentsSeparatedByString:@"."]; Class propertyClass = self.objectClass; for (NSString *property in components) { - propertyClass = [[RKPropertyInspector sharedInspector] classForPropertyNamed:property ofClass:propertyClass]; + propertyClass = [[RKPropertyInspector sharedInspector] classForPropertyNamed:property ofClass:propertyClass isPrimitive:nil]; if (! propertyClass) propertyClass = [[RKPropertyInspector sharedInspector] classForPropertyNamed:property ofEntity:self.entity]; if (! propertyClass) break; } diff --git a/Code/CoreData/RKInMemoryManagedObjectCache.h b/Code/CoreData/RKInMemoryManagedObjectCache.h index 6e725752f2..73c4e167f1 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. */ -- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; +- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; @end diff --git a/Code/CoreData/RKManagedObjectImporter.h b/Code/CoreData/RKManagedObjectImporter.h index 38e22276c8..c855d5bc36 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**. */ -- (id)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel storePath:(NSString *)storePath; +- (instancetype)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. */ -- (id)initWithPersistentStore:(NSPersistentStore *)persistentStore; +- (instancetype)initWithPersistentStore:(NSPersistentStore *)persistentStore; /** A Boolean value indicating whether existing managed objects in the persistent store should diff --git a/Code/CoreData/RKManagedObjectImporter.m b/Code/CoreData/RKManagedObjectImporter.m index a639425666..d0cc0ab406 100644 --- a/Code/CoreData/RKManagedObjectImporter.m +++ b/Code/CoreData/RKManagedObjectImporter.m @@ -213,7 +213,7 @@ - (NSUInteger)importObjectsFromFileAtPath:(NSString *)path withMapping:(RKMappin } NSDictionary *mappingDictionary = @{ (keyPath ?: [NSNull null]) : mapping }; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:parsedData mappingsDictionary:mappingDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:parsedData mappingsDictionary:mappingDictionary]; mapper.mappingOperationDataSource = self.mappingOperationDataSource; __block RKMappingResult *mappingResult; [self.managedObjectContext performBlockAndWait:^{ diff --git a/Code/CoreData/RKManagedObjectMappingOperationDataSource.h b/Code/CoreData/RKManagedObjectMappingOperationDataSource.h index c785ccba9f..9f8c19d9fa 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. */ -- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id)managedObjectCache; +- (instancetype)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 44795b7d11..3339c0ba7e 100644 --- a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m +++ b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m @@ -29,6 +29,8 @@ #import "RKRelationshipConnectionOperation.h" #import "RKMappingErrors.h" #import "RKValueTransformers.h" +#import "RKRelationshipMapping.h" +#import "RKObjectUtilities.h" extern NSString * const RKObjectMappingNestingAttributeKeyName; @@ -128,7 +130,7 @@ @interface RKManagedObjectMappingOperationDataSource () @implementation RKManagedObjectMappingOperationDataSource -- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id)managedObjectCache +- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext cache:(id)managedObjectCache { NSParameterAssert(managedObjectContext); @@ -136,11 +138,21 @@ - (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContex if (self) { self.managedObjectContext = managedObjectContext; self.managedObjectCache = managedObjectCache; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateCacheWithChangesFromContextWillSaveNotification:) + name:NSManagedObjectContextWillSaveNotification + object:managedObjectContext]; } return self; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + - (id)mappingOperation:(RKMappingOperation *)mappingOperation targetObjectForRepresentation:(NSDictionary *)representation withMapping:(RKObjectMapping *)mapping { NSAssert(representation, @"Mappable data cannot be nil"); @@ -239,4 +251,61 @@ - (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation return YES; } +// NOTE: In theory we should be able to use the userInfo dictionary, but the dictionary was coming in empty (12/18/2012) +- (void)updateCacheWithChangesFromContextWillSaveNotification:(NSNotification *)notification +{ + NSSet *objectsToAdd = [[self.managedObjectContext insertedObjects] setByAddingObjectsFromSet:[self.managedObjectContext updatedObjects]]; + + __block BOOL success; + __block NSError *error = nil; + [self.managedObjectContext performBlockAndWait:^{ + success = [self.managedObjectContext obtainPermanentIDsForObjects:[objectsToAdd allObjects] error:&error]; + }]; + + if (! success) { + RKLogWarning(@"Failed obtaining permanent managed object ID's for %ld objects: the managed object cache was not updated and duplicate objects may be created.", (long) [objectsToAdd count]); + RKLogError(@"Obtaining permanent managed object IDs failed with error: %@", error); + return; + } + + // Update the cache + if ([self.managedObjectCache respondsToSelector:@selector(didFetchObject:)]) { + for (NSManagedObject *managedObject in objectsToAdd) { + [self.managedObjectCache didFetchObject:managedObject]; + } + } + + if ([self.managedObjectCache respondsToSelector:@selector(didDeleteObject::)]) { + for (NSManagedObject *managedObject in [self.managedObjectContext deletedObjects]) { + [self.managedObjectCache didDeleteObject:managedObject]; + } + } +} + +- (BOOL)mappingOperation:(RKMappingOperation *)mappingOperation deleteExistingValueOfRelationshipWithMapping:(RKRelationshipMapping *)relationshipMapping error:(NSError **)error +{ + // Validate the assignment policy + if (! relationshipMapping.assignmentPolicy == RKReplaceAssignmentPolicy) { + NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Unable to satisfy deletion request: Relationship mapping was expected to have an assignment policy of `RKReplaceAssignmentPolicy`, but did not." }; + NSError *localError = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorInvalidAssignmentPolicy userInfo:userInfo]; + if (error) *error = localError; + return NO; + } + + // Delete any managed objects at the destination key path from the context + id existingValue = [mappingOperation.destinationObject valueForKeyPath:relationshipMapping.destinationKeyPath]; + if ([existingValue isKindOfClass:[NSManagedObject class]]) { + [self.managedObjectContext deleteObject:existingValue]; + } else { + if (RKObjectIsCollection(existingValue)) { + for (NSManagedObject *managedObject in existingValue) { + if (! [managedObject isKindOfClass:[NSManagedObject class]]) continue; + [self.managedObjectContext deleteObject:managedObject]; + } + } + } + + return YES; +} + @end diff --git a/Code/CoreData/RKManagedObjectStore.h b/Code/CoreData/RKManagedObjectStore.h index 85c9ae41b1..d50fd4969c 100644 --- a/Code/CoreData/RKManagedObjectStore.h +++ b/Code/CoreData/RKManagedObjectStore.h @@ -38,9 +38,6 @@ The managed object context hierarchy is designed to isolate the main thread from disk I/O and avoid deadlocks. Because the primary context manages its own private queue, saving the main queue context will not result in the objects being saved to the persistent store. The primary context must be saved as well for objects to be persisted to disk. It is also worth noting that because of the parent/child context hierarchy, objects created on the main thread will not obtain permanent managed object ID's even after the primary context has been saved. If you need to refer to the permanent representations of objects created on the main thread after a save, you may ask the main queue context to obtain permanent managed objects for your objects via `obtainPermanentIDsForObjects:error:`. Be warned that when obtaining permanent managed object ID's, you must include all newly created objects that are reachable from the object you are concerned with in the set of objects provided to `obtainPermanentIDsForObjects:error:`. This means any newly created object in a one-to-one or one-to-many relationship must be provided or you will face a crash from the managed object context. This is due to a bug in Core Data still present in iOS5, but fixed in iOS6 (see Open Radar http://openradar.appspot.com/11478919). - - @see `NSManagedObjectContext (RKAdditions)` - @see `NSEntityDescription (RKAdditions)` */ @interface RKManagedObjectStore : NSObject @@ -53,7 +50,7 @@ @return The default managed object store. */ -+ (RKManagedObjectStore *)defaultStore; ++ (instancetype)defaultStore; /** Sets the default managed object store for the application. @@ -79,7 +76,7 @@ RKManagedObjectStore *managedObjectStore = [[RKManagedObjectStore alloc] initWithManagedObjectModel:managedObjectModel]; */ -- (id)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel; +- (instancetype)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel; /** Initializes the receiver with an existing persistent store coordinator. @@ -91,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. */ -- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator; +- (instancetype)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. @@ -101,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. */ -- (id)init; +- (instancetype)init; ///----------------------------------------------------------------------------- /// @name Configuring Persistent Stores @@ -143,7 +140,7 @@ error:(NSError **)error; /** - Resets the persistent stores in the receiver's persistent store coordinator and recreates them. If a store being reset is backed by a file on disk (such as a SQLite file), the file will be removed prior to recreating the store. If the store was originally created using a seed database, the seed will be recopied to reset the store to its seeded state. + Resets the persistent stores in the receiver's persistent store coordinator and recreates them. If a store being reset is backed by a file on disk (such as a SQLite file), the file will be removed prior to recreating the store. If the store was originally created using a seed database, the seed will be recopied to reset the store to its seeded state. If the managed object model uses External Storage for any of its entities, then the external storage directory will be recursively deleted when the store is reset. @param error On input, a pointer to an error object. If an error occurs, this pointer is set to an actual error object containing the error information. You may specify nil for this parameter if you do not want the error information. @return A Boolean value indicating if the reset was successful. @@ -151,6 +148,32 @@ @bug This method will implictly result in the managed object contexts associated with the receiver to be discarded and recreated. Any managed objects or additional child contexts associated with the store will need to be discarded or else exceptions may be raised (i.e. `NSObjectInaccessibleException`). Also note that care must be taken to cancel/suspend all mapping operations, reset all managed object contexts, and disconnect all `NSFetchedResultController` objects that are associated with managed object contexts using the persistent stores of the receiver before attempting a reset. Failure to completely disconnect usage before calling this method is likely to result in a deadlock. + + As an alternative to resetting the persistent store, you may wish to consider simply deleting all managed objects out of the managed object context. If your data set is not very large, this can be a performant operation and is significantly easier to implement correctly. An example implementation for truncating all managed objects from the store is provided below: + + NSBlockOpertation *operation = [NSBlockOperation blockOperationWithBlock:^{ + NSManagedObjectContext *managedObjectContext = [RKManagedObjectStore defaultStore].persistentStoreManagedObjectContext; + [managedObjectContext performBlockAndWait:^{ + NSError *error = nil; + for (NSEntityDescription *entity in [RKManagedObjectStore defaultStore].managedObjectModel) { + NSFetchRequest *fetchRequest = [NSFetchRequest new]; + [fetchRequest setEntity:entity]; + [fetchRequest setIncludesSubentities:NO]; + NSArray *objects = [managedObjectContext executeFetchRequest:fetchRequest error:&error]; + if (! objects) RKLogWarning(@"Failed execution of fetch request %@: %@", fetchRequest, error); + for (NSManagedObject *managedObject in objects) { + [managedObjectContext deleteObject:managedObjectContext]; + } + } + + BOOL success = [managedObjectContext save:&error]; + if (!success) RKLogWarning(@"Failed saving managed object context: %@", error); + }]; + }]; + [operation setCompletionBlock:^{ + // Do stuff once the truncation is complete + }]; + [operation start]; */ - (BOOL)resetPersistentStores:(NSError **)error; diff --git a/Code/CoreData/RKManagedObjectStore.m b/Code/CoreData/RKManagedObjectStore.m index 2a18785d02..c945718270 100644 --- a/Code/CoreData/RKManagedObjectStore.m +++ b/Code/CoreData/RKManagedObjectStore.m @@ -45,8 +45,7 @@ @interface RKManagedObjectStore () @implementation RKManagedObjectStore - -+ (RKManagedObjectStore *)defaultStore ++ (instancetype)defaultStore { return defaultStore; } @@ -58,7 +57,7 @@ + (void)setDefaultStore:(RKManagedObjectStore *)managedObjectStore } } -- (id)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel +- (instancetype)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel { self = [super init]; if (self) { @@ -74,7 +73,7 @@ - (id)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel return self; } -- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator +- (instancetype)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator { self = [self initWithManagedObjectModel:persistentStoreCoordinator.managedObjectModel]; if (self) { @@ -226,6 +225,22 @@ - (BOOL)resetPersistentStores:(NSError **)error if (error) *error = localError; return NO; } + + // Check for and remove an external storage directory + NSString *supportDirectoryName = [NSString stringWithFormat:@".%@_SUPPORT", [[URL lastPathComponent] stringByDeletingPathExtension]]; + NSURL *supportDirectoryFileURL = [NSURL URLWithString:supportDirectoryName relativeToURL:[URL URLByDeletingLastPathComponent]]; + BOOL isDirectory = NO; + if ([[NSFileManager defaultManager] fileExistsAtPath:[supportDirectoryFileURL path] isDirectory:&isDirectory]) { + if (isDirectory) { + if (! [[NSFileManager defaultManager] removeItemAtURL:supportDirectoryFileURL error:&localError]) { + RKLogError(@"Failed to remove persistent store Support directory at URL %@: %@", supportDirectoryFileURL, localError); + if (error) *error = localError; + return NO; + } + } else { + RKLogWarning(@"Found external support item for store at path that is not a directory: %@", [supportDirectoryFileURL path]); + } + } } else { RKLogDebug(@"Skipped removal of persistent store file: URL for persistent store is not a file URL. (%@)", URL); } diff --git a/Code/CoreData/RKPropertyInspector+CoreData.h b/Code/CoreData/RKPropertyInspector+CoreData.h index e34d2b864b..2afbc8d636 100644 --- a/Code/CoreData/RKPropertyInspector+CoreData.h +++ b/Code/CoreData/RKPropertyInspector+CoreData.h @@ -31,7 +31,7 @@ @param entity The entity to retrieve the properties names and classes of. @return A dictionary containing the names and classes of the given entity. */ -- (NSDictionary *)propertyNamesAndClassesForEntity:(NSEntityDescription *)entity; +- (NSDictionary *)propertyInspectionForEntity:(NSEntityDescription *)entity; /** Returns the class used to represent the property with the given name on the given entity. diff --git a/Code/CoreData/RKPropertyInspector+CoreData.m b/Code/CoreData/RKPropertyInspector+CoreData.m index 0d196aa500..396330ee5c 100644 --- a/Code/CoreData/RKPropertyInspector+CoreData.m +++ b/Code/CoreData/RKPropertyInspector+CoreData.m @@ -31,14 +31,14 @@ @implementation RKPropertyInspector (CoreData) -- (NSDictionary *)propertyNamesAndClassesForEntity:(NSEntityDescription *)entity +- (NSDictionary *)propertyInspectionForEntity:(NSEntityDescription *)entity { - NSMutableDictionary *propertyNamesAndTypes = [_propertyNamesToTypesCache objectForKey:[entity name]]; - if (propertyNamesAndTypes) { - return propertyNamesAndTypes; + NSMutableDictionary *entityInspection = [_inspectionCache objectForKey:[entity name]]; + if (entityInspection) { + return entityInspection; } - propertyNamesAndTypes = [NSMutableDictionary dictionary]; + entityInspection = [NSMutableDictionary dictionary]; for (NSString *name in [entity attributesByName]) { NSAttributeDescription *attributeDescription = [[entity attributesByName] valueForKey:name]; if ([attributeDescription attributeValueClassName]) { @@ -46,7 +46,10 @@ - (NSDictionary *)propertyNamesAndClassesForEntity:(NSEntityDescription *)entity if ([cls isSubclassOfClass:[NSNumber class]] && [attributeDescription attributeType] == NSBooleanAttributeType) { cls = objc_getClass("NSCFBoolean") ?: objc_getClass("__NSCFBoolean") ?: cls; } - [propertyNamesAndTypes setValue:cls forKey:name]; + NSDictionary *propertyInspection = @{ RKPropertyInspectionNameKey: name, + RKPropertyInspectionKeyValueCodingClassKey: cls, + RKPropertyInspectionIsPrimitiveKey: @(NO) }; + [entityInspection setValue:propertyInspection forKey:name]; } else if ([attributeDescription attributeType] == NSTransformableAttributeType && ![name isEqualToString:@"_mapkit_hasPanoramaID"]) { @@ -60,7 +63,10 @@ - (NSDictionary *)propertyNamesAndClassesForEntity:(NSEntityDescription *)entity const char *attr = property_getAttributes(prop); Class destinationClass = RKKeyValueCodingClassFromPropertyAttributes(attr); if (destinationClass) { - [propertyNamesAndTypes setObject:destinationClass forKey:name]; + NSDictionary *propertyInspection = @{ RKPropertyInspectionNameKey: name, + RKPropertyInspectionKeyValueCodingClassKey: destinationClass, + RKPropertyInspectionIsPrimitiveKey: @(NO) }; + [entityInspection setObject:propertyInspection forKey:name]; } } } @@ -68,39 +74,55 @@ - (NSDictionary *)propertyNamesAndClassesForEntity:(NSEntityDescription *)entity for (NSString *name in [entity relationshipsByName]) { NSRelationshipDescription *relationshipDescription = [[entity relationshipsByName] valueForKey:name]; if ([relationshipDescription isToMany]) { - [propertyNamesAndTypes setValue:[NSSet class] forKey:name]; + if ([relationshipDescription isOrdered]) { + NSDictionary *propertyInspection = @{ RKPropertyInspectionNameKey: name, + RKPropertyInspectionKeyValueCodingClassKey: [NSOrderedSet class], + RKPropertyInspectionIsPrimitiveKey: @(NO) }; + [entityInspection setObject:propertyInspection forKey:name]; + } else { + NSDictionary *propertyInspection = @{ RKPropertyInspectionNameKey: name, + RKPropertyInspectionKeyValueCodingClassKey: [NSSet class], + RKPropertyInspectionIsPrimitiveKey: @(NO) }; + [entityInspection setObject:propertyInspection forKey:name]; + } } else { NSEntityDescription *destinationEntity = [relationshipDescription destinationEntity]; Class destinationClass = NSClassFromString([destinationEntity managedObjectClassName]); - [propertyNamesAndTypes setValue:destinationClass forKey:name]; + NSDictionary *propertyInspection = @{ RKPropertyInspectionNameKey: name, + RKPropertyInspectionKeyValueCodingClassKey: destinationClass, + RKPropertyInspectionIsPrimitiveKey: @(NO) }; + [entityInspection setObject:propertyInspection forKey:name]; } } - [_propertyNamesToTypesCache setObject:propertyNamesAndTypes forKey:[entity name]]; - RKLogDebug(@"Cached property names and types for Entity '%@': %@", entity, propertyNamesAndTypes); - return propertyNamesAndTypes; + [_inspectionCache setObject:entityInspection forKey:[entity name]]; + RKLogDebug(@"Cached property inspection for Entity '%@': %@", entity, entityInspection); + return entityInspection; } - (Class)classForPropertyNamed:(NSString *)propertyName ofEntity:(NSEntityDescription *)entity { - return [[self propertyNamesAndClassesForEntity:entity] valueForKey:propertyName]; + NSDictionary *entityInspection = [self propertyInspectionForEntity:entity]; + NSDictionary *propertyInspection = [entityInspection objectForKey:propertyName]; + return [propertyInspection objectForKey:RKPropertyInspectionKeyValueCodingClassKey]; } @end @interface NSManagedObject (RKPropertyInspection) -- (Class)rk_classForPropertyAtKeyPath:(NSString *)keyPath; +- (Class)rk_classForPropertyAtKeyPath:(NSString *)keyPath isPrimitive:(BOOL *)isPrimitive; @end @implementation NSManagedObject (RKPropertyInspection) -- (Class)rk_classForPropertyAtKeyPath:(NSString *)keyPath +- (Class)rk_classForPropertyAtKeyPath:(NSString *)keyPath isPrimitive:(BOOL *)isPrimitive { NSArray *components = [keyPath componentsSeparatedByString:@"."]; Class propertyClass = [self class]; for (NSString *property in components) { + if (isPrimitive) *isPrimitive = NO; // Core Data does not enable you to model primitives propertyClass = [[RKPropertyInspector sharedInspector] classForPropertyNamed:property ofEntity:[self entity]]; - propertyClass = propertyClass ?: [[RKPropertyInspector sharedInspector] classForPropertyNamed:property ofClass:propertyClass]; + propertyClass = propertyClass ?: [[RKPropertyInspector sharedInspector] classForPropertyNamed:property ofClass:propertyClass isPrimitive:isPrimitive]; if (! propertyClass) break; } diff --git a/Code/CoreData/RKRelationshipConnectionOperation.h b/Code/CoreData/RKRelationshipConnectionOperation.h index 5fc0cf3735..6be6081afb 100644 --- a/Code/CoreData/RKRelationshipConnectionOperation.h +++ b/Code/CoreData/RKRelationshipConnectionOperation.h @@ -44,9 +44,9 @@ @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. */ -- (id)initWithManagedObject:(NSManagedObject *)managedObject - connection:(RKConnectionDescription *)connection - managedObjectCache:(id)managedObjectCache; +- (instancetype)initWithManagedObject:(NSManagedObject *)managedObject + connection:(RKConnectionDescription *)connection + managedObjectCache:(id)managedObjectCache; ///-------------------------------------------- /// @name Accessing Details About the Operation diff --git a/Code/CoreData/RKRelationshipConnectionOperation.m b/Code/CoreData/RKRelationshipConnectionOperation.m index 8e0b4721d1..a6df90bc5f 100644 --- a/Code/CoreData/RKRelationshipConnectionOperation.m +++ b/Code/CoreData/RKRelationshipConnectionOperation.m @@ -38,6 +38,11 @@ static id RKMutableSetValueForRelationship(NSRelationshipDescription *relationsh return [relationship isOrdered] ? [NSMutableOrderedSet orderedSet] : [NSMutableSet set]; } +static BOOL RKConnectionAttributeValuesIsNotConnectable(NSDictionary *attributeValues) +{ + return [[NSSet setWithArray:[attributeValues allValues]] isEqualToSet:[NSSet setWithObject:[NSNull null]]]; +} + static NSDictionary *RKConnectionAttributeValuesWithObject(RKConnectionDescription *connection, NSManagedObject *managedObject) { NSCAssert([connection isForeignKeyConnection], @"Only valid for a foreign key connection"); @@ -45,9 +50,9 @@ static id RKMutableSetValueForRelationship(NSRelationshipDescription *relationsh for (NSString *sourceAttribute in connection.attributes) { NSString *destinationAttribute = [connection.attributes objectForKey:sourceAttribute]; id sourceValue = [managedObject valueForKey:sourceAttribute]; - [destinationEntityAttributeValues setValue:sourceValue forKey:destinationAttribute]; + [destinationEntityAttributeValues setValue:sourceValue ?: [NSNull null] forKey:destinationAttribute]; } - return destinationEntityAttributeValues; + return RKConnectionAttributeValuesIsNotConnectable(destinationEntityAttributeValues) ? nil : destinationEntityAttributeValues; } @interface RKRelationshipConnectionOperation () @@ -65,9 +70,9 @@ @interface RKRelationshipConnectionOperation () @implementation RKRelationshipConnectionOperation -- (id)initWithManagedObject:(NSManagedObject *)managedObject - connection:(RKConnectionDescription *)connection - managedObjectCache:(id)managedObjectCache; +- (instancetype)initWithManagedObject:(NSManagedObject *)managedObject + connection:(RKConnectionDescription *)connection + managedObjectCache:(id)managedObjectCache; { NSParameterAssert(managedObject); NSAssert([managedObject isKindOfClass:[NSManagedObject class]], @"Relationship connection requires an instance of NSManagedObject"); @@ -136,15 +141,24 @@ - (id)relationshipValueWithConnectionResult:(id)result return result; } -- (id)findConnected +- (id)findConnected:(BOOL *)shouldConnectRelationship { + *shouldConnectRelationship = YES; id connectionResult = nil; + if (self.connection.sourcePredicate && ![self.connection.sourcePredicate evaluateWithObject:self.managedObject]) return nil; + if ([self.connection isForeignKeyConnection]) { NSDictionary *attributeValues = RKConnectionAttributeValuesWithObject(self.connection, self.managedObject); + // If there are no attribute values available for connecting, skip the connection entirely + if (! attributeValues) { + *shouldConnectRelationship = NO; + return nil; + } NSSet *managedObjects = [self.managedObjectCache managedObjectsWithEntity:[self.connection.relationship destinationEntity] attributeValues:attributeValues inManagedObjectContext:self.managedObjectContext]; - if (self.connection.predicate) managedObjects = [managedObjects filteredSetUsingPredicate:self.connection.predicate]; + if (self.connection.destinationPredicate) managedObjects = [managedObjects filteredSetUsingPredicate:self.connection.destinationPredicate]; + if (!self.connection.includesSubentities) managedObjects = [managedObjects filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"entity == %@", [self.connection.relationship destinationEntity]]]; if ([self.connection.relationship isToMany]) { connectionResult = managedObjects; } else { @@ -170,10 +184,13 @@ - (void)main NSString *relationshipName = self.connection.relationship.name; RKLogTrace(@"Connecting relationship '%@' with mapping: %@", relationshipName, self.connection); [self.managedObjectContext performBlockAndWait:^{ - self.connectedValue = [self findConnected]; - [self.managedObject setValue:self.connectedValue forKeyPath:relationshipName]; - RKLogDebug(@"Connected relationship '%@' to object '%@'", relationshipName, self.connectedValue); - if (self.connectionBlock) self.connectionBlock(self, self.connectedValue); + BOOL shouldConnect = YES; + self.connectedValue = [self findConnected:&shouldConnect]; + if (shouldConnect) { + [self.managedObject setValue:self.connectedValue forKeyPath:relationshipName]; + RKLogDebug(@"Connected relationship '%@' to object '%@'", relationshipName, self.connectedValue); + if (self.connectionBlock) self.connectionBlock(self, self.connectedValue); + } }]; } diff --git a/Code/Network/RKHTTPRequestOperation.m b/Code/Network/RKHTTPRequestOperation.m index d07d57cc51..3cbd0081b2 100644 --- a/Code/Network/RKHTTPRequestOperation.m +++ b/Code/Network/RKHTTPRequestOperation.m @@ -18,6 +18,7 @@ // limitations under the License. // +#import #import "RKHTTPRequestOperation.h" #import "RKLog.h" #import "lcl_RK.h" @@ -126,9 +127,17 @@ - (void)dealloc [[NSNotificationCenter defaultCenter] removeObserver:self]; } +static void *RKHTTPRequestOperationStartDate = &RKHTTPRequestOperationStartDate; + - (void)HTTPOperationDidStart:(NSNotification *)notification { RKHTTPRequestOperation *operation = [notification object]; + + if (![operation isKindOfClass:[AFHTTPRequestOperation class]]) { + return; + } + + objc_setAssociatedObject(operation, RKHTTPRequestOperationStartDate, [NSDate date], OBJC_ASSOCIATION_RETAIN_NONATOMIC); if ((_RKlcl_component_level[(__RKlcl_log_symbol(RKlcl_cRestKitNetwork))]) >= (__RKlcl_log_symbol(RKlcl_vTrace))) { NSString *body = nil; @@ -147,19 +156,27 @@ - (void)HTTPOperationDidStart:(NSNotification *)notification - (void)HTTPOperationDidFinish:(NSNotification *)notification { RKHTTPRequestOperation *operation = [notification object]; + + if (![operation isKindOfClass:[AFHTTPRequestOperation class]]) { + return; + } + + NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSinceDate:objc_getAssociatedObject(operation, RKHTTPRequestOperationStartDate)]; + NSString *statusCodeString = RKStringFromStatusCode([operation.response statusCode]); - NSString *statusCodeFragment = statusCodeString ? [NSString stringWithFormat:@"(%ld %@)", (long)[operation.response statusCode], statusCodeString] : [NSString stringWithFormat:@"(%ld)", (long)[operation.response statusCode]]; + NSString *elapsedTimeString = [NSString stringWithFormat:@"[%.04f s]", elapsedTime]; + NSString *statusCodeAndElapsedTime = statusCodeString ? [NSString stringWithFormat:@"(%ld %@) %@", (long)[operation.response statusCode], statusCodeString, elapsedTimeString] : [NSString stringWithFormat:@"(%ld) %@", (long)[operation.response statusCode], elapsedTimeString]; if (operation.error) { if ((_RKlcl_component_level[(__RKlcl_log_symbol(RKlcl_cRestKitNetwork))]) >= (__RKlcl_log_symbol(RKlcl_vTrace))) { - RKLogError(@"%@ '%@' %@:\nerror=%@\nresponse.body=%@", [operation.request HTTPMethod], [[operation.request URL] absoluteString], statusCodeFragment, operation.error, operation.responseString); + RKLogError(@"%@ '%@' %@:\nerror=%@\nresponse.body=%@", [operation.request HTTPMethod], [[operation.request URL] absoluteString], statusCodeAndElapsedTime, operation.error, operation.responseString); } else { - RKLogError(@"%@ '%@' %@: %@", [operation.request HTTPMethod], [[operation.request URL] absoluteString], statusCodeFragment, operation.error); + RKLogError(@"%@ '%@' %@: %@", [operation.request HTTPMethod], [[operation.request URL] absoluteString], statusCodeAndElapsedTime, operation.error); } } else { if ((_RKlcl_component_level[(__RKlcl_log_symbol(RKlcl_cRestKitNetwork))]) >= (__RKlcl_log_symbol(RKlcl_vTrace))) { - RKLogTrace(@"%@ '%@' %@:\nresponse.headers=%@\nresponse.body=%@", [operation.request HTTPMethod], [[operation.request URL] absoluteString], statusCodeFragment, [operation.response allHeaderFields], RKLogTruncateString(operation.responseString)); + RKLogTrace(@"%@ '%@' %@:\nresponse.headers=%@\nresponse.body=%@", [operation.request HTTPMethod], [[operation.request URL] absoluteString], statusCodeAndElapsedTime, [operation.response allHeaderFields], RKLogTruncateString(operation.responseString)); } else { - RKLogInfo(@"%@ '%@' %@", [operation.request HTTPMethod], [[operation.request URL] absoluteString], statusCodeFragment); + RKLogInfo(@"%@ '%@' %@", [operation.request HTTPMethod], [[operation.request URL] absoluteString], statusCodeAndElapsedTime); } } } diff --git a/Code/Network/RKManagedObjectRequestOperation.m b/Code/Network/RKManagedObjectRequestOperation.m index b5b8f8d675..979d994ee0 100644 --- a/Code/Network/RKManagedObjectRequestOperation.m +++ b/Code/Network/RKManagedObjectRequestOperation.m @@ -69,22 +69,18 @@ - (NSSet *)keyPaths - (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]; - } else { - 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]]) { - 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]; - } + 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]]) { + 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]; } } } @@ -102,6 +98,28 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath return fetchRequests; } +/** + 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' + would return: 'this, 'another.one', 'another.two' + */ +NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths); +NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths) +{ + return [setOfKeyPaths objectsPassingTest:^BOOL(NSString *keyPath, BOOL *stop) { + if ([keyPath isEqual:[NSNull null]]) return YES; // Special case the root key path + NSArray *keyPathComponents = [keyPath componentsSeparatedByString:@"."]; + NSMutableSet *parentKeyPaths = [NSMutableSet set]; + for (NSUInteger index = 0; index < [keyPathComponents count] - 1; index++) { + [parentKeyPaths addObject:[[keyPathComponents subarrayWithRange:NSMakeRange(0, index + 1)] componentsJoinedByString:@"."]]; + } + for (NSString *parentKeyPath in parentKeyPaths) { + if ([setOfKeyPaths containsObject:parentKeyPath]) return NO; + } + return YES; + }]; +} + // When we map the root object, it is returned under the key `[NSNull null]` static id RKMappedValueForKeyPathInDictionary(NSString *keyPath, NSDictionary *dictionary) { @@ -110,17 +128,33 @@ static id RKMappedValueForKeyPathInDictionary(NSString *keyPath, NSDictionary *d static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath, NSMutableDictionary *dictionary) { + NSCParameterAssert(value); + NSCParameterAssert(keyPath); + NSCParameterAssert(dictionary); [keyPath isEqual:[NSNull null]] ? [dictionary setObject:value forKey:keyPath] : [dictionary setValue:value forKeyPath:keyPath]; } +// Precondition: Must be called from within the correct context +static NSManagedObject *RKRefetchManagedObjectInContext(NSManagedObject *managedObject, NSManagedObjectContext *managedObjectContext) +{ + NSManagedObjectID *managedObjectID = [managedObject objectID]; + if (! [managedObject managedObjectContext]) return nil; // Object has been deleted + if ([managedObjectID isTemporaryID]) { + RKLogWarning(@"Unable to refetch managed object %@: the object has a temporary managed object ID.", managedObject); + return managedObject; + } + NSError *error = nil; + NSManagedObject *refetchedObject = [managedObjectContext existingObjectWithID:managedObjectID error:&error]; + NSCAssert(refetchedObject, @"Failed to find existing object with ID %@ in context %@: %@", managedObjectID, managedObjectContext, error); + return refetchedObject; +} + // 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) { if (! [dictionaryOfManagedObjects count]) return dictionaryOfManagedObjects; NSMutableDictionary *newDictionary = [dictionaryOfManagedObjects mutableCopy]; [managedObjectContext performBlockAndWait:^{ - __block NSError *error = nil; - for (NSString *keyPath in keyPaths) { id value = RKMappedValueForKeyPathInDictionary(keyPath, dictionaryOfManagedObjects); if (! value) { @@ -129,52 +163,31 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath, BOOL isMutable = [value isKindOfClass:[NSMutableArray class]]; NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]]; for (__strong id object in value) { - if ([object isKindOfClass:[NSManagedObject class]]) { - if (![object managedObjectContext]) continue; // Object was deleted - object = [managedObjectContext existingObjectWithID:[object objectID] error:&error]; - NSCAssert(object, @"Failed to find existing object with ID %@ in context %@: %@", [object objectID], managedObjectContext, error); - } - - [newValue addObject:object]; + 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]]) { - if (![object managedObjectContext]) continue; // Object was deleted - object = [managedObjectContext existingObjectWithID:[object objectID] error:&error]; - NSCAssert(object, @"Failed to find existing object with ID %@ in context %@: %@", [object objectID], managedObjectContext, error); - } - - [newValue addObject:object]; + 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]]) { - if ([object managedObjectContext]) { - object = [managedObjectContext existingObjectWithID:[object objectID] error:&error]; - NSCAssert(object, @"Failed to find existing object with ID %@ in context %@: %@", [object objectID], managedObjectContext, error); - } else { - // Object was deleted - object = nil; - } - } - + 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]]) { - // Object becomes nil if deleted - value = [value managedObjectContext] ? [managedObjectContext existingObjectWithID:[value objectID] error:&error] : nil; - NSCAssert(value, @"Failed to find existing object with ID %@ in context %@: %@", [value objectID], managedObjectContext, error); + value = RKRefetchManagedObjectInContext(value, managedObjectContext); } - RKSetMappedValueForKeyPathInDictionary(value, keyPath, newDictionary); + if (value) RKSetMappedValueForKeyPathInDictionary(value, keyPath, newDictionary); } }]; @@ -187,7 +200,7 @@ static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath, NSCParameterAssert(responseDescriptors); NSArray *baseURLs = [responseDescriptors valueForKeyPath:@"@distinctUnionOfObjects.baseURL"]; if ([baseURLs count] == 1) { - NSURL *baseURL = baseURLs[0]; + NSURL *baseURL = [baseURLs objectAtIndex:0]; NSString *pathAndQueryString = RKPathAndQueryStringFromURLRelativeToURL(URL, baseURL); URL = [NSURL URLWithString:pathAndQueryString relativeToURL:baseURL]; } @@ -483,7 +496,8 @@ - (void)willFinish // Refetch all managed objects nested at key paths within the results dictionary before returning if (self.mappingResult) { - NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext([self.mappingResult dictionary], managedObjectMappingResultKeyPaths, self.managedObjectContext); + NSSet *nonNestedKeyPaths = RKSetByRemovingSubkeypathsFromSet(managedObjectMappingResultKeyPaths); + NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext([self.mappingResult dictionary], nonNestedKeyPaths, self.managedObjectContext); self.mappingResult = [[RKMappingResult alloc] initWithDictionary:resultsDictionaryFromOriginalContext]; } } diff --git a/Code/Network/RKObjectManager.h b/Code/Network/RKObjectManager.h index ad309cfd96..217496cbf1 100644 --- a/Code/Network/RKObjectManager.h +++ b/Code/Network/RKObjectManager.h @@ -85,7 +85,11 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; Once a path pattern has been registered via the routing system, the manager can automatically build full request URL's when given nothing but the object to be sent. - The second use case of path patterns is in the matching of path into a dictionary of attributes. In this case, the path pattern is evaluatd against a string and used to construct an `NSDictionary` object containing the matched key paths, optionally including the values of a query string. This functionality is provided via the `RKPathMatcher` class and is discussed in detail in the accompanying documentation. + The second use case of path patterns is in the matching of path into a dictionary of attributes. In this case, the path pattern is evaluatd against a string and used to construct an `NSDictionary` object containing the matched key paths, optionally including the values of a query string. This functionality is provided via the `RKPathMatcher` class and is discussed in detail in the accompanying documentation. + + ### Escaping Path Patterns + + Note that path patterns will by default interpret anything prefixed with a period that follows a dynamic path segment as a key path. This can cause an issue if you have a dynamic path segment that is followed by a file extension. For example, a path pattern of '/categories/:categoryID.json' would be erroneously interpretted as containing a dynamic path segment whose value is interpolated from the 'categoryID.json' key path. This key path evaluation behavior can be suppressed by escaping the period preceding the non-dynamic part of the pattern with two leading slashes, as in '/categories/:categoryID\\.json'. ## Request and Response Descriptors @@ -227,7 +231,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; @return The shared manager instance. */ -+ (RKObjectManager *)sharedManager; ++ (instancetype)sharedManager; /** Set the shared instance of the object manager @@ -248,7 +252,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; @param baseURL The base URL with which to initialize the `AFHTTPClient` object @return A new `RKObjectManager` initialized with an `AFHTTPClient` that was initialized with the given baseURL. */ -+ (id)managerWithBaseURL:(NSURL *)baseURL; ++ (instancetype)managerWithBaseURL:(NSURL *)baseURL; /** Initializes the receiver with the given AFNetworking HTTP client object, adopting the network configuration from the client. @@ -258,7 +262,7 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor; @param client The AFNetworking HTTP client with which to initialize the receiver. @return The receiver, initialized with the given client. */ -- (id)initWithHTTPClient:(AFHTTPClient *)client; +- (instancetype)initWithHTTPClient:(AFHTTPClient *)client; ///------------------------------------------ /// @name Accessing Object Manager Properties diff --git a/Code/Network/RKObjectManager.m b/Code/Network/RKObjectManager.m index 6ba30a25b4..4ec15a4907 100644 --- a/Code/Network/RKObjectManager.m +++ b/Code/Network/RKObjectManager.m @@ -169,6 +169,34 @@ static BOOL RKDoesArrayOfResponseDescriptorsContainEntityMapping(NSArray *respon return NO; } +static BOOL RKDoesArrayOfResponseDescriptorsContainMappingForClass(NSArray *responseDescriptors, Class classToBeMapped) +{ + // Visit all mappings accessible from the object graphs of all response descriptors + NSMutableSet *accessibleMappings = [NSMutableSet set]; + for (RKResponseDescriptor *responseDescriptor in responseDescriptors) { + if (! [accessibleMappings containsObject:responseDescriptor.mapping]) { + RKMappingGraphVisitor *graphVisitor = [[RKMappingGraphVisitor alloc] initWithMapping:responseDescriptor.mapping]; + [accessibleMappings unionSet:graphVisitor.mappings]; + } + } + + // Enumerate all mappings and search for a mapping matching the class + for (RKMapping *mapping in accessibleMappings) { + if ([mapping isKindOfClass:[RKObjectMapping class]]) { + if ([[(RKObjectMapping *)mapping objectClass] isSubclassOfClass:classToBeMapped]) return YES; + } + + if ([mapping isKindOfClass:[RKDynamicMapping class]]) { + RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)mapping; + for (RKObjectMapping *mapping in dynamicMapping.objectMappings) { + if ([[(RKObjectMapping *)mapping objectClass] isSubclassOfClass:classToBeMapped]) return YES; + } + } + } + + return NO; +} + static NSString *RKMIMETypeFromAFHTTPClientParameterEncoding(AFHTTPClientParameterEncoding encoding) { switch (encoding) { @@ -223,7 +251,7 @@ - (id)initWithHTTPClient:(AFHTTPClient *)client return self; } -+ (RKObjectManager *)sharedManager ++ (instancetype)sharedManager { return sharedManager; } @@ -381,7 +409,7 @@ - (RKManagedObjectRequestOperation *)managedObjectRequestOperationWithRequest:(N { RKManagedObjectRequestOperation *operation = [[RKManagedObjectRequestOperation alloc] initWithHTTPRequestOperation:[self HTTPOperationWithRequest:request] responseDescriptors:self.responseDescriptors]; [operation setCompletionBlockWithSuccess:success failure:failure]; - operation.managedObjectContext = managedObjectContext; + operation.managedObjectContext = managedObjectContext ?: self.managedObjectStore.mainQueueManagedObjectContext; operation.managedObjectCache = self.managedObjectStore.managedObjectCache; operation.fetchRequestBlocks = self.fetchRequestBlocks; return operation; @@ -419,7 +447,7 @@ - (id)appropriateObjectRequestOperationWithObject:(id)object operation = [self objectRequestOperationWithRequest:request success:nil failure:nil]; } - operation.targetObject = object; + if (RKDoesArrayOfResponseDescriptorsContainMappingForClass(self.responseDescriptors, [object class])) operation.targetObject = object; return operation; } @@ -526,6 +554,7 @@ - (RKPaginator *)paginatorWithPathPattern:(NSString *)pathPattern paginator.managedObjectContext = self.managedObjectStore.mainQueueManagedObjectContext; paginator.managedObjectCache = self.managedObjectStore.managedObjectCache; paginator.fetchRequestBlocks = self.fetchRequestBlocks; + paginator.operationQueue = self.operationQueue; return paginator; } diff --git a/Code/Network/RKObjectRequestOperation.h b/Code/Network/RKObjectRequestOperation.h index e1e725e2d8..6f11e4da9f 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. */ -- (id)initWithHTTPRequestOperation:(RKHTTPRequestOperation *)requestOperation responseDescriptors:(NSArray *)responseDescriptors; +- (instancetype)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. */ -- (id)initWithRequest:(NSURLRequest *)request responseDescriptors:(NSArray *)responseDescriptors; +- (instancetype)initWithRequest:(NSURLRequest *)request responseDescriptors:(NSArray *)responseDescriptors; ///--------------------------------- /// @name Configuring Object Mapping diff --git a/Code/Network/RKPaginator.h b/Code/Network/RKPaginator.h index ada67ed97b..99cde3e70c 100644 --- a/Code/Network/RKPaginator.h +++ b/Code/Network/RKPaginator.h @@ -56,9 +56,9 @@ @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. */ -- (id)initWithRequest:(NSURLRequest *)request - paginationMapping:(RKObjectMapping *)paginationMapping - responseDescriptors:(NSArray *)responseDescriptors; +- (instancetype)initWithRequest:(NSURLRequest *)request + paginationMapping:(RKObjectMapping *)paginationMapping + responseDescriptors:(NSArray *)responseDescriptors; /** A URL with a path pattern for building a complete URL from diff --git a/Code/Network/RKRequestDescriptor.h b/Code/Network/RKRequestDescriptor.h index 44072d32d6..2e7c05c50b 100644 --- a/Code/Network/RKRequestDescriptor.h +++ b/Code/Network/RKRequestDescriptor.h @@ -46,9 +46,9 @@ @see [RKObjectMapping requestMapping] @warning An exception will be raised if the objectClass of the given mapping is not `[NSMutableDictionary class]`. */ -+ (id)requestDescriptorWithMapping:(RKMapping *)mapping - objectClass:(Class)objectClass - rootKeyPath:(NSString *)rootKeyPath; ++ (instancetype)requestDescriptorWithMapping:(RKMapping *)mapping + objectClass:(Class)objectClass + rootKeyPath:(NSString *)rootKeyPath; ///----------------------------------------------------- /// @name Getting Information About a Request Descriptor diff --git a/Code/Network/RKRequestDescriptor.m b/Code/Network/RKRequestDescriptor.m index 6eb6c65f0f..aa24f0c26d 100644 --- a/Code/Network/RKRequestDescriptor.m +++ b/Code/Network/RKRequestDescriptor.m @@ -52,7 +52,7 @@ @interface RKRequestDescriptor () @implementation RKRequestDescriptor -+ (id)requestDescriptorWithMapping:(RKMapping *)mapping objectClass:(Class)objectClass rootKeyPath:(NSString *)rootKeyPath ++ (instancetype)requestDescriptorWithMapping:(RKMapping *)mapping objectClass:(Class)objectClass rootKeyPath:(NSString *)rootKeyPath { NSParameterAssert(mapping); NSParameterAssert(objectClass); diff --git a/Code/Network/RKResponseDescriptor.h b/Code/Network/RKResponseDescriptor.h index 48f85cae01..38993880bc 100644 --- a/Code/Network/RKResponseDescriptor.h +++ b/Code/Network/RKResponseDescriptor.h @@ -41,10 +41,10 @@ @param statusCodes A set of HTTP status codes for which the mapping is to be used. @return A new `RKResponseDescriptor` object. */ -+ (RKResponseDescriptor *)responseDescriptorWithMapping:(RKMapping *)mapping - pathPattern:(NSString *)pathPattern - keyPath:(NSString *)keyPath - statusCodes:(NSIndexSet *)statusCodes; ++ (instancetype)responseDescriptorWithMapping:(RKMapping *)mapping + pathPattern:(NSString *)pathPattern + keyPath:(NSString *)keyPath + statusCodes:(NSIndexSet *)statusCodes; ///------------------------------------------------------ /// @name Getting Information About a Response Descriptor diff --git a/Code/Network/RKResponseDescriptor.m b/Code/Network/RKResponseDescriptor.m index b19621ce6b..d8cc8179f1 100644 --- a/Code/Network/RKResponseDescriptor.m +++ b/Code/Network/RKResponseDescriptor.m @@ -65,10 +65,10 @@ @interface RKResponseDescriptor () @implementation RKResponseDescriptor -+ (RKResponseDescriptor *)responseDescriptorWithMapping:(RKMapping *)mapping - pathPattern:(NSString *)pathPattern - keyPath:(NSString *)keyPath - statusCodes:(NSIndexSet *)statusCodes ++ (instancetype)responseDescriptorWithMapping:(RKMapping *)mapping + pathPattern:(NSString *)pathPattern + keyPath:(NSString *)keyPath + statusCodes:(NSIndexSet *)statusCodes { NSParameterAssert(mapping); RKResponseDescriptor *mappingDescriptor = [self new]; diff --git a/Code/Network/RKResponseMapperOperation.h b/Code/Network/RKResponseMapperOperation.h index 703f452346..bd518f3892 100644 --- a/Code/Network/RKResponseMapperOperation.h +++ b/Code/Network/RKResponseMapperOperation.h @@ -53,9 +53,9 @@ @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. */ -- (id)initWithResponse:(NSHTTPURLResponse *)response - data:(NSData *)data - responseDescriptors:(NSArray *)responseDescriptors; +- (instancetype)initWithResponse:(NSHTTPURLResponse *)response + data:(NSData *)data + responseDescriptors:(NSArray *)responseDescriptors; ///------------------------------ /// @name Accessing Response Data @@ -197,9 +197,9 @@ /** Returns a representation of a mapping result as an `NSError` value. - The returned `NSError` object is in the `RKErrorDomain` domain and has the `RKMappingErrorFromMappingResult` code. The value for the `NSLocalizedDescriptionKey` is computed by retrieving the objects in the mapping result as an array, evaluating `valueForKeyPath:@"errorMessage"` against the array, and joining the returned error messages by comma to form a single string value. The source error objects are returned with the `NSError` in the `userInfo` dictionary under the `RKObjectMapperErrorObjectsKey` key. + The returned `NSError` object is in the `RKErrorDomain` domain and has the `RKMappingErrorFromMappingResult` code. The value for the `NSLocalizedDescriptionKey` is computed by retrieving the objects in the mapping result as an array, evaluating `valueForKeyPath:@"description"` against the array, and joining the returned error messages by comma to form a single string value. The source error objects are returned with the `NSError` in the `userInfo` dictionary under the `RKObjectMapperErrorObjectsKey` key. - The `errorMessage` property is significant as it is an informal protocol that must be adopted by objects wishing to representing response errors. + This implementation assumes that the class used to represent the response error will return a string description of the client side error when sent the `description` message. @return An error object representing the objects contained in the mapping result. @see `RKErrorMessage` diff --git a/Code/Network/RKResponseMapperOperation.m b/Code/Network/RKResponseMapperOperation.m index 9f272ee731..93d50a1f2b 100644 --- a/Code/Network/RKResponseMapperOperation.m +++ b/Code/Network/RKResponseMapperOperation.m @@ -37,7 +37,7 @@ NSArray *collection = [mappingResult array]; NSString *description = nil; if ([collection count] > 0) { - description = [[collection valueForKeyPath:@"errorMessage"] componentsJoinedByString:@", "]; + description = [[collection valueForKeyPath:@"description"] componentsJoinedByString:@", "]; } else { RKLogWarning(@"Expected mapping result to contain at least one object to construct an error"); } @@ -287,7 +287,7 @@ @implementation RKObjectResponseMapperOperation - (RKMappingResult *)performMappingWithObject:(id)sourceObject error:(NSError **)error { RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; - self.mapperOperation = [[RKMapperOperation alloc] initWithObject:sourceObject mappingsDictionary:self.responseMappingsDictionary]; + self.mapperOperation = [[RKMapperOperation alloc] initWithRepresentation:sourceObject mappingsDictionary:self.responseMappingsDictionary]; self.mapperOperation.mappingOperationDataSource = dataSource; if (NSLocationInRange(self.response.statusCode, RKStatusCodeRangeForClass(RKStatusCodeClassSuccessful))) { self.mapperOperation.targetObject = self.targetObject; @@ -327,7 +327,7 @@ - (RKMappingResult *)performMappingWithObject:(id)sourceObject error:(NSError ** self.operationQueue = [NSOperationQueue new]; [self.managedObjectContext performBlockAndWait:^{ // Configure the mapper - self.mapperOperation = [[RKMapperOperation alloc] initWithObject:sourceObject mappingsDictionary:self.responseMappingsDictionary]; + self.mapperOperation = [[RKMapperOperation alloc] initWithRepresentation:sourceObject mappingsDictionary:self.responseMappingsDictionary]; self.mapperOperation.delegate = self.mapperDelegate; // Configure a data source to defer execution of connection operations until mapping is complete diff --git a/Code/Network/RKRoute.h b/Code/Network/RKRoute.h index d7536bf334..2d38d7eecb 100644 --- a/Code/Network/RKRoute.h +++ b/Code/Network/RKRoute.h @@ -48,7 +48,7 @@ @param method The request method of the route. @return A new named route object with the given name, path pattern and request method. */ -+ (id)routeWithName:(NSString *)name pathPattern:(NSString *)pathPattern method:(RKRequestMethod)method; ++ (instancetype)routeWithName:(NSString *)name pathPattern:(NSString *)pathPattern method:(RKRequestMethod)method; /** Creates and returns a new class route object with the given object class, path pattern and method. @@ -58,7 +58,7 @@ @param method The request method of the route. @return A new class route object with the given object class, path pattern and request method. */ -+ (id)routeWithClass:(Class)objectClass pathPattern:(NSString *)pathPattern method:(RKRequestMethod)method; ++ (instancetype)routeWithClass:(Class)objectClass pathPattern:(NSString *)pathPattern method:(RKRequestMethod)method; /** Creates and returns a new relationship route object with the given relationship name, object class, path pattern and method. @@ -69,7 +69,7 @@ @param method The request method of the route. @return A new class route object with the given object class, path pattern and request method. */ -+ (id)routeWithRelationshipName:(NSString *)name objectClass:(Class)objectClass pathPattern:(NSString *)pathPattern method:(RKRequestMethod)method; ++ (instancetype)routeWithRelationshipName:(NSString *)name objectClass:(Class)objectClass pathPattern:(NSString *)pathPattern method:(RKRequestMethod)method; ///--------------------------------- /// @name Accessing Route Attributes diff --git a/Code/Network/RKRoute.m b/Code/Network/RKRoute.m index e29e98a808..94d6323a20 100644 --- a/Code/Network/RKRoute.m +++ b/Code/Network/RKRoute.m @@ -38,7 +38,7 @@ @interface RKRelationshipRoute : RKRoute @implementation RKRoute -+ (id)routeWithName:(NSString *)name pathPattern:(NSString *)pathPattern method:(RKRequestMethod)method ++ (instancetype)routeWithName:(NSString *)name pathPattern:(NSString *)pathPattern method:(RKRequestMethod)method { NSParameterAssert(name); NSParameterAssert(pathPattern); @@ -49,7 +49,7 @@ + (id)routeWithName:(NSString *)name pathPattern:(NSString *)pathPattern method: return route; } -+ (id)routeWithClass:(Class)objectClass pathPattern:(NSString *)pathPattern method:(RKRequestMethod)method ++ (instancetype)routeWithClass:(Class)objectClass pathPattern:(NSString *)pathPattern method:(RKRequestMethod)method { NSParameterAssert(objectClass); NSParameterAssert(pathPattern); @@ -60,7 +60,7 @@ + (id)routeWithClass:(Class)objectClass pathPattern:(NSString *)pathPattern meth return route; } -+ (id)routeWithRelationshipName:(NSString *)relationshipName objectClass:(Class)objectClass pathPattern:(NSString *)pathPattern method:(RKRequestMethod)method ++ (instancetype)routeWithRelationshipName:(NSString *)relationshipName objectClass:(Class)objectClass pathPattern:(NSString *)pathPattern method:(RKRequestMethod)method { NSParameterAssert(relationshipName); NSParameterAssert(objectClass); diff --git a/Code/Network/RKRouter.h b/Code/Network/RKRouter.h index 4bb3cdb4a4..3a8c322e2c 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. */ -- (id)initWithBaseURL:(NSURL *)baseURL; +- (instancetype)initWithBaseURL:(NSURL *)baseURL; ///---------------------- /// @name Generating URLs diff --git a/Code/ObjectMapping/RKAttributeMapping.h b/Code/ObjectMapping/RKAttributeMapping.h index deb461f4c6..6d08f968cf 100644 --- a/Code/ObjectMapping/RKAttributeMapping.h +++ b/Code/ObjectMapping/RKAttributeMapping.h @@ -51,10 +51,10 @@ mapped and attempts to transform the source content into the type of the desination property specified by the mapping. In this case, an NSDateFormatter object would be used to process the inbound `NSString` into an outbound `NSDate` object. - @param sourceKeyPath The key path on the source object from which to read the data being mapped. + @param sourceKeyPath The key path on the source object from which to read the data being mapped. If `nil`, then the entire source object representation is mapped to the specified destination attribute. @param destinationKeyPath The key path on the destination object on which to set the mapped data. @return A newly created attribute mapping object that is ready to be added to an object mapping. */ -+ (RKAttributeMapping *)attributeMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath; ++ (instancetype)attributeMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath; @end diff --git a/Code/ObjectMapping/RKAttributeMapping.m b/Code/ObjectMapping/RKAttributeMapping.m index 3a479f328d..a09064507a 100644 --- a/Code/ObjectMapping/RKAttributeMapping.m +++ b/Code/ObjectMapping/RKAttributeMapping.m @@ -27,9 +27,8 @@ @interface RKAttributeMapping () @implementation RKAttributeMapping -+ (RKAttributeMapping *)attributeMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath ++ (instancetype)attributeMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath { - NSParameterAssert(sourceKeyPath); NSParameterAssert(destinationKeyPath); RKAttributeMapping *attributeMapping = [self new]; attributeMapping.sourceKeyPath = sourceKeyPath; diff --git a/Code/ObjectMapping/RKErrorMessage.h b/Code/ObjectMapping/RKErrorMessage.h index 929d1fde4e..2aea33face 100644 --- a/Code/ObjectMapping/RKErrorMessage.h +++ b/Code/ObjectMapping/RKErrorMessage.h @@ -23,10 +23,6 @@ /** The `RKErrorMessage` is a simple class used for representing error messages returned by a remote backend system with which the client application is communicating. Error messages are typically returned in a response body in the Client Error class (status code 4xx range). - ## Error Message Informal Protocol - - The `errorMessage` property method is the sole method of an informal protocol that must be adopted by objects wishing to represent error messages within RestKit. This protocol is by the `RKErrorFromMappingResult` function when constructing `NSError` messages from a mapped response body. - @see `RKErrorFromMappingResult` */ @interface RKErrorMessage : NSObject diff --git a/Code/ObjectMapping/RKErrorMessage.m b/Code/ObjectMapping/RKErrorMessage.m index 6ff7bc859d..1a102c4aa7 100644 --- a/Code/ObjectMapping/RKErrorMessage.m +++ b/Code/ObjectMapping/RKErrorMessage.m @@ -24,8 +24,7 @@ @implementation RKErrorMessage - (NSString *)description { - return [NSString stringWithFormat:@"<%@:%p error message = \"%@\" userInfo = %@>", - NSStringFromClass([self class]), self, self.errorMessage, self.userInfo]; + return self.errorMessage; } @end diff --git a/Code/ObjectMapping/RKMapperOperation.h b/Code/ObjectMapping/RKMapperOperation.h index eb5bfff716..cd5ac81ef5 100644 --- a/Code/ObjectMapping/RKMapperOperation.h +++ b/Code/ObjectMapping/RKMapperOperation.h @@ -37,7 +37,7 @@ ## Mappings Dictionary - The mappings dictionary describes how to object map the source object. The keys of the dictionary are key paths into the `sourceObject` and the values are `RKMapping` objects describing how to map the representations at the corresponding key path. This dictionary based approach enables a single document to contain an arbitrary number of object representations that can be mapped independently. Consider the following example JSON structure: + The mappings dictionary describes how to object map the source object. The keys of the dictionary are key paths into the `representation` and the values are `RKMapping` objects describing how to map the representations at the corresponding key path. This dictionary based approach enables a single document to contain an arbitrary number of object representations that can be mapped independently. Consider the following example JSON structure: { "tags": [ "hacking", "phreaking" ], "authors": [ "Captain Crunch", "Emmanuel Goldstein" ], "magazine": { "title": "2600 The Hacker Quarterly" } } @@ -50,9 +50,11 @@ Note that the keys of the dictionary are **key paths**. Deeply nested content can be mapped by specifying the full key path as the key of the mappings dictionary. - ### The NSNull Key + ### Mapping the Root Object Representation - A mapping set for the key `[NSNull null]` value has special significance to the mapper operation. When a mapping is encountered with the a null key, the entire `sourceObject` is processed using the given mapping. This provides support for mapping content that does not have an outer nesting attribute. + A mapping set for the key `[NSNull null]` value has special significance to the mapper operation. When a mapping is encountered with the a null key, the entire `representation` is processed using the given mapping. This provides support for mapping content that does not have an outer nesting attribute. + + Note that it is possible to map the same representation with multiple mappings, including a combination of a root key mapping and nested keypaths. ## Data Source @@ -60,7 +62,7 @@ ## Target Object - If a `targetObject` is configured on the mapper operation, all mapping work on the `sourceObject` will target the specified object. For transient `NSObject` mappings, this ensures that the properties of an existing object are updated rather than an new object being created for the mapped representation. If an array of representations is being processed and a `targetObject` is provided, it must be a mutable collection object else an exception will be raised. + If a `targetObject` is configured on the mapper operation, all mapping work on the `representation` will target the specified object. For transient `NSObject` mappings, this ensures that the properties of an existing object are updated rather than an new object being created for the mapped representation. If an array of representations is being processed and a `targetObject` is provided, it must be a mutable collection object else an exception will be raised. ## Core Data @@ -75,11 +77,11 @@ /** Initializes the operation with a source object and a mappings dictionary. - @param object An `NSDictionary` or `NSArray` of `NSDictionary` object representations to be mapped into local domain objects. + @param representation An `NSDictionary` or `NSArray` of `NSDictionary` object representations to be mapped into local domain objects. @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. */ -- (id)initWithObject:(id)object mappingsDictionary:(NSDictionary *)mappingsDictionary; +- (instancetype)initWithRepresentation:(id)representation mappingsDictionary:(NSDictionary *)mappingsDictionary; ///------------------------------------------ /// @name Accessing Mapping Result and Errors @@ -100,14 +102,14 @@ ///------------------------------------- /** - The source object representation against which the mapping is performed. + The representation of one or more objects against which the mapping is performed. Either an `NSDictionary` or an `NSArray` of `NSDictionary` objects. */ -@property (nonatomic, strong, readonly) id sourceObject; +@property (nonatomic, strong, readonly) id representation; /** - A dictionary of key paths to `RKMapping` objects specifying how object representations in the `sourceObject` are to be mapped. + A dictionary of key paths to `RKMapping` objects specifying how object representations in the `representation` are to be mapped. Please see the above discussion for in-depth details about the mappings dictionary. */ @@ -130,6 +132,8 @@ */ @property (nonatomic, weak) id delegate; +- (BOOL)execute:(NSError **)error; + @end ///-------------------------------------- @@ -177,7 +181,7 @@ @param mapper The mapper operation performing the mapping. @param dictionaryOrArrayOfDictionaries The `NSDictictionary` or `NSArray` of `NSDictionary` object representations that was found at the `keyPath`. - @param keyPath The key path that the representation was read from in the `sourceObject`. If the `keyPath` was `[NSNull null]` in the `mappingsDictionary`, it will be given as `nil` to the delegate. + @param keyPath The key path that the representation was read from in the `representation`. If the `keyPath` was `[NSNull null]` in the `mappingsDictionary`, it will be given as `nil` to the delegate. */ - (void)mapper:(RKMapperOperation *)mapper didFindRepresentationOrArrayOfRepresentations:(id)dictionaryOrArrayOfDictionaries atKeyPath:(NSString *)keyPath; @@ -194,11 +198,11 @@ ///---------------------------------------------- /** - Tells the delegate that the mapper is about to start a mapping operation to map a representation found in the `sourceObject`. + Tells the delegate that the mapper is about to start a mapping operation to map a representation found in the `representation`. @param mapper The mapper operation performing the mapping. @param mappingOperation The mapping operation that is about to be started. - @param keyPath The key path that was mapped. A `nil` key path indicates that the mapping matched the entire `sourceObject`. + @param keyPath The key path that was mapped. A `nil` key path indicates that the mapping matched the entire `representation`. */ - (void)mapper:(RKMapperOperation *)mapper willStartMappingOperation:(RKMappingOperation *)mappingOperation forKeyPath:(NSString *)keyPath; @@ -207,7 +211,7 @@ @param mapper The mapper operation performing the mapping. @param mappingOperation The mapping operation that has finished. - @param keyPath The key path that was mapped. A `nil` key path indicates that the mapping matched the entire `sourceObject`. + @param keyPath The key path that was mapped. A `nil` key path indicates that the mapping matched the entire `representation`. */ - (void)mapper:(RKMapperOperation *)mapper didFinishMappingOperation:(RKMappingOperation *)mappingOperation forKeyPath:(NSString *)keyPath; @@ -216,7 +220,7 @@ @param mapper The mapper operation performing the mapping. @param mappingOperation The mapping operation that has failed. - @param keyPath The key path that was mapped. A `nil` key path indicates that the mapping matched the entire `sourceObject`. + @param keyPath The key path that was mapped. A `nil` key path indicates that the mapping matched the entire `representation`. @param error The error that occurred during the execution of the mapping operation. */ - (void)mapper:(RKMapperOperation *)mapper didFailMappingOperation:(RKMappingOperation *)mappingOperation forKeyPath:(NSString *)keyPath withError:(NSError *)error; diff --git a/Code/ObjectMapping/RKMapperOperation.m b/Code/ObjectMapping/RKMapperOperation.m index dd8db29397..2cd53b8a41 100644 --- a/Code/ObjectMapping/RKMapperOperation.m +++ b/Code/ObjectMapping/RKMapperOperation.m @@ -37,23 +37,35 @@ return ([keyPath isEqual:[NSNull null]]) ? nil : keyPath; } + +static NSString *RKFailureReasonErrorStringForMappingNotFoundError(id representation, NSDictionary *mappingsDictionary) +{ + NSMutableString *failureReason = [NSMutableString string]; + [failureReason appendFormat:@"The mapping operation was unable to find any nested object representations at the key paths searched: %@", [[mappingsDictionary allKeys] componentsJoinedByString:@", "]]; + if ([representation respondsToSelector:@selector(allKeys)]) { + [failureReason appendFormat:@"\nThe representation inputted to the mapper was found to contain nested object representations at the following key paths: %@", [[representation allKeys] componentsJoinedByString:@", "]]; + } + [failureReason appendFormat:@"\nThis likely indicates that you have misconfigured the key paths for your mappings."]; + return failureReason; +} + @interface RKMapperOperation () @property (nonatomic, strong, readwrite) NSError *error; @property (nonatomic, strong, readwrite) RKMappingResult *mappingResult; @property (nonatomic, strong) NSMutableArray *mappingErrors; -@property (nonatomic, strong) id sourceObject; +@property (nonatomic, strong) id representation; @property (nonatomic, strong, readwrite) NSDictionary *mappingsDictionary; @end @implementation RKMapperOperation -- (id)initWithObject:(id)object mappingsDictionary:(NSDictionary *)mappingsDictionary; +- (id)initWithRepresentation:(id)representation mappingsDictionary:(NSDictionary *)mappingsDictionary; { self = [super init]; if (self) { - self.sourceObject = object; + self.representation = representation; self.mappingsDictionary = mappingsDictionary; self.mappingErrors = [NSMutableArray new]; self.mappingOperationDataSource = [RKObjectMappingOperationDataSource new]; @@ -117,16 +129,17 @@ - (BOOL)isNullCollection:(id)object #pragma mark - Mapping Primitives -- (id)mapObject:(id)mappableObject atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping +// Maps a singular object representation +- (id)mapRepresentation:(id)representation atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping { - NSAssert([mappableObject respondsToSelector:@selector(setValue:forKeyPath:)], @"Expected self.object to be KVC compliant"); + NSAssert([representation respondsToSelector:@selector(setValue:forKeyPath:)], @"Expected self.object to be KVC compliant"); id destinationObject = nil; if (self.targetObject) { destinationObject = self.targetObject; RKObjectMapping *objectMapping = nil; if ([mapping isKindOfClass:[RKDynamicMapping class]]) { - objectMapping = [(RKDynamicMapping *)mapping objectMappingForRepresentation:mappableObject]; + objectMapping = [(RKDynamicMapping *)mapping objectMappingForRepresentation:representation]; } else if ([mapping isKindOfClass:[RKObjectMapping class]]) { objectMapping = (RKObjectMapping *)mapping; } else { @@ -142,15 +155,15 @@ - (id)mapObject:(id)mappableObject atKeyPath:(NSString *)keyPath usingMapping:(R return nil; } else { // There is more than one mapping present. We are likely mapping secondary key paths to new objects - destinationObject = [self objectWithMapping:mapping andData:mappableObject]; + destinationObject = [self objectForRepresentation:representation withMapping:mapping]; } } } else { - destinationObject = [self objectWithMapping:mapping andData:mappableObject]; + destinationObject = [self objectForRepresentation:representation withMapping:mapping]; } if (mapping && destinationObject) { - BOOL success = [self mapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping]; + BOOL success = [self mapRepresentation:representation toObject:destinationObject atKeyPath:keyPath usingMapping:mapping]; if (success) { return destinationObject; } @@ -163,28 +176,29 @@ - (id)mapObject:(id)mappableObject atKeyPath:(NSString *)keyPath usingMapping:(R return nil; } -- (NSArray *)mapCollection:(NSArray *)mappableObjects atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping +// Map a collection of object representations +- (NSArray *)mapRepresentations:(id)representations atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping { - NSAssert(mappableObjects != nil, @"Cannot map without an collection of mappable objects"); + NSAssert(representations != nil, @"Cannot map without an collection of mappable objects"); NSAssert(mapping != nil, @"Cannot map without a mapping to consult"); - NSArray *objectsToMap = mappableObjects; + NSArray *objectsToMap = representations; if (mapping.forceCollectionMapping) { // If we have forced mapping of a dictionary, map each subdictionary - if ([mappableObjects isKindOfClass:[NSDictionary class]]) { + if ([representations isKindOfClass:[NSDictionary class]]) { RKLogDebug(@"Collection mapping forced for NSDictionary, mapping each key/value independently..."); - objectsToMap = [NSMutableArray arrayWithCapacity:[mappableObjects count]]; - for (id key in mappableObjects) { - NSDictionary *dictionaryToMap = [NSDictionary dictionaryWithObject:[mappableObjects valueForKey:key] forKey:key]; + objectsToMap = [NSMutableArray arrayWithCapacity:[representations count]]; + for (id key in representations) { + NSDictionary *dictionaryToMap = [NSDictionary dictionaryWithObject:[representations valueForKey:key] forKey:key]; [(NSMutableArray *)objectsToMap addObject:dictionaryToMap]; } } else { - RKLogWarning(@"Collection mapping forced but mappable objects is of type '%@' rather than NSDictionary", NSStringFromClass([mappableObjects class])); + RKLogWarning(@"Collection mapping forced but representations is of type '%@' rather than NSDictionary", NSStringFromClass([representations class])); } } // Ensure we are mapping onto a mutable collection if there is a target - NSMutableArray *mappedObjects = self.targetObject ? self.targetObject : [NSMutableArray arrayWithCapacity:[mappableObjects count]]; + NSMutableArray *mappedObjects = self.targetObject ? self.targetObject : [NSMutableArray arrayWithCapacity:[representations count]]; if (NO == [mappedObjects respondsToSelector:@selector(addObject:)]) { NSString *errorMessage = [NSString stringWithFormat: @"Cannot map a collection of objects onto a non-mutable collection. Unexpected destination object type '%@'", @@ -194,12 +208,12 @@ - (NSArray *)mapCollection:(NSArray *)mappableObjects atKeyPath:(NSString *)keyP } for (id mappableObject in objectsToMap) { - id destinationObject = [self objectWithMapping:mapping andData:mappableObject]; + id destinationObject = [self objectForRepresentation:mappableObject withMapping:mapping]; if (! destinationObject) { continue; } - BOOL success = [self mapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping]; + BOOL success = [self mapRepresentation:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping]; if (success) { [mappedObjects addObject:destinationObject]; } @@ -209,7 +223,7 @@ - (NSArray *)mapCollection:(NSArray *)mappableObjects atKeyPath:(NSString *)keyP } // The workhorse of this entire process. Emits object loading operations -- (BOOL)mapFromObject:(id)mappableObject toObject:(id)destinationObject atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping +- (BOOL)mapRepresentation:(id)mappableObject toObject:(id)destinationObject atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping { NSAssert(destinationObject != nil, @"Cannot map without a target object to assign the results to"); NSAssert(mappableObject != nil, @"Cannot map without a collection of attributes"); @@ -239,16 +253,16 @@ - (BOOL)mapFromObject:(id)mappableObject toObject:(id)destinationObject atKeyPat } } -- (id)objectWithMapping:(RKMapping *)mapping andData:(id)mappableData +- (id)objectForRepresentation:(id)representation withMapping:(RKMapping *)mapping { NSAssert([mapping isKindOfClass:[RKMapping class]], @"Expected an RKMapping object"); NSAssert(self.mappingOperationDataSource, @"Cannot find or instantiate objects without a data source"); RKObjectMapping *objectMapping = nil; if ([mapping isKindOfClass:[RKDynamicMapping class]]) { - objectMapping = [(RKDynamicMapping *)mapping objectMappingForRepresentation:mappableData]; + objectMapping = [(RKDynamicMapping *)mapping objectMappingForRepresentation:representation]; if (! objectMapping) { - RKLogDebug(@"Mapping %@ declined mapping for data %@: returned nil objectMapping", mapping, mappableData); + RKLogDebug(@"Mapping %@ declined mapping for representation %@: returned nil objectMapping", mapping, representation); } } else if ([mapping isKindOfClass:[RKObjectMapping class]]) { objectMapping = (RKObjectMapping *)mapping; @@ -257,27 +271,29 @@ - (id)objectWithMapping:(RKMapping *)mapping andData:(id)mappableData } if (objectMapping) { - return [self.mappingOperationDataSource mappingOperation:nil targetObjectForRepresentation:mappableData withMapping:objectMapping]; + return [self.mappingOperationDataSource mappingOperation:nil targetObjectForRepresentation:representation withMapping:objectMapping]; } return nil; } -- (id)performMappingForObject:(id)mappableValue atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping +- (id)mapRepresentationOrRepresentations:(id)mappableValue atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping { id mappingResult; if (mapping.forceCollectionMapping || [mappableValue isKindOfClass:[NSArray class]] || [mappableValue isKindOfClass:[NSSet class]]) { RKLogDebug(@"Found mappable collection at keyPath '%@': %@", keyPath, mappableValue); - mappingResult = [self mapCollection:mappableValue atKeyPath:keyPath usingMapping:mapping]; + mappingResult = [self mapRepresentations:mappableValue atKeyPath:keyPath usingMapping:mapping]; } else { RKLogDebug(@"Found mappable data at keyPath '%@': %@", keyPath, mappableValue); - mappingResult = [self mapObject:mappableValue atKeyPath:keyPath usingMapping:mapping]; + mappingResult = [self mapRepresentation:mappableValue atKeyPath:keyPath usingMapping:mapping]; } return mappingResult; } -- (NSMutableDictionary *)performKeyPathMappingUsingMappingDictionary:(NSDictionary *)mappingsByKeyPath +#pragma mark - + +- (NSMutableDictionary *)mapSourceRepresentationWithMappingsDictionary:(NSDictionary *)mappingsByKeyPath { BOOL foundMappable = NO; NSMutableDictionary *results = [NSMutableDictionary dictionary]; @@ -285,18 +301,18 @@ - (NSMutableDictionary *)performKeyPathMappingUsingMappingDictionary:(NSDictiona if ([self isCancelled]) return nil; id mappingResult = nil; - id mappableValue = nil; + id nestedRepresentation = nil; RKLogTrace(@"Examining keyPath '%@' for mappable content...", keyPath); if ([keyPath isEqual:[NSNull null]] || [keyPath isEqualToString:@""]) { - mappableValue = self.sourceObject; + nestedRepresentation = self.representation; } else { - mappableValue = [self.sourceObject valueForKeyPath:keyPath]; + nestedRepresentation = [self.representation valueForKeyPath:keyPath]; } // Not found... - if (mappableValue == nil || mappableValue == [NSNull null] || [self isNullCollection:mappableValue]) { + if (nestedRepresentation == nil || nestedRepresentation == [NSNull null] || [self isNullCollection:nestedRepresentation]) { RKLogDebug(@"Found unmappable value at keyPath: %@", keyPath); if ([self.delegate respondsToSelector:@selector(mapper:didNotFindRepresentationOrArrayOfRepresentationsAtKeyPath:)]) { @@ -310,10 +326,10 @@ - (NSMutableDictionary *)performKeyPathMappingUsingMappingDictionary:(NSDictiona foundMappable = YES; RKMapping *mapping = [mappingsByKeyPath objectForKey:keyPath]; if ([self.delegate respondsToSelector:@selector(mapper:didFindRepresentationOrArrayOfRepresentations:atKeyPath:)]) { - [self.delegate mapper:self didFindRepresentationOrArrayOfRepresentations:mappableValue atKeyPath:RKDelegateKeyPathFromKeyPath(keyPath)]; + [self.delegate mapper:self didFindRepresentationOrArrayOfRepresentations:nestedRepresentation atKeyPath:RKDelegateKeyPathFromKeyPath(keyPath)]; } - mappingResult = [self performMappingForObject:mappableValue atKeyPath:keyPath usingMapping:mapping]; + mappingResult = [self mapRepresentationOrRepresentations:nestedRepresentation atKeyPath:keyPath usingMapping:mapping]; if (mappingResult) { [results setObject:mappingResult forKey:keyPath]; @@ -336,12 +352,12 @@ - (void)cancel - (void)main { - NSAssert(self.sourceObject != nil, @"Cannot perform object mapping without a source object to map from"); + NSAssert(self.representation != nil, @"Cannot perform object mapping without a source object to map from"); NSAssert(self.mappingsDictionary, @"Cannot perform object mapping without a dictionary of mappings"); if ([self isCancelled]) return; - RKLogDebug(@"Performing object mapping sourceObject: %@\n and targetObject: %@", self.sourceObject, self.targetObject); + RKLogDebug(@"Executing mapping operation for representation: %@\n and targetObject: %@", self.representation, self.targetObject); if ([self.delegate respondsToSelector:@selector(mapperWillStartMapping:)]) { [self.delegate mapperWillStartMapping:self]; @@ -349,7 +365,7 @@ - (void)main // Perform the mapping BOOL foundMappable = NO; - NSMutableDictionary *results = [self performKeyPathMappingUsingMappingDictionary:self.mappingsDictionary]; + NSMutableDictionary *results = [self mapSourceRepresentationWithMappingsDictionary:self.mappingsDictionary]; if ([self isCancelled]) return; foundMappable = (results != nil); @@ -359,13 +375,12 @@ - (void)main // If we found nothing eligible for mapping in the content, add an unmappable key path error and fail mapping // If the content is empty, we don't consider it an error - BOOL isEmpty = [self.sourceObject respondsToSelector:@selector(count)] && ([self.sourceObject count] == 0); + BOOL isEmpty = [self.representation respondsToSelector:@selector(count)] && ([self.representation count] == 0); if (foundMappable == NO && !isEmpty) { - NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: - NSLocalizedString(@"Unable to find any mappings for the given content", nil), NSLocalizedDescriptionKey, - [NSNull null], RKMappingErrorKeyPathErrorKey, - self.errors, RKDetailedErrorsKey, - nil]; + NSMutableDictionary *userInfo = [@{ NSLocalizedDescriptionKey: NSLocalizedString(@"No mappable object representations were found at the key paths searched.", nil), + NSLocalizedFailureReasonErrorKey: RKFailureReasonErrorStringForMappingNotFoundError(self.representation, self.mappingsDictionary), + RKMappingErrorKeyPathErrorKey: [NSNull null], + RKDetailedErrorsKey: self.errors} mutableCopy]; NSError *compositeError = [[NSError alloc] initWithDomain:RKErrorDomain code:RKMappingErrorNotFound userInfo:userInfo]; self.error = compositeError; return; @@ -376,4 +391,11 @@ - (void)main if (results) self.mappingResult = [[RKMappingResult alloc] initWithDictionary:results]; } +- (BOOL)execute:(NSError **)error +{ + [self start]; + if (error) *error = self.error; + return self.mappingResult != nil; +} + @end diff --git a/Code/ObjectMapping/RKMapperOperation_Private.h b/Code/ObjectMapping/RKMapperOperation_Private.h index 1be7fd9c78..c31e01d22d 100644 --- a/Code/ObjectMapping/RKMapperOperation_Private.h +++ b/Code/ObjectMapping/RKMapperOperation_Private.h @@ -20,9 +20,9 @@ @interface RKMapperOperation (Private) -- (id)mapObject:(id)mappableObject atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping; -- (NSArray *)mapCollection:(NSArray *)mappableObjects atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping; -- (BOOL)mapFromObject:(id)mappableObject toObject:(id)destinationObject atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping; -- (id)objectWithMapping:(RKMapping *)objectMapping andData:(id)mappableData; +- (id)mapRepresentation:(id)mappableObject atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping; +- (NSArray *)mapRepresentations:(NSArray *)mappableObjects atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping; +- (BOOL)mapRepresentation:(id)mappableObject toObject:(id)destinationObject atKeyPath:(NSString *)keyPath usingMapping:(RKMapping *)mapping; +- (id)objectForRepresentation:(id)representation withMapping:(RKMapping *)mapping; @end diff --git a/Code/ObjectMapping/RKMappingErrors.h b/Code/ObjectMapping/RKMappingErrors.h index 25c9c264f8..794e70340a 100644 --- a/Code/ObjectMapping/RKMappingErrors.h +++ b/Code/ObjectMapping/RKMappingErrors.h @@ -30,7 +30,8 @@ enum { RKMappingErrorUnableToDetermineMapping = 1006, // The mapping operation was unable to obtain a concrete object mapping from a given dynamic mapping RKMappingErrorNilDestinationObject = 1007, // The mapping operation failed due to a nil destination object. RKMappingErrorNilManagedObjectCache = 1008, // A managed object cache is required to satisfy the mapping, but none was given. - RKMappingErrorMappingDeclined = 1009 // Mapping was declined by a callback. + RKMappingErrorMappingDeclined = 1009, // Mapping was declined by a callback. + RKMappingErrorInvalidAssignmentPolicy = 1010, // The assignment policy for the relationship is invalid. }; extern NSString * const RKMappingErrorKeyPathErrorKey; // The key path the error is associated with diff --git a/Code/ObjectMapping/RKMappingOperation.h b/Code/ObjectMapping/RKMappingOperation.h index 344980d458..df60662ebf 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. */ -- (id)initWithSourceObject:(id)sourceObject destinationObject:(id)destinationObject mapping:(RKMapping *)objectOrDynamicMapping; +- (instancetype)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 d334f4963b..fb78e79465 100644 --- a/Code/ObjectMapping/RKMappingOperation.m +++ b/Code/ObjectMapping/RKMappingOperation.m @@ -71,6 +71,9 @@ id RKTransformedValueWithClass(id value, Class destinationType, NSValueTransform if ([value isKindOfClass:destinationType]) { // No transformation necessary return value; + } else if (RKClassIsCollection(destinationType) && !RKObjectIsCollection(value)) { + // Call ourself recursively with an array value to transform as appropriate + return RKTransformedValueWithClass(@[ value ], destinationType, dateToStringValueTransformer); } else if ([sourceType isSubclassOfClass:[NSString class]] && [destinationType isSubclassOfClass:[NSDate class]]) { // String -> Date return [dateToStringValueTransformer transformedValue:value]; @@ -148,7 +151,16 @@ id RKTransformedValueWithClass(id value, Class destinationType, NSValueTransform return nil; } -// Applies +// Returns the appropriate value for `nil` value of a primitive type +static id RKPrimitiveValueForNilValueOfClass(Class keyValueCodingClass) +{ + if ([keyValueCodingClass isSubclassOfClass:[NSNumber class]]) { + return @(0); + } else { + return nil; + } +} + // Key comes from: [[_nestedAttributeSubstitution allKeys] lastObject]] AND [[_nestedAttributeSubstitution allValues] lastObject]; NSArray *RKApplyNestingAttributeValueToMappings(NSString *attributeName, id value, NSArray *propertyMappings); NSArray *RKApplyNestingAttributeValueToMappings(NSString *attributeName, id value, NSArray *propertyMappings) @@ -227,7 +239,7 @@ - (id)transformValue:(id)value atKeyPath:(NSString *)keyPath toType:(Class)desti { RKLogTrace(@"Found transformable value at keyPath '%@'. Transforming from type '%@' to '%@'", keyPath, NSStringFromClass([value class]), NSStringFromClass(destinationType)); RKDateToStringValueTransformer *transformer = [[RKDateToStringValueTransformer alloc] initWithDateToStringFormatter:self.objectMapping.preferredDateFormatter stringToDateFormatters:self.objectMapping.dateFormatters]; - id transformedValue = RKTransformedValueWithClass(value, destinationType, transformer); + id transformedValue = RKTransformedValueWithClass(value, destinationType, transformer); if (transformedValue != value) return transformedValue; RKLogWarning(@"Failed transformation of value at keyPath '%@'. No strategy for transforming from '%@' to '%@'", keyPath, NSStringFromClass([value class]), NSStringFromClass(destinationType)); @@ -235,11 +247,6 @@ - (id)transformValue:(id)value atKeyPath:(NSString *)keyPath toType:(Class)desti return nil; } -- (BOOL)isValue:(id)sourceValue equalToValue:(id)destinationValue -{ - return RKObjectIsEqualToObject(sourceValue, destinationValue); -} - - (BOOL)validateValue:(id *)value atKeyPath:(NSString *)keyPath { BOOL success = YES; @@ -288,7 +295,7 @@ - (BOOL)shouldSetValue:(id *)value atKeyPath:(NSString *)keyPath return [self validateValue:value atKeyPath:keyPath]; } - if (! [self isValue:*value equalToValue:currentValue]) { + if (! RKObjectIsEqualToObject(*value, currentValue)) { // Validate value for key return [self validateValue:value atKeyPath:keyPath]; } @@ -345,6 +352,16 @@ - (void)applyAttributeMapping:(RKAttributeMapping *)attributeMapping withValue:( if (type && NO == [[value class] isSubclassOfClass:type]) { value = [self transformValue:value atKeyPath:attributeMapping.sourceKeyPath toType:type]; } + + // If we have a nil value for a primitive property, we need to coerce it into a KVC usable value or bail out + if (value == nil && RKPropertyInspectorIsPropertyAtKeyPathOfObjectPrimitive(attributeMapping.destinationKeyPath, self.destinationObject)) { + RKLogDebug(@"Detected `nil` value transformation for primitive property at keyPath '%@'", attributeMapping.destinationKeyPath); + value = RKPrimitiveValueForNilValueOfClass(type); + if (! value) { + RKLogTrace(@"Skipped mapping of attribute value from keyPath '%@ to keyPath '%@' -- Unable to transform `nil` into primitive value representation", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath); + return; + } + } RKSetIntermediateDictionaryValuesOnObjectForKeyPath(self.destinationObject, attributeMapping.destinationKeyPath); @@ -382,13 +399,7 @@ - (BOOL)applyAttributeMappings:(NSArray *)attributeMappings continue; } - id value = nil; - if ([attributeMapping.sourceKeyPath isEqualToString:@""]) { - value = self.sourceObject; - } else { - value = [self.sourceObject valueForKeyPath:attributeMapping.sourceKeyPath]; - } - + id value = (attributeMapping.sourceKeyPath == nil) ? self.sourceObject : [self.sourceObject valueForKeyPath:attributeMapping.sourceKeyPath]; if (value) { appliedMappings = YES; [self applyAttributeMapping:attributeMapping withValue:value]; @@ -433,10 +444,35 @@ - (BOOL)mapNestedObject:(id)anObject toObject:(id)anotherObject withRelationship return YES; } +- (BOOL)applyReplaceAssignmentPolicyForRelationshipMapping:(RKRelationshipMapping *)relationshipMapping +{ + if (relationshipMapping.assignmentPolicy == RKReplaceAssignmentPolicy) { + if ([self.dataSource respondsToSelector:@selector(mappingOperation:deleteExistingValueOfRelationshipWithMapping:error:)]) { + NSError *error = nil; + BOOL success = [self.dataSource mappingOperation:self deleteExistingValueOfRelationshipWithMapping:relationshipMapping error:&error]; + if (! success) { + RKLogError(@"Failed to delete existing value of relationship mapped with RKReplaceAssignmentPolicy: %@", error); + self.error = error; + return NO; + } + } else { + RKLogWarning(@"Requested mapping with `RKReplaceAssignmentPolicy` assignment policy, but the data source does not support it. Mapping has proceeded identically to the `RKSetAssignmentPolicy`."); + } + } + + return YES; +} + - (BOOL)mapOneToOneRelationshipWithValue:(id)value mapping:(RKRelationshipMapping *)relationshipMapping { // One to one relationship RKLogDebug(@"Mapping one to one relationship value at keyPath '%@' to '%@'", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath); + + if (relationshipMapping.assignmentPolicy == RKUnionAssignmentPolicy) { + NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Invalid assignment policy: cannot union a one-to-one relationship." }; + self.error = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorInvalidAssignmentPolicy userInfo:userInfo]; + return NO; + } id destinationObject = [self destinationObjectForMappingRepresentation:value withMapping:relationshipMapping.mapping]; if (! destinationObject) { @@ -447,6 +483,10 @@ - (BOOL)mapOneToOneRelationshipWithValue:(id)value mapping:(RKRelationshipMappin // If the relationship has changed, set it if ([self shouldSetValue:&destinationObject atKeyPath:relationshipMapping.destinationKeyPath]) { + if (! [self applyReplaceAssignmentPolicyForRelationshipMapping:relationshipMapping]) { + return NO; + } + RKLogTrace(@"Mapped relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, destinationObject); [self.destinationObject setValue:destinationObject forKey:relationshipMapping.destinationKeyPath]; } else { @@ -489,6 +529,14 @@ - (BOOL)mapOneToManyRelationshipWithValue:(id)value mapping:(RKRelationshipMappi RKLogWarning(@"WARNING: Detected a relationship mapping for a collection containing another collection. This is probably not what you want. Consider using a KVC collection operator (such as @unionOfArrays) to flatten your mappable collection."); RKLogWarning(@"Key path '%@' yielded collection containing another collection rather than a collection of objects: %@", relationshipMapping.sourceKeyPath, value); } + + if (relationshipMapping.assignmentPolicy == RKUnionAssignmentPolicy) { + RKLogDebug(@"Mapping relationship with union assignment policy: constructing combined relationship value."); + id existingObjects = [self.destinationObject valueForKeyPath:relationshipMapping.destinationKeyPath]; + NSArray *existingObjectsArray = RKTransformedValueWithClass(existingObjects, [NSArray class], nil); + [relationshipCollection addObjectsFromArray:existingObjectsArray]; + } + for (id nestedObject in value) { id mappableObject = [self destinationObjectForMappingRepresentation:nestedObject withMapping:relationshipMapping.mapping]; if (! mappableObject) { @@ -509,6 +557,9 @@ - (BOOL)mapOneToManyRelationshipWithValue:(id)value mapping:(RKRelationshipMappi // If the relationship has changed, set it if ([self shouldSetValue:&valueForRelationship atKeyPath:relationshipMapping.destinationKeyPath]) { + if (! [self applyReplaceAssignmentPolicyForRelationshipMapping:relationshipMapping]) { + return NO; + } if (! [self mapCoreDataToManyRelationshipValue:valueForRelationship withMapping:relationshipMapping]) { RKLogTrace(@"Mapped relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, valueForRelationship); [self.destinationObject setValue:valueForRelationship forKeyPath:relationshipMapping.destinationKeyPath]; @@ -533,7 +584,8 @@ - (BOOL)applyRelationshipMappings for (RKRelationshipMapping *relationshipMapping in [self relationshipMappings]) { if ([self isCancelled]) return NO; - id value = [self.sourceObject valueForKeyPath:relationshipMapping.sourceKeyPath]; + // 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]; // Track that we applied this mapping [mappingsApplied addObject:relationshipMapping]; diff --git a/Code/ObjectMapping/RKMappingOperationDataSource.h b/Code/ObjectMapping/RKMappingOperationDataSource.h index de2d426737..df9ed009f0 100644 --- a/Code/ObjectMapping/RKMappingOperationDataSource.h +++ b/Code/ObjectMapping/RKMappingOperationDataSource.h @@ -20,7 +20,7 @@ #import -@class RKObjectMapping, RKMappingOperation; +@class RKObjectMapping, RKMappingOperation, RKRelationshipMapping; /** An object that adopts the `RKMappingOperationDataSource` protocol is responsible for the retrieval or creation of target objects within an `RKMapperOperation` or `RKMappingOperation`. A data source is responsible for meeting the requirements of the underlying data store implementation and must return a key-value coding compliant object instance that can be used as the target object of a mapping operation. It is also responsible for commiting any changes necessary to the underlying data store once a mapping operation has completed its work. @@ -56,4 +56,14 @@ */ - (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation error:(NSError **)error; +/** + Tells the data source to delete the existing value for a relationship that has been mapped with an assignment policy of `RKReplaceAssignmentPolicy`. + + @param mappingOperation The mapping operation that is executing. + @param relationshipMapping The relationship mapping for which the existing value is being replaced. + @param error A pointer to an error to be set in the event that the deletion operation could not be completed. + @return A Boolean value indicating if the existing objects for the relationship were successfully deleted. + */ +- (BOOL)mappingOperation:(RKMappingOperation *)mappingOperation deleteExistingValueOfRelationshipWithMapping:(RKRelationshipMapping *)relationshipMapping error:(NSError **)error; + @end diff --git a/Code/ObjectMapping/RKMappingResult.h b/Code/ObjectMapping/RKMappingResult.h index de53e4db47..b4d7bce9a8 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. */ -- (id)initWithDictionary:(NSDictionary *)dictionary; +- (instancetype)initWithDictionary:(NSDictionary *)dictionary; ///---------------------------------------- /// @name Retrieving Result Representations diff --git a/Code/ObjectMapping/RKObjectMapping.h b/Code/ObjectMapping/RKObjectMapping.h index bb20f9d497..e6f0cefe49 100644 --- a/Code/ObjectMapping/RKObjectMapping.h +++ b/Code/ObjectMapping/RKObjectMapping.h @@ -75,7 +75,7 @@ @param objectClass The class that the mapping targets. @return A new mapping object. */ -+ (id)mappingForClass:(Class)objectClass; ++ (instancetype)mappingForClass:(Class)objectClass; /** Initializes the receiver with a given object class. This is the designated initializer. @@ -83,7 +83,7 @@ @param objectClass The class that the mapping targets. Cannot be `nil`. @return The receiver, initialized with the given class. */ -- (id)initWithClass:(Class)objectClass; +- (instancetype)initWithClass:(Class)objectClass; /** Returns an object mapping with an `objectClass` of `NSMutableDictionary`. @@ -94,7 +94,7 @@ @see `RKObjectParameterization` @see `RKObjectManager` */ -+ (id)requestMapping; ++ (instancetype)requestMapping; ///--------------------------------- /// @name Managing Property Mappings @@ -294,11 +294,13 @@ @property (nonatomic, strong) NSFormatter *preferredDateFormatter; /** - Generates an inverse mapping for the rules specified within this object mapping. This can be used to - quickly generate a corresponding serialization mapping from a configured object mapping. The inverse - mapping will have the source and destination keyPaths swapped for all attribute and relationship mappings. + Generates an inverse mapping for the rules specified within this object mapping. + + This can be used to quickly generate a corresponding serialization mapping from a configured object mapping. The inverse mapping will have the source and destination keyPaths swapped for all attribute and relationship mappings. All mapping configuration and date formatters are copied from the parent to the inverse mapping. + + @return A new mapping that will map the inverse of the receiver. */ -- (RKObjectMapping *)inverseMapping; +- (instancetype)inverseMapping; ///--------------------------------------------------- /// @name Obtaining Information About the Target Class @@ -313,7 +315,6 @@ @return The class of the property. */ - (Class)classForProperty:(NSString *)propertyName; -// TODO: Can I eliminate this and just use classForKeyPath:???? /** Returns the class of the attribute or relationship property of the target `objectClass` at the given key path. diff --git a/Code/ObjectMapping/RKObjectMapping.m b/Code/ObjectMapping/RKObjectMapping.m index 2ecf4b56cf..00ced82c2a 100644 --- a/Code/ObjectMapping/RKObjectMapping.m +++ b/Code/ObjectMapping/RKObjectMapping.m @@ -18,6 +18,7 @@ // limitations under the License. // +#import #import "RKObjectMapping.h" #import "RKRelationshipMapping.h" #import "RKPropertyInspector.h" @@ -30,13 +31,71 @@ // Constants NSString * const RKObjectMappingNestingAttributeKeyName = @""; -static NSUInteger RKObjectMappingMaximumInverseMappingRecursionDepth = 100; // Private declaration NSDate *RKDateFromStringWithFormatters(NSString *dateString, NSArray *formatters); static RKSourceToDesinationKeyTransformationBlock defaultSourceToDestinationKeyTransformationBlock = nil; +@interface RKObjectMapping (Copying) +- (void)copyPropertiesFromMapping:(RKObjectMapping *)mapping; +@end + +@interface RKMappingInverter : NSObject +@property (nonatomic, strong) RKObjectMapping *mapping; +@property (nonatomic, strong) NSMutableDictionary *invertedMappings; + +- (id)initWithMapping:(RKObjectMapping *)mapping; +- (RKObjectMapping *)inverseMapping; +@end + +@implementation RKMappingInverter + +- (id)initWithMapping:(RKObjectMapping *)mapping +{ + self = [self init]; + if (self) { + self.mapping = mapping; + self.invertedMappings = [NSMutableDictionary dictionary]; + } + return self; +} + +- (RKObjectMapping *)invertMapping:(RKObjectMapping *)mapping +{ + // Use an NSValue to obtain a non-copied key into our inversed mappings dictionary + NSValue *dictionaryKey = [NSValue valueWithNonretainedObject:mapping]; + RKObjectMapping *inverseMapping = [self.invertedMappings objectForKey:dictionaryKey]; + if (inverseMapping) return inverseMapping; + + inverseMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + [self.invertedMappings setObject:inverseMapping forKey:dictionaryKey]; + [inverseMapping copyPropertiesFromMapping:mapping]; + + for (RKAttributeMapping *attributeMapping in mapping.attributeMappings) { + [inverseMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:attributeMapping.destinationKeyPath toKeyPath:attributeMapping.sourceKeyPath]]; + } + + for (RKRelationshipMapping *relationshipMapping in mapping.relationshipMappings) { + RKObjectMapping *mapping = (RKObjectMapping *) relationshipMapping.mapping; + if (! [mapping isKindOfClass:[RKObjectMapping class]]) { + RKLogWarning(@"Unable to generate inverse mapping for relationship '%@': %@ relationships cannot be inversed.", relationshipMapping.sourceKeyPath, NSStringFromClass([mapping class])); + continue; + } + RKMapping *inverseRelationshipMapping = [self invertMapping:mapping]; + if (inverseRelationshipMapping) [inverseMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relationshipMapping.destinationKeyPath toKeyPath:relationshipMapping.sourceKeyPath withMapping:inverseRelationshipMapping]]; + } + + return inverseMapping; +} + +- (RKObjectMapping *)inverseMapping +{ + return [self invertMapping:self.mapping]; +} + +@end + @interface RKPropertyMapping () @property (nonatomic, weak, readwrite) RKObjectMapping *objectMapping; @end @@ -51,12 +110,12 @@ @interface RKObjectMapping () @implementation RKObjectMapping -+ (id)mappingForClass:(Class)objectClass ++ (instancetype)mappingForClass:(Class)objectClass { return [[self alloc] initWithClass:objectClass]; } -+ (id)requestMapping ++ (instancetype)requestMapping { return [self mappingForClass:[NSMutableDictionary class]]; } @@ -77,21 +136,26 @@ - (id)initWithClass:(Class)objectClass return self; } +- (void)copyPropertiesFromMapping:(RKObjectMapping *)mapping +{ + self.setDefaultValueForMissingAttributes = mapping.setDefaultValueForMissingAttributes; + self.setNilForMissingRelationships = mapping.setNilForMissingRelationships; + self.forceCollectionMapping = mapping.forceCollectionMapping; + self.performKeyValueValidation = mapping.performKeyValueValidation; + self.dateFormatters = mapping.dateFormatters; + self.preferredDateFormatter = mapping.preferredDateFormatter; + self.sourceToDestinationKeyTransformationBlock = self.sourceToDestinationKeyTransformationBlock; +} + - (id)copyWithZone:(NSZone *)zone { RKObjectMapping *copy = [[[self class] allocWithZone:zone] init]; copy.objectClass = self.objectClass; - copy.setDefaultValueForMissingAttributes = self.setDefaultValueForMissingAttributes; - copy.setNilForMissingRelationships = self.setNilForMissingRelationships; - copy.forceCollectionMapping = self.forceCollectionMapping; - copy.performKeyValueValidation = self.performKeyValueValidation; - copy.dateFormatters = self.dateFormatters; - copy.preferredDateFormatter = self.preferredDateFormatter; + [copy copyPropertiesFromMapping:self]; copy.mutablePropertyMappings = [NSMutableArray new]; - copy.sourceToDestinationKeyTransformationBlock = self.sourceToDestinationKeyTransformationBlock; for (RKPropertyMapping *propertyMapping in self.propertyMappings) { - [copy addPropertyMapping:propertyMapping]; + [copy addPropertyMapping:[propertyMapping copy]]; } return copy; @@ -264,29 +328,10 @@ - (void)removePropertyMapping:(RKPropertyMapping *)attributeOrRelationshipMappin } } -- (RKObjectMapping *)inverseMappingAtDepth:(NSInteger)depth -{ - NSAssert(depth < RKObjectMappingMaximumInverseMappingRecursionDepth, @"Exceeded max recursion level in inverseMapping. This is likely due to a loop in the serialization graph. To break this loop, specify one-way relationships by setting serialize to NO in mapKeyPath:toRelationship:withObjectMapping:serialize:"); - RKObjectMapping *inverseMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; - for (RKAttributeMapping *attributeMapping in self.attributeMappings) { - [inverseMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:attributeMapping.destinationKeyPath toKeyPath:attributeMapping.sourceKeyPath]]; - } - - for (RKRelationshipMapping *relationshipMapping in self.relationshipMappings) { - RKMapping *mapping = relationshipMapping.mapping; - if (! [mapping isKindOfClass:[RKObjectMapping class]]) { - RKLogWarning(@"Unable to generate inverse mapping for relationship '%@': %@ relationships cannot be inversed.", relationshipMapping.sourceKeyPath, NSStringFromClass([mapping class])); - continue; - } - [inverseMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relationshipMapping.destinationKeyPath toKeyPath:relationshipMapping.sourceKeyPath withMapping:[(RKObjectMapping *)mapping inverseMappingAtDepth:depth+1]]]; - } - - return inverseMapping; -} - -- (RKObjectMapping *)inverseMapping +- (instancetype)inverseMapping { - return [self inverseMappingAtDepth:0]; + RKMappingInverter *mappingInverter = [[RKMappingInverter alloc] initWithMapping:self]; + return [mappingInverter inverseMapping]; } - (void)addAttributeMappingFromKeyOfRepresentationToAttribute:(NSString *)attributeName @@ -328,7 +373,7 @@ - (id)defaultValueForAttribute:(NSString *)attributeName - (Class)classForProperty:(NSString *)propertyName { - return [[RKPropertyInspector sharedInspector] classForPropertyNamed:propertyName ofClass:self.objectClass]; + return [[RKPropertyInspector sharedInspector] classForPropertyNamed:propertyName ofClass:self.objectClass isPrimitive:nil]; } - (Class)classForKeyPath:(NSString *)keyPath @@ -336,7 +381,7 @@ - (Class)classForKeyPath:(NSString *)keyPath NSArray *components = [keyPath componentsSeparatedByString:@"."]; Class propertyClass = self.objectClass; for (NSString *property in components) { - propertyClass = [[RKPropertyInspector sharedInspector] classForPropertyNamed:property ofClass:propertyClass]; + propertyClass = [[RKPropertyInspector sharedInspector] classForPropertyNamed:property ofClass:propertyClass isPrimitive:nil]; if (! propertyClass) break; } diff --git a/Code/ObjectMapping/RKPropertyInspector.h b/Code/ObjectMapping/RKPropertyInspector.h index 404b8924b3..f2f6dc8ede 100644 --- a/Code/ObjectMapping/RKPropertyInspector.h +++ b/Code/ObjectMapping/RKPropertyInspector.h @@ -22,12 +22,33 @@ @class NSEntityDescription; +///-------------------------------------------------- +/// @name Keys for the Property Inspection Dictionary +///-------------------------------------------------- + /** - The `RKPropertyInspector` class provides an interface for introspecting the properties and attributes of classes using the reflection capabilities of the Objective-C runtime. Once inspected, the properties and types are cached. + The name of the property + */ +extern NSString * const RKPropertyInspectionNameKey; + +/** + The class used for key-value coding access to the property. + + If the property is an object object type, then the class set for this key will be the type of the property. If the property is a primitive, then the class set for the key will be the boxed type used for KVC access to the property. For example, an `NSInteger` property is boxed to an `NSNumber` for KVC purposes. + */ +extern NSString * const RKPropertyInspectionKeyValueCodingClassKey; + +/** + A Boolean value that indicates if the property is a primitive (non-object) value. + */ +extern NSString * const RKPropertyInspectionIsPrimitiveKey; + +/** + The `RKPropertyInspector` class provides an interface for introspecting the properties and attributes of classes using the reflection capabilities of the Objective-C runtime. Once inspected, the properties inspection details are cached. */ @interface RKPropertyInspector : NSObject { @protected - NSCache *_propertyNamesToTypesCache; + NSCache *_inspectionCache; } ///----------------------------------------------- @@ -46,21 +67,22 @@ ///------------------------------------------------------ /** - Returns a dictionary of names and types for the properties of a given class. - - @param objectClass The class to retrieve the property name and types for. - @return A dictionary containing metadata about the properties of the given class, where the keys in the dictionary are the property names and the values are `Class` objects specifying the type of the property. + Returns a dictionary keyed by property name that includes the key-value coding class of the property and a Boolean indicating if the property is backed by a primitive (non-object) value. The dictionary for each property includes details about the key-value coding class representing the property and if the property is backed by a primitive type. + + @param objectClass The class to inspect the properties of. + @return A dictionary keyed by property name that includes details about all declared properties of the class. */ -- (NSDictionary *)propertyNamesAndTypesForClass:(Class)objectClass; +- (NSDictionary *)propertyInspectionForClass:(Class)objectClass; /** Returns the `Class` object specifying the type of the property with given name on a class. @param propertyName The name of the property to retrieve the type of. @param objectClass The class to retrieve the property from. + @param isPrimitive A pointer to a Boolean value to set indicating if the specified property is of a primitive (non-object) type. @return A `Class` object specifying the type of the requested property. */ -- (Class)classForPropertyNamed:(NSString *)propertyName ofClass:(Class)objectClass; +- (Class)classForPropertyNamed:(NSString *)propertyName ofClass:(Class)objectClass isPrimitive:(BOOL *)isPrimitive; @end @@ -74,6 +96,16 @@ Given a key path to a string property, this will return an `NSString`, etc. @param keyPath The key path to the property to retrieve the class of. + @param object The object to evaluate. @return The class of the property at the given key path. */ Class RKPropertyInspectorGetClassForPropertyAtKeyPathOfObject(NSString *keyPath, id object); + +/** + Returns a Boolean value indicating if the property at the specified key path for a given object is modeled by a primitive type. + + @param keyPath The key path to inspect the property of. + @param object The object to evaluate. + @return `YES` if the property is a primitive, else `NO`. + */ +BOOL RKPropertyInspectorIsPropertyAtKeyPathOfObjectPrimitive(NSString *keyPath, id object); diff --git a/Code/ObjectMapping/RKPropertyInspector.m b/Code/ObjectMapping/RKPropertyInspector.m index 3c5fcc0eff..3f559858cc 100644 --- a/Code/ObjectMapping/RKPropertyInspector.m +++ b/Code/ObjectMapping/RKPropertyInspector.m @@ -27,6 +27,10 @@ #undef RKLogComponent #define RKLogComponent RKlcl_cRestKitObjectMapping +NSString * const RKPropertyInspectionNameKey = @"name"; +NSString * const RKPropertyInspectionKeyValueCodingClassKey = @"keyValueCodingClass"; +NSString * const RKPropertyInspectionIsPrimitiveKey = @"isPrimitive"; + @implementation RKPropertyInspector + (RKPropertyInspector *)sharedInspector @@ -44,22 +48,21 @@ - (id)init { self = [super init]; if (self) { - _propertyNamesToTypesCache = [[NSCache alloc] init]; + _inspectionCache = [[NSCache alloc] init]; } return self; } -- (NSDictionary *)propertyNamesAndTypesForClass:(Class)theClass +- (NSDictionary *)propertyInspectionForClass:(Class)objectClass { - NSMutableDictionary *propertyNames = [_propertyNamesToTypesCache objectForKey:theClass]; - if (propertyNames) { - return propertyNames; - } - propertyNames = [NSMutableDictionary dictionary]; + NSMutableDictionary *inspection = [_inspectionCache objectForKey:objectClass]; + if (inspection) return inspection; + + inspection = [NSMutableDictionary dictionary]; //include superclass properties - Class currentClass = theClass; + Class currentClass = objectClass; while (currentClass != nil) { // Get the raw list of properties unsigned int outCount = 0; @@ -75,9 +78,20 @@ - (NSDictionary *)propertyNamesAndTypesForClass:(Class)theClass if (attr) { Class aClass = RKKeyValueCodingClassFromPropertyAttributes(attr); if (aClass) { - NSString *propNameObj = [[NSString alloc] initWithCString:propName encoding:NSUTF8StringEncoding]; - if (propNameObj) { - [propertyNames setObject:aClass forKey:propNameObj]; + NSString *propNameString = [[NSString alloc] initWithCString:propName encoding:NSUTF8StringEncoding]; + if (propNameString) { + BOOL isPrimitive = NO; + if (attr) { + const char *typeIdentifierLoc = strchr(attr, 'T'); + if (typeIdentifierLoc) { + isPrimitive = (typeIdentifierLoc[1] != '@'); + } + } + + NSDictionary *propertyInspection = @{ RKPropertyInspectionNameKey: propNameString, + RKPropertyInspectionKeyValueCodingClassKey: aClass, + RKPropertyInspectionIsPrimitiveKey: @(isPrimitive) }; + [inspection setObject:propertyInspection forKey:propNameString]; } } } @@ -88,32 +102,34 @@ - (NSDictionary *)propertyNamesAndTypesForClass:(Class)theClass currentClass = [currentClass superclass]; } - [_propertyNamesToTypesCache setObject:propertyNames forKey:theClass]; - RKLogDebug(@"Cached property names and types for Class '%@': %@", NSStringFromClass(theClass), propertyNames); - return propertyNames; + [_inspectionCache setObject:inspection forKey:objectClass]; + RKLogDebug(@"Cached property inspection for Class '%@': %@", NSStringFromClass(objectClass), inspection); + return inspection; } -- (Class)classForPropertyNamed:(NSString *)propertyName ofClass:(Class)objectClass +- (Class)classForPropertyNamed:(NSString *)propertyName ofClass:(Class)objectClass isPrimitive:(BOOL *)isPrimitive { - NSDictionary *dictionary = [self propertyNamesAndTypesForClass:objectClass]; - return [dictionary objectForKey:propertyName]; + NSDictionary *classInspection = [self propertyInspectionForClass:objectClass]; + NSDictionary *propertyInspection = [classInspection objectForKey:propertyName]; + if (isPrimitive) *isPrimitive = [[propertyInspection objectForKey:RKPropertyInspectionIsPrimitiveKey] boolValue]; + return [propertyInspection objectForKey:RKPropertyInspectionKeyValueCodingClassKey]; } @end @interface NSObject (RKPropertyInspection) -- (Class)rk_classForPropertyAtKeyPath:(NSString *)keyPath; +- (Class)rk_classForPropertyAtKeyPath:(NSString *)keyPath isPrimitive:(BOOL *)isPrimitive; @end @implementation NSObject (RKPropertyInspection) -- (Class)rk_classForPropertyAtKeyPath:(NSString *)keyPath +- (Class)rk_classForPropertyAtKeyPath:(NSString *)keyPath isPrimitive:(BOOL *)isPrimitive { NSArray *components = [keyPath componentsSeparatedByString:@"."]; Class propertyClass = [self class]; for (NSString *property in components) { - propertyClass = [[RKPropertyInspector sharedInspector] classForPropertyNamed:property ofClass:propertyClass]; + propertyClass = [[RKPropertyInspector sharedInspector] classForPropertyNamed:property ofClass:propertyClass isPrimitive:isPrimitive]; if (! propertyClass) break; } @@ -124,5 +140,12 @@ - (Class)rk_classForPropertyAtKeyPath:(NSString *)keyPath Class RKPropertyInspectorGetClassForPropertyAtKeyPathOfObject(NSString *keyPath, id object) { - return [object rk_classForPropertyAtKeyPath:keyPath]; + return [object rk_classForPropertyAtKeyPath:keyPath isPrimitive:nil]; +} + +BOOL RKPropertyInspectorIsPropertyAtKeyPathOfObjectPrimitive(NSString *keyPath, id object) +{ + BOOL isPrimitive = NO; + [object rk_classForPropertyAtKeyPath:keyPath isPrimitive:&isPrimitive]; + return isPrimitive; } diff --git a/Code/ObjectMapping/RKRelationshipMapping.h b/Code/ObjectMapping/RKRelationshipMapping.h index e40db308b3..73ff8b975d 100644 --- a/Code/ObjectMapping/RKRelationshipMapping.h +++ b/Code/ObjectMapping/RKRelationshipMapping.h @@ -22,10 +22,31 @@ @class RKMapping; +typedef enum { + RKSetAssignmentPolicy, // Set the relationship to the new value and leave the existing objects alone, breaking the relationship to existing objects at the destination. This is the default policy for `RKRelationshipMapping`. + RKReplaceAssignmentPolicy, // Set the relationship to the new value and destroy the previous value, replacing the existing objects at the destination of the relationship. + RKUnionAssignmentPolicy, // Set the relationship to the union of the existing value and the new value being assigned. Only applicable for to-many relationships. +} RKAssignmentPolicy; + /** The `RKRelationshipMapping` class is used to describe relationships of a class in an `RKObjectMapping` or an entity in an `RKEntityMapping` object. `RKRelationshipMapping` extends `RKPropertyMapping` to describe features specific to relationships, including the `RKMapping` object describing how to map the destination object. + + Relationship mappings are described in terms of a source key path, which identifies a key in the parent object representation under which the data for the relationship is nested, and a destination key path, which specifies the key path at which the mapped object is to be assigned on the parent entity. The key-paths of the property mappings of the `RKMapping` object in the relationship mapping are evaluated against the nested object representationship at the source key path. + + ## Mapping a Non-nested Relationship from the Parent Representation + + It can often be desirable to map data for a relationship directly from the parent object representation, rather than under a nested key path. When a relationship mapping is constructed with a `nil` value for the source key path, then the `RKMapping` object is evaluated against the parent representation. + + ## Assignment Policy + + When mapping a relationship, the typical desired behavior is to set the destination of the relationship to the newly mapped values from the object representation being processed. There are times in which it is desirable to use different assignment behaviors. The way in which the relationship is assigned can be controlled by the assignmentPolicy property. There are currently three distinct assignment policies available: + + 1. `RKSetAssignmentPolicy` - Instructs the mapper to assign the new destination value to the relationship directly. No further action is taken and the relationship to the old objects is broken. This is the default assignment policy. + 1. `RKReplaceAssignmentPolicy` - Instructs the mapper to assign the new destination value to the relationship and delete any existing object or objects at the destination. The deletion behavior is contextual based on the type of objects being mapped (i.e. Core Data vs NSObject) and is delegated to the mapping operation data source. + 1. `RKUnionAssignmentPolicy` - Instructs the mapper to build a new value for the relationship by unioning the existing value with the new value and set the combined value to the relationship. The union assignment policy is only appropriate for use with a to-many relationship. + */ @interface RKRelationshipMapping : RKPropertyMapping @@ -38,11 +59,11 @@ The mapping may describe a to-one or a to-many relationship. The appropriate handling of the source representation is deferred until run-time and is determined by performing reflection on the data retrieved from the source object representation by sending a `valueForKeyPath:` message where the key path is the value given in `sourceKeyPath`. If an `NSArray`, `NSSet` or `NSOrderedSet` object is returned, the related object representation is processed as a to-many collection. Otherwise the representation is considered to be a to-one. - @param sourceKeyPath A key path from which to retrieve data in the source object representation that is to be mapped as a relationship. + @param sourceKeyPath A key path from which to retrieve data in the source object representation that is to be mapped as a relationship. If `nil`, then the mapping is performed directly against the parent object representation. @param destinationKeyPath The key path on the destination object to set the object mapped results. @param mapping A mapping object describing how to map the data retrieved from `sourceKeyPath` that is to be set on `destinationKeyPath`. */ -+ (RKRelationshipMapping *)relationshipMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath withMapping:(RKMapping *)mapping; ++ (instancetype)relationshipMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath withMapping:(RKMapping *)mapping; ///---------------------------------------- /// @name Accessing the Destination Mapping @@ -53,4 +74,17 @@ */ @property (nonatomic, strong, readonly) RKMapping *mapping; +///---------------------------------------- +/// @name Configuring the Assignment Policy +///---------------------------------------- + +/** + The assignment policy to use when applying the relationship mapping. + + The assignment policy determines how a relationship is set when there are existing objects at the destination of the relationship. The existing values can be disconnected from the parent and left in the graph (`RKSetAssignmentPolicy`), deleted and replaced by the new value (`RKReplaceAssignmentPolicy`), or the new value can be unioned with the existing objects to create a new combined value (`RKUnionAssignmentPolicy`). + + **Default**: `RKSetAssignmentPolicy` + */ +@property (nonatomic, assign) RKAssignmentPolicy assignmentPolicy; + @end diff --git a/Code/ObjectMapping/RKRelationshipMapping.m b/Code/ObjectMapping/RKRelationshipMapping.m index 254eda902f..6d69512d7d 100644 --- a/Code/ObjectMapping/RKRelationshipMapping.m +++ b/Code/ObjectMapping/RKRelationshipMapping.m @@ -29,7 +29,7 @@ @interface RKRelationshipMapping () @implementation RKRelationshipMapping -+ (RKRelationshipMapping *)relationshipMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath withMapping:(RKMapping *)mapping ++ (instancetype)relationshipMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath withMapping:(RKMapping *)mapping { RKRelationshipMapping *relationshipMapping = [self new]; relationshipMapping.sourceKeyPath = sourceKeyPath; @@ -38,10 +38,20 @@ + (RKRelationshipMapping *)relationshipMappingFromKeyPath:(NSString *)sourceKeyP return relationshipMapping; } +- (id)init +{ + self = [super init]; + if (self) { + self.assignmentPolicy = RKSetAssignmentPolicy; + } + return self; +} + - (id)copyWithZone:(NSZone *)zone { RKRelationshipMapping *copy = [super copyWithZone:zone]; copy.mapping = self.mapping; + copy.assignmentPolicy = self.assignmentPolicy; return copy; } diff --git a/Code/Support.h b/Code/Support.h index fdc8522687..5fe1d7895a 100644 --- a/Code/Support.h +++ b/Code/Support.h @@ -20,6 +20,7 @@ // Load shared support code #import "RKErrors.h" +#import "RKErrorMessage.h" #import "RKMIMETypes.h" #import "RKLog.h" #import "RKPathMatcher.h" diff --git a/Code/Support/RKDictionaryUtilities.m b/Code/Support/RKDictionaryUtilities.m index 4a31e2e2ff..5f2a31a9fb 100644 --- a/Code/Support/RKDictionaryUtilities.m +++ b/Code/Support/RKDictionaryUtilities.m @@ -13,12 +13,12 @@ NSMutableDictionary *mergedDictionary = [dict1 mutableCopy]; [dict2 enumerateKeysAndObjectsUsingBlock:^(id key2, id obj2, BOOL *stop) { - id obj1 = dict1[key2]; + id obj1 = [dict1 valueForKey:key2]; if ([obj1 isKindOfClass:[NSDictionary class]] && [obj2 isKindOfClass:[NSDictionary class]]) { NSDictionary *mergedSubdict = RKDictionaryByMergingDictionaryWithDictionary(obj1, obj2); - mergedDictionary[key2] = mergedSubdict; + [mergedDictionary setValue:mergedSubdict forKey:key2]; } else { - mergedDictionary[key2] = obj2; + [mergedDictionary setValue:obj2 forKey:key2]; } }]; diff --git a/Code/Support/RKDotNetDateFormatter.h b/Code/Support/RKDotNetDateFormatter.h index 9eaaded4c8..03bc2b4b32 100644 --- a/Code/Support/RKDotNetDateFormatter.h +++ b/Code/Support/RKDotNetDateFormatter.h @@ -37,7 +37,7 @@ @return An autoreleased `RKDotNetDateFormatter` object @see dotNetDateFormatter */ -+ (RKDotNetDateFormatter *)dotNetDateFormatterWithTimeZone:(NSTimeZone *)timeZone; ++ (instancetype)dotNetDateFormatterWithTimeZone:(NSTimeZone *)timeZone; /** Returns an `NSDate` object from an ASP.NET style date string respresentation, as seen in JSON. diff --git a/Code/Support/RKDotNetDateFormatter.m b/Code/Support/RKDotNetDateFormatter.m index 2a98a4e033..96ce68e9ff 100644 --- a/Code/Support/RKDotNetDateFormatter.m +++ b/Code/Support/RKDotNetDateFormatter.m @@ -33,9 +33,9 @@ - (NSString *)millisecondsFromString:(NSString *)string; @implementation RKDotNetDateFormatter -+ (RKDotNetDateFormatter *)dotNetDateFormatterWithTimeZone:(NSTimeZone *)newTimeZone ++ (instancetype)dotNetDateFormatterWithTimeZone:(NSTimeZone *)newTimeZone { - RKDotNetDateFormatter *formatter = [[RKDotNetDateFormatter alloc] init]; + RKDotNetDateFormatter *formatter = [self new]; if (newTimeZone) formatter.timeZone = newTimeZone; return formatter; } diff --git a/Code/Support/RKMIMETypeSerialization.m b/Code/Support/RKMIMETypeSerialization.m index 1d8f91a3ba..fc2cf90b0c 100644 --- a/Code/Support/RKMIMETypeSerialization.m +++ b/Code/Support/RKMIMETypeSerialization.m @@ -23,6 +23,7 @@ #import "RKSerialization.h" #import "RKLog.h" #import "RKURLEncodedSerialization.h" +#import "RKNSJSONSerialization.h" // Define logging component #undef RKLogComponent @@ -101,29 +102,13 @@ - (id)init } - (void)addRegistrationsForKnownSerializations -{ - Class serializationClass = nil; - +{ // URL Encoded [self.registrations addObject:[[RKMIMETypeSerializationRegistration alloc] initWithMIMEType:RKMIMETypeFormURLEncoded serializationClass:[RKURLEncodedSerialization class]]]; // JSON - NSArray *JSONSerializationClassNames = @[ @"RKNSJSONSerialization", @"RKJSONKitSerialization" ]; - for (NSString *serializationClassName in JSONSerializationClassNames) { - serializationClass = NSClassFromString(serializationClassName); - if (serializationClass) { - RKLogInfo(@"JSON Serialization class '%@' detected: Registering for MIME Type '%@", serializationClassName, RKMIMETypeJSON); - [self.registrations addObject:[[RKMIMETypeSerializationRegistration alloc] initWithMIMEType:RKMIMETypeJSON - serializationClass:serializationClass]]; - } - } - - // XML -// parserClass = NSClassFromString(@"RKXMLParserXMLReader"); -// if (parserClass) { -// [self setParserClass:parserClass forMIMEType:RKMIMETypeXML]; -// [self setParserClass:parserClass forMIMEType:RKMIMETypeTextXML]; -// } + [self.registrations addObject:[[RKMIMETypeSerializationRegistration alloc] initWithMIMEType:RKMIMETypeJSON + serializationClass:[RKNSJSONSerialization class]]]; } #pragma mark - Public diff --git a/Code/Support/RKPathMatcher.h b/Code/Support/RKPathMatcher.h index 72fef5af92..1eaa8aaf99 100644 --- a/Code/Support/RKPathMatcher.h +++ b/Code/Support/RKPathMatcher.h @@ -40,7 +40,7 @@ @param pathString The string to evaluate and parse, such as `/districts/tx/upper/?apikey=GC5512354` @return An instantiated `RKPathMatcher` without an established pattern. */ -+ (RKPathMatcher *)pathMatcherWithPath:(NSString *)pathString; ++ (instancetype)pathMatcherWithPath:(NSString *)pathString; /** Determines if the path string matches the provided pattern, and yields a dictionary with the resulting matched key/value pairs. Use of this method should be preceded by `pathMatcherWithPath:` Pattern strings should include encoded parameter keys, delimited by a single colon at the beginning of the key name. @@ -74,7 +74,7 @@ @param patternString The pattern to use for evaluating, such as `/:entityName/:stateID/:chamber/` @return An instantiated `RKPathMatcher` with an established pattern. */ -+ (RKPathMatcher *)pathMatcherWithPattern:(NSString *)patternString; ++ (instancetype)pathMatcherWithPattern:(NSString *)patternString; /** Determines if the given path string matches a pattern, and yields a dictionary with the resulting matched key/value pairs. Use of this method should be preceded by `pathMatcherWithPattern:`. diff --git a/Code/Support/RKPathMatcher.m b/Code/Support/RKPathMatcher.m index 85d94276f3..c3357cd43c 100644 --- a/Code/Support/RKPathMatcher.m +++ b/Code/Support/RKPathMatcher.m @@ -59,18 +59,18 @@ - (id)copyWithZone:(NSZone *)zone return copy; } -+ (RKPathMatcher *)pathMatcherWithPattern:(NSString *)patternString ++ (instancetype)pathMatcherWithPattern:(NSString *)patternString { NSAssert(patternString != NULL, @"Pattern string must not be empty in order to perform pattern matching."); - RKPathMatcher *matcher = [[RKPathMatcher alloc] init]; + RKPathMatcher *matcher = [self new]; matcher.socPattern = [SOCPattern patternWithString:patternString]; matcher.patternString = patternString; return matcher; } -+ (RKPathMatcher *)pathMatcherWithPath:(NSString *)pathString ++ (instancetype)pathMatcherWithPath:(NSString *)pathString { - RKPathMatcher *matcher = [[RKPathMatcher alloc] init]; + RKPathMatcher *matcher = [self new]; matcher.sourcePath = pathString; matcher.rootPath = pathString; return matcher; diff --git a/Code/Testing/RKConnectionTestExpectation.h b/Code/Testing/RKConnectionTestExpectation.h index e8f9910c6d..5ad9a60a7e 100644 --- a/Code/Testing/RKConnectionTestExpectation.h +++ b/Code/Testing/RKConnectionTestExpectation.h @@ -40,7 +40,7 @@ @param value The value that is expected to be set for the relationship when the connection is established. @return A newly constructed connection expectation, initialized with the given relationship name, attributes dictionary, and expected value. */ -+ (id)expectationWithRelationshipName:(NSString *)relationshipName attributes:(NSDictionary *)attributes value:(id)value; ++ (instancetype)expectationWithRelationshipName:(NSString *)relationshipName attributes:(NSDictionary *)attributes value:(id)value; /** Initializes the receiver with the given relationship name, attributes dictionary, and value. @@ -50,7 +50,7 @@ @param value The value that is expected to be set for the relationship when the connection is established. @return The receiver, initialized with the given relationship name, attributes dictionary, and expected value. */ -- (id)initWithRelationshipName:(NSString *)relationshipName attributes:(NSDictionary *)attributes value:(id)value; +- (instancetype)initWithRelationshipName:(NSString *)relationshipName attributes:(NSDictionary *)attributes value:(id)value; ///------------------------------------ /// @name Accessing Expectation Details diff --git a/Code/Testing/RKConnectionTestExpectation.m b/Code/Testing/RKConnectionTestExpectation.m index ff4250ea6e..4f2a3af9f2 100644 --- a/Code/Testing/RKConnectionTestExpectation.m +++ b/Code/Testing/RKConnectionTestExpectation.m @@ -30,12 +30,12 @@ @interface RKConnectionTestExpectation () @implementation RKConnectionTestExpectation -+ (id)expectationWithRelationshipName:(NSString *)relationshipName attributes:(NSDictionary *)attributes value:(id)value ++ (instancetype)expectationWithRelationshipName:(NSString *)relationshipName attributes:(NSDictionary *)attributes value:(id)value { return [[self alloc] initWithRelationshipName:relationshipName attributes:attributes value:value]; } -- (id)initWithRelationshipName:(NSString *)relationshipName attributes:(NSDictionary *)attributes value:(id)value +- (instancetype)initWithRelationshipName:(NSString *)relationshipName attributes:(NSDictionary *)attributes value:(id)value { NSParameterAssert(relationshipName); NSAssert(value == nil || diff --git a/Code/Testing/RKMappingTest.h b/Code/Testing/RKMappingTest.h index d92d4d30fa..9c6d0f0df1 100644 --- a/Code/Testing/RKMappingTest.h +++ b/Code/Testing/RKMappingTest.h @@ -95,7 +95,7 @@ extern NSString * const RKMappingTestExpectationErrorKey; @param destinationObject The destionation object being to. @return A new mapping test object for a mapping, a source object and a destination object. */ -+ (RKMappingTest *)testForMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject; ++ (instancetype)testForMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject; /** Initializes the receiver with a given object mapping, source object, and destination object. @@ -105,7 +105,7 @@ extern NSString * const RKMappingTestExpectationErrorKey; @param destinationObject The destionation object being to. @return The receiver, initialized with mapping, sourceObject and destinationObject. */ -- (id)initWithMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject; +- (instancetype)initWithMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject; ///---------------------------- /// @name Managing Expectations diff --git a/Code/Testing/RKMappingTest.m b/Code/Testing/RKMappingTest.m index 06007e0b03..a6464bcbef 100644 --- a/Code/Testing/RKMappingTest.m +++ b/Code/Testing/RKMappingTest.m @@ -130,7 +130,7 @@ - (void)verifyExpectation:(RKPropertyMappingTestExpectation *)expectation; @implementation RKMappingTest -+ (RKMappingTest *)testForMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject ++ (instancetype)testForMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject { return [[self alloc] initWithMapping:mapping sourceObject:sourceObject destinationObject:destinationObject]; } diff --git a/Code/Testing/RKPropertyMappingTestExpectation.h b/Code/Testing/RKPropertyMappingTestExpectation.h index f7962d65fa..58b0f8567e 100644 --- a/Code/Testing/RKPropertyMappingTestExpectation.h +++ b/Code/Testing/RKPropertyMappingTestExpectation.h @@ -48,7 +48,7 @@ typedef BOOL (^RKMappingTestExpectationEvaluationBlock)(RKPropertyMappingTestExp @param destinationKeyPath A key path on the destination object that should be mapped onto. @return An expectation specifying that sourceKeyPath should be mapped to destinationKeyPath. */ -+ (RKPropertyMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath; ++ (instancetype)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath; /** Creates and returns a new expectation specifying that a key path in a source object should be mapped to another key path on a destination object with a given value. @@ -58,7 +58,7 @@ typedef BOOL (^RKMappingTestExpectationEvaluationBlock)(RKPropertyMappingTestExp @param value The value that is expected to be assigned to the destination object at destinationKeyPath. @return An expectation specifying that sourceKeyPath should be mapped to destinationKeyPath with value. */ -+ (RKPropertyMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath value:(id)value; ++ (instancetype)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath value:(id)value; /** Creates and returns a new expectation specifying that a key path in a source object should be mapped to another key path on a destinaton object and that the attribute mapping and value should evaluate to true with a given block. @@ -68,7 +68,7 @@ typedef BOOL (^RKMappingTestExpectationEvaluationBlock)(RKPropertyMappingTestExp @param evaluationBlock A block with which to evaluate the success of the mapping. @return An expectation specifying that sourceKeyPath should be mapped to destinationKeyPath with value. */ -+ (RKPropertyMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath evaluationBlock:(RKMappingTestExpectationEvaluationBlock)evaluationBlock; ++ (instancetype)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath evaluationBlock:(RKMappingTestExpectationEvaluationBlock)evaluationBlock; /** Creates and returns a new expectation specifying that a key path in a source object should be mapped to another key path on a destinaton object using a specific object mapping for the relationship. @@ -78,7 +78,7 @@ typedef BOOL (^RKMappingTestExpectationEvaluationBlock)(RKPropertyMappingTestExp @param mapping An object mapping that is expected to be used for mapping the nested relationship. @return An expectation specifying that sourceKeyPath should be mapped to destinationKeyPath using a specific object mapping. */ -+ (RKPropertyMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath mapping:(RKMapping *)mapping; ++ (instancetype)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath mapping:(RKMapping *)mapping; ///------------------------- /// @name Expectation Values diff --git a/Code/Testing/RKPropertyMappingTestExpectation.m b/Code/Testing/RKPropertyMappingTestExpectation.m index 6b016f58dc..8023f57426 100644 --- a/Code/Testing/RKPropertyMappingTestExpectation.m +++ b/Code/Testing/RKPropertyMappingTestExpectation.m @@ -31,7 +31,7 @@ @interface RKPropertyMappingTestExpectation () @implementation RKPropertyMappingTestExpectation -+ (RKPropertyMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath ++ (instancetype)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath { RKPropertyMappingTestExpectation *expectation = [self new]; expectation.sourceKeyPath = sourceKeyPath; @@ -40,7 +40,7 @@ + (RKPropertyMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)s return expectation; } -+ (RKPropertyMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath value:(id)value ++ (instancetype)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath value:(id)value { RKPropertyMappingTestExpectation *expectation = [self new]; expectation.sourceKeyPath = sourceKeyPath; @@ -50,7 +50,7 @@ + (RKPropertyMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)s return expectation; } -+ (RKPropertyMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath evaluationBlock:(RKMappingTestExpectationEvaluationBlock)evaluationBlock ++ (instancetype)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath evaluationBlock:(RKMappingTestExpectationEvaluationBlock)evaluationBlock { RKPropertyMappingTestExpectation *expectation = [self new]; expectation.sourceKeyPath = sourceKeyPath; @@ -60,7 +60,7 @@ + (RKPropertyMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)s return expectation; } -+ (RKPropertyMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath mapping:(RKMapping *)mapping ++ (instancetype)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath mapping:(RKMapping *)mapping { RKPropertyMappingTestExpectation *expectation = [self new]; expectation.sourceKeyPath = sourceKeyPath; diff --git a/README.md b/README.md index c365e03cfe..54da48540f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RestKit -RestKit is a modern Objective-C framework for implementing RESTful web services clients on iOS and Mac OS X. It provides a powerful [object mapping]() engine that seamlessly integrates with [Core Data](http://developer.apple.com/library/mac/#documentation/cocoa/Conceptual/CoreData/cdProgrammingGuide.html) and a simple set of networking primitives for mapping HTTP requests and responses built on top of [AFNetworking](https://github.com/AFNetworking/AFNetworking). It has an elegant, carefully designed set of APIs that make accessing and modeling RESTful resources feel almost magical. For example, here's how to access the Twitter public timeline and turn the JSON contents into an array of Tweet objects: +RestKit is a modern Objective-C framework for implementing RESTful web services clients on iOS and Mac OS X. It provides a powerful [object mapping](https://github.com/RestKit/RestKit/wiki/Object-mapping) engine that seamlessly integrates with [Core Data](http://developer.apple.com/library/mac/#documentation/cocoa/Conceptual/CoreData/cdProgrammingGuide.html) and a simple set of networking primitives for mapping HTTP requests and responses built on top of [AFNetworking](https://github.com/AFNetworking/AFNetworking). It has an elegant, carefully designed set of APIs that make accessing and modeling RESTful resources feel almost magical. For example, here's how to access the Twitter public timeline and turn the JSON contents into an array of Tweet objects: ``` objective-c @interface Tweet : NSObject @@ -29,11 +29,11 @@ RKObjectRequestOperation *operation = [[RKObjectRequestOperation alloc] initWith ## Getting Started - [Download RestKit](https://github.com/RestKit/RestKit/downloads) and play with the [examples](https://github.com/RestKit/RestKit/tree/development/Examples) for iPhone and Mac OS X -- First time with RestKit? Read the ["Overview"](#Overview) section below and then check out the ["Getting Acquainted with RestKit"]() tutorial and [Object Mapping Reference](https://github.com/RestKit/RestKit/wiki/Object-mapping) documents in the wiki to jump right in. -- Upgrading from RestKit 0.9.x or 0.10.x? Read the ["Upgrading to RestKit 0.20.x"]() guide in the wiki -- Adding RestKit to an existing [AFNetworking]() application? Read the [AFNetworking Integration]() document to learn details about how the frameworks fit together. -- Review the [source code API documentation](http://restkit.org/api/0.20.0) for a detailed look at the classes and API's in RestKit -- Still need some help? Get support from [Stack Overflow](), the [RestKit mailing list](http://groups.google.com/group/restkit) or ping us on [Twitter](http://twitter.com/RestKit) +- First time with RestKit? Read the ["Overview"](#Overview) section below and then check out the ["Getting Acquainted with RestKit"](https://github.com/RestKit/RestKit/wiki/Getting-Acquainted-with-RestKit) tutorial and [Object Mapping Reference](https://github.com/RestKit/RestKit/wiki/Object-mapping) documents in the wiki to jump right in. +- Upgrading from RestKit 0.9.x or 0.10.x? Read the ["Upgrading to RestKit 0.20.x"](https://github.com/RestKit/RestKit/wiki/Upgrading-from-v0.10.x-to-v0.20.0) guide in the wiki +- Adding RestKit to an existing [AFNetworking](http://afnetworking.org) application? Read the [AFNetworking Integration](https://github.com/RestKit/RestKit/wiki/AFNetworking-Integration) document to learn details about how the frameworks fit together. +- Review the [source code API documentation](http://restkit.org/api/latest) for a detailed look at the classes and API's in RestKit. A great place to start is [RKObjectManager](http://restkit.org/api/latest/Classes/RKObjectManager.html). +- Still need some help? Get support from [Stack Overflow](http://stackoverflow.com/questions/tagged/restkit), the [RestKit mailing list](http://groups.google.com/group/restkit) or ping us on [Twitter](http://twitter.com/RestKit) ## Overview @@ -56,52 +56,52 @@ RestKit is broken into several modules that cleanly separate the mapping engine - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
Object Mapping
RKObjectMappingRKObjectMapping Encapsulates configuration for transforming object representations as expressed by key-value coding keypaths.
RKAttributeMappingRKAttributeMapping Specifies a desired transformation between attributes within an object or entity mapping in terms of a source and destination key path.
RKRelationshipMappingRKRelationshipMapping Specifies a desired mapping of a nested to-one or to-many child objects in in terms of a source and destination key path and an RKObjectMapping with which to map the attributes of the child object.
RKDynamicMappingRKDynamicMapping Specifies a flexible mapping in which the decision about which RKObjectMapping is to be used to process a given document is deferred to run time.
RKObjectMapperRKObjectMapper Provides an interface for mapping a parsed document into a set of local domain objects.
RKObjectMappingOperationRKObjectMappingOperation An NSOperation that performs a mapping between object representations using an RKObjectMapping.
Networking
RKRequestDescriptorRKRequestDescriptor Describes a request that can be sent from the application to a remote web application for a given object type.
RKResponseDescriptorRKResponseDescriptor Describes an object mappable response that may be returned from a remote web application in terms of an object mapping, a key path, a SOCKit pattern for matching the URL, and a set of status codes that define the circumstances in which the mapping is appropriate for a given response.
RKObjectParameterizationRKObjectParameterization Performs mapping of a given object into an NSDictionary represenation suitable for use as the parameters of an HTTP request.
RKObjectRequestOperationRKObjectRequestOperation An NSOperation that sends an HTTP request and performs object mapping on the parsed response body using the configurations expressed in a set of RKResponseDescriptor objects.
RKResponseMapperOperationRKResponseMapperOperation An NSOperation that provides support for object mapping an NSHTTPURLResponse using a set of RKResponseDescriptor objects.
RKObjectManagerRKObjectManager Captures the common patterns for communicating with a RESTful web application over HTTP using object mapping including:
  • Centralizing RKRequestDescriptor and RKResponseDescriptor configurations
  • @@ -113,32 +113,32 @@ RestKit is broken into several modules that cleanly separate the mapping engine
RKRouterRKRouter Generates NSURL objects from a base URL and a set of RKRoute objects describing relative paths used by the application.
RKRouteRKRoute Describes a single relative path for a given object type and HTTP method, the relationship of an object, or a symbolic name.
Core Data
RKManagedObjectStoreRKManagedObjectStore Encapsulates Core Data configuration including an NSManagedObjectModel, a NSPersistentStoreCoordinator, and a pair of NSManagedObjectContext objects.
RKEntityMappingRKEntityMapping Models a mapping for transforming an object representation into a NSManagedObject instance for a given NSEntityDescription.
RKConnectionDescriptionRKConnectionDescription Describes a mapping for establishing a relationship between Core Data entities using foreign key attributes.
RKManagedObjectRequestOperationRKManagedObjectRequestOperation An NSOperation subclass that sends an HTTP request and performs object mapping on the parsed response body to create NSManagedObject instances, establishes relationships between objects using RKConnectionDescription objects, and cleans up orphaned objects that no longer exist in the remote backend system.
RKManagedObjectImporterRKManagedObjectImporter Provides support for bulk mapping of managed objects using RKEntityMapping objects for two use cases:
  1. Bulk importing of parsed documents into an NSPersistentStore.
  2. @@ -148,24 +148,24 @@ RestKit is broken into several modules that cleanly separate the mapping engine
Search
RKSearchIndexerRKSearchIndexer Provides support for generating a full-text searchable index within Core Data for string attributes of entities within an application.
RKSearchPredicateRKSearchPredicate Generates an NSCompoundPredicate given a string of text that will search an index built with an RKSearchIndexer across any indexed entity.
Testing
RKMappingTestRKMappingTest Provides support for unit testing object mapping configurations given a parsed document and an object or entity mapping. Expectations are configured in terms of expected key path mappings and/or expected transformation results.
RKTestFixtureRKTestFixture Provides an interface for easily generating test fixture data for unit testing.
RKTestFactoryRKTestFactory Provides support for creating objects for use in testing.
@@ -230,9 +230,10 @@ operation.managedObjectCache = managedObjectStore.managedObjectCache; // GET /articles/error.json returns a 422 (Unprocessable Entity) // JSON looks like {"errors": "Some Error Has Occurred"} +// 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:@"message"]]; +[errorMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:[NSNull null] toKeyPath:@"errorMessage"]]; NSIndexSet *statusCodes = RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError); // Any response in the 4xx status code range with an "errors" key path uses this mapping @@ -241,6 +242,7 @@ RKResponseDescriptor *errorDescriptor = [RKResponseDescriptor responseDescriptor NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://restkit.org/articles/error.json"]]; RKObjectRequestOperation *operation = [[RKObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[errorDescriptor]]; [operation setCompletionBlockWithSuccess:nil failure:^(RKObjectRequestOperation *operation, NSError *error) { + // The `description` method of the class the error is mapped to is used to construct the value of the localizedDescription NSLog(@"Loaded this error: %@", [error localizedDescription]); }]; ``` @@ -504,7 +506,7 @@ If you are including the RestKit sources directly into a project that does not y ### Serialization Formats -RestKit provides a pluggable interface for handling arbitrary serialization formats via the [`RKSerialization`](http://restkit.org/api/0.20.0/Classes/RKSerialization.html) protocol and the [`RKMIMETypeSerialization`](http://restkit.org/api/0.20.0/Classes/RKMIMETypeSerialization.html) class. Out of the box, RestKit supports handling the [JSON](http://www.json.org/) format for serializing and deserializing object representations via the [`NSJSONSerialization`](http://developer.apple.com/library/mac/#documentation/Foundation/Reference/NSJSONSerialization_Class/Reference/Reference.html) class. +RestKit provides a pluggable interface for handling arbitrary serialization formats via the [`RKSerialization`](http://restkit.org/api/latest/Classes/RKSerialization.html) protocol and the [`RKMIMETypeSerialization`](http://restkit.org/api/latest/Classes/RKMIMETypeSerialization.html) class. Out of the box, RestKit supports handling the [JSON](http://www.json.org/) format for serializing and deserializing object representations via the [`NSJSONSerialization`](http://developer.apple.com/library/mac/#documentation/Foundation/Reference/NSJSONSerialization_Class/Reference/Reference.html) class. #### Additional Serializations diff --git a/RestKit.podspec b/RestKit.podspec index 19c843bf71..16091c9c43 100644 --- a/RestKit.podspec +++ b/RestKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'RestKit' - s.version = '0.20.0pre4' + s.version = '0.20.0pre5' 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' } diff --git a/RestKit.xcodeproj/project.pbxproj b/RestKit.xcodeproj/project.pbxproj index e1b0bd3070..fb94a83ded 100644 --- a/RestKit.xcodeproj/project.pbxproj +++ b/RestKit.xcodeproj/project.pbxproj @@ -417,43 +417,43 @@ 259AC481162B05C80012D2F9 /* RKObjectRequestOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 259AC480162B05C80012D2F9 /* RKObjectRequestOperationTest.m */; }; 259AC482162B05C80012D2F9 /* RKObjectRequestOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 259AC480162B05C80012D2F9 /* RKObjectRequestOperationTest.m */; }; 259B96D51604CCCC0000C250 /* AFHTTPClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96C21604CCCC0000C250 /* AFHTTPClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 259B96D61604CCCC0000C250 /* AFHTTPClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96C21604CCCC0000C250 /* AFHTTPClient.h */; }; + 259B96D61604CCCC0000C250 /* AFHTTPClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96C21604CCCC0000C250 /* AFHTTPClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 259B96D71604CCCC0000C250 /* AFHTTPClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96C31604CCCC0000C250 /* AFHTTPClient.m */; }; 259B96D81604CCCC0000C250 /* AFHTTPClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96C31604CCCC0000C250 /* AFHTTPClient.m */; }; 259B96D91604CCCC0000C250 /* AFHTTPRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96C41604CCCC0000C250 /* AFHTTPRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 259B96DA1604CCCC0000C250 /* AFHTTPRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96C41604CCCC0000C250 /* AFHTTPRequestOperation.h */; }; + 259B96DA1604CCCC0000C250 /* AFHTTPRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96C41604CCCC0000C250 /* AFHTTPRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 259B96DB1604CCCC0000C250 /* AFHTTPRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96C51604CCCC0000C250 /* AFHTTPRequestOperation.m */; }; 259B96DC1604CCCC0000C250 /* AFHTTPRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96C51604CCCC0000C250 /* AFHTTPRequestOperation.m */; }; 259B96DD1604CCCC0000C250 /* AFImageRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96C61604CCCC0000C250 /* AFImageRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 259B96DE1604CCCC0000C250 /* AFImageRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96C61604CCCC0000C250 /* AFImageRequestOperation.h */; }; + 259B96DE1604CCCC0000C250 /* AFImageRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96C61604CCCC0000C250 /* AFImageRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 259B96DF1604CCCC0000C250 /* AFImageRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96C71604CCCC0000C250 /* AFImageRequestOperation.m */; }; 259B96E01604CCCC0000C250 /* AFImageRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96C71604CCCC0000C250 /* AFImageRequestOperation.m */; }; 259B96E11604CCCC0000C250 /* AFJSONRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96C81604CCCC0000C250 /* AFJSONRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 259B96E21604CCCC0000C250 /* AFJSONRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96C81604CCCC0000C250 /* AFJSONRequestOperation.h */; }; + 259B96E21604CCCC0000C250 /* AFJSONRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96C81604CCCC0000C250 /* AFJSONRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 259B96E31604CCCC0000C250 /* AFJSONRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96C91604CCCC0000C250 /* AFJSONRequestOperation.m */; }; 259B96E41604CCCC0000C250 /* AFJSONRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96C91604CCCC0000C250 /* AFJSONRequestOperation.m */; }; 259B96E51604CCCC0000C250 /* AFNetworkActivityIndicatorManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96CA1604CCCC0000C250 /* AFNetworkActivityIndicatorManager.m */; }; 259B96E61604CCCC0000C250 /* AFNetworkActivityIndicatorManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96CA1604CCCC0000C250 /* AFNetworkActivityIndicatorManager.m */; }; 259B96E71604CCCC0000C250 /* AFPropertyListRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96CB1604CCCC0000C250 /* AFPropertyListRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 259B96E81604CCCC0000C250 /* AFPropertyListRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96CB1604CCCC0000C250 /* AFPropertyListRequestOperation.h */; }; + 259B96E81604CCCC0000C250 /* AFPropertyListRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96CB1604CCCC0000C250 /* AFPropertyListRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 259B96E91604CCCC0000C250 /* AFPropertyListRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96CC1604CCCC0000C250 /* AFPropertyListRequestOperation.m */; }; 259B96EA1604CCCC0000C250 /* AFPropertyListRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96CC1604CCCC0000C250 /* AFPropertyListRequestOperation.m */; }; 259B96EB1604CCCC0000C250 /* AFURLConnectionOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96CD1604CCCC0000C250 /* AFURLConnectionOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 259B96EC1604CCCC0000C250 /* AFURLConnectionOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96CD1604CCCC0000C250 /* AFURLConnectionOperation.h */; }; + 259B96EC1604CCCC0000C250 /* AFURLConnectionOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96CD1604CCCC0000C250 /* AFURLConnectionOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 259B96ED1604CCCC0000C250 /* AFURLConnectionOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96CE1604CCCC0000C250 /* AFURLConnectionOperation.m */; }; 259B96EE1604CCCC0000C250 /* AFURLConnectionOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96CE1604CCCC0000C250 /* AFURLConnectionOperation.m */; }; 259B96EF1604CCCC0000C250 /* AFXMLRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96CF1604CCCC0000C250 /* AFXMLRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 259B96F01604CCCC0000C250 /* AFXMLRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96CF1604CCCC0000C250 /* AFXMLRequestOperation.h */; }; + 259B96F01604CCCC0000C250 /* AFXMLRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96CF1604CCCC0000C250 /* AFXMLRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 259B96F11604CCCC0000C250 /* AFXMLRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96D01604CCCC0000C250 /* AFXMLRequestOperation.m */; }; 259B96F21604CCCC0000C250 /* AFXMLRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96D01604CCCC0000C250 /* AFXMLRequestOperation.m */; }; 259B96F31604CCCC0000C250 /* UIImageView+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96D11604CCCC0000C250 /* UIImageView+AFNetworking.m */; }; 259B96F41604CCCC0000C250 /* UIImageView+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 259B96D11604CCCC0000C250 /* UIImageView+AFNetworking.m */; }; 259B96F51604CCCC0000C250 /* AFNetworkActivityIndicatorManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96D21604CCCC0000C250 /* AFNetworkActivityIndicatorManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 259B96F61604CCCC0000C250 /* AFNetworkActivityIndicatorManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96D21604CCCC0000C250 /* AFNetworkActivityIndicatorManager.h */; }; + 259B96F61604CCCC0000C250 /* AFNetworkActivityIndicatorManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96D21604CCCC0000C250 /* AFNetworkActivityIndicatorManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 259B96F71604CCCC0000C250 /* UIImageView+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96D31604CCCC0000C250 /* UIImageView+AFNetworking.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 259B96F81604CCCC0000C250 /* UIImageView+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96D31604CCCC0000C250 /* UIImageView+AFNetworking.h */; }; + 259B96F81604CCCC0000C250 /* UIImageView+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96D31604CCCC0000C250 /* UIImageView+AFNetworking.h */; settings = {ATTRIBUTES = (Public, ); }; }; 259B96F91604CCCC0000C250 /* AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96D41604CCCC0000C250 /* AFNetworking.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 259B96FA1604CCCC0000C250 /* AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96D41604CCCC0000C250 /* AFNetworking.h */; }; + 259B96FA1604CCCC0000C250 /* AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = 259B96D41604CCCC0000C250 /* AFNetworking.h */; settings = {ATTRIBUTES = (Public, ); }; }; 259D983C154F6C90008C90F5 /* benchmark_parents_and_children.json in Resources */ = {isa = PBXBuildFile; fileRef = 259D983B154F6C90008C90F5 /* benchmark_parents_and_children.json */; }; 259D983D154F6C90008C90F5 /* benchmark_parents_and_children.json in Resources */ = {isa = PBXBuildFile; fileRef = 259D983B154F6C90008C90F5 /* benchmark_parents_and_children.json */; }; 259D98541550C69A008C90F5 /* RKEntityByAttributeCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 259D98521550C69A008C90F5 /* RKEntityByAttributeCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; diff --git a/Tests/Fixtures/JSON/humans/1.json b/Tests/Fixtures/JSON/humans/1.json index 96d2488cba..86c92e325c 100644 --- a/Tests/Fixtures/JSON/humans/1.json +++ b/Tests/Fixtures/JSON/humans/1.json @@ -1 +1 @@ -{"human":{"birthday":null,"created_at":null,"updated_at":null,"sex":null,"name":"Blake Watters","id":null,"age":28}} +{"human":{"birthday":null,"created_at":null,"updated_at":null,"sex":null,"name":"Blake Watters","id":1,"age":28}} diff --git a/Tests/Logic/CoreData/RKEntityMappingTest.m b/Tests/Logic/CoreData/RKEntityMappingTest.m index 3fc2b9bd0d..46c674f762 100644 --- a/Tests/Logic/CoreData/RKEntityMappingTest.m +++ b/Tests/Logic/CoreData/RKEntityMappingTest.m @@ -76,7 +76,7 @@ - (void)testShouldMapACollectionOfObjectsWithDynamicKeys attributeValues:@{ @"name": @"rachit" } inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"DynamicKeys.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext cache:managedObjectStore.managedObjectCache]; mapper.mappingOperationDataSource = dataSource; @@ -120,8 +120,8 @@ - (void)testShouldIncludeTransformableAttributesInPropertyNamesAndTypes assertThat([propertiesByName objectForKey:@"favoriteColors"], is(notNilValue())); assertThat([relationshipsByName objectForKey:@"favoriteColors"], is(nilValue())); - NSDictionary *propertyNamesAndTypes = [[RKPropertyInspector sharedInspector] propertyNamesAndClassesForEntity:entity]; - assertThat([propertyNamesAndTypes objectForKey:@"favoriteColors"], is(notNilValue())); + NSDictionary *propertyNamesAndTypes = [[RKPropertyInspector sharedInspector] propertyInspectionForEntity:entity]; + assertThat([propertyNamesAndTypes objectForKey:@"favoriteColors"][RKPropertyInspectionKeyValueCodingClassKey], is(notNilValue())); } - (void)testThatMappingAnEmptyArrayOnToAnExistingRelationshipDisassociatesTheRelatedObjects @@ -504,4 +504,40 @@ - (void)testInferenceOfSnakeCasedEntityNameWithAbbreviation expect([identificationAttributes valueForKey:@"name"]).to.equal(attributeNames); } +- (void)testEntityIdentifierInferenceSearchesParentEntities +{ + NSEntityDescription *entity = [[NSEntityDescription alloc] init]; + [entity setName:@"Monkey"]; + NSEntityDescription *parentEntity = [[NSEntityDescription alloc] init]; + [parentEntity setName:@"Parent"]; + [parentEntity setSubentities:@[ entity ]]; + NSAttributeDescription *identifierAttribute = [NSAttributeDescription new]; + [identifierAttribute setName:@"monkeyID"]; + [parentEntity setProperties:@[ identifierAttribute ]]; + NSArray *identificationAttributes = RKIdentificationAttributesInferredFromEntity(entity); + expect(identificationAttributes).notTo.beNil(); + NSArray *attributeNames = @[ @"monkeyID" ]; + expect([identificationAttributes valueForKey:@"name"]).to.equal(attributeNames); +} + +- (void)testEntityIdentifierInferenceFromUserInfoSearchesParentEntities +{ + NSEntityDescription *entity = [[NSEntityDescription alloc] init]; + [entity setName:@"Monkey"]; + NSAttributeDescription *identifierAttribute = [NSAttributeDescription new]; + [identifierAttribute setName:@"monkeyID"]; // We ignore this by specifying the userInfo key + NSAttributeDescription *nameAttribute = [NSAttributeDescription new]; + [nameAttribute setName:@"name"]; + [entity setProperties:@[ identifierAttribute, nameAttribute ]]; + [entity setUserInfo:@{ RKEntityIdentificationAttributesUserInfoKey: @"name" }]; + + NSEntityDescription *subentity = [NSEntityDescription new]; + [subentity setName:@"SubMonkey"]; + [entity setSubentities:@[ subentity ]]; + NSArray *identificationAttributes = RKIdentificationAttributesInferredFromEntity(subentity); + expect(identificationAttributes).notTo.beNil(); + NSArray *attributeNames = @[ @"name" ]; + expect([identificationAttributes valueForKey:@"name"]).to.equal(attributeNames); +} + @end diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m index b51be8e889..a33f8a42e4 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m @@ -902,7 +902,7 @@ - (void)testShouldConnectRelationshipsByPrimaryKeyRegardlessOfOrder [operationQueue setSuspended:YES]; mappingOperationDataSource.operationQueue = operationQueue; [managedObjectContext performBlockAndWait:^{ - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:JSON mappingsDictionary:mappingsDictionary]; mapper.mappingOperationDataSource = mappingOperationDataSource; [mapper start]; }]; @@ -945,7 +945,7 @@ - (void)testMappingAPayloadContainingRepeatedObjectsDoesNotYieldDuplicatesWithFe NSDictionary *mappingsDictionary = @{ @"parents": parentMapping }; NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"parents_and_children.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:JSON mappingsDictionary:mappingsDictionary]; mapper.mappingOperationDataSource = mappingOperationDataSource; [mapper start]; @@ -977,7 +977,7 @@ - (void)testMappingAPayloadContainingRepeatedObjectsDoesNotYieldDuplicatesWithIn NSDictionary *mappingsDictionary = @{ @"parents": parentMapping }; NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"parents_and_children.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:JSON mappingsDictionary:mappingsDictionary]; mapper.mappingOperationDataSource = mappingOperationDataSource; [mapper start]; @@ -994,6 +994,66 @@ - (void)testMappingAPayloadContainingRepeatedObjectsDoesNotYieldDuplicatesWithIn assertThatInteger(childrenCount, is(equalToInteger(4))); } +- (void)testThatMappingObjectsWithTheSameIdentificationAttributesAcrossTwoContextsDoesNotCreateDuplicateObjects +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKInMemoryManagedObjectCache *inMemoryCache = [[RKInMemoryManagedObjectCache alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + managedObjectStore.managedObjectCache = inMemoryCache; + NSEntityDescription *humanEntity = [NSEntityDescription entityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKEntityMapping *mapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + mapping.identificationAttributes = @[ @"railsID" ]; + [mapping addAttributeMappingsFromArray:@[ @"name", @"railsID" ]]; + + // Create two contexts with common parent + NSManagedObjectContext *firstContext = [managedObjectStore newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType]; + NSManagedObjectContext *secondContext = [managedObjectStore newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType]; + + // Map into the first context + NSDictionary *objectRepresentation = @{ @"name": @"Blake", @"railsID": @(31337) }; + + // Check that the cache contains a value for our identification attributes + __block BOOL success; + __block NSError *error; + [firstContext performBlockAndWait:^{ + RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:firstContext + cache:inMemoryCache]; + RKMapperOperation *mapperOperation = [[RKMapperOperation alloc] initWithRepresentation:objectRepresentation mappingsDictionary:@{ [NSNull null]: mapping }]; + mapperOperation.mappingOperationDataSource = dataSource; + success = [mapperOperation execute:&error]; + expect(success).to.equal(YES); + expect([mapperOperation.mappingResult count]).to.equal(1); + + [firstContext save:nil]; + }]; + + // Check that there is an entry in the cache + NSSet *objects = [inMemoryCache managedObjectsWithEntity:humanEntity attributeValues:@{ @"railsID": @(31337) } inManagedObjectContext:firstContext]; + expect(objects).to.haveCountOf(1); + + // Map into the second context + [secondContext performBlockAndWait:^{ + RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:secondContext + cache:inMemoryCache]; + RKMapperOperation *mapperOperation = [[RKMapperOperation alloc] initWithRepresentation:objectRepresentation mappingsDictionary:@{ [NSNull null]: mapping }]; + mapperOperation.mappingOperationDataSource = dataSource; + success = [mapperOperation execute:&error]; + expect(success).to.equal(YES); + expect([mapperOperation.mappingResult count]).to.equal(1); + + [secondContext save:nil]; + }]; + + // Now check the count + objects = [inMemoryCache managedObjectsWithEntity:humanEntity attributeValues:@{ @"railsID": @(31337) } inManagedObjectContext:secondContext]; + expect(objects).to.haveCountOf(1); + + // Now pull the count back from the parent context + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Human"]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"railsID == 31337"]; + NSArray *fetchedObjects = [managedObjectStore.persistentStoreManagedObjectContext executeFetchRequest:fetchRequest error:nil]; + expect(fetchedObjects).to.haveCountOf(1); +} + - (void)testConnectingToSubentitiesByFetchRequestCache { diff --git a/Tests/Logic/CoreData/RKManagedObjectStoreTest.m b/Tests/Logic/CoreData/RKManagedObjectStoreTest.m index 703081d192..8bc14794d3 100644 --- a/Tests/Logic/CoreData/RKManagedObjectStoreTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectStoreTest.m @@ -271,4 +271,70 @@ - (void)testResetPersistentStoresDoesNotTriggerDeadlock }]; } +- (void)testCleanupOfExternalStorageDirectoryOnReset +{ + RKManagedObjectStore *managedObjectStore = [[RKManagedObjectStore alloc] init]; + NSEntityDescription *humanEntity = [managedObjectStore.managedObjectModel entitiesByName][@"Human"]; + NSAttributeDescription *photoAttribute = [[NSAttributeDescription alloc] init]; + [photoAttribute setName:@"photo"]; + [photoAttribute setAttributeType:NSTransformableAttributeType]; + [photoAttribute setAllowsExternalBinaryDataStorage:YES]; + NSArray *newProperties = [[humanEntity properties] arrayByAddingObject:photoAttribute]; + [humanEntity setProperties:newProperties]; + NSError *error = nil; + NSString *storePath = [RKApplicationDataDirectory() stringByAppendingPathComponent:@"RKTestsStore.sqlite"]; + NSPersistentStore *persistentStore = [managedObjectStore addSQLitePersistentStoreAtPath:storePath fromSeedDatabaseAtPath:nil withConfiguration:nil options:nil error:&error]; + assertThat(persistentStore, is(notNilValue())); + [managedObjectStore createManagedObjectContexts]; + + // Check that there is a support directory + NSString *supportDirectoryName = [NSString stringWithFormat:@".%@_SUPPORT", [[persistentStore.URL lastPathComponent] stringByDeletingPathExtension]]; + NSURL *supportDirectoryFileURL = [NSURL URLWithString:supportDirectoryName relativeToURL:[persistentStore.URL URLByDeletingLastPathComponent]]; + + BOOL isDirectory = NO; + BOOL supportDirectoryExists = [[NSFileManager defaultManager] fileExistsAtPath:[supportDirectoryFileURL path] isDirectory:&isDirectory]; + expect(supportDirectoryExists).to.equal(YES); + expect(isDirectory).to.equal(YES); + NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[supportDirectoryFileURL path] error:&error]; + NSDate *creationDate = attributes[NSFileCreationDate]; + + BOOL success = [managedObjectStore resetPersistentStores:&error]; + expect(success).to.equal(YES); + + attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[supportDirectoryFileURL path] error:&error]; + NSDate *newCreationDate = attributes[NSFileCreationDate]; + + expect([creationDate laterDate:newCreationDate]).to.equal(newCreationDate); +} + +- (void)testThatPersistentStoreWithLongNameHasExternalStorageResetCorrectly +{ + // Create a store with an object to serve as our seed database + RKManagedObjectStore *managedObjectStore = [[RKManagedObjectStore alloc] init]; + NSError *error = nil; + NSString *storePath = [RKApplicationDataDirectory() stringByAppendingPathComponent:@"This is the Store.sqlite"]; + NSPersistentStore *persistentStore = [managedObjectStore addSQLitePersistentStoreAtPath:storePath fromSeedDatabaseAtPath:nil withConfiguration:nil options:nil error:&error]; + assertThat(persistentStore, is(notNilValue())); + [managedObjectStore createManagedObjectContexts]; + + // Check that there is a support directory + NSString *supportDirectoryName = [NSString stringWithFormat:@".%@_SUPPORT", [[persistentStore.URL lastPathComponent] stringByDeletingPathExtension]]; + NSString *supportDirectoryPath = [[[[persistentStore URL] path] stringByDeletingLastPathComponent] stringByAppendingPathComponent:supportDirectoryName]; + + BOOL isDirectory = NO; + BOOL supportDirectoryExists = [[NSFileManager defaultManager] fileExistsAtPath:supportDirectoryPath isDirectory:&isDirectory]; + expect(supportDirectoryExists).to.equal(YES); + expect(isDirectory).to.equal(YES); + NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:supportDirectoryPath error:&error]; + NSDate *creationDate = attributes[NSFileCreationDate]; + + BOOL success = [managedObjectStore resetPersistentStores:&error]; + expect(success).to.equal(YES); + + attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:supportDirectoryPath error:&error]; + NSDate *newCreationDate = attributes[NSFileCreationDate]; + + expect([creationDate laterDate:newCreationDate]).to.equal(newCreationDate); +} + @end diff --git a/Tests/Logic/CoreData/RKRelationshipConnectionOperationTest.m b/Tests/Logic/CoreData/RKRelationshipConnectionOperationTest.m index 9ebf0ee2ce..9de95afeb2 100644 --- a/Tests/Logic/CoreData/RKRelationshipConnectionOperationTest.m +++ b/Tests/Logic/CoreData/RKRelationshipConnectionOperationTest.m @@ -11,6 +11,7 @@ #import "RKCat.h" #import "RKHouse.h" #import "RKResident.h" +#import "RKChild.h" #import "RKRelationshipConnectionOperation.h" #import "RKFetchRequestManagedObjectCache.h" @@ -207,4 +208,162 @@ - (void)testConnectingToManyOrderedSetRelationshipWithEmptyTargetViaKeyPath expect([human.friendsInTheOrderWeMet set]).to.beEmpty(); } +- (void)testConnectingToSubentities +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *childEntity = [NSEntityDescription entityForName:@"Child" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + NSRelationshipDescription *relationship = [childEntity relationshipsByName][@"friends"]; + RKConnectionDescription *connection = [[RKConnectionDescription alloc] initWithRelationship:relationship attributes:@{ @"friendIDs": @"railsID" }]; + connection.includesSubentities = YES; + + RKHuman *human = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:nil withProperties:nil]; + human.railsID = @(12345); + RKChild *child = [RKTestFactory insertManagedObjectForEntityForName:@"Child" inManagedObjectContext:nil withProperties:nil]; + child.railsID = @(12345); + + RKChild *secondChild = [RKTestFactory insertManagedObjectForEntityForName:@"Child" inManagedObjectContext:nil withProperties:nil]; + secondChild.friendIDs = @[ @(12345) ]; + + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:secondChild connection:connection managedObjectCache:managedObjectCache]; + [operation start]; + + NSSet *expectedFriends = [NSSet setWithObjects:human, child, nil]; + expect(secondChild.friends).to.equal(expectedFriends); +} + +- (void)testNotConnectingToSubentities +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *childEntity = [NSEntityDescription entityForName:@"Child" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + NSRelationshipDescription *relationship = [childEntity relationshipsByName][@"friends"]; + RKConnectionDescription *connection = [[RKConnectionDescription alloc] initWithRelationship:relationship attributes:@{ @"friendIDs": @"railsID" }]; + connection.includesSubentities = NO; + + RKHuman *human = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:nil withProperties:nil]; + human.railsID = @(12345); + RKChild *child = [RKTestFactory insertManagedObjectForEntityForName:@"Child" inManagedObjectContext:nil withProperties:nil]; + child.railsID = @(12345); + + RKChild *secondChild = [RKTestFactory insertManagedObjectForEntityForName:@"Child" inManagedObjectContext:nil withProperties:nil]; + secondChild.friendIDs = @[ @(12345) ]; + + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:secondChild connection:connection managedObjectCache:managedObjectCache]; + [operation start]; + + NSSet *expectedFriends = [NSSet setWithObjects:human, nil]; + expect(secondChild.friends).to.equal(expectedFriends); +} + +- (void)testConnectionWithSourcePredicate +{ + RKHuman *human = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:nil withProperties:nil]; + human.sex = @"female"; + RKCat *asia = [RKTestFactory insertManagedObjectForEntityForName:@"Cat" inManagedObjectContext:nil withProperties:nil]; + asia.sex = @"female"; + RKCat *lola = [RKTestFactory insertManagedObjectForEntityForName:@"Cat" inManagedObjectContext:nil withProperties:nil]; + lola.sex = @"female"; + RKCat *roy = [RKTestFactory insertManagedObjectForEntityForName:@"Cat" inManagedObjectContext:nil withProperties:nil]; + roy.sex = @"male"; + + RKEntityMapping *mapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:[RKTestFactory managedObjectStore]]; + [mapping addConnectionForRelationship:@"cats" connectedBy:@"sex"]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKConnectionDescription *connection = [mapping connectionForRelationship:@"cats"]; + connection.sourcePredicate = [NSPredicate predicateWithFormat:@"sex == %@", @"male"]; + + RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:human connection:connection managedObjectCache:managedObjectCache]; + [operation start]; + assertThat(human.cats, hasCountOf(0)); +} + +- (void)testConnectionWithDestinationPredicate +{ + RKHuman *human = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:nil withProperties:nil]; + human.sex = @"female"; + + RKCat *asia = [RKTestFactory insertManagedObjectForEntityForName:@"Cat" inManagedObjectContext:nil withProperties:@{@"birthYear": @2011}]; + asia.sex = @"female"; + RKCat *lola = [RKTestFactory insertManagedObjectForEntityForName:@"Cat" inManagedObjectContext:nil withProperties:@{@"birthYear": @2012}]; + lola.sex = @"female"; + + RKEntityMapping *mapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:[RKTestFactory managedObjectStore]]; + [mapping addConnectionForRelationship:@"cats" connectedBy:@"sex"]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKConnectionDescription *connection = [mapping connectionForRelationship:@"cats"]; + connection.destinationPredicate = [NSPredicate predicateWithFormat:@"birthYear = 2011"]; + + + RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:human connection:connection managedObjectCache:managedObjectCache]; + [operation start]; + assertThat(human.cats, hasCountOf(1)); + assertThat(human.cats, hasItems(asia, nil)); +} + +- (void)testConnectionOfOptionalRelationshipIsSkippedWhenAllConnectionAttributesEvaluateToNil +{ + RKHuman *human = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:nil withProperties:nil]; + RKCat __unused *asia = [RKTestFactory insertManagedObjectForEntityForName:@"Cat" inManagedObjectContext:nil withProperties:@{@"birthYear": @2011}]; + RKCat __unused *lola = [RKTestFactory insertManagedObjectForEntityForName:@"Cat" inManagedObjectContext:nil withProperties:@{@"birthYear": @2012}]; + + RKEntityMapping *mapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:[RKTestFactory managedObjectStore]]; + [mapping addConnectionForRelationship:@"cats" connectedBy:@"sex"]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKConnectionDescription *connection = [mapping connectionForRelationship:@"cats"]; + + RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:human connection:connection managedObjectCache:managedObjectCache]; + [operation start]; + assertThat(human.cats, hasCountOf(0)); +} + +- (void)testConnectionOfOptionalRelationshipIsEvaluatedWhenAtLeastOneAttributeEvaluatesToNonNil +{ + RKHuman *human = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:nil withProperties:nil]; + human.sex = @"female"; + + RKCat *asia = [RKTestFactory insertManagedObjectForEntityForName:@"Cat" inManagedObjectContext:nil withProperties:@{@"birthYear": @2011}]; + asia.sex = @"female"; + asia.name = @"Asia"; + RKCat *lola = [RKTestFactory insertManagedObjectForEntityForName:@"Cat" inManagedObjectContext:nil withProperties:@{@"birthYear": @2012}]; + lola.sex = @"female"; + lola.name = nil; + + RKEntityMapping *mapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:[RKTestFactory managedObjectStore]]; + [mapping addConnectionForRelationship:@"cats" connectedBy:@[ @"sex", @"name" ]]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKConnectionDescription *connection = [mapping connectionForRelationship:@"cats"]; + + RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:human connection:connection managedObjectCache:managedObjectCache]; + [operation start]; + assertThat(human.cats, hasCountOf(1)); + assertThat(human.cats, hasItems(lola, nil)); +} + +- (void)testConnectionOfOptionalRelationshipIsSkippedWhenAllAttributesEvaluateToNil +{ + RKHuman *human = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:nil withProperties:nil]; + + RKCat *asia = [RKTestFactory insertManagedObjectForEntityForName:@"Cat" inManagedObjectContext:nil withProperties:@{@"birthYear": @2011}]; + asia.sex = @"female"; + asia.name = @"Asia"; + RKCat *lola = [RKTestFactory insertManagedObjectForEntityForName:@"Cat" inManagedObjectContext:nil withProperties:@{@"birthYear": @2012}]; + lola.sex = @"female"; + lola.name = nil; + + human.cats = [NSSet setWithObject:asia]; + + RKEntityMapping *mapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:[RKTestFactory managedObjectStore]]; + [mapping addConnectionForRelationship:@"cats" connectedBy:@[ @"sex", @"name" ]]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + RKConnectionDescription *connection = [mapping connectionForRelationship:@"cats"]; + + RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:human connection:connection managedObjectCache:managedObjectCache]; + [operation start]; + + // Operation should be skipped due to lack of connectable attributes + assertThat(human.cats, hasCountOf(1)); + assertThat(human.cats, hasItems(asia, nil)); +} + @end diff --git a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m index 8bf0b92686..5a84363672 100644 --- a/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKManagedObjectRequestOperationTest.m @@ -16,6 +16,7 @@ @interface RKManagedObjectRequestOperation () - (NSSet *)localObjectsFromFetchRequestsMatchingRequestURL:(NSError **)error; @end +NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths); @interface RKManagedObjectRequestOperationTest : RKTestCase @@ -367,6 +368,27 @@ - (void)testThatManagedObjectMappedToNSOrderedSetRelationshipOfNonManagedObjects expect([[testUser.friendsOrderedSet firstObject] managedObjectContext]).to.equal(managedObjectStore.persistentStoreManagedObjectContext); } +- (void)testDeletionOfOrphanedManagedObjects +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKHuman *orphanedHuman = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + [entityMapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:entityMapping pathPattern:nil keyPath:@"human" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/with_to_one_relationship.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Human"]; + }; + managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).to.beNil(); + expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); + expect(orphanedHuman.managedObjectContext).to.beNil(); +} + - (void)testDeletionOfOrphanedObjectsMappedOnRelationships { RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; @@ -390,6 +412,172 @@ - (void)testDeletionOfOrphanedObjectsMappedOnRelationships expect(orphanedHuman.managedObjectContext).to.beNil(); } -// TODO: test deletion of nested objects +- (void)testDeletionOfOrphanedTagsOfPosts +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSManagedObject *orphanedTag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + [orphanedTag setValue:@"orphaned" forKey:@"name"]; + RKEntityMapping *postMapping = [RKEntityMapping mappingForEntityForName:@"Post" inManagedObjectStore:managedObjectStore]; + [postMapping addAttributeMappingsFromArray:@[ @"title", @"body" ]]; + RKEntityMapping *tagMapping = [RKEntityMapping mappingForEntityForName:@"Tag" inManagedObjectStore:managedObjectStore]; + [tagMapping addAttributeMappingsFromArray:@[ @"name" ]]; + [postMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"tags" toKeyPath:@"tags" withMapping:tagMapping]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:postMapping pathPattern:nil keyPath:@"posts" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/posts.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + return [NSFetchRequest fetchRequestWithEntityName:@"Tag"]; + }; + managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).to.beNil(); + expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); + expect(orphanedTag.managedObjectContext).to.beNil(); + + // Create 3 tags. Update the post entity so it only points to 2 tags. Tag should be deleted. + // Create 3 tags. Create another post pointing to one of the tags. Update the post entity so it only points to 2 tags. Tag should be deleted. +} + +- (void)testThatDeletionOfOrphanedObjectsCanBeSuppressedByPredicate +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSManagedObject *tagOnDiferentObject = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + [tagOnDiferentObject setValue:@"orphaned" forKey:@"name"]; + + NSManagedObject *otherPost = [NSEntityDescription insertNewObjectForEntityForName:@"Post" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + [otherPost setValue:[NSSet setWithObject:tagOnDiferentObject] forKey:@"tags"]; + + RKEntityMapping *postMapping = [RKEntityMapping mappingForEntityForName:@"Post" inManagedObjectStore:managedObjectStore]; + [postMapping addAttributeMappingsFromArray:@[ @"title", @"body" ]]; + RKEntityMapping *tagMapping = [RKEntityMapping mappingForEntityForName:@"Tag" inManagedObjectStore:managedObjectStore]; + [tagMapping addAttributeMappingsFromArray:@[ @"name" ]]; + [postMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"tags" toKeyPath:@"tags" withMapping:tagMapping]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:postMapping pathPattern:nil keyPath:@"posts" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/posts.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Tag"]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"posts.@count == 0"]; + return fetchRequest; + }; + managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).to.beNil(); + expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); + expect(tagOnDiferentObject.managedObjectContext).notTo.beNil(); +} + +- (void)testThatObjectsOrphanedByRequestOperationAreDeletedAppropriately +{ + // create tags: development, restkit, orphaned + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + __block NSManagedObject *post = nil; + __block NSManagedObject *orphanedTag; + __block NSManagedObject *anotherTag; + [managedObjectStore.persistentStoreManagedObjectContext performBlockAndWait:^{ + NSManagedObject *developmentTag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + [developmentTag setValue:@"development" forKey:@"name"]; + NSManagedObject *restkitTag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + [restkitTag setValue:@"restkit" forKey:@"name"]; + orphanedTag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + [orphanedTag setValue:@"orphaned" forKey:@"name"]; + + post = [NSEntityDescription insertNewObjectForEntityForName:@"Post" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + [post setValue:@"Post Title" forKey:@"title"]; + [post setValue:[NSSet setWithObjects:developmentTag, restkitTag, orphanedTag, nil] forKey:@"tags"]; + + anotherTag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + [anotherTag setValue:@"another" forKey:@"name"]; + NSManagedObject *anotherPost = [NSEntityDescription insertNewObjectForEntityForName:@"Post" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + [anotherPost setValue:@"Another Post" forKey:@"title"]; + [anotherPost setValue:[NSSet setWithObject:anotherTag] forKey:@"tags"]; + + [managedObjectStore.persistentStoreManagedObjectContext save:nil]; + }]; + + RKEntityMapping *postMapping = [RKEntityMapping mappingForEntityForName:@"Post" inManagedObjectStore:managedObjectStore]; + postMapping.identificationAttributes = @[ @"title" ]; + [postMapping addAttributeMappingsFromArray:@[ @"title", @"body" ]]; + RKEntityMapping *tagMapping = [RKEntityMapping mappingForEntityForName:@"Tag" inManagedObjectStore:managedObjectStore]; + tagMapping.identificationAttributes = @[ @"name" ]; + [tagMapping addAttributeMappingsFromArray:@[ @"name" ]]; + [postMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"tags" toKeyPath:@"tags" withMapping:tagMapping]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:postMapping pathPattern:nil keyPath:@"posts" statusCodes:[NSIndexSet indexSetWithIndex:200]]; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/posts.json" relativeToURL:[RKTestFactory baseURL]]]; + RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new]; + managedObjectRequestOperation.managedObjectCache = managedObjectCache; + RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) { + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Tag"]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"posts.@count == 0"]; + return fetchRequest; + }; + managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ]; + managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + [managedObjectRequestOperation start]; + expect(managedObjectRequestOperation.error).to.beNil(); + expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1); + + NSSet *tagNames = [post valueForKeyPath:@"tags.name"]; + NSSet *expectedTagNames = [NSSet setWithObjects:@"development", @"restkit", nil ]; + expect(tagNames).to.equal(expectedTagNames); + + expect([orphanedTag hasBeenDeleted]).to.equal(YES); + expect([anotherTag hasBeenDeleted]).to.equal(NO); +} + +- (void)testPruningOfSubkeypathsFromSet +{ + NSSet *keyPaths = [NSSet setWithObjects:@"posts", @"posts.tags", @"another", @"something.else.entirely", @"another.this.that", @"somewhere.out.there", @"some.posts", nil]; + NSSet *prunedSet = RKSetByRemovingSubkeypathsFromSet(keyPaths); + NSSet *expectedSet = [NSSet setWithObjects:@"posts", @"another", @"something.else.entirely", @"somewhere.out.there", @"some.posts", nil]; + expect(prunedSet).to.equal(expectedSet); +} + +- (void)testThatMappingObjectsWithTheSameIdentificationAttributesAcrossTwoObjectRequestOperationConcurrentlyDoesNotCreateDuplicateObjects +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKInMemoryManagedObjectCache *inMemoryCache = [[RKInMemoryManagedObjectCache alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + managedObjectStore.managedObjectCache = inMemoryCache; + RKEntityMapping *mapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + mapping.identificationAttributes = @[ @"railsID" ]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + [mapping addAttributeMappingsFromDictionary:@{ @"id": @"railsID" }]; + + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:mapping pathPattern:nil keyPath:@"human" statusCodes:nil]; + + NSURL *URL = [NSURL URLWithString:@"humans/1" relativeToURL:[RKTestFactory baseURL]]; +// [RKMIMETypeSerialization registerClass:[RKNSJSONSerialization class] forMIMEType:@"text/plain"]; +// NSURL *URL = [NSURL URLWithString:@"http://restkit.org/human_1.json"]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; + + RKManagedObjectRequestOperation *firstOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + firstOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + firstOperation.managedObjectCache = inMemoryCache; + RKManagedObjectRequestOperation *secondOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]]; + secondOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext; + secondOperation.managedObjectCache = inMemoryCache; + + NSOperationQueue *operationQueue = [NSOperationQueue new]; + [operationQueue setMaxConcurrentOperationCount:2]; + [operationQueue setSuspended:YES]; + [operationQueue addOperation:firstOperation]; + [operationQueue addOperation:secondOperation]; + + // Start both operations + [operationQueue setSuspended:NO]; + [operationQueue waitUntilAllOperationsAreFinished]; + + // Now pull the count back from the parent context + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Human"]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"railsID == 1"]; + NSArray *fetchedObjects = [managedObjectStore.persistentStoreManagedObjectContext executeFetchRequest:fetchRequest error:nil]; + expect(fetchedObjects).to.haveCountOf(1); +} @end diff --git a/Tests/Logic/Network/RKObjectRequestOperationTest.m b/Tests/Logic/Network/RKObjectRequestOperationTest.m index 62f261aec8..2a0aab2b02 100644 --- a/Tests/Logic/Network/RKObjectRequestOperationTest.m +++ b/Tests/Logic/Network/RKObjectRequestOperationTest.m @@ -54,7 +54,7 @@ - (RKResponseDescriptor *)responseDescriptorForComplexUser - (RKResponseDescriptor *)errorResponseDescriptor { RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[RKErrorMessage class]]; - [errorMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:@"" toKeyPath:@"errorMessage"]]; + [errorMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:nil toKeyPath:@"errorMessage"]]; NSMutableIndexSet *errorCodes = [NSMutableIndexSet indexSet]; [errorCodes addIndexes:RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError)]; diff --git a/Tests/Logic/Network/RKResponseMapperOperationTest.m b/Tests/Logic/Network/RKResponseMapperOperationTest.m index b1cf03bcdd..484d582614 100644 --- a/Tests/Logic/Network/RKResponseMapperOperationTest.m +++ b/Tests/Logic/Network/RKResponseMapperOperationTest.m @@ -14,6 +14,20 @@ NSString *RKPathAndQueryStringFromURLRelativeToURL(NSURL *URL, NSURL *baseURL); +@interface RKServerError : NSObject +@property (nonatomic, copy) NSString *message; +@property (nonatomic, assign) NSInteger code; +@end + +@implementation RKServerError + +- (NSString *)description +{ + return [NSString stringWithFormat:@"%@ (%ld)", self.message, (long) self.code]; +} + +@end + @interface RKObjectResponseMapperOperationTest : RKTestCase @end @@ -121,6 +135,21 @@ - (void)testThatMappingEmptyJSONDictionaryClientErrorResponseReturnsErrorNoDescr expect([mapper.error localizedDescription]).to.equal(@"Loaded an unprocessable client error response (422)"); } +- (void)testMappingServerErrorToCustomErrorClass +{ + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKServerError class]]; + [mapping addAttributeMappingsFromArray:@[ @"code", @"message" ]]; + RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:mapping pathPattern:nil keyPath:nil statusCodes:[NSIndexSet indexSetWithIndex:422]]; + NSURL *URL = [NSURL URLWithString:@"http://restkit.org"]; + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:422 HTTPVersion:@"1.1" headerFields:@{@"Content-Type": @"application/json"}]; + NSData *data = [@"{\"code\": 12345, \"message\": \"This is the error message\"}" dataUsingEncoding:NSUTF8StringEncoding]; + RKObjectResponseMapperOperation *mapper = [[RKObjectResponseMapperOperation alloc] initWithResponse:response data:data responseDescriptors:@[responseDescriptor]]; + [mapper start]; + expect(mapper.error).notTo.beNil(); + expect(mapper.error.code).to.equal(RKMappingErrorFromMappingResult); + expect([mapper.error localizedDescription]).to.equal(@"This is the error message (12345)"); +} + #pragma mark - Response Descriptor Matching - (void)testThatResponseMapperMatchesBaseURLWithoutPathAppropriately diff --git a/Tests/Logic/ObjectMapping/RKMappingOperationTest.m b/Tests/Logic/ObjectMapping/RKMappingOperationTest.m index c8a4dac13f..fa6d5a1f5a 100644 --- a/Tests/Logic/ObjectMapping/RKMappingOperationTest.m +++ b/Tests/Logic/ObjectMapping/RKMappingOperationTest.m @@ -434,14 +434,14 @@ - (void)testCancellationOfMapperOperation RKObjectMapping *childMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; [childMapping addAttributeMappingsFromArray:@[@"name"]]; - RKEntityMapping *parentMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + RKObjectMapping *parentMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; [parentMapping addAttributeMappingsFromArray:@[@"name"]]; [parentMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"children" toKeyPath:@"friends" withMapping:childMapping]]; NSDictionary *mappingsDictionary = @{ @"parents": parentMapping }; NSOperationQueue *operationQueue = [NSOperationQueue new]; NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"benchmark_parents_and_children.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:JSON mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:JSON mappingsDictionary:mappingsDictionary]; [operationQueue addOperation:mapper]; [mapper cancel]; [operationQueue waitUntilAllOperationsAreFinished]; diff --git a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m index 6a5499e2c1..4438020d50 100644 --- a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m @@ -574,6 +574,28 @@ - (void)testChangingHTTPClient expect([manager.baseURL absoluteString]).to.equal(@"http://google.com/"); } +- (void)testPostingOneObjectAndGettingResponseMatchingAnotherClass +{ + RKObjectManager *manager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKObjectMapping *userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [userMapping addAttributeMappingsFromDictionary:@{ @"fullname": @"name" }]; + RKObjectMapping *metaMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + [metaMapping addAttributeMappingsFromArray:@[ @"status", @"version" ]]; + RKResponseDescriptor *metaResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:metaMapping pathPattern:nil keyPath:@"meta" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]; + + [manager addResponseDescriptorsFromArray:@[ metaResponseDescriptor ]]; + RKTestUser *user = [RKTestUser new]; + RKObjectRequestOperation *requestOperation = [manager appropriateObjectRequestOperationWithObject:user method:RKRequestMethodPOST path:@"/ComplexUser" parameters:nil]; + [requestOperation start]; + [requestOperation waitUntilFinished]; + + expect(requestOperation.error).to.beNil(); + expect(requestOperation.mappingResult).notTo.beNil(); + expect([requestOperation.mappingResult array]).to.haveCountOf(1); + NSDictionary *expectedObject = @{ @"status": @"ok", @"version": @"0.3" }; + expect([requestOperation.mappingResult firstObject]).to.equal(expectedObject); +} + - (void)testPostingOneObjectAndGettingResponseMatchingMultipleDescriptors { RKObjectManager *manager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m index 2f11a6b6b3..f458c40e06 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m @@ -183,19 +183,18 @@ - (void)testShouldGenerateRelationshipMappings assertThat(mapping.propertyMappingsBySourceKeyPath[@"another"], isNot(nilValue())); } -// TODO: Decide about inverse... -//- (void)testShouldGenerateAnInverseMappings -//{ -// RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; -// [mapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:@"first_name" toKeyPath:@"firstName"]]; -// [mapping addAttributeMappingsFromArray:@[@"city", @"state", @"zip"]]; -// RKObjectMapping *otherMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; -// [otherMapping addAttributeMappingsFromArray:@[@"street"]]; -// [mapping mapRelationship:@"address" withMapping:otherMapping]; -// RKObjectMapping *inverse = [mapping inverseMapping]; -// assertThat(inverse.objectClass, is(equalTo([NSMutableDictionary class]))); -// assertThat([inverse mappingForKeyPath:@"firstName"], isNot(nilValue())); -//} +- (void)testShouldGenerateAnInverseMappings +{ + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:@"first_name" toKeyPath:@"firstName"]]; + [mapping addAttributeMappingsFromArray:@[@"city", @"state", @"zip"]]; + RKObjectMapping *otherMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; + [otherMapping addAttributeMappingsFromArray:@[@"street"]]; + [mapping addRelationshipMappingWithSourceKeyPath:@"address" mapping:otherMapping]; + RKObjectMapping *inverse = [mapping inverseMapping]; + assertThat(inverse.objectClass, is(equalTo([NSMutableDictionary class]))); + assertThat([inverse propertyMappingsBySourceKeyPath][@"firstName"], isNot(nilValue())); +} - (void)testShouldLetYouRetrieveMappingsByAttribute { @@ -228,7 +227,7 @@ - (void)testShouldPerformBasicMapping mapper.mappingOperationDataSource = [RKObjectMappingOperationDataSource new]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKTestUser *user = [RKTestUser user]; - BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:mapping]; + BOOL success = [mapper mapRepresentation:userInfo toObject:user atKeyPath:@"" usingMapping:mapping]; assertThatBool(success, is(equalToBool(YES))); assertThatInt([user.userID intValue], is(equalToInt(31337))); assertThat(user.name, is(equalTo(@"Blake Watters"))); @@ -245,13 +244,12 @@ - (void)testShouldMapACollectionOfSimpleObjectDictionaries RKMapperOperation *mapper = [RKMapperOperation new]; mapper.mappingOperationDataSource = [RKObjectMappingOperationDataSource new]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; - NSArray *users = [mapper mapCollection:userInfo atKeyPath:@"" usingMapping:mapping]; + NSArray *users = [mapper mapRepresentations:userInfo atKeyPath:@"" usingMapping:mapping]; assertThatUnsignedInteger([users count], is(equalToInt(3))); RKTestUser *blake = [users objectAtIndex:0]; assertThat(blake.name, is(equalTo(@"Blake Watters"))); } -// TODO: This doesn't really test anything anymore... - (void)testShouldDetermineTheObjectMappingByConsultingTheMappingProviderWhenThereIsATargetObject { RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; @@ -259,7 +257,7 @@ - (void)testShouldDetermineTheObjectMappingByConsultingTheMappingProviderWhenThe [mappingsDictionary setObject:mapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; mapper.targetObject = [RKTestUser user]; [mapper start]; } @@ -268,7 +266,7 @@ - (void)testShouldAddAnErrorWhenTheKeyPathMappingAndObjectClassDoNotAgree { RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:@{[NSNull null] : mapping}]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:@{[NSNull null] : mapping}]; NSDictionary *dictionary = [NSDictionary new]; mapper.targetObject = dictionary; [mapper start]; @@ -284,11 +282,8 @@ - (void)testShouldMapToATargetObject RKAttributeMapping *nameMapping = [RKAttributeMapping attributeMappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addPropertyMapping:nameMapping]; - NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:mapping forKey:[NSNull null]]; - id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:@{[NSNull null] : mapping}]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:@{[NSNull null] : mapping}]; RKTestUser *user = [RKTestUser user]; mapper.targetObject = user; [mapper start]; @@ -305,11 +300,8 @@ - (void)testShouldCreateANewInstanceOfTheAppropriateDestinationObjectWhenThereIs RKAttributeMapping *nameMapping = [RKAttributeMapping attributeMappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addPropertyMapping:nameMapping]; - NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:mapping forKey:[NSNull null]]; - id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:@{ [NSNull null]: mapping }]; [mapper start]; id mappingResult = [mapper.mappingResult firstObject]; assertThatBool([mappingResult isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); @@ -317,18 +309,14 @@ - (void)testShouldCreateANewInstanceOfTheAppropriateDestinationObjectWhenThereIs - (void)testShouldMapWithoutATargetMapping { - RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelTrace); RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKAttributeMapping *idMapping = [RKAttributeMapping attributeMappingFromKeyPath:@"id" toKeyPath:@"userID"]; [mapping addPropertyMapping:idMapping]; RKAttributeMapping *nameMapping = [RKAttributeMapping attributeMappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addPropertyMapping:nameMapping]; - NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:mapping forKey:[NSNull null]]; - id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:@{ [NSNull null]: mapping }]; [mapper start]; RKTestUser *user = [mapper.mappingResult firstObject]; assertThat(user, is(notNilValue())); @@ -343,11 +331,9 @@ - (void)testShouldMapACollectionOfObjects [mapping addPropertyMapping:idMapping]; RKAttributeMapping *nameMapping = [RKAttributeMapping attributeMappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addPropertyMapping:nameMapping]; - NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:mapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:@{ [NSNull null]: mapping }]; [mapper start]; NSArray *users = [mapper.mappingResult array]; assertThatBool([users isKindOfClass:[NSArray class]], is(equalToBool(YES))); @@ -369,7 +355,7 @@ - (void)testShouldMapACollectionOfObjectsWithDynamicKeys id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"DynamicKeys.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; NSArray *users = [mapper.mappingResult array]; assertThatBool([users isKindOfClass:[NSArray class]], is(equalToBool(YES))); @@ -395,7 +381,7 @@ - (void)testShouldMapACollectionOfObjectsWithDynamicKeysAndRelationships [mappingsDictionary setObject:mapping forKey:@"users"]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"DynamicKeysWithRelationship.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; RKMappingResult *result = mapper.mappingResult; NSArray *users = [result array]; @@ -433,7 +419,7 @@ - (void)testShouldMapANestedArrayOfObjectsWithDynamicKeysAndArrayRelationships [mappingsDictionary setObject:mapping forKey:@"groups"]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"DynamicKeysWithNestedRelationship.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; RKMappingResult *result = mapper.mappingResult; @@ -489,7 +475,7 @@ - (void)testShouldMapANestedArrayOfObjectsWithDynamicKeysAndSetRelationships [mappingsDictionary setObject:mapping forKey:@"groups"]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"DynamicKeysWithNestedRelationship.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; RKMappingResult *result = mapper.mappingResult; @@ -535,14 +521,12 @@ - (void)testShouldBeAbleToMapFromAUserObjectToADictionary [mapping addPropertyMapping:idMapping]; RKAttributeMapping *nameMapping = [RKAttributeMapping attributeMappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addPropertyMapping:nameMapping]; - NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:mapping forKey:[NSNull null]]; RKTestUser *user = [RKTestUser user]; user.name = @"Blake Watters"; user.userID = [NSNumber numberWithInt:123]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:user mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:user mappingsDictionary:@{ [NSNull null]: mapping }]; [mapper start]; RKMappingResult *result = mapper.mappingResult; NSDictionary *userInfo = [result firstObject]; @@ -561,7 +545,7 @@ - (void)testShouldMapRegisteredSubKeyPathsOfAnUnmappableDictionaryAndReturnTheRe [mappingsDictionary setObject:mapping forKey:@"user"]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"nested_user.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; NSDictionary *dictionary = [mapper.mappingResult dictionary]; assertThatBool([dictionary isKindOfClass:[NSDictionary class]], is(equalToBool(YES))); @@ -579,11 +563,9 @@ - (void)testShouldAddAnErrorWhenYouTryToMapAnArrayToATargetObject [mapping addPropertyMapping:idMapping]; RKAttributeMapping *nameMapping = [RKAttributeMapping attributeMappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addPropertyMapping:nameMapping]; - NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:mapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:@{ [NSNull null]: mapping }]; mapper.targetObject = [RKTestUser user]; [mapper start]; assertThatInteger(mapper.error.code, is(equalToInt(RKMappingErrorTypeMismatch))); @@ -592,19 +574,42 @@ - (void)testShouldAddAnErrorWhenYouTryToMapAnArrayToATargetObject - (void)testShouldAddAnErrorWhenAttemptingToMapADictionaryWithoutAnObjectMapping { id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; - NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:@{}]; [mapper start]; - assertThat([mapper.error localizedDescription], is(equalTo(@"Unable to find any mappings for the given content"))); + assertThat([mapper.error localizedDescription], is(equalTo(@"No mappable object representations were found at the key paths searched."))); } - (void)testShouldAddAnErrorWhenAttemptingToMapACollectionWithoutAnObjectMapping { NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; - assertThat([mapper.error localizedDescription], is(equalTo(@"Unable to find any mappings for the given content"))); + assertThat([mapper.error localizedDescription], is(equalTo(@"No mappable object representations were found at the key paths searched."))); +} + +- (void)testThatAnErrorIsSetWithAHelpfulDescriptionWhenNoKeyPathsMatchTheArrayOfObjectsRepresentationBeingMapped +{ + RKObjectMapping *mapping1 = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + RKObjectMapping *mapping2 = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + NSDictionary *mappingsDictionary = @{ @"this": mapping1, @"that": mapping2 }; + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; + [mapper start]; + assertThat([mapper.error localizedDescription], is(equalTo(@"No mappable object representations were found at the key paths searched."))); + assertThat([mapper.error localizedFailureReason], is(equalTo(@"The mapping operation was unable to find any nested object representations at the key paths searched: this, that\nThis likely indicates that you have misconfigured the key paths for your mappings."))); +} + +- (void)testThatAnErrorIsSetWithAHelpfulDescriptionWhenNoKeyPathsMatchTheObjectRepresentationBeingMapped +{ + RKObjectMapping *mapping1 = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + RKObjectMapping *mapping2 = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + NSDictionary *mappingsDictionary = @{ @"this": mapping1, @"that": mapping2 }; + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"RailsUser.json"]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; + [mapper start]; + assertThat([mapper.error localizedDescription], is(equalTo(@"No mappable object representations were found at the key paths searched."))); + assertThat([mapper.error localizedFailureReason], is(equalTo(@"The mapping operation was unable to find any nested object representations at the key paths searched: this, that\nThe representation inputted to the mapper was found to contain nested object representations at the following key paths: user\nThis likely indicates that you have misconfigured the key paths for your mappings."))); } #pragma mark RKMapperOperationDelegate Tests @@ -613,11 +618,9 @@ - (void)testShouldInformTheDelegateWhenMappingBegins { id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKMapperOperationDelegate)]; RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; - NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:mapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:@{ [NSNull null]: mapping }]; [[mockDelegate expect] mapperWillStartMapping:mapper]; mapper.delegate = mockDelegate; [mapper start]; @@ -628,11 +631,9 @@ - (void)testShouldInformTheDelegateWhenMappingEnds { id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKMapperOperationDelegate)]; RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; - NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:mapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:@{ [NSNull null]: mapping }]; [[mockDelegate stub] mapperWillStartMapping:mapper]; [[mockDelegate expect] mapperDidFinishMapping:mapper]; mapper.delegate = mockDelegate; @@ -644,11 +645,9 @@ - (void)testShouldInformTheDelegateWhenCheckingForObjectMappingForKeyPathIsSucce { id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKMapperOperationDelegate)]; RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; - NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:mapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:@{ [NSNull null]: mapping }]; [[mockDelegate expect] mapper:mapper didFindRepresentationOrArrayOfRepresentations:OCMOCK_ANY atKeyPath:nil]; mapper.delegate = mockDelegate; [mapper start]; @@ -662,7 +661,7 @@ - (void)testShouldInformTheDelegateWhenCheckingForObjectMappingForKeyPathIsNotSu [mappingsDictionary setObject:mapping forKey:@"users"]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKMapperOperationDelegate)]; [[mockDelegate expect] mapper:mapper didNotFindRepresentationOrArrayOfRepresentationsAtKeyPath:@"users"]; mapper.delegate = mockDelegate; @@ -676,11 +675,9 @@ - (void)testShouldNotifyTheDelegateWhenItDidMapAnObject RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKAttributeMapping *nameMapping = [RKAttributeMapping attributeMappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addPropertyMapping:nameMapping]; - NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:mapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:@{ [NSNull null]: mapping }]; [[mockDelegate expect] mapper:mapper didFinishMappingOperation:OCMOCK_ANY forKeyPath:nil]; mapper.delegate = mockDelegate; [mapper start]; @@ -698,11 +695,9 @@ - (void)testShouldNotifyTheDelegateWhenItFailedToMapAnObject id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKMapperOperationDelegate)]; RKObjectMapping *mapping = [RKObjectMapping mappingForClass:NSClassFromString(@"OCPartialMockObject")]; [mapping addAttributeMappingsFromArray:@[@"name"]]; - NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:mapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:@{ [NSNull null]: mapping }]; RKTestUser *exampleUser = [RKTestUser new]; id mockObject = [OCMockObject partialMockForObject:exampleUser]; [[[mockObject expect] andCall:@selector(fakeValidateValue:forKeyPath:error:) onObject:self] validateValue:(id __autoreleasing *)[OCMArg anyPointer] forKeyPath:OCMOCK_ANY error:(NSError * __autoreleasing *)[OCMArg anyPointer]]; @@ -743,7 +738,7 @@ - (void)testShouldConsiderADictionaryContainingOnlyNullValuesForKeysMappable RKAttributeMapping *nameMapping = [RKAttributeMapping attributeMappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addPropertyMapping:nameMapping]; - NSMutableDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:[NSNull null], @"name", nil]; + NSDictionary *dictionary = @{ @"name": [NSNull null] }; RKTestUser *user = [RKTestUser user]; RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; @@ -1288,6 +1283,146 @@ - (void)testMappingToAnNSDataAttributeUsingKeyedArchiver expect(decodedDictionary).to.equal(expectedValue); } +- (void)testShouldMapNSDateDistantFutureDateStringToADate +{ + [RKObjectMapping resetDefaultDateFormatters]; + + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + RKAttributeMapping *birthDateMapping = [RKAttributeMapping attributeMappingFromKeyPath:@"birthdate" toKeyPath:@"birthDate"]; + [mapping addPropertyMapping:birthDateMapping]; + + NSMutableDictionary *dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; + [dictionary setObject:@"3001-01-01T00:00:00Z" forKey:@"birthdate"]; + RKTestUser *user = [RKTestUser user]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + NSError *error = nil; + [operation performMapping:&error]; + + NSDateFormatter *dateFormatter = [NSDateFormatter new]; + dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + [dateFormatter setDateFormat:@"MM/dd/yyyy"]; + assertThat([dateFormatter stringFromDate:user.birthDate], is(equalTo(@"01/01/3001"))); +} + +- (void)testMappingASingularValueToAnArray +{ + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"favoriteColors" ]]; + + NSDictionary *dictionary = @{ @"favoriteColors": @"Blue" }; + RKTestUser *user = [RKTestUser user]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + NSError *error = nil; + [operation performMapping:&error]; + + assertThat(user.favoriteColors, is(equalTo(@[ @"Blue" ]))); +} + +- (void)testMappingASingularValueToASet +{ + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"friendsSet" ]]; + + NSDictionary *dictionary = @{ @"friendsSet": @"Jeff" }; + RKTestUser *user = [RKTestUser user]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + NSError *error = nil; + [operation performMapping:&error]; + + assertThat(user.friendsSet, is(equalTo([NSSet setWithObject:@"Jeff" ]))); +} + +- (void)testMappingASingularValueToAnOrderedSet +{ + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"friendsOrderedSet" ]]; + + NSDictionary *dictionary = @{ @"friendsOrderedSet": @"Jeff" }; + RKTestUser *user = [RKTestUser user]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + NSError *error = nil; + [operation performMapping:&error]; + + assertThat(user.friendsOrderedSet, is(equalTo([NSOrderedSet orderedSetWithObject:@"Jeff" ]))); +} + +- (void)testTypeTransformationAtKeyPath +{ + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + RKAttributeMapping *websiteMapping = [RKAttributeMapping attributeMappingFromKeyPath:@"this.that" toKeyPath:@"address.addressID"]; + [mapping addPropertyMapping:websiteMapping]; + + NSDictionary *dictionary = @{ @"this": @{ @"that": @"12345" }}; + RKTestUser *user = [RKTestUser user]; + RKTestAddress *address = [RKTestAddress new]; + user.address = address; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + NSError *error = nil; + [operation performMapping:&error]; + + assertThat(user.address.addressID, is(instanceOf([NSNumber class]))); + assertThat(user.address.addressID, is(equalTo(@(12345)))); +} + +- (void)testThatAttributeMappingToAPrimitiveValueFromNullDoesNotCrash +{ + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"age" ]]; + + NSDictionary *dictionary = @{ @"age": [NSNull null] }; + RKTestUser *user = [RKTestUser user]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + NSError *error = nil; + [operation performMapping:&error]; + + expect(user.age).to.equal(0); +} + +- (void)testThatAttributeMappingToAPrimitiveValueFromUnexpectedObjectTypeDoesNotCrash +{ + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"age" ]]; + + NSDictionary *dictionary = @{ @"age": @{ @"wrong": @"data" }}; + RKTestUser *user = [RKTestUser user]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + NSError *error = nil; + [operation performMapping:&error]; + + expect(user.age).to.equal(0); +} + +- (void)testThatMappingNullValueToTransformablePropertyDoesNotGenerateWarning +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + [humanMapping addAttributeMappingsFromArray:@[ @"catIDs" ]]; + + NSDictionary *dictionary = @{ @"catsIDs": [NSNull null] }; + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext]; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:human mapping:humanMapping]; + RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext cache:nil]; + operation.dataSource = dataSource; + NSError *error = nil; + [operation performMapping:&error]; + + expect(human.catIDs).to.beNil(); +} + #pragma mark - Relationship Mapping - (void)testShouldMapANestedObject @@ -1306,7 +1441,7 @@ - (void)testShouldMapANestedObject mapper.mappingOperationDataSource = [RKObjectMappingOperationDataSource new]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKTestUser *user = [RKTestUser user]; - BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; + BOOL success = [mapper mapRepresentation:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; assertThatBool(success, is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"Blake Watters"))); assertThat(user.address, isNot(nilValue())); @@ -1328,7 +1463,7 @@ - (void)testShouldMapANestedObjectToCollection mapper.mappingOperationDataSource = [RKObjectMappingOperationDataSource new]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKTestUser *user = [RKTestUser user]; - BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; + BOOL success = [mapper mapRepresentation:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; assertThatBool(success, is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"Blake Watters"))); assertThat(user.friends, isNot(nilValue())); @@ -1351,7 +1486,7 @@ - (void)testShouldMapANestedObjectToOrderedSetCollection mapper.mappingOperationDataSource = [RKObjectMappingOperationDataSource new]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKTestUser *user = [RKTestUser user]; - BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; + BOOL success = [mapper mapRepresentation:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; assertThatBool(success, is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"Blake Watters"))); assertThat(user.friendsOrderedSet, isNot(nilValue())); @@ -1371,7 +1506,7 @@ - (void)testShouldMapANestedObjectCollection mapper.mappingOperationDataSource = [RKObjectMappingOperationDataSource new]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKTestUser *user = [RKTestUser user]; - BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; + BOOL success = [mapper mapRepresentation:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; assertThatBool(success, is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"Blake Watters"))); assertThat(user.friends, isNot(nilValue())); @@ -1393,7 +1528,7 @@ - (void)testShouldMapANestedArrayIntoASet mapper.mappingOperationDataSource = [RKObjectMappingOperationDataSource new]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKTestUser *user = [RKTestUser user]; - BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; + BOOL success = [mapper mapRepresentation:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; assertThatBool(success, is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"Blake Watters"))); assertThat(user.friendsSet, isNot(nilValue())); @@ -1416,7 +1551,7 @@ - (void)testShouldMapANestedArrayIntoAnOrderedSet mapper.mappingOperationDataSource = [RKObjectMappingOperationDataSource new]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKTestUser *user = [RKTestUser user]; - BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; + BOOL success = [mapper mapRepresentation:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; assertThatBool(success, is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"Blake Watters"))); assertThat(user.friendsOrderedSet, isNot(nilValue())); @@ -1448,7 +1583,7 @@ - (void)testShouldNotSetThePropertyWhenTheNestedObjectIsIdentical RKMapperOperation *mapper = [RKMapperOperation new]; mapper.mappingOperationDataSource = [RKObjectMappingOperationDataSource new]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; - [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; + [mapper mapRepresentation:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; } - (void)testSkippingOfIdenticalObjectsInformsDelegate @@ -1508,7 +1643,7 @@ - (void)testShouldNotSetThePropertyWhenTheNestedObjectCollectionIsIdentical id mockUser = [OCMockObject partialMockForObject:user]; [[mockUser reject] setFriends:OCMOCK_ANY]; - [mapper mapFromObject:userInfo toObject:mockUser atKeyPath:@"" usingMapping:userMapping]; + [mapper mapRepresentation:userInfo toObject:mockUser atKeyPath:@"" usingMapping:userMapping]; [mockUser verify]; } @@ -1574,6 +1709,184 @@ - (void)testShouldNotNilOutTheRelationshipIfItIsMissingAndCurrentlyNilOnTheTarge [mockUser verify]; } +#pragma mark Assignment Policies + +- (void)testThatAttemptingToUnionOneToOneRelationshipGeneratesMappingError +{ + RKTestUser *user = [RKTestUser new]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + RKObjectMapping *addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"address" toKeyPath:@"address" withMapping:addressMapping]; + relationshipMapping.assignmentPolicy = RKUnionAssignmentPolicy; + [mapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"address": @{ @"city": @"NYC" } }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect(error).notTo.beNil(); + expect(error.code).to.equal(RKMappingErrorInvalidAssignmentPolicy); + expect([error localizedDescription]).to.equal(@"Invalid assignment policy: cannot union a one-to-one relationship."); +} + +- (void)testUnionAssignmentPolicyWithSet +{ + RKTestUser *user = [RKTestUser new]; + RKTestUser *friend = [RKTestUser new]; + friend.name = @"Jeff"; + user.friendsSet = [NSSet setWithObject:friend]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"friendsSet" toKeyPath:@"friendsSet" withMapping:mapping]; + relationshipMapping.assignmentPolicy = RKUnionAssignmentPolicy; + [mapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"friendsSet": @[ @{ @"name": @"Zach" } ] }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect([user.friendsSet count]).to.equal(2); + NSArray *names = [user.friendsSet valueForKey:@"name"]; + assertThat(names, hasItems(@"Jeff", @"Zach", nil)); +} + +- (void)testUnionAssignmentPolicyWithOrderedSet +{ + RKTestUser *user = [RKTestUser new]; + RKTestUser *friend = [RKTestUser new]; + friend.name = @"Jeff"; + user.friendsOrderedSet = [NSOrderedSet orderedSetWithObject:friend]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"friendsOrderedSet" toKeyPath:@"friendsOrderedSet" withMapping:mapping]; + relationshipMapping.assignmentPolicy = RKUnionAssignmentPolicy; + [mapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"friendsOrderedSet": @[ @{ @"name": @"Zach" } ] }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect([user.friendsOrderedSet count]).to.equal(2); + NSArray *names = [user.friendsOrderedSet valueForKey:@"name"]; + assertThat(names, hasItems(@"Jeff", @"Zach", nil)); +} + +- (void)testUnionAssignmentPolicyWithArray +{ + RKTestUser *user = [RKTestUser new]; + RKTestUser *friend = [RKTestUser new]; + friend.name = @"Jeff"; + user.friends = [NSArray arrayWithObject:friend]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:mapping]; + relationshipMapping.assignmentPolicy = RKUnionAssignmentPolicy; + [mapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"friends": @[ @{ @"name": @"Zach" } ] }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect([user.friends count]).to.equal(2); + NSArray *names = [user.friends valueForKey:@"name"]; + assertThat(names, hasItems(@"Jeff", @"Zach", nil)); +} + +- (void)testReplacementPolicyForUnmanagedRelationship +{ + RKTestUser *user = [RKTestUser new]; + RKTestUser *friend = [RKTestUser new]; + friend.name = @"Jeff"; + user.friends = [NSArray arrayWithObject:friend]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:mapping]; + relationshipMapping.assignmentPolicy = RKReplaceAssignmentPolicy; + [mapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"friends": @[ @{ @"name": @"Zach" } ] }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect([user.friends count]).to.equal(1); + NSArray *names = [user.friends valueForKey:@"name"]; + assertThat(names, hasItems(@"Zach", nil)); +} + +- (void)testReplacmentPolicyForToManyCoreDataRelationshipDeletesExistingValues +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext]; + RKCat *existingCat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext]; + existingCat.name = @"Lola"; + human.cats = [NSSet setWithObject:existingCat]; + + RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + [entityMapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + [catMapping addAttributeMappingsFromArray:@[ @"name" ]]; + catMapping.identificationAttributes = @[ @"name" ]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"cats" toKeyPath:@"cats" withMapping:catMapping]; + relationshipMapping.assignmentPolicy = RKReplaceAssignmentPolicy; + [entityMapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"name": @"Blake", @"cats": @[ @{ @"name": @"Roy" } ] }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:human mapping:entityMapping]; + RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext cache:nil]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect([human.cats count]).to.equal(1); + NSArray *names = [human.cats valueForKey:@"name"]; + assertThat(names, hasItems(@"Roy", nil)); + expect([existingCat isDeleted]).to.equal(YES); +} + +- (void)testReplacmentPolicyForToOneCoreDataRelationshipDeletesExistingValues +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext]; + RKCat *existingCat = [NSEntityDescription insertNewObjectForEntityForName:@"Cat" inManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext]; + existingCat.name = @"Lola"; + human.favoriteCat = existingCat; + + RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore]; + [entityMapping addAttributeMappingsFromArray:@[ @"name" ]]; + RKEntityMapping *catMapping = [RKEntityMapping mappingForEntityForName:@"Cat" inManagedObjectStore:managedObjectStore]; + [catMapping addAttributeMappingsFromArray:@[ @"name" ]]; + catMapping.identificationAttributes = @[ @"name" ]; + RKRelationshipMapping *relationshipMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:@"favoriteCat" toKeyPath:@"favoriteCat" withMapping:catMapping]; + relationshipMapping.assignmentPolicy = RKReplaceAssignmentPolicy; + [entityMapping addPropertyMapping:relationshipMapping]; + + NSDictionary *dictionary = @{ @"name": @"Blake", @"favoriteCat": @{ @"name": @"Roy" } }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:dictionary destinationObject:human mapping:entityMapping]; + RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext cache:nil]; + operation.dataSource = dataSource; + + NSError *error = nil; + [operation performMapping:&error]; + expect(human.favoriteCat.name).to.equal(@"Roy"); + expect([existingCat isDeleted]).to.equal(YES); +} + #pragma mark - RKDynamicMapping - (void)testShouldMapASingleObjectDynamically @@ -1594,10 +1907,10 @@ - (void)testShouldMapASingleObjectDynamically }]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:dynamicMapping forKey:@""]; + [mappingsDictionary setObject:dynamicMapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; Boy *user = [mapper.mappingResult firstObject]; assertThat(user, is(instanceOf([Boy class]))); @@ -1615,10 +1928,10 @@ - (void)testShouldMapASingleObjectDynamicallyWithADeclarativeMatcher [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"type" isEqualTo:@"Girl"]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:dynamicMapping forKey:@""]; + [mappingsDictionary setObject:dynamicMapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; Boy *user = [mapper.mappingResult firstObject]; assertThat(user, is(instanceOf([Boy class]))); @@ -1636,10 +1949,10 @@ - (void)testShouldACollectionOfObjectsDynamically [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"type" isEqualTo:@"Girl"]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:dynamicMapping forKey:@""]; + [mappingsDictionary setObject:dynamicMapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"mixed.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; NSArray *objects = [mapper.mappingResult array]; expect(objects).to.haveCountOf(2); @@ -1663,10 +1976,10 @@ - (void)testShouldMapARelationshipDynamically [boyMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:dynamicMapping]];; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:dynamicMapping forKey:@""]; + [mappingsDictionary setObject:dynamicMapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"friends.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; Boy *blake = [mapper.mappingResult firstObject]; NSArray *friends = blake.friends; @@ -1699,10 +2012,10 @@ - (void)testShouldBeAbleToDeclineMappingAnObjectByReturningANilObjectMapping }]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:dynamicMapping forKey:@""]; + [mappingsDictionary setObject:dynamicMapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"mixed.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; NSArray *boys = [mapper.mappingResult array]; assertThat(boys, hasCountOf(1)); @@ -1731,10 +2044,10 @@ - (void)testShouldBeAbleToDeclineMappingObjectsInARelationshipByReturningANilObj [boyMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:dynamicMapping]];; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:dynamicMapping forKey:@""]; + [mappingsDictionary setObject:dynamicMapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"friends.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; [mapper start]; Boy *blake = [mapper.mappingResult firstObject]; assertThat(blake, is(notNilValue())); @@ -1762,11 +2075,11 @@ - (void)testShouldMapATargetObjectWithADynamicMapping }]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:dynamicMapping forKey:@""]; + [mappingsDictionary setObject:dynamicMapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]; Boy *blake = [Boy new]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; mapper.targetObject = blake; [mapper start]; Boy *user = [mapper.mappingResult firstObject]; @@ -1784,11 +2097,11 @@ - (void)testShouldFailWithAnErrorIfATargetObjectIsProvidedAndTheDynamicMappingRe }]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:dynamicMapping forKey:@""]; + [mappingsDictionary setObject:dynamicMapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]; Boy *blake = [Boy new]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; mapper.targetObject = blake; [mapper start]; Boy *user = [mapper.mappingResult firstObject]; @@ -1810,11 +2123,11 @@ - (void)testShouldFailWithAnErrorIfATargetObjectIsProvidedAndTheDynamicMappingRe }]; NSMutableDictionary *mappingsDictionary = [NSMutableDictionary dictionary]; - [mappingsDictionary setObject:dynamicMapping forKey:@""]; + [mappingsDictionary setObject:dynamicMapping forKey:[NSNull null]]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]; Boy *blake = [Boy new]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:userInfo mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:userInfo mappingsDictionary:mappingsDictionary]; mapper.targetObject = blake; [mapper start]; Boy *user = [mapper.mappingResult firstObject]; @@ -2048,7 +2361,7 @@ - (void)testUpdatingArrayOfExistingCats human2.railsID = [NSNumber numberWithInt:202]; [managedObjectStore.persistentStoreManagedObjectContext save:nil]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:array mappingsDictionary:mappingsDictionary]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:array mappingsDictionary:mappingsDictionary]; RKFetchRequestManagedObjectCache *managedObjectCache = [[RKFetchRequestManagedObjectCache alloc] init]; mapper.mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext cache:managedObjectCache]; @@ -2072,7 +2385,7 @@ - (void)testMappingMultipleKeyPathsAtRootOfObject [mapping2 addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:@"catId" toKeyPath:@"userID"]]; NSDictionary *dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"SameKeyDifferentTargetClasses.json"]; - RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithObject:dictionary mappingsDictionary:@{ @"products": mapping1, @"categories": mapping2 }]; + RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:dictionary mappingsDictionary:@{ @"products": mapping1, @"categories": mapping2 }]; RKObjectMappingOperationDataSource *dataSource = [RKObjectMappingOperationDataSource new]; mapper.mappingOperationDataSource = dataSource; [mapper start]; @@ -2082,4 +2395,23 @@ - (void)testMappingMultipleKeyPathsAtRootOfObject expect([mapper.mappingResult array]).to.haveCountOf(4); } +- (void)testAggregatingPropertyMappingUsingNilKeyPath +{ + NSDictionary *objectRepresentation = @{ @"name": @"Blake", @"latitude": @(125.55), @"longitude": @(200.5) }; + 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).notTo.beNil(); + expect(user.coordinate.latitude).to.equal(125.55); + expect(user.coordinate.longitude).to.equal(200.5); +} + @end diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingTest.m index 41d818f4a7..6f97a52df9 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingTest.m @@ -219,4 +219,18 @@ - (void)testDefaultSourceToDestinationKeyTransformationBlock expect([mapping.propertyMappingsByDestinationKeyPath allKeys]).to.equal(expectedNames); } +- (void)testBreakageOfRecursiveInverseCyclicGraphs +{ + RKObjectMapping *parentMapping = [RKObjectMapping mappingForClass:[NSObject class]]; + [parentMapping addAttributeMappingsFromDictionary:@{ @"first_name": @"firstName", @"last_name": @"lastName" }]; + RKObjectMapping *childMapping = [RKObjectMapping mappingForClass:[NSObject class]]; + [childMapping addAttributeMappingsFromDictionary:@{ @"first_name": @"firstName", @"last_name": @"lastName" }]; + [parentMapping addRelationshipMappingWithSourceKeyPath:@"children" mapping:childMapping]; + [childMapping addRelationshipMappingWithSourceKeyPath:@"parents" mapping:parentMapping]; + RKObjectMapping *inverseMapping = [parentMapping inverseMapping]; + expect([inverseMapping propertyMappingsBySourceKeyPath][@"firstName"]).notTo.beNil(); + expect([inverseMapping propertyMappingsBySourceKeyPath][@"lastName"]).notTo.beNil(); + expect([inverseMapping propertyMappingsBySourceKeyPath][@"children"]).notTo.beNil(); +} + @end diff --git a/Tests/Models/Data Model.xcdatamodel/elements b/Tests/Models/Data Model.xcdatamodel/elements index 96c4ab2413..4a2cdf976b 100644 Binary files a/Tests/Models/Data Model.xcdatamodel/elements and b/Tests/Models/Data Model.xcdatamodel/elements differ diff --git a/Tests/Models/Data Model.xcdatamodel/layout b/Tests/Models/Data Model.xcdatamodel/layout index 95ac823686..6d4db1b784 100644 Binary files a/Tests/Models/Data Model.xcdatamodel/layout and b/Tests/Models/Data Model.xcdatamodel/layout differ diff --git a/Tests/Models/RKChild.h b/Tests/Models/RKChild.h index 960e86f3ad..2ef5a8cb2b 100644 --- a/Tests/Models/RKChild.h +++ b/Tests/Models/RKChild.h @@ -32,5 +32,7 @@ @property (nonatomic, strong) NSNumber *fatherID; @property (nonatomic, strong) RKParent *father; @property (nonatomic, strong) RKParent *mother; +@property (nonatomic, strong) NSArray *friendIDs; +@property (nonatomic, strong) NSSet *friends; @end diff --git a/Tests/Models/RKChild.m b/Tests/Models/RKChild.m index 042bae3b24..39de94f8be 100644 --- a/Tests/Models/RKChild.m +++ b/Tests/Models/RKChild.m @@ -28,5 +28,7 @@ @implementation RKChild @dynamic fatherID; @dynamic father; @dynamic mother; +@dynamic friends; +@dynamic friendIDs; @end diff --git a/Tests/Models/RKTestUser.h b/Tests/Models/RKTestUser.h index bb90c76e4b..0d4a6c4b48 100644 --- a/Tests/Models/RKTestUser.h +++ b/Tests/Models/RKTestUser.h @@ -9,6 +9,11 @@ #import #import "RKTestAddress.h" +@interface RKTestCoordinate : NSObject +@property (nonatomic, assign) double latitude; +@property (nonatomic, assign) double longitude; +@end + @interface RKTestUser : NSObject @property (nonatomic, strong) NSNumber *userID; @@ -28,6 +33,8 @@ @property (nonatomic, strong) NSSet *friendsSet; @property (nonatomic, strong) NSOrderedSet *friendsOrderedSet; @property (nonatomic, strong) NSData *data; +@property (nonatomic, strong) RKTestCoordinate *coordinate; +@property (nonatomic, assign) NSInteger age; + (RKTestUser *)user; diff --git a/Tests/Models/RKTestUser.m b/Tests/Models/RKTestUser.m index 38656abd48..ce1640620c 100644 --- a/Tests/Models/RKTestUser.m +++ b/Tests/Models/RKTestUser.m @@ -9,6 +9,9 @@ #import "RKTestUser.h" #import "RKLog.h" +@implementation RKTestCoordinate +@end + @implementation RKTestUser + (RKTestUser *)user diff --git a/Tests/Server/server.rb b/Tests/Server/server.rb index 24c12304d2..3fca541098 100755 --- a/Tests/Server/server.rb +++ b/Tests/Server/server.rb @@ -98,7 +98,7 @@ def render_fixture(path, options = {}) delete '/humans/success' do status 200 content_type 'application/json' - {:humans => {:status => 'OK'}}.to_json + {:human => {:status => 'OK'}}.to_json end post '/echo_params' do @@ -299,6 +299,11 @@ def render_fixture(path, options = {}) content_type 'application/json' render_fixture('/JSON/ComplexNestedUser.json', :status => 200) end + + get '/posts.json' do + content_type 'application/json' + { :posts => [{:title => 'Post Title', :body => 'Some body.', :tags => [{ :name => 'development' }, { :name => 'restkit' }] }] }.to_json + end # start the server if ruby file executed directly run! if app_file == $0 diff --git a/VERSION b/VERSION index 9b35803401..f2864e8013 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.20.0-pre4 \ No newline at end of file +0.20.0-pre5 \ No newline at end of file