diff --git a/.gitignore b/.gitignore index 3d395db466..1391d1b041 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,4 @@ Docs/API Examples/RKDiscussionBoardExample/discussion_board_backend/public/system/attachments/* -# Thin -log -tmp \ No newline at end of file +test-reports/ \ No newline at end of file diff --git a/Code/CoreData/CoreData.h b/Code/CoreData/CoreData.h index 3e5a4ad152..592f8d7b0f 100644 --- a/Code/CoreData/CoreData.h +++ b/Code/CoreData/CoreData.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/30/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,7 +25,7 @@ #import "RKManagedObjectSeeder.h" #import "RKManagedObjectMapping.h" #import "RKManagedObjectMappingOperation.h" -#import "RKManagedObjectMappingCache.h" +#import "RKManagedObjectCaching.h" #import "RKInMemoryManagedObjectCache.h" #import "RKFetchRequestManagedObjectCache.h" #import "RKSearchableManagedObject.h" diff --git a/Code/CoreData/NSEntityDescription+RKAdditions.h b/Code/CoreData/NSEntityDescription+RKAdditions.h index ea11bcafdc..5da928b455 100644 --- a/Code/CoreData/NSEntityDescription+RKAdditions.h +++ b/Code/CoreData/NSEntityDescription+RKAdditions.h @@ -16,6 +16,14 @@ */ extern NSString * const RKEntityDescriptionPrimaryKeyAttributeUserInfoKey; +/** + The substitution variable used in predicateForPrimaryKeyAttribute. + + **Value**: @"PRIMARY_KEY_VALUE" + @see predicateForPrimaryKeyAttribute + */ +extern NSString * const RKEntityDescriptionPrimaryKeyAttributeValuePredicateSubstitutionVariable; + /** Provides extensions to NSEntityDescription for various common tasks. */ @@ -34,6 +42,52 @@ extern NSString * const RKEntityDescriptionPrimaryKeyAttributeUserInfoKey; Programmatically configured values take precedence over the user info dictionary. */ -@property(nonatomic, retain) NSString *primaryKeyAttribute; +@property (nonatomic, retain) NSString *primaryKeyAttributeName; + +/** + The attribute description object for the attribute designated as the primary key for the receiver. + */ +@property (nonatomic, readonly) NSAttributeDescription *primaryKeyAttribute; + +/** + The class representing the value of the attribute designated as the primary key for the receiver. + */ +@property (nonatomic, readonly) Class primaryKeyAttributeClass; + +/** + Returns a cached predicate specifying that the primary key attribute is equal to the $PRIMARY_KEY_VALUE + substitution variable. + + This predicate is cached to avoid parsing overhead during object mapping operations + and must be evaluated using [NSPredicate predicateWithSubstitutionVariables:] + + @return A cached predicate specifying the value of the primary key attribute is equal to the $PRIMARY_KEY_VALUE + substitution variable. + */ +- (NSPredicate *)predicateForPrimaryKeyAttribute; + +/** + Returns a predicate specifying that the value of the primary key attribute is equal to a given + value. This predicate is constructed by evaluating the cached predicate returned by the + predicateForPrimaryKeyAttribute with a dictionary of substitution variables specifying that + $PRIMARY_KEY_VALUE is equal to the given value. + + **NOTE**: This method considers the type of the receiver's primary key attribute when constructing + the predicate. It will coerce the given value into either an NSString or an NSNumber as + appropriate. This behavior is a convenience to avoid annoying issues related to Core Data's + handling of predicates for NSString and NSNumber types that were not appropriately casted. + + @return A predicate speciying that the value of the primary key attribute is equal to a given value. + */ +- (NSPredicate *)predicateForPrimaryKeyAttributeWithValue:(id)value; + +/** + Coerces the given value into the class representing the primary key. Currently support NSString + and NSNumber coercsions. + + @bug **NOTE** This API is temporary and will be deprecated and replaced. + @since 0.10.1 + */ +- (id)coerceValueForPrimaryKey:(id)primaryKeyValue; @end diff --git a/Code/CoreData/NSEntityDescription+RKAdditions.m b/Code/CoreData/NSEntityDescription+RKAdditions.m index 3d40fa9fed..c244f4aeee 100644 --- a/Code/CoreData/NSEntityDescription+RKAdditions.m +++ b/Code/CoreData/NSEntityDescription+RKAdditions.m @@ -10,29 +10,98 @@ #import "NSEntityDescription+RKAdditions.h" NSString * const RKEntityDescriptionPrimaryKeyAttributeUserInfoKey = @"primaryKeyAttribute"; -static char primaryKeyAttributeKey; +NSString * const RKEntityDescriptionPrimaryKeyAttributeValuePredicateSubstitutionVariable = @"PRIMARY_KEY_VALUE"; + +static char primaryKeyAttributeNameKey, primaryKeyPredicateKey; @implementation NSEntityDescription (RKAdditions) -- (NSString *)primaryKeyAttribute +- (void)setPredicateForPrimaryKeyAttribute:(NSString *)primaryKeyAttribute +{ + NSPredicate *predicate = (primaryKeyAttribute) ? [NSPredicate predicateWithFormat:@"%K == $PRIMARY_KEY_VALUE", primaryKeyAttribute] : nil; + objc_setAssociatedObject(self, + &primaryKeyPredicateKey, + predicate, + OBJC_ASSOCIATION_RETAIN); +} + +#pragma mark - Public + +- (NSAttributeDescription *)primaryKeyAttribute +{ + return [[self attributesByName] valueForKey:[self primaryKeyAttributeName]]; +} + +- (Class)primaryKeyAttributeClass +{ + NSAttributeDescription *attributeDescription = [self primaryKeyAttribute]; + if (attributeDescription) { + return NSClassFromString(attributeDescription.attributeValueClassName); + } + + return nil; +} + +- (NSString *)primaryKeyAttributeName { // Check for an associative object reference - NSString *primaryKeyAttribute = (NSString *) objc_getAssociatedObject(self, &primaryKeyAttributeKey); + NSString *primaryKeyAttribute = (NSString *) objc_getAssociatedObject(self, &primaryKeyAttributeNameKey); // Fall back to the userInfo dictionary if (! primaryKeyAttribute) { primaryKeyAttribute = [self.userInfo valueForKey:RKEntityDescriptionPrimaryKeyAttributeUserInfoKey]; + + // If we have loaded from the user info, ensure we have a predicate + if (! [self predicateForPrimaryKeyAttribute]) { + [self setPredicateForPrimaryKeyAttribute:primaryKeyAttribute]; + } } return primaryKeyAttribute; } -- (void)setPrimaryKeyAttribute:(NSString *)primaryKeyAttribute +- (void)setPrimaryKeyAttributeName:(NSString *)primaryKeyAttributeName { objc_setAssociatedObject(self, - &primaryKeyAttributeKey, - primaryKeyAttribute, + &primaryKeyAttributeNameKey, + primaryKeyAttributeName, OBJC_ASSOCIATION_RETAIN); + [self setPredicateForPrimaryKeyAttribute:primaryKeyAttributeName]; +} + +- (NSPredicate *)predicateForPrimaryKeyAttribute +{ + return (NSPredicate *) objc_getAssociatedObject(self, &primaryKeyPredicateKey); +} + +- (id)coerceValueForPrimaryKey:(id)primaryKeyValue +{ + id searchValue = primaryKeyValue; + Class theClass = [self primaryKeyAttributeClass]; + if (theClass) { + // TODO: This coercsion behavior should be pluggable and reused from the mapper + if ([theClass isSubclassOfClass:[NSNumber class]] && ![searchValue isKindOfClass:[NSNumber class]]) { + // Handle NSString -> NSNumber + if ([searchValue isKindOfClass:[NSString class]]) { + searchValue = [NSNumber numberWithDouble:[searchValue doubleValue]]; + } + } else if ([theClass isSubclassOfClass:[NSString class]] && ![searchValue isKindOfClass:[NSString class]]) { + // Coerce to string + if ([searchValue respondsToSelector:@selector(stringValue)]) { + searchValue = [searchValue stringValue]; + } + } + } + + return searchValue; +} + +- (NSPredicate *)predicateForPrimaryKeyAttributeWithValue:(id)value +{ + id substitutionValue = [self coerceValueForPrimaryKey:value]; + NSDictionary *variables = [NSDictionary dictionaryWithObject:substitutionValue + forKey:RKEntityDescriptionPrimaryKeyAttributeValuePredicateSubstitutionVariable]; + return [[self predicateForPrimaryKeyAttribute] predicateWithSubstitutionVariables:variables]; } @end diff --git a/Code/CoreData/NSManagedObject+ActiveRecord.h b/Code/CoreData/NSManagedObject+ActiveRecord.h index c4603ed281..1d05f63e15 100644 --- a/Code/CoreData/NSManagedObject+ActiveRecord.h +++ b/Code/CoreData/NSManagedObject+ActiveRecord.h @@ -28,13 +28,13 @@ @interface NSManagedObject (ActiveRecord) /** - * The NSEntityDescription for the Subclass - * defaults to the subclass className, may be overridden + * The NSEntityDescription for the Subclass + * defaults to the subclass className, may be overridden */ + (NSEntityDescription*)entity; /** - * Returns an initialized NSFetchRequest for the entity, with no predicate + * Returns an initialized NSFetchRequest for the entity, with no predicate */ + (NSFetchRequest*)fetchRequest; @@ -94,7 +94,7 @@ + (NSUInteger)count DEPRECATED_ATTRIBUTE; /** - * Creates a new managed object and inserts it into the managedObjectContext. + * Creates a new managed object and inserts it into the managedObjectContext. */ + (id)object; diff --git a/Code/CoreData/NSManagedObject+ActiveRecord.m b/Code/CoreData/NSManagedObject+ActiveRecord.m index 172d330a53..a246f020ee 100644 --- a/Code/CoreData/NSManagedObject+ActiveRecord.m +++ b/Code/CoreData/NSManagedObject+ActiveRecord.m @@ -52,84 +52,84 @@ @implementation NSManagedObject (ActiveRecord) #pragma mark - RKManagedObject methods + (NSEntityDescription*)entity { - NSString* className = [NSString stringWithCString:class_getName([self class]) encoding:NSASCIIStringEncoding]; - return [NSEntityDescription entityForName:className inManagedObjectContext:[NSManagedObjectContext contextForCurrentThread]]; + NSString* className = [NSString stringWithCString:class_getName([self class]) encoding:NSASCIIStringEncoding]; + return [NSEntityDescription entityForName:className inManagedObjectContext:[NSManagedObjectContext contextForCurrentThread]]; } + (NSFetchRequest*)fetchRequest { - NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease]; - NSEntityDescription *entity = [self entity]; - [fetchRequest setEntity:entity]; - return fetchRequest; + NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease]; + NSEntityDescription *entity = [self entity]; + [fetchRequest setEntity:entity]; + return fetchRequest; } + (NSArray*)objectsWithFetchRequest:(NSFetchRequest*)fetchRequest { - NSError* error = nil; - NSArray* objects = [[NSManagedObjectContext contextForCurrentThread] executeFetchRequest:fetchRequest error:&error]; - if (objects == nil) { - RKLogError(@"Error: %@", [error localizedDescription]); - } - return objects; + NSError* error = nil; + NSArray* objects = [[NSManagedObjectContext contextForCurrentThread] executeFetchRequest:fetchRequest error:&error]; + if (objects == nil) { + RKLogError(@"Error: %@", [error localizedDescription]); + } + return objects; } + (NSUInteger)countOfObjectsWithFetchRequest:(NSFetchRequest*)fetchRequest { - NSError* error = nil; - NSUInteger objectCount = [[NSManagedObjectContext contextForCurrentThread] countForFetchRequest:fetchRequest error:&error]; - if (objectCount == NSNotFound) { - RKLogError(@"Error: %@", [error localizedDescription]); - } - return objectCount; + NSError* error = nil; + NSUInteger objectCount = [[NSManagedObjectContext contextForCurrentThread] countForFetchRequest:fetchRequest error:&error]; + if (objectCount == NSNotFound) { + RKLogError(@"Error: %@", [error localizedDescription]); + } + return objectCount; } + (NSArray*)objectsWithFetchRequests:(NSArray*)fetchRequests { - NSMutableArray* mutableObjectArray = [[NSMutableArray alloc] init]; - for (NSFetchRequest* fetchRequest in fetchRequests) { - [mutableObjectArray addObjectsFromArray:[self objectsWithFetchRequest:fetchRequest]]; - } - NSArray* objects = [NSArray arrayWithArray:mutableObjectArray]; - [mutableObjectArray release]; - return objects; + NSMutableArray* mutableObjectArray = [[NSMutableArray alloc] init]; + for (NSFetchRequest* fetchRequest in fetchRequests) { + [mutableObjectArray addObjectsFromArray:[self objectsWithFetchRequest:fetchRequest]]; + } + NSArray* objects = [NSArray arrayWithArray:mutableObjectArray]; + [mutableObjectArray release]; + return objects; } + (id)objectWithFetchRequest:(NSFetchRequest*)fetchRequest { - [fetchRequest setFetchLimit:1]; - NSArray* objects = [self objectsWithFetchRequest:fetchRequest]; - if ([objects count] == 0) { - return nil; - } else { - return [objects objectAtIndex:0]; - } + [fetchRequest setFetchLimit:1]; + NSArray* objects = [self objectsWithFetchRequest:fetchRequest]; + if ([objects count] == 0) { + return nil; + } else { + return [objects objectAtIndex:0]; + } } + (NSArray*)objectsWithPredicate:(NSPredicate*)predicate { - NSFetchRequest* fetchRequest = [self fetchRequest]; - [fetchRequest setPredicate:predicate]; - return [self objectsWithFetchRequest:fetchRequest]; + NSFetchRequest* fetchRequest = [self fetchRequest]; + [fetchRequest setPredicate:predicate]; + return [self objectsWithFetchRequest:fetchRequest]; } + (id)objectWithPredicate:(NSPredicate*)predicate { - NSFetchRequest* fetchRequest = [self fetchRequest]; - [fetchRequest setPredicate:predicate]; - return [self objectWithFetchRequest:fetchRequest]; + NSFetchRequest* fetchRequest = [self fetchRequest]; + [fetchRequest setPredicate:predicate]; + return [self objectWithFetchRequest:fetchRequest]; } + (NSArray*)allObjects { - return [self objectsWithPredicate:nil]; + return [self objectsWithPredicate:nil]; } + (NSUInteger)count:(NSError**)error { - NSFetchRequest* fetchRequest = [self fetchRequest]; - return [[NSManagedObjectContext contextForCurrentThread] countForFetchRequest:fetchRequest error:error]; + NSFetchRequest* fetchRequest = [self fetchRequest]; + return [[NSManagedObjectContext contextForCurrentThread] countForFetchRequest:fetchRequest error:error]; } + (NSUInteger)count { - NSError *error = nil; - return [self count:&error]; + NSError *error = nil; + return [self count:&error]; } + (id)object { - id object = [[self alloc] initWithEntity:[self entity] insertIntoManagedObjectContext:[NSManagedObjectContext contextForCurrentThread]]; - return [object autorelease]; + id object = [[self alloc] initWithEntity:[self entity] insertIntoManagedObjectContext:[NSManagedObjectContext contextForCurrentThread]]; + return [object autorelease]; } - (BOOL)isNew { @@ -138,14 +138,13 @@ - (BOOL)isNew { } + (id)findByPrimaryKey:(id)primaryKeyValue inContext:(NSManagedObjectContext *)context { - NSEntityDescription *entity = [self entityDescriptionInContext:context]; - NSString *primaryKeyAttribute = entity.primaryKeyAttribute; - if (! primaryKeyAttribute) { - RKLogWarning(@"Attempt to findByPrimaryKey for entity with nil primaryKeyAttribute. Set the primaryKeyAttribute and try again! %@", entity); + NSPredicate *predicate = [[self entityDescriptionInContext:context] predicateForPrimaryKeyAttributeWithValue:primaryKeyValue]; + if (! predicate) { + RKLogWarning(@"Attempt to findByPrimaryKey for entity with nil primaryKeyAttribute. Set the primaryKeyAttributeName and try again! %@", self); return nil; } - - return [self findFirstByAttribute:primaryKeyAttribute withValue:primaryKeyValue inContext:context]; + + return [self findFirstWithPredicate:predicate inContext:context]; } + (id)findByPrimaryKey:(id)primaryKeyValue { @@ -160,91 +159,91 @@ + (NSManagedObjectContext*)currentContext; { + (void)setDefaultBatchSize:(NSUInteger)newBatchSize { - @synchronized(defaultBatchSize) - { - defaultBatchSize = [NSNumber numberWithUnsignedInteger:newBatchSize]; - } + @synchronized(defaultBatchSize) + { + defaultBatchSize = [NSNumber numberWithUnsignedInteger:newBatchSize]; + } } + (NSInteger)defaultBatchSize { - if (defaultBatchSize == nil) - { - [self setDefaultBatchSize:kActiveRecordDefaultBatchSize]; - } - return [defaultBatchSize integerValue]; + if (defaultBatchSize == nil) + { + [self setDefaultBatchSize:kActiveRecordDefaultBatchSize]; + } + return [defaultBatchSize integerValue]; } + (void)handleErrors:(NSError *)error { - if (error) - { - NSDictionary *userInfo = [error userInfo]; - for (NSArray *detailedError in [userInfo allValues]) - { - if ([detailedError isKindOfClass:[NSArray class]]) - { - for (NSError *e in detailedError) - { - if ([e respondsToSelector:@selector(userInfo)]) - { - RKLogError(@"Error Details: %@", [e userInfo]); - } - else - { - RKLogError(@"Error Details: %@", e); - } - } - } - else - { - RKLogError(@"Error: %@", detailedError); - } - } - RKLogError(@"Error Domain: %@", [error domain]); - RKLogError(@"Recovery Suggestion: %@", [error localizedRecoverySuggestion]); - } + if (error) + { + NSDictionary *userInfo = [error userInfo]; + for (NSArray *detailedError in [userInfo allValues]) + { + if ([detailedError isKindOfClass:[NSArray class]]) + { + for (NSError *e in detailedError) + { + if ([e respondsToSelector:@selector(userInfo)]) + { + RKLogError(@"Error Details: %@", [e userInfo]); + } + else + { + RKLogError(@"Error Details: %@", e); + } + } + } + else + { + RKLogError(@"Error: %@", detailedError); + } + } + RKLogError(@"Error Domain: %@", [error domain]); + RKLogError(@"Recovery Suggestion: %@", [error localizedRecoverySuggestion]); + } } + (NSArray *)executeFetchRequest:(NSFetchRequest *)request inContext:(NSManagedObjectContext *)context { - NSError *error = nil; + NSError *error = nil; - NSArray *results = [context executeFetchRequest:request error:&error]; - [self handleErrors:error]; - return results; + NSArray *results = [context executeFetchRequest:request error:&error]; + [self handleErrors:error]; + return results; } + (NSArray *)executeFetchRequest:(NSFetchRequest *)request { - return [self executeFetchRequest:request inContext:[self currentContext]]; + return [self executeFetchRequest:request inContext:[self currentContext]]; } + (id)executeFetchRequestAndReturnFirstObject:(NSFetchRequest *)request inContext:(NSManagedObjectContext *)context { - [request setFetchLimit:1]; + [request setFetchLimit:1]; - NSArray *results = [self executeFetchRequest:request inContext:context]; - if ([results count] == 0) - { - return nil; - } - return [results objectAtIndex:0]; + NSArray *results = [self executeFetchRequest:request inContext:context]; + if ([results count] == 0) + { + return nil; + } + return [results objectAtIndex:0]; } + (id)executeFetchRequestAndReturnFirstObject:(NSFetchRequest *)request { - return [self executeFetchRequestAndReturnFirstObject:request inContext:[self currentContext]]; + return [self executeFetchRequestAndReturnFirstObject:request inContext:[self currentContext]]; } #if TARGET_OS_IPHONE + (void)performFetch:(NSFetchedResultsController *)controller { - NSError *error = nil; - if (![controller performFetch:&error]) - { - [self handleErrors:error]; - } + NSError *error = nil; + if (![controller performFetch:&error]) + { + [self handleErrors:error]; + } } #endif @@ -256,84 +255,84 @@ + (NSEntityDescription *)entityDescriptionInContext:(NSManagedObjectContext *)co + (NSEntityDescription *)entityDescription { - return [self entityDescriptionInContext:[self currentContext]]; + return [self entityDescriptionInContext:[self currentContext]]; } + (NSArray *)propertiesNamed:(NSArray *)properties { - NSEntityDescription *description = [self entityDescription]; - NSMutableArray *propertiesWanted = [NSMutableArray array]; - - if (properties) - { - NSDictionary *propDict = [description propertiesByName]; - - for (NSString *propertyName in properties) - { - NSPropertyDescription *property = [propDict objectForKey:propertyName]; - if (property) - { - [propertiesWanted addObject:property]; - } - else - { - RKLogError(@"Property '%@' not found in %@ properties for %@", propertyName, [propDict count], NSStringFromClass(self)); - } - } - } - return propertiesWanted; + NSEntityDescription *description = [self entityDescription]; + NSMutableArray *propertiesWanted = [NSMutableArray array]; + + if (properties) + { + NSDictionary *propDict = [description propertiesByName]; + + for (NSString *propertyName in properties) + { + NSPropertyDescription *property = [propDict objectForKey:propertyName]; + if (property) + { + [propertiesWanted addObject:property]; + } + else + { + RKLogError(@"Property '%@' not found in %@ properties for %@", propertyName, [propDict count], NSStringFromClass(self)); + } + } + } + return propertiesWanted; } + (NSArray *)sortAscending:(BOOL)ascending attributes:(id)attributesToSortBy, ... { - NSMutableArray *attributes = [NSMutableArray array]; - - if ([attributesToSortBy isKindOfClass:[NSArray class]]) - { - id attributeName; - va_list variadicArguments; - va_start(variadicArguments, attributesToSortBy); - while ((attributeName = va_arg(variadicArguments, id))!= nil) - { - NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:attributeName ascending:ascending]; - [attributes addObject:sortDescriptor]; - [sortDescriptor release]; - } - va_end(variadicArguments); - - } - else if ([attributesToSortBy isKindOfClass:[NSString class]]) - { - va_list variadicArguments; - va_start(variadicArguments, attributesToSortBy); - [attributes addObject:[[[NSSortDescriptor alloc] initWithKey:attributesToSortBy ascending:ascending] autorelease] ]; - va_end(variadicArguments); - } - - return attributes; + NSMutableArray *attributes = [NSMutableArray array]; + + if ([attributesToSortBy isKindOfClass:[NSArray class]]) + { + id attributeName; + va_list variadicArguments; + va_start(variadicArguments, attributesToSortBy); + while ((attributeName = va_arg(variadicArguments, id))!= nil) + { + NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:attributeName ascending:ascending]; + [attributes addObject:sortDescriptor]; + [sortDescriptor release]; + } + va_end(variadicArguments); + + } + else if ([attributesToSortBy isKindOfClass:[NSString class]]) + { + va_list variadicArguments; + va_start(variadicArguments, attributesToSortBy); + [attributes addObject:[[[NSSortDescriptor alloc] initWithKey:attributesToSortBy ascending:ascending] autorelease] ]; + va_end(variadicArguments); + } + + return attributes; } + (NSArray *)ascendingSortDescriptors:(id)attributesToSortBy, ... { - return [self sortAscending:YES attributes:attributesToSortBy]; + return [self sortAscending:YES attributes:attributesToSortBy]; } + (NSArray *)descendingSortDescriptors:(id)attributesToSortyBy, ... { - return [self sortAscending:NO attributes:attributesToSortyBy]; + return [self sortAscending:NO attributes:attributesToSortyBy]; } + (NSFetchRequest *)createFetchRequestInContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease]; - [request setEntity:[self entityDescriptionInContext:context]]; + NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease]; + [request setEntity:[self entityDescriptionInContext:context]]; - return request; + return request; } + (NSFetchRequest *)createFetchRequest { - return [self createFetchRequestInContext:[self currentContext]]; + return [self createFetchRequestInContext:[self currentContext]]; } #pragma mark - @@ -341,34 +340,34 @@ + (NSFetchRequest *)createFetchRequest + (NSNumber *)numberOfEntitiesWithContext:(NSManagedObjectContext *)context { - NSError *error = nil; - NSUInteger count = [context countForFetchRequest:[self createFetchRequestInContext:context] error:&error]; - [self handleErrors:error]; + NSError *error = nil; + NSUInteger count = [context countForFetchRequest:[self createFetchRequestInContext:context] error:&error]; + [self handleErrors:error]; - return [NSNumber numberWithUnsignedInteger:count]; + return [NSNumber numberWithUnsignedInteger:count]; } + (NSNumber *)numberOfEntities { - return [self numberOfEntitiesWithContext:[self currentContext]]; + return [self numberOfEntitiesWithContext:[self currentContext]]; } + (NSNumber *)numberOfEntitiesWithPredicate:(NSPredicate *)searchTerm inContext:(NSManagedObjectContext *)context { - NSError *error = nil; - NSFetchRequest *request = [self createFetchRequestInContext:context]; - [request setPredicate:searchTerm]; + NSError *error = nil; + NSFetchRequest *request = [self createFetchRequestInContext:context]; + [request setPredicate:searchTerm]; + + NSUInteger count = [context countForFetchRequest:request error:&error]; + [self handleErrors:error]; - NSUInteger count = [context countForFetchRequest:request error:&error]; - [self handleErrors:error]; - - return [NSNumber numberWithUnsignedInteger:count]; + return [NSNumber numberWithUnsignedInteger:count]; } + (NSNumber *)numberOfEntitiesWithPredicate:(NSPredicate *)searchTerm; { - return [self numberOfEntitiesWithPredicate:searchTerm - inContext:[self currentContext]]; + return [self numberOfEntitiesWithPredicate:searchTerm + inContext:[self currentContext]]; } + (BOOL)hasAtLeastOneEntityInContext:(NSManagedObjectContext *)context @@ -385,12 +384,12 @@ + (BOOL)hasAtLeastOneEntity #pragma mark Reqest Helpers + (NSFetchRequest *)requestAll { - return [self createFetchRequestInContext:[self currentContext]]; + return [self createFetchRequestInContext:[self currentContext]]; } + (NSFetchRequest *)requestAllInContext:(NSManagedObjectContext *)context { - return [self createFetchRequestInContext:context]; + return [self createFetchRequestInContext:context]; } + (NSFetchRequest *)requestAllWhere:(NSString *)property isEqualTo:(id)value inContext:(NSManagedObjectContext *)context @@ -435,45 +434,45 @@ + (NSFetchRequest *)requestFirstByAttribute:(NSString *)attribute withValue:(id) + (NSFetchRequest *)requestAllSortedBy:(NSString *)sortTerm ascending:(BOOL)ascending inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [self requestAllInContext:context]; + NSFetchRequest *request = [self requestAllInContext:context]; - NSSortDescriptor *sortBy = [[NSSortDescriptor alloc] initWithKey:sortTerm ascending:ascending]; - [request setSortDescriptors:[NSArray arrayWithObject:sortBy]]; - [sortBy release]; + NSSortDescriptor *sortBy = [[NSSortDescriptor alloc] initWithKey:sortTerm ascending:ascending]; + [request setSortDescriptors:[NSArray arrayWithObject:sortBy]]; + [sortBy release]; - return request; + return request; } + (NSFetchRequest *)requestAllSortedBy:(NSString *)sortTerm ascending:(BOOL)ascending { - return [self requestAllSortedBy:sortTerm - ascending:ascending - inContext:[self currentContext]]; + return [self requestAllSortedBy:sortTerm + ascending:ascending + inContext:[self currentContext]]; } + (NSFetchRequest *)requestAllSortedBy:(NSString *)sortTerm ascending:(BOOL)ascending withPredicate:(NSPredicate *)searchTerm inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [self requestAllInContext:context]; - [request setPredicate:searchTerm]; - [request setIncludesSubentities:NO]; - [request setFetchBatchSize:[self defaultBatchSize]]; + NSFetchRequest *request = [self requestAllInContext:context]; + [request setPredicate:searchTerm]; + [request setIncludesSubentities:NO]; + [request setFetchBatchSize:[self defaultBatchSize]]; - if (sortTerm != nil){ - NSSortDescriptor *sortBy = [[NSSortDescriptor alloc] initWithKey:sortTerm ascending:ascending]; - [request setSortDescriptors:[NSArray arrayWithObject:sortBy]]; - [sortBy release]; - } + if (sortTerm != nil){ + NSSortDescriptor *sortBy = [[NSSortDescriptor alloc] initWithKey:sortTerm ascending:ascending]; + [request setSortDescriptors:[NSArray arrayWithObject:sortBy]]; + [sortBy release]; + } - return request; + return request; } + (NSFetchRequest *)requestAllSortedBy:(NSString *)sortTerm ascending:(BOOL)ascending withPredicate:(NSPredicate *)searchTerm; { - NSFetchRequest *request = [self requestAllSortedBy:sortTerm - ascending:ascending - withPredicate:searchTerm - inContext:[self currentContext]]; - return request; + NSFetchRequest *request = [self requestAllSortedBy:sortTerm + ascending:ascending + withPredicate:searchTerm + inContext:[self currentContext]]; + return request; } @@ -482,44 +481,44 @@ + (NSFetchRequest *)requestAllSortedBy:(NSString *)sortTerm ascending:(BOOL)asce + (NSArray *)findAllInContext:(NSManagedObjectContext *)context { - return [self executeFetchRequest:[self requestAllInContext:context] inContext:context]; + return [self executeFetchRequest:[self requestAllInContext:context] inContext:context]; } + (NSArray *)findAll { - return [self findAllInContext:[self currentContext]]; + return [self findAllInContext:[self currentContext]]; } + (NSArray *)findAllSortedBy:(NSString *)sortTerm ascending:(BOOL)ascending inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [self requestAllSortedBy:sortTerm ascending:ascending inContext:context]; + NSFetchRequest *request = [self requestAllSortedBy:sortTerm ascending:ascending inContext:context]; - return [self executeFetchRequest:request inContext:context]; + return [self executeFetchRequest:request inContext:context]; } + (NSArray *)findAllSortedBy:(NSString *)sortTerm ascending:(BOOL)ascending { - return [self findAllSortedBy:sortTerm - ascending:ascending - inContext:[self currentContext]]; + return [self findAllSortedBy:sortTerm + ascending:ascending + inContext:[self currentContext]]; } + (NSArray *)findAllSortedBy:(NSString *)sortTerm ascending:(BOOL)ascending withPredicate:(NSPredicate *)searchTerm inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [self requestAllSortedBy:sortTerm - ascending:ascending - withPredicate:searchTerm - inContext:context]; + NSFetchRequest *request = [self requestAllSortedBy:sortTerm + ascending:ascending + withPredicate:searchTerm + inContext:context]; - return [self executeFetchRequest:request inContext:context]; + return [self executeFetchRequest:request inContext:context]; } + (NSArray *)findAllSortedBy:(NSString *)sortTerm ascending:(BOOL)ascending withPredicate:(NSPredicate *)searchTerm { - return [self findAllSortedBy:sortTerm - ascending:ascending - withPredicate:searchTerm - inContext:[self currentContext]]; + return [self findAllSortedBy:sortTerm + ascending:ascending + withPredicate:searchTerm + inContext:[self currentContext]]; } #pragma mark - @@ -529,73 +528,73 @@ + (NSArray *)findAllSortedBy:(NSString *)sortTerm ascending:(BOOL)ascending with + (NSFetchedResultsController *)fetchRequestAllGroupedBy:(NSString *)group withPredicate:(NSPredicate *)searchTerm sortedBy:(NSString *)sortTerm ascending:(BOOL)ascending inContext:(NSManagedObjectContext *)context { - NSString *cacheName = nil; + NSString *cacheName = nil; #ifdef STORE_USE_CACHE - cacheName = [NSString stringWithFormat:@"ActiveRecord-Cache-%@", NSStringFromClass(self)]; + cacheName = [NSString stringWithFormat:@"ActiveRecord-Cache-%@", NSStringFromClass(self)]; #endif - NSFetchRequest *request = [self requestAllSortedBy:sortTerm - ascending:ascending - withPredicate:searchTerm - inContext:context]; + NSFetchRequest *request = [self requestAllSortedBy:sortTerm + ascending:ascending + withPredicate:searchTerm + inContext:context]; - NSFetchedResultsController *controller = [[NSFetchedResultsController alloc] initWithFetchRequest:request - managedObjectContext:context - sectionNameKeyPath:group - cacheName:cacheName]; - return [controller autorelease]; + NSFetchedResultsController *controller = [[NSFetchedResultsController alloc] initWithFetchRequest:request + managedObjectContext:context + sectionNameKeyPath:group + cacheName:cacheName]; + return [controller autorelease]; } + (NSFetchedResultsController *)fetchRequestAllGroupedBy:(NSString *)group withPredicate:(NSPredicate *)searchTerm sortedBy:(NSString *)sortTerm ascending:(BOOL)ascending { - return [self fetchRequestAllGroupedBy:group - withPredicate:searchTerm - sortedBy:sortTerm - ascending:ascending - inContext:[self currentContext]]; + return [self fetchRequestAllGroupedBy:group + withPredicate:searchTerm + sortedBy:sortTerm + ascending:ascending + inContext:[self currentContext]]; } + (NSFetchedResultsController *)fetchAllSortedBy:(NSString *)sortTerm ascending:(BOOL)ascending withPredicate:(NSPredicate *)searchTerm groupBy:(NSString *)groupingKeyPath inContext:(NSManagedObjectContext *)context { - NSFetchedResultsController *controller = [self fetchRequestAllGroupedBy:groupingKeyPath - withPredicate:searchTerm - sortedBy:sortTerm - ascending:ascending - inContext:context]; + NSFetchedResultsController *controller = [self fetchRequestAllGroupedBy:groupingKeyPath + withPredicate:searchTerm + sortedBy:sortTerm + ascending:ascending + inContext:context]; - [self performFetch:controller]; - return controller; + [self performFetch:controller]; + return controller; } + (NSFetchedResultsController *)fetchAllSortedBy:(NSString *)sortTerm ascending:(BOOL)ascending withPredicate:(NSPredicate *)searchTerm groupBy:(NSString *)groupingKeyPath { - return [self fetchAllSortedBy:sortTerm - ascending:ascending - withPredicate:searchTerm - groupBy:groupingKeyPath - inContext:[self currentContext]]; + return [self fetchAllSortedBy:sortTerm + ascending:ascending + withPredicate:searchTerm + groupBy:groupingKeyPath + inContext:[self currentContext]]; } + (NSFetchedResultsController *)fetchRequest:(NSFetchRequest *)request groupedBy:(NSString *)group inContext:(NSManagedObjectContext *)context { - NSString *cacheName = nil; + NSString *cacheName = nil; #ifdef STORE_USE_CACHE - cacheName = [NSString stringWithFormat:@"ActiveRecord-Cache-%@", NSStringFromClass([self class])]; + cacheName = [NSString stringWithFormat:@"ActiveRecord-Cache-%@", NSStringFromClass([self class])]; #endif - NSFetchedResultsController *controller = + NSFetchedResultsController *controller = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:context sectionNameKeyPath:group cacheName:cacheName]; [self performFetch:controller]; - return [controller autorelease]; + return [controller autorelease]; } + (NSFetchedResultsController *)fetchRequest:(NSFetchRequest *)request groupedBy:(NSString *)group { - return [self fetchRequest:request - groupedBy:group - inContext:[self currentContext]]; + return [self fetchRequest:request + groupedBy:group + inContext:[self currentContext]]; } #endif @@ -603,43 +602,43 @@ + (NSFetchedResultsController *)fetchRequest:(NSFetchRequest *)request groupedBy + (NSArray *)findAllWithPredicate:(NSPredicate *)searchTerm inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [self createFetchRequestInContext:context]; - [request setPredicate:searchTerm]; + NSFetchRequest *request = [self createFetchRequestInContext:context]; + [request setPredicate:searchTerm]; - return [self executeFetchRequest:request - inContext:context]; + return [self executeFetchRequest:request + inContext:context]; } + (NSArray *)findAllWithPredicate:(NSPredicate *)searchTerm { - return [self findAllWithPredicate:searchTerm - inContext:[self currentContext]]; + return [self findAllWithPredicate:searchTerm + inContext:[self currentContext]]; } + (id)findFirstInContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [self createFetchRequestInContext:context]; + NSFetchRequest *request = [self createFetchRequestInContext:context]; - return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; + return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; } + (id)findFirst { - return [self findFirstInContext:[self currentContext]]; + return [self findFirstInContext:[self currentContext]]; } + (id)findFirstByAttribute:(NSString *)attribute withValue:(id)searchValue inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [self requestFirstByAttribute:attribute withValue:searchValue inContext:context]; + NSFetchRequest *request = [self requestFirstByAttribute:attribute withValue:searchValue inContext:context]; - return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; + return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; } + (id)findFirstByAttribute:(NSString *)attribute withValue:(id)searchValue { - return [self findFirstByAttribute:attribute - withValue:searchValue - inContext:[self currentContext]]; + return [self findFirstByAttribute:attribute + withValue:searchValue + inContext:[self currentContext]]; } + (id)findFirstWithPredicate:(NSPredicate *)searchTerm inContext:(NSManagedObjectContext *)context @@ -656,85 +655,85 @@ + (id)findFirstWithPredicate:(NSPredicate *)searchTerm + (id)findFirstWithPredicate:(NSPredicate *)searchterm sortedBy:(NSString *)property ascending:(BOOL)ascending inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [self requestAllSortedBy:property ascending:ascending withPredicate:searchterm inContext:context]; + NSFetchRequest *request = [self requestAllSortedBy:property ascending:ascending withPredicate:searchterm inContext:context]; - return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; + return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; } + (id)findFirstWithPredicate:(NSPredicate *)searchterm sortedBy:(NSString *)property ascending:(BOOL)ascending { - return [self findFirstWithPredicate:searchterm - sortedBy:property - ascending:ascending - inContext:[self currentContext]]; + return [self findFirstWithPredicate:searchterm + sortedBy:property + ascending:ascending + inContext:[self currentContext]]; } + (id)findFirstWithPredicate:(NSPredicate *)searchTerm andRetrieveAttributes:(NSArray *)attributes inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [self createFetchRequestInContext:context]; - [request setPredicate:searchTerm]; + NSFetchRequest *request = [self createFetchRequestInContext:context]; + [request setPredicate:searchTerm]; - return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; + return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; } + (id)findFirstWithPredicate:(NSPredicate *)searchTerm andRetrieveAttributes:(NSArray *)attributes { - return [self findFirstWithPredicate:searchTerm - andRetrieveAttributes:attributes - inContext:[self currentContext]]; + return [self findFirstWithPredicate:searchTerm + andRetrieveAttributes:attributes + inContext:[self currentContext]]; } + (id)findFirstWithPredicate:(NSPredicate *)searchTerm sortedBy:(NSString *)sortBy ascending:(BOOL)ascending inContext:(NSManagedObjectContext *)context andRetrieveAttributes:(id)attributes, ... { - NSFetchRequest *request = [self requestAllSortedBy:sortBy - ascending:ascending - withPredicate:searchTerm - inContext:context]; + NSFetchRequest *request = [self requestAllSortedBy:sortBy + ascending:ascending + withPredicate:searchTerm + inContext:context]; - return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; + return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; } + (id)findFirstWithPredicate:(NSPredicate *)searchTerm sortedBy:(NSString *)sortBy ascending:(BOOL)ascending andRetrieveAttributes:(id)attributes, ... { - return [self findFirstWithPredicate:searchTerm - sortedBy:sortBy - ascending:ascending + return [self findFirstWithPredicate:searchTerm + sortedBy:sortBy + ascending:ascending inContext:[self currentContext] - andRetrieveAttributes:attributes]; + andRetrieveAttributes:attributes]; } + (NSArray *)findByAttribute:(NSString *)attribute withValue:(id)searchValue inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [self createFetchRequestInContext:context]; + NSFetchRequest *request = [self createFetchRequestInContext:context]; - [request setPredicate:[NSPredicate predicateWithFormat:@"%K = %@", attribute, searchValue]]; + [request setPredicate:[NSPredicate predicateWithFormat:@"%K = %@", attribute, searchValue]]; - return [self executeFetchRequest:request inContext:context]; + return [self executeFetchRequest:request inContext:context]; } + (NSArray *)findByAttribute:(NSString *)attribute withValue:(id)searchValue { - return [self findByAttribute:attribute - withValue:searchValue - inContext:[self currentContext]]; + return [self findByAttribute:attribute + withValue:searchValue + inContext:[self currentContext]]; } + (NSArray *)findByAttribute:(NSString *)attribute withValue:(id)searchValue andOrderBy:(NSString *)sortTerm ascending:(BOOL)ascending inContext:(NSManagedObjectContext *)context { - NSPredicate *searchTerm = [NSPredicate predicateWithFormat:@"%K = %@", attribute, searchValue]; - NSFetchRequest *request = [self requestAllSortedBy:sortTerm ascending:ascending withPredicate:searchTerm inContext:context]; + NSPredicate *searchTerm = [NSPredicate predicateWithFormat:@"%K = %@", attribute, searchValue]; + NSFetchRequest *request = [self requestAllSortedBy:sortTerm ascending:ascending withPredicate:searchTerm inContext:context]; - return [self executeFetchRequest:request]; + return [self executeFetchRequest:request]; } + (NSArray *)findByAttribute:(NSString *)attribute withValue:(id)searchValue andOrderBy:(NSString *)sortTerm ascending:(BOOL)ascending { - return [self findByAttribute:attribute - withValue:searchValue - andOrderBy:sortTerm - ascending:ascending - inContext:[self currentContext]]; + return [self findByAttribute:attribute + withValue:searchValue + andOrderBy:sortTerm + ascending:ascending + inContext:[self currentContext]]; } + (id)createInContext:(NSManagedObjectContext *)context @@ -745,21 +744,21 @@ + (id)createInContext:(NSManagedObjectContext *)context + (id)createEntity { - NSManagedObject *newEntity = [self createInContext:[self currentContext]]; + NSManagedObject *newEntity = [self createInContext:[self currentContext]]; - return newEntity; + return newEntity; } - (BOOL)deleteInContext:(NSManagedObjectContext *)context { - [context deleteObject:self]; - return YES; + [context deleteObject:self]; + return YES; } - (BOOL)deleteEntity { - [self deleteInContext:[[self class] currentContext]]; - return YES; + [self deleteInContext:[[self class] currentContext]]; + return YES; } + (BOOL)truncateAllInContext:(NSManagedObjectContext *)context @@ -780,25 +779,25 @@ + (BOOL)truncateAll + (NSNumber *)maxValueFor:(NSString *)property { - NSManagedObject *obj = [[self class] findFirstByAttribute:property - withValue:[NSString stringWithFormat:@"max(%@)", property]]; + NSManagedObject *obj = [[self class] findFirstByAttribute:property + withValue:[NSString stringWithFormat:@"max(%@)", property]]; - return [obj valueForKey:property]; + return [obj valueForKey:property]; } + (id)objectWithMinValueFor:(NSString *)property inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [[self class] createFetchRequestInContext:context]; + NSFetchRequest *request = [[self class] createFetchRequestInContext:context]; - NSPredicate *searchFor = [NSPredicate predicateWithFormat:@"SELF = %@ AND %K = min(%@)", self, property, property]; - [request setPredicate:searchFor]; + NSPredicate *searchFor = [NSPredicate predicateWithFormat:@"SELF = %@ AND %K = min(%@)", self, property, property]; + [request setPredicate:searchFor]; - return [[self class] executeFetchRequestAndReturnFirstObject:request inContext:context]; + return [[self class] executeFetchRequestAndReturnFirstObject:request inContext:context]; } + (id)objectWithMinValueFor:(NSString *)property { - return [[self class] objectWithMinValueFor:property inContext:[self currentContext]]; + return [[self class] objectWithMinValueFor:property inContext:[self currentContext]]; } @end diff --git a/Code/CoreData/RKEntityByAttributeCache.h b/Code/CoreData/RKEntityByAttributeCache.h new file mode 100644 index 0000000000..9abf9361ec --- /dev/null +++ b/Code/CoreData/RKEntityByAttributeCache.h @@ -0,0 +1,180 @@ +// +// RKEntityByAttributeCache.h +// RestKit +// +// Created by Blake Watters on 5/1/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import + +/** + Instances of RKEntityByAttributeCache provide an in-memory caching mechanism + for managed objects instances of an entity in a managed object context with + the value of one of the object's attributes acting as the cache key. When loaded, + the cache will retrieve all instances of an entity from the store and build a + dictionary mapping values for the given cache key attribute to the managed object + ID for all objects matching the value. The cache can then be used to quickly retrieve + objects by attribute value for the cache key without executing another fetch request + against the managed object context. This can provide a large performance improvement + when a large number of objects are being retrieved using a particular attribute as + the key. + + RKEntityByAttributeCache instances are used by the RKEntityCache to provide + caching for multiple entities at once. + + @see RKEntityCache + */ +@interface RKEntityByAttributeCache : NSObject + +///----------------------------------------------------------------------------- +/// @name Creating a Cache +///----------------------------------------------------------------------------- + +/** + Initializes the receiver with a given entity, attribute, and managed object context. + + @param entity The Core Data entity description for the managed objects being cached. + @param attributeName The name of an attribute within the cached entity that acts as the cache key. + @param managedObjectContext The managed object context the cache retrieves the cached + objects from + @return The receiver, initialized with the given entity, attribute, and managed object + context. + */ +- (id)initWithEntity:(NSEntityDescription *)entity attribute:(NSString *)attributeName managedObjectContext:(NSManagedObjectContext *)context; + +///----------------------------------------------------------------------------- +/// @name Getting Cache Identity +///----------------------------------------------------------------------------- + +/** + The Core Data entity description for the managed objects being cached. + */ +@property (nonatomic, readonly) NSEntityDescription *entity; + +/** + An attribute that is part of the cached entity that acts as the cache key. + */ +@property (nonatomic, readonly) NSString *attribute; + +/** + The managed object context the receiver fetches cached objects from. + */ +@property (nonatomic, readonly) NSManagedObjectContext *managedObjectContext; + +/** + A Boolean value determining if the receiever monitors the managed object context + for changes and updates the cache entries using the notifications emitted. + */ +@property (nonatomic, assign) BOOL monitorsContextForChanges; + +///----------------------------------------------------------------------------- +/// @name Loading and Flushing the Cache +///----------------------------------------------------------------------------- + +/** + Loads the cache by finding all instances of the configured entity and building + an association between the value of the cached attribute's value and the + managed object ID for the object. + */ +- (void)load; + +/** + Flushes the cache by releasing all cache attribute value to managed object ID + associations. + */ +- (void)flush; + +///----------------------------------------------------------------------------- +/// @name Inspecting Cache State +///----------------------------------------------------------------------------- + +/** + A Boolean value indicating if the cache has loaded associations between cache + attribute values and managed object ID's. + */ +- (BOOL)isLoaded; + +/** + Returns a count of the total number of cached objects. + */ +- (NSUInteger)count; + +/** + Returns the total number of cached objects with a given value for + the attribute acting as the cache key. + + @param attributeValue The value for the cache key attribute to retrieve + a count of the objects with a matching value. + @return The number of objects in the cache with the given value for the cache + attribute of the receiver. + */ +- (NSUInteger)countWithAttributeValue:(id)attributeValue; + +/** + Returns the number of unique attribute values contained within the receiver. + + @return The number of unique attribute values within the receiver. + */ +- (NSUInteger)countOfAttributeValues; + +/** + Returns a Boolean value that indicates whether a given object is present + in the cache. + + @param object An object. + @return YES if object is present in the cache, otherwise NO. + */ +- (BOOL)containsObject:(NSManagedObject *)object; + +/** + Returns a Boolean value that indicates whether one of more objects is present + in the cache with a given value of the cache key attribute. + + @param attributeValue The value with which to check the cache for objects with + a matching value. + @return YES if one or more objects with the given value for the cache key + attribute is present in the cache, otherwise NO. + */ +- (BOOL)containsObjectWithAttributeValue:(id)attributeValue; + +/** + Returns the first object with a matching value for the cache key attribute. + + @param attributeValue A value for the cache key attribute. + @return An object with the value of attribute matching attributeValue or nil. + */ +- (NSManagedObject *)objectWithAttributeValue:(id)attributeValue; + +/** + Returns the collection of objects with a matching value for the cache key attribute. + + @param attributeValue A value for the cache key attribute. + @return An array of objects with the value of attribute matching attributeValue or + an empty array. + */ +- (NSArray *)objectsWithAttributeValue:(id)attributeValue; + +///----------------------------------------------------------------------------- +/// @name Managing Cached Objects +///----------------------------------------------------------------------------- + +/** + Adds a managed object to the cache. + + The object must be an instance of the cached entity. + + @param object The managed object to add to the cache. + */ +- (void)addObject:(NSManagedObject *)object; + +/** + Removes a managed object from the cache. + + The object must be an instance of the cached entity. + + @param object The managed object to remove from the cache. + */ +- (void)removeObject:(NSManagedObject *)object; + +@end diff --git a/Code/CoreData/RKEntityByAttributeCache.m b/Code/CoreData/RKEntityByAttributeCache.m new file mode 100644 index 0000000000..4a64204c04 --- /dev/null +++ b/Code/CoreData/RKEntityByAttributeCache.m @@ -0,0 +1,279 @@ +// +// RKEntityByAttributeCache.m +// RestKit +// +// Created by Blake Watters on 5/1/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#if TARGET_OS_IPHONE +#import +#endif + +#import "RKEntityByAttributeCache.h" +#import "RKLog.h" +#import "RKObjectPropertyInspector.h" +#import "RKObjectPropertyInspector+CoreData.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitCoreDataCache + +@interface RKEntityByAttributeCache () +@property (nonatomic, retain) NSMutableDictionary *attributeValuesToObjectIDs; +@end + +@implementation RKEntityByAttributeCache + +@synthesize entity = _entity; +@synthesize attribute = _attribute; +@synthesize managedObjectContext = _managedObjectContext; +@synthesize attributeValuesToObjectIDs = _attributeValuesToObjectIDs; +@synthesize monitorsContextForChanges = _monitorsContextForChanges; + +- (id)initWithEntity:(NSEntityDescription *)entity attribute:(NSString *)attributeName managedObjectContext:(NSManagedObjectContext *)context +{ + self = [self init]; + if (self) { + _entity = [entity retain]; + _attribute = [attributeName retain]; + _managedObjectContext = [context retain]; + _monitorsContextForChanges = YES; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(managedObjectContextDidChange:) + name:NSManagedObjectContextObjectsDidChangeNotification + object:context]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(managedObjectContextDidSave:) + name:NSManagedObjectContextDidSaveNotification + object:context]; +#if TARGET_OS_IPHONE + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didReceiveMemoryWarning:) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; +#endif + } + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [_entity release]; + [_attribute release]; + [_managedObjectContext release]; + [_attributeValuesToObjectIDs release]; + + [super dealloc]; +} + +- (NSUInteger)count +{ + return [[[self.attributeValuesToObjectIDs allValues] valueForKeyPath:@"@sum.@count"] integerValue]; +} + +- (NSUInteger)countOfAttributeValues +{ + return [self.attributeValuesToObjectIDs count]; +} + +- (NSUInteger)countWithAttributeValue:(id)attributeValue +{ + return [[self objectsWithAttributeValue:attributeValue] count]; +} + +- (BOOL)shouldCoerceAttributeToString:(NSString *)attributeValue +{ + if ([attributeValue isKindOfClass:[NSString class]] || [attributeValue isEqual:[NSNull null]]) { + return NO; + } + + Class attributeType = [[RKObjectPropertyInspector sharedInspector] typeForProperty:self.attribute ofEntity:self.entity]; + return [attributeType instancesRespondToSelector:@selector(stringValue)]; +} + +- (void)load +{ + RKLogDebug(@"Loading entity cache for Entity '%@' by attribute '%@'", self.entity.name, self.attribute); + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:self.entity]; + [fetchRequest setResultType:NSManagedObjectIDResultType]; + + NSError *error = nil; + NSArray *objectIDs = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; + [fetchRequest release]; + if (error) { + RKLogError(@"Failed to load entity cache: %@", error); + return; + } + + self.attributeValuesToObjectIDs = [NSMutableDictionary dictionaryWithCapacity:[objectIDs count]]; + for (NSManagedObjectID *objectID in objectIDs) { + NSError *error = nil; + NSManagedObject *object = [self.managedObjectContext existingObjectWithID:objectID error:&error]; + if (! object && error) { + RKLogError(@"Failed to retrieve managed object with ID %@: %@", objectID, error); + } + + [self addObject:object]; + } +} + +- (void)flush +{ + RKLogDebug(@"Flushing entity cache for Entity '%@' by attribute '%@'", self.entity.name, self.attribute); + self.attributeValuesToObjectIDs = nil; +} + +- (void)reload +{ + [self flush]; + [self load]; +} + +- (BOOL)isLoaded +{ + return (self.attributeValuesToObjectIDs != nil); +} + +- (NSManagedObject *)objectWithAttributeValue:(id)attributeValue +{ + NSArray *objects = [self objectsWithAttributeValue:attributeValue]; + return ([objects count] > 0) ? [objects objectAtIndex:0] : nil; +} + +- (NSManagedObject *)objectWithID:(NSManagedObjectID *)objectID { + /* + NOTE: + We use existingObjectWithID: as opposed to objectWithID: as objectWithID: can return us a fault + that will raise an exception when fired. existingObjectWithID:error: will return nil if the ID has been + deleted. objectRegisteredForID: is also an acceptable approach. + */ + NSError *error = nil; + NSManagedObject *object = [self.managedObjectContext existingObjectWithID:objectID error:&error]; + if (! object && error) { + RKLogError(@"Failed to retrieve managed object with ID %@. Error %@\n%@", objectID, [error localizedDescription], [error userInfo]); + return nil; + } + + return object; +} + +- (NSArray *)objectsWithAttributeValue:(id)attributeValue +{ + attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue; + NSMutableArray *objectIDs = [self.attributeValuesToObjectIDs objectForKey:attributeValue]; + if (objectIDs) { + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:[objectIDs count]]; + for (NSManagedObjectID *objectID in objectIDs) { + NSManagedObject *object = [self objectWithID:objectID]; + if (object) [objects addObject:object]; + } + + return objects; + } + + return [NSArray array]; +} + +- (void)addObject:(NSManagedObject *)object +{ + NSAssert([object.entity isEqual:self.entity], @"Cannot add object with entity '%@' to cache with entity of '%@'", [[object entity] name], [self.entity name]); + id attributeValue = [object valueForKey:self.attribute]; + // Coerce to a string if possible + attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue; + if (attributeValue) { + NSManagedObjectID *objectID = [object objectID]; + NSMutableArray *objectIDs = [self.attributeValuesToObjectIDs objectForKey:attributeValue]; + if (objectIDs) { + if (! [objectIDs containsObject:objectID]) { + [objectIDs addObject:objectID]; + } + } else { + objectIDs = [NSMutableArray arrayWithObject:objectID]; + } + + if (nil == self.attributeValuesToObjectIDs) self.attributeValuesToObjectIDs = [NSMutableDictionary dictionary]; + [self.attributeValuesToObjectIDs setValue:objectIDs forKey:attributeValue]; + } else { + RKLogWarning(@"Unable to add object with nil value for attribute '%@': %@", self.attribute, object); + } +} + +- (void)removeObject:(NSManagedObject *)object +{ + NSAssert([object.entity isEqual:self.entity], @"Cannot remove object with entity '%@' from cache with entity of '%@'", [[object entity] name], [self.entity name]); + id attributeValue = [object valueForKey:self.attribute]; + // Coerce to a string if possible + attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue; + if (attributeValue) { + NSManagedObjectID *objectID = [object objectID]; + NSMutableArray *objectIDs = [self.attributeValuesToObjectIDs objectForKey:attributeValue]; + if (objectIDs && [objectIDs containsObject:objectID]) { + [objectIDs removeObject:objectID]; + } + } else { + RKLogWarning(@"Unable to remove object with nil value for attribute '%@': %@", self.attribute, object); + } +} + +- (BOOL)containsObjectWithAttributeValue:(id)attributeValue +{ + // Coerce to a string if possible + attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue; + return [[self objectsWithAttributeValue:attributeValue] count] > 0; +} + +- (BOOL)containsObject:(NSManagedObject *)object +{ + if (! [object.entity isEqual:self.entity]) return NO; + id attributeValue = [object valueForKey:self.attribute]; + // Coerce to a string if possible + attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue; + return [[self objectsWithAttributeValue:attributeValue] containsObject:object]; +} + +- (void)managedObjectContextDidChange:(NSNotification *)notification +{ + if (self.monitorsContextForChanges == NO) return; + + NSDictionary *userInfo = notification.userInfo; + NSSet *insertedObjects = [userInfo objectForKey:NSInsertedObjectsKey]; + NSSet *updatedObjects = [userInfo objectForKey:NSUpdatedObjectsKey]; + NSSet *deletedObjects = [userInfo objectForKey:NSDeletedObjectsKey]; + RKLogTrace(@"insertedObjects=%@, updatedObjects=%@, deletedObjects=%@", insertedObjects, updatedObjects, deletedObjects); + + NSMutableSet *objectsToAdd = [NSMutableSet setWithSet:insertedObjects]; + [objectsToAdd unionSet:updatedObjects]; + + for (NSManagedObject *object in objectsToAdd) { + if ([object.entity isEqual:self.entity]) { + [self addObject:object]; + } + } + + for (NSManagedObject *object in deletedObjects) { + if ([object.entity isEqual:self.entity]) { + [self removeObject:object]; + } + } +} + +- (void)managedObjectContextDidSave:(NSNotification *)notification +{ + // After the MOC has been saved, we flush to ensure any temporary + // objectID references are converted into permanent ID's on the next load. + [self flush]; +} + +- (void)didReceiveMemoryWarning:(NSNotification *)notification +{ + [self flush]; +} + +@end diff --git a/Code/CoreData/RKEntityCache.h b/Code/CoreData/RKEntityCache.h new file mode 100644 index 0000000000..282dab783f --- /dev/null +++ b/Code/CoreData/RKEntityCache.h @@ -0,0 +1,133 @@ +// +// RKEntityCache.h +// RestKit +// +// Created by Blake Watters on 5/2/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import + +@class RKEntityByAttributeCache; + +/** + Instances of RKInMemoryEntityCache provide an in-memory caching mechanism for + objects in a Core Data managed object context. Managed objects can be cached by + attribute for fast retrieval without repeatedly hitting the Core Data persistent store. + This can provide a substantial speed advantage over issuing fetch requests + in cases where repeated look-ups of the same data are performed using a small set + of attributes as the query key. Internally, the cache entries are maintained as + references to the NSManagedObjectID of corresponding cached objects. + */ +@interface RKEntityCache : NSObject + +///----------------------------------------------------------------------------- +/// @name Initializing the Cache +///----------------------------------------------------------------------------- + +/** + Initializes the receiver with a managed object context containing the entity instances to be cached. + + @param context The managed object context containing objects to be cached. + @returns self, initialized with context. + */ +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)context; + +/** + The managed object context with which the receiver is associated. + */ +@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext; + +///----------------------------------------------------------------------------- +/// @name Caching Objects by Attribute +///----------------------------------------------------------------------------- + +/** + Caches all instances of an entity using the value for an attribute as the cache key. + + @param entity The entity to cache all instances of. + @param attributeName The attribute to cache the instances by. + */ +- (void)cacheObjectsForEntity:(NSEntityDescription *)entity byAttribute:(NSString *)attributeName; + +/** + Returns a Boolean value indicating if all instances of an entity have been cached by a given attribute name. + + @param entity The entity to check the cache status of. + @param attributeName The attribute to check the cache status with. + @return YES if the cache has been loaded with instances with the given attribute, else NO. + */ +- (BOOL)isEntity:(NSEntityDescription *)entity cachedByAttribute:(NSString *)attributeName; + +/** + Retrieves the first cached instance of a given entity where the specified attribute matches the given value. + + @param entity The entity to search the cache for instances of. + @param attributeName The attribute to search the cache for matches with. + @param attributeValue The value of the attribute to return a match for. + @return A matching managed object instance or nil. + @raise NSInvalidArgumentException Raised if instances of the entity and attribute have not been cached. + */ +- (NSManagedObject *)objectForEntity:(NSEntityDescription *)entity withAttribute:(NSString *)attributeName value:(id)attributeValue; + +/** + Retrieves all cached instances of a given entity where the specified attribute matches the given value. + + @param entity The entity to search the cache for instances of. + @param attributeName The attribute to search the cache for matches with. + @param attributeValue The value of the attribute to return a match for. + @return All matching managed object instances or nil. + @raise NSInvalidArgumentException Raised if instances of the entity and attribute have not been cached. + */ +- (NSArray *)objectsForEntity:(NSEntityDescription *)entity withAttribute:(NSString *)attributeName value:(id)attributeValue; + +///----------------------------------------------------------------------------- +// @name Accessing Underlying Caches +///----------------------------------------------------------------------------- + +/** + Retrieves the underlying entity attribute cache for a given entity and attribute. + + @param entity The entity to retrieve the entity attribute cache object for. + @param attributeName The attribute to retrieve the entity attribute cache object for. + @return The entity attribute cache for the given entity and attribute, or nil if none was found. + */ +- (RKEntityByAttributeCache *)attributeCacheForEntity:(NSEntityDescription *)entity attribute:(NSString *)attributeName; + +/** + Retrieves all entity attributes caches for a given entity. + + @param entity The entity to retrieve the collection of entity attribute caches for. + @return An array of entity attribute cache objects for the given entity or an empty array if none were found. + */ +- (NSArray *)attributeCachesForEntity:(NSEntityDescription *)entity; + +///----------------------------------------------------------------------------- +// @name Managing the Cache +///----------------------------------------------------------------------------- + +/** + Flushes the entity cache by sending a flush message to each entity attribute cache + contained within the receiver. + + @see [RKEntityByAttributeCache flush] + */ +- (void)flush; + +/** + Adds a given object to all entity attribute caches for the object's entity contained + within the receiver. + + @param object The object to add to the appropriate entity attribute caches. + */ +- (void)addObject:(NSManagedObject *)object; + +/** + Removed a given object from all entity attribute caches for the object's entity contained + within the receiver. + + @param object The object to remove from the appropriate entity attribute caches. + */ +- (void)removeObject:(NSManagedObject *)object; + +@end diff --git a/Code/CoreData/RKEntityCache.m b/Code/CoreData/RKEntityCache.m new file mode 100644 index 0000000000..01d72fe2d5 --- /dev/null +++ b/Code/CoreData/RKEntityCache.m @@ -0,0 +1,141 @@ +// +// RKEntityCache.m +// RestKit +// +// Created by Blake Watters on 5/2/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKEntityCache.h" +#import "RKEntityByAttributeCache.h" + +@interface RKEntityCache () +@property (nonatomic, retain) NSMutableSet *attributeCaches; +@end + +@implementation RKEntityCache + +@synthesize managedObjectContext = _managedObjectContext; +@synthesize attributeCaches = _attributeCaches; + +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)context +{ + NSAssert(context, @"Cannot initialize entity cache with a nil context"); + self = [super init]; + if (self) { + _managedObjectContext = [context retain]; + _attributeCaches = [[NSMutableSet alloc] init]; + } + + return self; +} + +- (id)init +{ + return [self initWithManagedObjectContext:nil]; +} + +- (void)dealloc +{ + [_managedObjectContext release]; + [_attributeCaches release]; + [super dealloc]; +} + +- (void)cacheObjectsForEntity:(NSEntityDescription *)entity byAttribute:(NSString *)attributeName +{ + NSAssert(entity, @"Cannot cache objects for a nil entity"); + NSAssert(attributeName, @"Cannot cache objects without an attribute"); + RKEntityByAttributeCache *attributeCache = [self attributeCacheForEntity:entity attribute:attributeName]; + if (attributeCache && !attributeCache.isLoaded) { + [attributeCache load]; + } else { + attributeCache = [[RKEntityByAttributeCache alloc] initWithEntity:entity attribute:attributeName managedObjectContext:self.managedObjectContext]; + [attributeCache load]; + [self.attributeCaches addObject:attributeCache]; + [attributeCache release]; + } +} + +- (BOOL)isEntity:(NSEntityDescription *)entity cachedByAttribute:(NSString *)attributeName +{ + NSAssert(entity, @"Cannot check cache status for a nil entity"); + NSAssert(attributeName, @"Cannot check cache status for a nil attribute"); + RKEntityByAttributeCache *attributeCache = [self attributeCacheForEntity:entity attribute:attributeName]; + return (attributeCache && attributeCache.isLoaded); +} + +- (NSManagedObject *)objectForEntity:(NSEntityDescription *)entity withAttribute:(NSString *)attributeName value:(id)attributeValue +{ + NSAssert(entity, @"Cannot retrieve cached objects with a nil entity"); + NSAssert(attributeName, @"Cannot retrieve cached objects by a nil entity"); + RKEntityByAttributeCache *attributeCache = [self attributeCacheForEntity:entity attribute:attributeName]; + if (attributeCache) { + return [attributeCache objectWithAttributeValue:attributeValue]; + } + + return nil; +} + +- (NSArray *)objectsForEntity:(NSEntityDescription *)entity withAttribute:(NSString *)attributeName value:(id)attributeValue +{ + NSAssert(entity, @"Cannot retrieve cached objects with a nil entity"); + NSAssert(attributeName, @"Cannot retrieve cached objects by a nil entity"); + RKEntityByAttributeCache *attributeCache = [self attributeCacheForEntity:entity attribute:attributeName]; + if (attributeCache) { + return [attributeCache objectsWithAttributeValue:attributeValue]; + } + + return [NSSet set]; +} + +- (RKEntityByAttributeCache *)attributeCacheForEntity:(NSEntityDescription *)entity attribute:(NSString *)attributeName +{ + NSAssert(entity, @"Cannot retrieve attribute cache for a nil entity"); + NSAssert(attributeName, @"Cannot retrieve attribute cache for a nil attribute"); + for (RKEntityByAttributeCache *cache in self.attributeCaches) { + if ([cache.entity isEqual:entity] && [cache.attribute isEqualToString:attributeName]) { + return cache; + } + } + + return nil; +} + +- (NSSet *)attributeCachesForEntity:(NSEntityDescription *)entity +{ + NSAssert(entity, @"Cannot retrieve attribute caches for a nil entity"); + NSMutableSet *set = [NSMutableSet set]; + for (RKEntityByAttributeCache *cache in self.attributeCaches) { + if ([cache.entity isEqual:entity]) { + [set addObject:cache]; + } + } + + return [NSSet setWithSet:set]; +} + +- (void)flush +{ + [self.attributeCaches makeObjectsPerformSelector:@selector(flush)]; +} + +- (void)addObject:(NSManagedObject *)object +{ + NSAssert(object, @"Cannot add a nil object to the cache"); + NSArray *attributeCaches = [self attributeCachesForEntity:object.entity]; + for (RKEntityByAttributeCache *cache in attributeCaches) { + [cache addObject:object]; + } +} + +- (void)removeObject:(NSManagedObject *)object +{ + NSAssert(object, @"Cannot remove a nil object from the cache"); + NSArray *attributeCaches = [self attributeCachesForEntity:object.entity]; + for (RKEntityByAttributeCache *cache in attributeCaches) { + [cache removeObject:object]; + } +} + +@end diff --git a/Code/CoreData/RKFetchRequestManagedObjectCache.h b/Code/CoreData/RKFetchRequestManagedObjectCache.h index 8f7dea11a1..d68ab6a04b 100644 --- a/Code/CoreData/RKFetchRequestManagedObjectCache.h +++ b/Code/CoreData/RKFetchRequestManagedObjectCache.h @@ -6,7 +6,7 @@ // Copyright (c) 2009-2012 RestKit. All rights reserved. // -#import "RKManagedObjectMappingCache.h" +#import "RKManagedObjectCaching.h" /** Provides a simple managed object cache strategy in which every request for an object @@ -14,6 +14,6 @@ Performance can be disappointing for data sets with a large amount of redundant data being mapped and connected together, but the memory footprint stays flat. */ -@interface RKFetchRequestManagedObjectCache : NSObject +@interface RKFetchRequestManagedObjectCache : NSObject @end diff --git a/Code/CoreData/RKFetchRequestManagedObjectCache.m b/Code/CoreData/RKFetchRequestManagedObjectCache.m index 539a042a43..3409a03968 100644 --- a/Code/CoreData/RKFetchRequestManagedObjectCache.m +++ b/Code/CoreData/RKFetchRequestManagedObjectCache.m @@ -10,6 +10,8 @@ #import "NSManagedObject+ActiveRecord.h" #import "NSEntityDescription+RKAdditions.h" #import "RKLog.h" +#import "RKObjectPropertyInspector.h" +#import "RKObjectPropertyInspector+CoreData.h" // Set Logging Component #undef RKLogComponent @@ -27,12 +29,31 @@ - (NSManagedObject *)findInstanceOfEntity:(NSEntityDescription *)entity NSAssert(primaryKeyValue, @"Cannot find existing managed object by primary key without a value"); NSAssert(managedObjectContext, @"Cannot find existing managed object with a context"); - NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init]; - [fetchRequest setEntity:entity]; - [fetchRequest setFetchLimit:1]; - [fetchRequest setPredicate:[NSPredicate predicateWithFormat:@"%K = %@", primaryKeyAttribute, primaryKeyValue]]; - NSArray *objects = [NSManagedObject executeFetchRequest:fetchRequest]; + id searchValue = primaryKeyValue; + Class type = [[RKObjectPropertyInspector sharedInspector] typeForProperty:primaryKeyAttribute ofEntity:entity]; + if (type && ([type isSubclassOfClass:[NSString class]] && NO == [primaryKeyValue isKindOfClass:[NSString class]])) { + searchValue = [NSString stringWithFormat:@"%@", primaryKeyValue]; + } else if (type && ([type isSubclassOfClass:[NSNumber class]] && NO == [primaryKeyValue isKindOfClass:[NSNumber class]])) { + if ([primaryKeyValue isKindOfClass:[NSString class]]) { + searchValue = [NSNumber numberWithDouble:[(NSString *)primaryKeyValue doubleValue]]; + } + } + + // Use cached predicate if primary key matches + NSPredicate *predicate = nil; + if ([entity.primaryKeyAttributeName isEqualToString:primaryKeyAttribute]) { + predicate = [entity predicateForPrimaryKeyAttributeWithValue:searchValue]; + } else { + // Parse a predicate + predicate = [NSPredicate predicateWithFormat:@"%K = %@", primaryKeyAttribute, searchValue]; + } + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + fetchRequest.entity = entity; + fetchRequest.fetchLimit = 1; + fetchRequest.predicate = predicate; + NSArray *objects = [NSManagedObject executeFetchRequest:fetchRequest inContext:managedObjectContext]; RKLogDebug(@"Found objects '%@' using fetchRequest '%@'", objects, fetchRequest); + [fetchRequest release]; NSManagedObject *object = nil; if ([objects count] > 0) { diff --git a/Code/CoreData/RKInMemoryEntityCache.h b/Code/CoreData/RKInMemoryEntityCache.h deleted file mode 100644 index 47859f365b..0000000000 --- a/Code/CoreData/RKInMemoryEntityCache.h +++ /dev/null @@ -1,83 +0,0 @@ -// -// RKInMemoryEntityCache.h -// RestKit -// -// Created by Jeff Arena on 1/24/12. -// Copyright (c) 2009-2012 RestKit. All rights reserved. -// - -#import - -/** - Instances of RKInMemoryEntityCache provide an in-memory caching mechanism for - objects in a Core Data managed object context. Managed objects can be cached by - attribute for fast retrieval without repeatedly hitting the Core Data persistent store. - This can provide a substantial speed advantage over issuing fetch requests - in cases where repeated look-ups of the same data are performed using a small set - of attributes as the query key. Internally, the cache entries are maintained as - references to the NSManagedObjectID of corresponding cached objects. - */ -@interface RKInMemoryEntityCache : NSObject - -/** - The managed object context from which objects will be cached. - */ -@property(nonatomic, readonly) NSManagedObjectContext *managedObjectContext; - -/// @name Initializing the Cache - -/** - Initializes the receiver with a managed object context containing the entity instances to be cached. - - @param context The managed object context containing objects to be cached. - @returns self, initialized with context. - */ -- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; - -/// @name Cacheing Objects by Attribute - -/** - Retrieves all objects within the cache - */ -- (NSMutableDictionary *)cachedObjectsForEntity:(NSEntityDescription *)entity - byAttribute:(NSString *)attributeName - inContext:(NSManagedObjectContext *)managedObjectContext; - -/** - */ -- (NSManagedObject *)cachedObjectForEntity:(NSEntityDescription *)entity - withAttribute:(NSString *)attributeName - value:(id)attributeValue - inContext:(NSManagedObjectContext *)managedObjectContext; - -/** - Caches all instances of an entity in a given managed object context by the value - */ -- (void)cacheObjectsForEntity:(NSEntityDescription *)entity - byAttribute:(NSString *)attributeName - inContext:(NSManagedObjectContext *)managedObjectContext; - -/** - */ -- (void)cacheObject:(NSManagedObject *)managedObject - byAttribute:(NSString *)attributeName - inContext:(NSManagedObjectContext *)managedObjectContext; - -/** - */ -- (void)cacheObject:(NSEntityDescription *)entity - byAttribute:(NSString *)attributeName - value:(id)primaryKeyValue - inContext:(NSManagedObjectContext *)managedObjectContext; - -/** - */ -- (void)expireCacheEntryForObject:(NSManagedObject *)managedObject - byAttribute:(NSString *)attributeName - inContext:(NSManagedObjectContext *)managedObjectContext; - -/** - */ -- (void)expireCacheEntriesForEntity:(NSEntityDescription *)entity; - -@end diff --git a/Code/CoreData/RKInMemoryEntityCache.m b/Code/CoreData/RKInMemoryEntityCache.m deleted file mode 100644 index 2310854905..0000000000 --- a/Code/CoreData/RKInMemoryEntityCache.m +++ /dev/null @@ -1,291 +0,0 @@ -// -// RKInMemoryEntityCache.m -// RestKit -// -// Created by Jeff Arena on 1/24/12. -// Copyright (c) 2009-2012 RestKit. All rights reserved. -// - -#if TARGET_OS_IPHONE -#import -#endif - -#import "RKInMemoryEntityCache.h" -#import "NSManagedObject+ActiveRecord.h" -#import "../ObjectMapping/RKObjectPropertyInspector.h" -#import "RKObjectPropertyInspector+CoreData.h" -#import "RKLog.h" - -// Set Logging Component -#undef RKLogComponent -#define RKLogComponent lcl_cRestKitCoreData - -@interface RKInMemoryEntityCache () -@property(nonatomic, retain) NSMutableArray *attributeCaches; - -@property(nonatomic, retain) NSMutableDictionary *entityCache; - -- (BOOL)shouldCoerceAttributeToString:(NSString *)attribute forEntity:(NSEntityDescription *)entity; -- (NSManagedObject *)objectWithID:(NSManagedObjectID *)objectID inContext:(NSManagedObjectContext *)managedObjectContext; -@end - -@implementation RKInMemoryEntityCache - -@synthesize entityCache = _entityCache; -@synthesize managedObjectContext = _managedObjectContext; -@synthesize attributeCaches = _attributeCaches; - -- (id)init { - self = [super init]; - if (self) { - _entityCache = [[NSMutableDictionary alloc] init]; -#if TARGET_OS_IPHONE - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(didReceiveMemoryWarning) - name:UIApplicationDidReceiveMemoryWarningNotification - object:nil]; -#endif - } - return self; -} - -- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext -{ - self = [self init]; - if (self) { - _managedObjectContext = [managedObjectContext retain]; - } - - return self; -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [_entityCache release]; - [_managedObjectContext release]; - [_attributeCaches release]; - - [super dealloc]; -} - -- (void)didReceiveMemoryWarning { - [[NSNotificationCenter defaultCenter] removeObserver:self - name:NSManagedObjectContextObjectsDidChangeNotification - object:nil]; - [_entityCache removeAllObjects]; -} - -- (NSMutableDictionary *)cachedObjectsForEntity:(NSEntityDescription *)entity - byAttribute:(NSString *)attributeName - inContext:(NSManagedObjectContext *)managedObjectContext { - NSAssert(entity, @"Cannot retrieve cached objects without an entity"); - NSAssert(attributeName, @"Cannot retrieve cached objects without an attributeName"); - NSAssert(managedObjectContext, @"Cannot retrieve cached objects without a managedObjectContext"); - - NSMutableDictionary *cachedObjectsForEntity = [_entityCache objectForKey:entity.name]; - if (cachedObjectsForEntity == nil) { - [self cacheObjectsForEntity:entity byAttribute:attributeName inContext:managedObjectContext]; - cachedObjectsForEntity = [_entityCache objectForKey:entity.name]; - } - return cachedObjectsForEntity; -} - -- (NSManagedObject *)cachedObjectForEntity:(NSEntityDescription *)entity - withAttribute:(NSString *)attributeName - value:(id)attributeValue - inContext:(NSManagedObjectContext *)managedObjectContext { - NSAssert(entity, @"Cannot retrieve a cached object without an entity"); - NSAssert(attributeName, @"Cannot retrieve a cached object without a mapping"); - NSAssert(attributeValue, @"Cannot retrieve a cached object without a primaryKeyValue"); - NSAssert(managedObjectContext, @"Cannot retrieve a cached object without a managedObjectContext"); - - NSMutableDictionary *cachedObjectsForEntity = [self cachedObjectsForEntity:entity - byAttribute:attributeName - inContext:managedObjectContext]; - - // NOTE: We coerce the primary key into a string (if possible) for convenience. Generally - // primary keys are expressed either as a number of a string, so this lets us support either case interchangeably - id lookupValue = [attributeValue respondsToSelector:@selector(stringValue)] ? [attributeValue stringValue] : attributeValue; - NSManagedObjectID *objectID = [cachedObjectsForEntity objectForKey:lookupValue]; - NSManagedObject *object = nil; - if (objectID) { - object = [self objectWithID:objectID inContext:managedObjectContext]; - } - return object; -} - -- (void)cacheObjectsForEntity:(NSEntityDescription *)entity - byAttribute:(NSString *)attributeName - inContext:(NSManagedObjectContext *)managedObjectContext { - NSAssert(entity, @"Cannot cache objects without an entity"); - NSAssert(attributeName, @"Cannot cache objects without an attributeName"); - NSAssert(managedObjectContext, @"Cannot cache objects without a managedObjectContext"); - - NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; - [fetchRequest setEntity:entity]; - [fetchRequest setResultType:NSManagedObjectIDResultType]; - - NSArray *objectIds = [NSManagedObject executeFetchRequest:fetchRequest inContext:managedObjectContext]; - [fetchRequest release]; - - RKLogInfo(@"Caching all %ld %@ objectsIDs to thread local storage", (long) [objectIds count], entity.name); - NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; - if ([objectIds count] > 0) { - BOOL coerceToString = [self shouldCoerceAttributeToString:attributeName forEntity:entity]; - for (NSManagedObjectID* theObjectID in objectIds) { - NSManagedObject* theObject = [self objectWithID:theObjectID inContext:managedObjectContext]; - id attributeValue = [theObject valueForKey:attributeName]; - // Coerce to a string if possible - attributeValue = coerceToString ? [attributeValue stringValue] : attributeValue; - if (attributeValue) { - [dictionary setObject:theObjectID forKey:attributeValue]; - } - } - } - [_entityCache setObject:dictionary forKey:entity.name]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(objectsDidChange:) - name:NSManagedObjectContextObjectsDidChangeNotification - object:managedObjectContext]; -} - -- (void)cacheObject:(NSManagedObject *)managedObject byAttribute:(NSString *)attributeName inContext:(NSManagedObjectContext *)managedObjectContext { - NSAssert(managedObject, @"Cannot cache an object without a managedObject"); - NSAssert(attributeName, @"Cannot cache an object without a mapping"); - NSAssert(managedObjectContext, @"Cannot cache an object without a managedObjectContext"); - - NSManagedObjectID *objectID = [managedObject objectID]; - if (objectID) { - NSEntityDescription *entity = managedObject.entity; - BOOL coerceToString = [self shouldCoerceAttributeToString:attributeName forEntity:entity]; - id attributeValue = [managedObject valueForKey:attributeName]; - // Coerce to a string if possible - attributeValue = coerceToString ? [attributeValue stringValue] : attributeValue; - if (attributeValue) { - NSMutableDictionary *cachedObjectsForEntity = [self cachedObjectsForEntity:entity - byAttribute:attributeName - inContext:managedObjectContext]; - [cachedObjectsForEntity setObject:objectID forKey:attributeValue]; - } - } - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(objectsDidChange:) - name:NSManagedObjectContextObjectsDidChangeNotification - object:managedObjectContext]; -} - -- (void)cacheObject:(NSEntityDescription *)entity byAttribute:(NSString *)attributeName value:(id)attributeValue inContext:(NSManagedObjectContext *)managedObjectContext { - NSAssert(entity, @"Cannot cache an object without an entity"); - NSAssert(attributeName, @"Cannot cache an object without a mapping"); - NSAssert(managedObjectContext, @"Cannot cache an object without a managedObjectContext"); - - // NOTE: We coerce the primary key into a string (if possible) for convenience. Generally - // primary keys are expressed either as a number or a string, so this lets us support either case interchangeably - id lookupValue = [attributeValue respondsToSelector:@selector(stringValue)] ? [attributeValue stringValue] : attributeValue; - - NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; - [fetchRequest setEntity:entity]; - [fetchRequest setFetchLimit:1]; - [fetchRequest setPredicate:[NSPredicate predicateWithFormat:@"%K = %@", attributeName, lookupValue]]; - [fetchRequest setResultType:NSManagedObjectIDResultType]; - - NSArray *objectIds = [NSManagedObject executeFetchRequest:fetchRequest inContext:managedObjectContext]; - [fetchRequest release]; - - NSManagedObjectID *objectID = nil; - if ([objectIds count] > 0) { - objectID = [objectIds objectAtIndex:0]; - if (objectID && lookupValue) { - NSMutableDictionary *cachedObjectsForEntity = [self cachedObjectsForEntity:entity - byAttribute:attributeName - inContext:managedObjectContext]; - [cachedObjectsForEntity setObject:objectID forKey:lookupValue]; - } - } - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(objectsDidChange:) - name:NSManagedObjectContextObjectsDidChangeNotification - object:managedObjectContext]; -} - -- (void)expireCacheEntryForObject:(NSManagedObject *)managedObject byAttribute:(NSString *)attributeName inContext:(NSManagedObjectContext *)managedObjectContext { - NSAssert(managedObject, @"Cannot expire cache entry for an object without a managedObject"); - NSAssert(attributeName, @"Cannot expire cache entry for an object without a mapping"); - NSAssert(managedObjectContext, @"Cannot expire cache entry for an object without a managedObjectContext"); - - NSEntityDescription *entity = managedObject.entity; - BOOL coerceToString = [self shouldCoerceAttributeToString:attributeName forEntity:entity]; - id attributeValue = [managedObject valueForKey:attributeName]; - // Coerce to a string if possible - attributeValue = coerceToString ? [attributeValue stringValue] : attributeValue; - if (attributeValue) { - NSMutableDictionary *cachedObjectsForEntity = [self cachedObjectsForEntity:entity - byAttribute:attributeName - inContext:managedObjectContext]; - [cachedObjectsForEntity removeObjectForKey:attributeValue]; - - if ([cachedObjectsForEntity count] == 0) { - [self expireCacheEntriesForEntity:entity]; - } - } -} - -- (void)expireCacheEntriesForEntity:(NSEntityDescription *)entity { - NSAssert(entity, @"Cannot expire cache entry for an entity without an entity"); - RKLogTrace(@"About to expire cache for entity name=%@", entity.name); - [_entityCache removeObjectForKey:entity.name]; -} - - -#pragma mark Helper Methods - -- (BOOL)shouldCoerceAttributeToString:(NSString *)attribute forEntity:(NSEntityDescription *)entity { - Class attributeType = [[RKObjectPropertyInspector sharedInspector] typeForProperty:attribute ofEntity:entity]; - return [attributeType instancesRespondToSelector:@selector(stringValue)]; -} - -- (NSManagedObject *)objectWithID:(NSManagedObjectID *)objectID inContext:(NSManagedObjectContext *)managedObjectContext { - NSAssert(objectID, @"Cannot fetch a managedObject with a nil objectID"); - NSAssert(managedObjectContext, @"Cannot fetch a managedObject with a nil managedObjectContext"); - /* - NOTE: - We use existingObjectWithID: as opposed to objectWithID: as objectWithID: can return us a fault - that will raise an exception when fired. existingObjectWithID:error: will return nil if the ID has been - deleted. objectRegisteredForID: is also an acceptable approach. - */ - NSError *error = nil; - NSManagedObject *object = [managedObjectContext existingObjectWithID:objectID error:&error]; - if (! object && error) { - RKLogError(@"Failed to retrieve managed object with ID %@: %@", objectID, error); - } - - return object; -} - - -#pragma mark Notifications - -- (void)objectsDidChange:(NSNotification *)notification { - NSDictionary *userInfo = notification.userInfo; - NSSet *insertedObjects = [userInfo objectForKey:NSInsertedObjectsKey]; - NSSet *deletedObjects = [userInfo objectForKey:NSDeletedObjectsKey]; - RKLogTrace(@"insertedObjects=%@, deletedObjects=%@", insertedObjects, deletedObjects); - - NSMutableSet *entitiesToExpire = [NSMutableSet set]; - for (NSManagedObject *object in insertedObjects) { - [entitiesToExpire addObject:object.entity]; - } - - for (NSManagedObject *object in deletedObjects) { - [entitiesToExpire addObject:object.entity]; - } - - for (NSEntityDescription *entity in entitiesToExpire) { - [self expireCacheEntriesForEntity:entity]; - } -} - -@end diff --git a/Code/CoreData/RKInMemoryManagedObjectCache.h b/Code/CoreData/RKInMemoryManagedObjectCache.h index e0d0ae7260..4ae4642b36 100644 --- a/Code/CoreData/RKInMemoryManagedObjectCache.h +++ b/Code/CoreData/RKInMemoryManagedObjectCache.h @@ -6,14 +6,13 @@ // Copyright (c) 2009-2012 RestKit. All rights reserved. // -#import "RKManagedObjectMappingCache.h" -#import "RKInMemoryEntityCache.h" +#import "RKManagedObjectCaching.h" /** Provides a fast managed object cache where-in object instances are retained in memory to avoid hitting the Core Data persistent store. Performance is greatly increased over fetch request based strategy at the expense of memory consumption. */ -@interface RKInMemoryManagedObjectCache : NSObject +@interface RKInMemoryManagedObjectCache : NSObject @end diff --git a/Code/CoreData/RKInMemoryManagedObjectCache.m b/Code/CoreData/RKInMemoryManagedObjectCache.m index 794dae3fb3..01d5932629 100644 --- a/Code/CoreData/RKInMemoryManagedObjectCache.m +++ b/Code/CoreData/RKInMemoryManagedObjectCache.m @@ -7,6 +7,8 @@ // #import "RKInMemoryManagedObjectCache.h" +#import "NSEntityDescription+RKAdditions.h" +#import "RKEntityCache.h" #import "RKLog.h" // Set Logging Component @@ -17,13 +19,9 @@ @implementation RKInMemoryManagedObjectCache -- (NSManagedObject *)findInstanceOfEntity:(NSEntityDescription *)entity - withPrimaryKeyAttribute:(NSString *)primaryKeyAttribute - value:(id)primaryKeyValue - inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext { +- (RKEntityCache *)cacheForEntity:(NSEntityDescription *)entity inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ NSAssert(entity, @"Cannot find existing managed object without a target class"); - NSAssert(primaryKeyAttribute, @"Cannot find existing managed object instance without mapping"); - NSAssert(primaryKeyValue, @"Cannot find existing managed object by primary key without a value"); NSAssert(managedObjectContext, @"Cannot find existing managed object with a context"); NSMutableDictionary *contextDictionary = [[[NSThread currentThread] threadDictionary] objectForKey:RKInMemoryObjectManagedObjectCacheThreadDictionaryKey]; if (! contextDictionary) { @@ -31,13 +29,49 @@ - (NSManagedObject *)findInstanceOfEntity:(NSEntityDescription *)entity [[[NSThread currentThread] threadDictionary] setObject:contextDictionary forKey:RKInMemoryObjectManagedObjectCacheThreadDictionaryKey]; } NSNumber *hashNumber = [NSNumber numberWithUnsignedInteger:[managedObjectContext hash]]; - RKInMemoryEntityCache *cache = [contextDictionary objectForKey:hashNumber]; - if (! cache) { - cache = [[RKInMemoryEntityCache alloc] initWithManagedObjectContext:managedObjectContext]; - [contextDictionary setObject:cache forKey:hashNumber]; - [cache release]; + RKEntityCache *entityCache = [contextDictionary objectForKey:hashNumber]; + if (! entityCache) { + RKLogInfo(@"Creating thread-local entity cache for managed object context: %@", managedObjectContext); + entityCache = [[RKEntityCache alloc] initWithManagedObjectContext:managedObjectContext]; + [contextDictionary setObject:entityCache forKey:hashNumber]; + [entityCache release]; } - return [cache cachedObjectForEntity:entity withAttribute:primaryKeyAttribute value:primaryKeyValue inContext:managedObjectContext]; + + return entityCache; +} + +- (NSManagedObject *)findInstanceOfEntity:(NSEntityDescription *)entity + withPrimaryKeyAttribute:(NSString *)primaryKeyAttribute + value:(id)primaryKeyValue + inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + RKEntityCache *entityCache = [self cacheForEntity:entity inManagedObjectContext:managedObjectContext]; + if (! [entityCache isEntity:entity cachedByAttribute:primaryKeyAttribute]) { + RKLogInfo(@"Caching instances of Entity '%@' by primary key attribute '%@'", entity.name, primaryKeyAttribute); + [entityCache cacheObjectsForEntity:entity byAttribute:primaryKeyAttribute]; + RKEntityByAttributeCache *attributeCache = [entityCache attributeCacheForEntity:entity attribute:primaryKeyAttribute]; + RKLogTrace(@"Cached %ld objects", (long) [attributeCache count]); + } + + return [entityCache objectForEntity:entity withAttribute:primaryKeyAttribute value:primaryKeyValue]; +} + +- (void)didFetchObject:(NSManagedObject *)object +{ + RKEntityCache *entityCache = [self cacheForEntity:object.entity inManagedObjectContext:object.managedObjectContext]; + [entityCache addObject:object]; +} + +- (void)didCreateObject:(NSManagedObject *)object +{ + RKEntityCache *entityCache = [self cacheForEntity:object.entity inManagedObjectContext:object.managedObjectContext]; + [entityCache addObject:object]; +} + +- (void)didDeleteObject:(NSManagedObject *)object +{ + RKEntityCache *entityCache = [self cacheForEntity:object.entity inManagedObjectContext:object.managedObjectContext]; + [entityCache removeObject:object]; } @end diff --git a/Code/CoreData/RKManagedObjectMappingCache.h b/Code/CoreData/RKManagedObjectCaching.h similarity index 63% rename from Code/CoreData/RKManagedObjectMappingCache.h rename to Code/CoreData/RKManagedObjectCaching.h index d89cbffc92..64fe4a5096 100644 --- a/Code/CoreData/RKManagedObjectMappingCache.h +++ b/Code/CoreData/RKManagedObjectCaching.h @@ -1,5 +1,5 @@ // -// RKManagedObjectCacheing.h +// RKManagedObjectCaching.h // RestKit // // Created by Jeff Arena on 1/24/12. @@ -9,12 +9,14 @@ #import /** - Objects implementing the RKManagedObjectCacheing protocol can act as the cache + Objects implementing the RKManagedObjectCaching protocol can act as the cache strategy for RestKit managed object stores. The managed object cache is consulted when objects are retrieved from Core Data during object mapping operations and provide an opportunity to accelerate the mapping process by trading memory for speed. */ -@protocol RKManagedObjectCacheing +@protocol RKManagedObjectCaching + +@required /** Retrieves a model object from the object store given a Core Data entity and @@ -32,4 +34,27 @@ value:(id)primaryKeyValue inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; +@optional + +/** + Tells the receiver that an object was fetched and should be added to the cache. + + @param object The object that was fetched from a managed object context. + */ +- (void)didFetchObject:(NSManagedObject *)object; + +/** + Tells the receiver that an object was created and should be added to the cache. + + @param object The object that was created in a managed object context. + */ +- (void)didCreateObject:(NSManagedObject *)object; + +/** + Tells the receiver that an object was deleted and should be removed to the cache. + + @param object The object that was deleted from a managed object context. + */ +- (void)didDeleteObject:(NSManagedObject *)object; + @end diff --git a/Code/CoreData/RKManagedObjectLoader.h b/Code/CoreData/RKManagedObjectLoader.h index 4713a14bf8..5a8d46e290 100644 --- a/Code/CoreData/RKManagedObjectLoader.h +++ b/Code/CoreData/RKManagedObjectLoader.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 2/13/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,14 +29,14 @@ */ @interface RKManagedObjectLoader : RKObjectLoader { RKManagedObjectStore *_objectStore; - NSManagedObjectID* _targetObjectID; + NSManagedObjectID* _targetObjectID; NSMutableSet* _managedObjectKeyPaths; BOOL _deleteObjectOnFailure; } /** A reference to a RestKit managed object store for interacting with Core Data - + @see RKManagedObjectStore */ @property (nonatomic, retain) RKManagedObjectStore* objectStore; diff --git a/Code/CoreData/RKManagedObjectLoader.m b/Code/CoreData/RKManagedObjectLoader.m index ef7d1420b2..bc65db0752 100644 --- a/Code/CoreData/RKManagedObjectLoader.m +++ b/Code/CoreData/RKManagedObjectLoader.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 2/13/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -46,7 +46,7 @@ - (id)initWithURL:(RKURL *)URL mappingProvider:(RKObjectMappingProvider *)mappin if (self) { _objectStore = [objectStore retain]; } - + return self; } @@ -55,22 +55,22 @@ - (id)initWithURL:(RKURL *)URL mappingProvider:(RKObjectMappingProvider *)mappin if (self) { _managedObjectKeyPaths = [[NSMutableSet alloc] init]; } - + return self; } - + - (void)dealloc { [_targetObjectID release]; _targetObjectID = nil; _deleteObjectOnFailure = NO; [_managedObjectKeyPaths release]; [_objectStore release]; - + [super dealloc]; } - (void)reset { - [super reset]; + [super reset]; [_targetObjectID release]; _targetObjectID = nil; } @@ -88,16 +88,16 @@ - (void)objectMapper:(RKObjectMapper*)objectMapper didMapFromObject:(id)sourceOb // Overload the target object reader to return a thread-local copy of the target object - (id)targetObject { if ([NSThread isMainThread] == NO && _targetObjectID) { - return [self.objectStore objectWithID:_targetObjectID]; + return [self.objectStore objectWithID:_targetObjectID]; } - + return _targetObject; } - (void)setTargetObject:(NSObject*)targetObject { [_targetObject release]; - _targetObject = nil; - _targetObject = [targetObject retain]; + _targetObject = nil; + _targetObject = [targetObject retain]; [_targetObjectID release]; _targetObjectID = nil; @@ -105,7 +105,7 @@ - (void)setTargetObject:(NSObject*)targetObject { - (BOOL)prepareURLRequest { // TODO: Can we just do this if the object hasn't been saved already??? - + // NOTE: There is an important sequencing issue here. You MUST save the // managed object context before retaining the objectID or you will run // into an error where the object context cannot be saved. We do this @@ -116,7 +116,7 @@ - (BOOL)prepareURLRequest { [self.objectStore save:nil]; _targetObjectID = [[(NSManagedObject*)self.targetObject objectID] retain]; } - + return [super prepareURLRequest]; } @@ -125,7 +125,7 @@ - (NSArray *)cachedObjects { if (fetchRequest) { return [NSManagedObject objectsWithFetchRequest:fetchRequest]; } - + return nil; } @@ -134,7 +134,7 @@ - (void)deleteCachedObjectsMissingFromResult:(RKObjectMappingResult*)result { RKLogDebug(@"Skipping cleanup of objects via managed object cache: only used for GET requests."); return; } - + if ([self.URL isKindOfClass:[RKURL class]]) { NSArray *results = [result asCollection]; NSArray *cachedObjects = [self cachedObjects]; @@ -146,7 +146,7 @@ - (void)deleteCachedObjectsMissingFromResult:(RKObjectMappingResult*)result { } } else { RKLogWarning(@"Unable to perform cleanup of server-side object deletions: unable to determine resource path."); - } + } } // NOTE: We are on the background thread here, be mindful of Core Data's threading needs @@ -155,9 +155,9 @@ - (void)processMappingResult:(RKObjectMappingResult*)result { if (_targetObjectID && self.targetObject && self.method == RKRequestMethodDELETE) { NSManagedObject* backgroundThreadObject = [self.objectStore objectWithID:_targetObjectID]; RKLogInfo(@"Deleting local object %@ due to DELETE request", backgroundThreadObject); - [[self.objectStore managedObjectContextForCurrentThread] deleteObject:backgroundThreadObject]; + [[self.objectStore managedObjectContextForCurrentThread] deleteObject:backgroundThreadObject]; } - + // If the response was successful, save the store... if ([self.response isSuccessful]) { [self deleteCachedObjectsMissingFromResult:result]; @@ -167,14 +167,18 @@ - (void)processMappingResult:(RKObjectMappingResult*)result { RKLogError(@"Failed to save managed object context after mapping completed: %@", [error localizedDescription]); NSMethodSignature* signature = [(NSObject *)self methodSignatureForSelector:@selector(informDelegateOfError:)]; RKManagedObjectThreadSafeInvocation* invocation = [RKManagedObjectThreadSafeInvocation invocationWithMethodSignature:signature]; - [invocation setTarget:self.delegate]; + [invocation setTarget:self]; [invocation setSelector:@selector(informDelegateOfError:)]; [invocation setArgument:&error atIndex:2]; [invocation invokeOnMainThread]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self finalizeLoad:success]; + }); return; } } - + NSDictionary* dictionary = [result asDictionary]; NSMethodSignature* signature = [self methodSignatureForSelector:@selector(informDelegateOfObjectLoadWithResultDictionary:)]; RKManagedObjectThreadSafeInvocation* invocation = [RKManagedObjectThreadSafeInvocation invocationWithMethodSignature:signature]; @@ -189,7 +193,7 @@ - (void)processMappingResult:(RKObjectMappingResult*)result { // Overloaded to handle deleting an object orphaned by a failed postObject: - (void)handleResponseError { [super handleResponseError]; - + if (_targetObjectID) { if (_deleteObjectOnFailure) { RKLogInfo(@"Error response encountered: Deleting existing managed object with ID: %@", _targetObjectID); diff --git a/Code/CoreData/RKManagedObjectMapping.h b/Code/CoreData/RKManagedObjectMapping.h index 8490805b43..876ab23c9f 100644 --- a/Code/CoreData/RKManagedObjectMapping.h +++ b/Code/CoreData/RKManagedObjectMapping.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/31/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -75,7 +75,7 @@ /** Returns a dictionary containing Core Data relationships and attribute pairs containing - the primary key for + the primary key for */ @property (nonatomic, readonly) NSDictionary *relationshipsAndPrimaryKeyAttributes; @@ -85,18 +85,18 @@ @property (nonatomic, readonly) RKManagedObjectStore *objectStore; /** - Instructs RestKit to automatically connect a relationship of the object being mapped by looking up + Instructs RestKit to automatically connect a relationship of the object being mapped by looking up the related object by primary key. - + For example, given a Project object associated with a User, where the 'user' relationship is specified by a userID property on the managed object: - + [mapping connectRelationship:@"user" withObjectForPrimaryKeyAttribute:@"userID"]; - + Will hydrate the 'user' association on the managed object with the object in the local object graph having the primary key specified in the managed object's userID property. - + In effect, this approach allows foreign key relationships between managed objects to be automatically maintained from the server to the underlying Core Data object graph. */ @@ -105,7 +105,7 @@ /** Connects relationships using the primary key values contained in the specified attribute. This method is a short-cut for repeated invocation of `connectRelationship:withObjectForPrimaryKeyAttribute:`. - + @see connectRelationship:withObjectForPrimaryKeyAttribute: */ - (void)connectRelationshipsWithObjectsForPrimaryKeyAttributes:(NSString *)firstRelationshipName, ... NS_REQUIRES_NIL_TERMINATION; diff --git a/Code/CoreData/RKManagedObjectMapping.m b/Code/CoreData/RKManagedObjectMapping.m index 114d5f418a..8008218f90 100644 --- a/Code/CoreData/RKManagedObjectMapping.m +++ b/Code/CoreData/RKManagedObjectMapping.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/31/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -69,7 +69,7 @@ - (id)initWithEntity:(NSEntityDescription*)entity inManagedObjectStore:(RKManage [self addObserver:self forKeyPath:@"entity" options:NSKeyValueObservingOptionInitial context:nil]; [self addObserver:self forKeyPath:@"primaryKeyAttribute" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil]; } - + return self; } @@ -78,7 +78,7 @@ - (id)init { if (self) { _relationshipToPrimaryKeyMappings = [[NSMutableDictionary alloc] init]; } - + return self; } @@ -104,7 +104,7 @@ - (void)connectRelationshipsWithObjectsForPrimaryKeyAttributes:(NSString*)firstR va_list args; va_start(args, firstRelationshipName); for (NSString* relationshipName = firstRelationshipName; relationshipName != nil; relationshipName = va_arg(args, NSString*)) { - NSString* primaryKeyAttribute = va_arg(args, NSString*); + NSString* primaryKeyAttribute = va_arg(args, NSString*); NSAssert(primaryKeyAttribute != nil, @"Cannot connect a relationship without an attribute containing the primary key"); [self connectRelationship:relationshipName withObjectForPrimaryKeyAttribute:primaryKeyAttribute]; // TODO: Raise proper exception here, argument error... @@ -131,16 +131,16 @@ - (id)defaultValueForMissingAttribute:(NSString*)attributeName { return [desc defaultValue]; } -- (id)mappableObjectForData:(id)mappableData { +- (id)mappableObjectForData:(id)mappableData { NSAssert(mappableData, @"Mappable data cannot be nil"); id object = nil; id primaryKeyValue = nil; NSString* primaryKeyAttribute; - + NSEntityDescription* entity = [self entity]; RKObjectAttributeMapping* primaryKeyAttributeMapping = nil; - + primaryKeyAttribute = [self primaryKeyAttribute]; if (primaryKeyAttribute) { // If a primary key has been set on the object mapping, find the attribute mapping @@ -151,8 +151,8 @@ - (id)mappableObjectForData:(id)mappableData { break; } } - - // Get the primary key value out of the mappable data (if any) + + // Get the primary key value out of the mappable data (if any) if ([primaryKeyAttributeMapping isMappingForKeyOfNestedDictionary]) { RKLogDebug(@"Detected use of nested dictionary key as primaryKey attribute..."); primaryKeyValue = [[mappableData allKeys] lastObject]; @@ -160,19 +160,35 @@ - (id)mappableObjectForData:(id)mappableData { NSString* keyPathForPrimaryKeyElement = primaryKeyAttributeMapping.sourceKeyPath; if (keyPathForPrimaryKeyElement) { primaryKeyValue = [mappableData valueForKeyPath:keyPathForPrimaryKeyElement]; + } else { + RKLogWarning(@"Unable to find source attribute for primaryKeyAttribute '%@': unable to find existing object instances by primary key.", primaryKeyAttribute); } - } + } } - + // If we have found the primary key attribute & value, try to find an existing instance to update - if (primaryKeyAttribute && primaryKeyValue) { + if (primaryKeyAttribute && primaryKeyValue && NO == [primaryKeyValue isEqual:[NSNull null]]) { object = [self.objectStore.cacheStrategy findInstanceOfEntity:entity - withPrimaryKeyAttribute:self.primaryKeyAttribute value:primaryKeyValue inManagedObjectContext:[self.objectStore managedObjectContextForCurrentThread]]; + withPrimaryKeyAttribute:primaryKeyAttribute + value:primaryKeyValue + inManagedObjectContext:[self.objectStore managedObjectContextForCurrentThread]]; + + if (object && [self.objectStore.cacheStrategy respondsToSelector:@selector(didFetchObject:)]) { + [self.objectStore.cacheStrategy didFetchObject:object]; + } } if (object == nil) { object = [[[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:[_objectStore managedObjectContextForCurrentThread]] autorelease]; + if (primaryKeyAttribute && primaryKeyValue && ![primaryKeyValue isEqual:[NSNull null]]) { + id coercedPrimaryKeyValue = [entity coerceValueForPrimaryKey:primaryKeyValue]; + [object setValue:coercedPrimaryKeyValue forKey:primaryKeyAttribute]; + } + + if ([self.objectStore.cacheStrategy respondsToSelector:@selector(didCreateObject:)]) { + [self.objectStore.cacheStrategy didCreateObject:object]; + } } return object; } @@ -182,22 +198,22 @@ - (Class)classForProperty:(NSString*)propertyName { if (! propertyClass) { propertyClass = [[RKObjectPropertyInspector sharedInspector] typeForProperty:propertyName ofEntity:self.entity]; } - + return propertyClass; } /* - Allows the primaryKeyAttribute property on the NSEntityDescription to configure the mapping and vice-versa + Allows the primaryKeyAttributeName property on the NSEntityDescription to configure the mapping and vice-versa */ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"entity"]) { if (! self.primaryKeyAttribute) { - self.primaryKeyAttribute = [self.entity primaryKeyAttribute]; + self.primaryKeyAttribute = [self.entity primaryKeyAttributeName]; } } else if ([keyPath isEqualToString:@"primaryKeyAttribute"]) { if (! self.entity.primaryKeyAttribute) { - self.entity.primaryKeyAttribute = self.primaryKeyAttribute; + self.entity.primaryKeyAttributeName = self.primaryKeyAttribute; } } } diff --git a/Code/CoreData/RKManagedObjectMappingOperation.h b/Code/CoreData/RKManagedObjectMappingOperation.h index c7f6864954..14391d582a 100644 --- a/Code/CoreData/RKManagedObjectMappingOperation.h +++ b/Code/CoreData/RKManagedObjectMappingOperation.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/31/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,7 +24,7 @@ Enhances the object mapping operation process with Core Data specific logic */ @interface RKManagedObjectMappingOperation : RKObjectMappingOperation { - + } @end diff --git a/Code/CoreData/RKManagedObjectMappingOperation.m b/Code/CoreData/RKManagedObjectMappingOperation.m index 85db0d0c45..4d52293e6b 100644 --- a/Code/CoreData/RKManagedObjectMappingOperation.m +++ b/Code/CoreData/RKManagedObjectMappingOperation.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/31/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,6 +22,8 @@ #import "RKManagedObjectMapping.h" #import "NSManagedObject+ActiveRecord.h" #import "RKDynamicObjectMappingMatcher.h" +#import "RKManagedObjectCaching.h" +#import "RKManagedObjectStore.h" #import "RKLog.h" // Set Logging Component @@ -67,23 +69,31 @@ - (void)connectRelationship:(NSString *)relationshipName { id relatedObject = nil; if ([valueOfLocalPrimaryKeyAttribute conformsToProtocol:@protocol(NSFastEnumeration)]) { RKLogTrace(@"Connecting has-many relationship at keyPath '%@' to object with primaryKey attribute '%@'", relationshipName, primaryKeyAttributeOfRelatedObject); - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K IN %@", primaryKeyAttributeOfRelatedObject, valueOfLocalPrimaryKeyAttribute]; - NSFetchRequest *fetchRequest = [NSManagedObject requestAllInContext:[self.destinationObject managedObjectContext]]; - fetchRequest.predicate = predicate; - fetchRequest.entity = objectMapping.entity; - NSArray *objects = [NSManagedObject executeFetchRequest:fetchRequest inContext:[self.destinationObject managedObjectContext]]; - relatedObject = [NSSet setWithArray:objects]; + + // Implemented for issue 284 - https://github.com/RestKit/RestKit/issues/284 + relatedObject = [NSMutableSet set]; + NSObject *cache = [[(RKManagedObjectMapping*)[self objectMapping] objectStore] cacheStrategy]; + for (id foreignKey in valueOfLocalPrimaryKeyAttribute) { + id searchResult = [cache findInstanceOfEntity:objectMapping.entity withPrimaryKeyAttribute:primaryKeyAttributeOfRelatedObject value:foreignKey inManagedObjectContext:[[(RKManagedObjectMapping*)[self objectMapping] objectStore] managedObjectContextForCurrentThread]]; + if (searchResult) { + [relatedObject addObject:searchResult]; + } + } } else { RKLogTrace(@"Connecting has-one relationship at keyPath '%@' to object with primaryKey attribute '%@'", relationshipName, primaryKeyAttributeOfRelatedObject); - NSFetchRequest *fetchRequest = [NSManagedObject requestFirstByAttribute:primaryKeyAttributeOfRelatedObject withValue:valueOfLocalPrimaryKeyAttribute inContext:[self.destinationObject managedObjectContext]]; - fetchRequest.entity = objectMapping.entity; - NSArray *objects = [NSManagedObject executeFetchRequest:fetchRequest inContext:[self.destinationObject managedObjectContext]]; - relatedObject = [objects lastObject]; + + // Normal foreign key + NSObject *cache = [[(RKManagedObjectMapping*)[self objectMapping] objectStore] cacheStrategy]; + relatedObject = [cache findInstanceOfEntity:objectMapping.entity withPrimaryKeyAttribute:primaryKeyAttributeOfRelatedObject value:valueOfLocalPrimaryKeyAttribute inManagedObjectContext:[self.destinationObject managedObjectContext]]; } - if (relatedObject) { + if (relatedObject) { RKLogDebug(@"Connected relationship '%@' to object with primary key value '%@': %@", relationshipName, valueOfLocalPrimaryKeyAttribute, relatedObject); } else { -RKLogDebug(@"Failed to find instance of '%@' to connect relationship '%@' with primary key value '%@'", [[objectMapping entity] name], relationshipName, valueOfLocalPrimaryKeyAttribute); + RKLogDebug(@"Failed to find instance of '%@' to connect relationship '%@' with primary key value '%@'", [[objectMapping entity] name], relationshipName, valueOfLocalPrimaryKeyAttribute); + } + if ([relatedObject isKindOfClass:[NSManagedObject class]]) { + // Sanity check the managed object contexts + NSAssert([[(NSManagedObject *)self.destinationObject managedObjectContext] isEqual:[(NSManagedObject *)relatedObject managedObjectContext]], nil); } RKLogTrace(@"setValue of %@ forKeyPath %@", relatedObject, relationshipName); [self.destinationObject setValue:relatedObject forKeyPath:relationshipName]; @@ -93,26 +103,33 @@ - (void)connectRelationship:(NSString *)relationshipName { } - (void)connectRelationships { - if ([self.objectMapping isKindOfClass:[RKManagedObjectMapping class]]) { - NSDictionary* relationshipsAndPrimaryKeyAttributes = [(RKManagedObjectMapping*)self.objectMapping relationshipsAndPrimaryKeyAttributes]; - RKLogTrace(@"relationshipsAndPrimaryKeyAttributes: %@", relationshipsAndPrimaryKeyAttributes); - for (NSString* relationshipName in relationshipsAndPrimaryKeyAttributes) { - if (self.queue) { - RKLogTrace(@"Enqueueing relationship connection using operation queue"); - __block RKManagedObjectMappingOperation *selfRef = self; - [self.queue addOperationWithBlock:^{ - [selfRef connectRelationship:relationshipName]; - }]; - } else { - [self connectRelationship:relationshipName]; - } + NSDictionary* relationshipsAndPrimaryKeyAttributes = [(RKManagedObjectMapping *)self.objectMapping relationshipsAndPrimaryKeyAttributes]; + RKLogTrace(@"relationshipsAndPrimaryKeyAttributes: %@", relationshipsAndPrimaryKeyAttributes); + for (NSString* relationshipName in relationshipsAndPrimaryKeyAttributes) { + if (self.queue) { + RKLogTrace(@"Enqueueing relationship connection using operation queue"); + __block RKManagedObjectMappingOperation *selfRef = self; + [self.queue addOperationWithBlock:^{ + [selfRef connectRelationship:relationshipName]; + }]; + } else { + [self connectRelationship:relationshipName]; } } } - (BOOL)performMapping:(NSError **)error { BOOL success = [super performMapping:error]; - [self connectRelationships]; + if ([self.objectMapping isKindOfClass:[RKManagedObjectMapping class]]) { + /** + NOTE: Processing the pending changes here ensures that the managed object context generates observable + callbacks that are important for maintaining any sort of cache that is consistent within a single + object mapping operation. As the MOC is only saved when the aggregate operation is processed, we must + manually invoke processPendingChanges to prevent recreating objects with the same primary key. + See https://github.com/RestKit/RestKit/issues/661 + */ + [self connectRelationships]; + } return success; } diff --git a/Code/CoreData/RKManagedObjectSearchEngine.h b/Code/CoreData/RKManagedObjectSearchEngine.h index 92b0091156..64abcf570e 100644 --- a/Code/CoreData/RKManagedObjectSearchEngine.h +++ b/Code/CoreData/RKManagedObjectSearchEngine.h @@ -21,7 +21,7 @@ #import "RKSearchEngine.h" @interface RKManagedObjectSearchEngine : NSObject { - RKSearchMode _mode; + RKSearchMode _mode; } /** diff --git a/Code/CoreData/RKManagedObjectSearchEngine.m b/Code/CoreData/RKManagedObjectSearchEngine.m index d2a9f6a205..0740d99bc7 100644 --- a/Code/CoreData/RKManagedObjectSearchEngine.m +++ b/Code/CoreData/RKManagedObjectSearchEngine.m @@ -33,68 +33,68 @@ @implementation RKManagedObjectSearchEngine @synthesize mode = _mode; + (id)searchEngine { - RKManagedObjectSearchEngine* searchEngine = [[[RKManagedObjectSearchEngine alloc] init] autorelease]; - return searchEngine; + RKManagedObjectSearchEngine* searchEngine = [[[RKManagedObjectSearchEngine alloc] init] autorelease]; + return searchEngine; } - (id)init { - if (self = [super init]) { - _mode = RKSearchModeOr; - } + if (self = [super init]) { + _mode = RKSearchModeOr; + } - return self; + return self; } #pragma mark - #pragma mark Private - (NSPredicate*)predicateForSearch:(NSArray*)searchTerms compoundSelector:(SEL)selector { - NSMutableArray* termPredicates = [NSMutableArray array]; - for (NSString* searchTerm in searchTerms) { - [termPredicates addObject: - [NSPredicate predicateWithFormat:@"(ANY searchWords.word beginswith %@)", searchTerm]]; - } - return [NSCompoundPredicate performSelector:selector withObject:termPredicates]; + NSMutableArray* termPredicates = [NSMutableArray array]; + for (NSString* searchTerm in searchTerms) { + [termPredicates addObject: + [NSPredicate predicateWithFormat:@"(ANY searchWords.word beginswith %@)", searchTerm]]; + } + return [NSCompoundPredicate performSelector:selector withObject:termPredicates]; } #pragma mark - #pragma mark Public + (NSArray*)tokenizedNormalizedString:(NSString*)string { - if (__removeSet == nil) { - NSMutableCharacterSet* removeSet = [[NSCharacterSet alphanumericCharacterSet] mutableCopy]; - [removeSet formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]]; - [removeSet invert]; - __removeSet = removeSet; - } - - NSString* scannerString = [[[[string lowercaseString] decomposedStringWithCanonicalMapping] - stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] - stringByReplacingOccurrencesOfString:@"-" withString:@" "]; - - NSArray* tokens = [[[scannerString componentsSeparatedByCharactersInSet:__removeSet] - componentsJoinedByString:@""] componentsSeparatedByString:@" "]; - return tokens; + if (__removeSet == nil) { + NSMutableCharacterSet* removeSet = [[NSCharacterSet alphanumericCharacterSet] mutableCopy]; + [removeSet formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]]; + [removeSet invert]; + __removeSet = removeSet; + } + + NSString* scannerString = [[[[string lowercaseString] decomposedStringWithCanonicalMapping] + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] + stringByReplacingOccurrencesOfString:@"-" withString:@" "]; + + NSArray* tokens = [[[scannerString componentsSeparatedByCharactersInSet:__removeSet] + componentsJoinedByString:@""] componentsSeparatedByString:@" "]; + return tokens; } - (NSPredicate*)predicateForSearch:(NSString*)searchText { - NSString* searchQuery = [searchText copy]; - NSArray* searchTerms = [RKManagedObjectSearchEngine tokenizedNormalizedString:searchQuery]; - [searchQuery release]; - - if ([searchTerms count] == 0) { - return nil; - } - - if (_mode == RKSearchModeOr) { - return [self predicateForSearch:searchTerms - compoundSelector:@selector(orPredicateWithSubpredicates:)]; - } else if (_mode == RKSearchModeAnd) { - return [self predicateForSearch:searchTerms - compoundSelector:@selector(andPredicateWithSubpredicates:)]; - } else { - return nil; - } + NSString* searchQuery = [searchText copy]; + NSArray* searchTerms = [RKManagedObjectSearchEngine tokenizedNormalizedString:searchQuery]; + [searchQuery release]; + + if ([searchTerms count] == 0) { + return nil; + } + + if (_mode == RKSearchModeOr) { + return [self predicateForSearch:searchTerms + compoundSelector:@selector(orPredicateWithSubpredicates:)]; + } else if (_mode == RKSearchModeAnd) { + return [self predicateForSearch:searchTerms + compoundSelector:@selector(andPredicateWithSubpredicates:)]; + } else { + return nil; + } } @end diff --git a/Code/CoreData/RKManagedObjectSeeder.h b/Code/CoreData/RKManagedObjectSeeder.h index 56a8787ec5..c856a81ce6 100644 --- a/Code/CoreData/RKManagedObjectSeeder.h +++ b/Code/CoreData/RKManagedObjectSeeder.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 3/4/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -39,7 +39,7 @@ extern NSString* const RKDefaultSeedDatabaseFileName; * data immediately available for use within Core Data. */ @interface RKManagedObjectSeeder : NSObject { - RKObjectManager* _manager; + RKObjectManager* _manager; NSObject* _delegate; } diff --git a/Code/CoreData/RKManagedObjectSeeder.m b/Code/CoreData/RKManagedObjectSeeder.m index 754e5999a3..f779984f41 100644 --- a/Code/CoreData/RKManagedObjectSeeder.m +++ b/Code/CoreData/RKManagedObjectSeeder.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 3/4/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -44,20 +44,20 @@ @implementation RKManagedObjectSeeder + (void)generateSeedDatabaseWithObjectManager:(RKObjectManager*)objectManager fromFiles:(NSString*)firstFileName, ... { RKManagedObjectSeeder* seeder = [RKManagedObjectSeeder objectSeederWithObjectManager:objectManager]; - + va_list args; va_start(args, firstFileName); - NSMutableArray* fileNames = [NSMutableArray array]; + NSMutableArray* fileNames = [NSMutableArray array]; for (NSString* fileName = firstFileName; fileName != nil; fileName = va_arg(args, id)) { [fileNames addObject:fileName]; } va_end(args); - + // Seed the files for (NSString* fileName in fileNames) { [seeder seedObjectsFromFile:fileName withObjectMapping:nil]; } - + [seeder finalizeSeedingAndExit]; } @@ -67,24 +67,24 @@ + (RKManagedObjectSeeder*)objectSeederWithObjectManager:(RKObjectManager*)object - (id)initWithObjectManager:(RKObjectManager*)manager { self = [self init]; - if (self) { - _manager = [manager retain]; - + if (self) { + _manager = [manager retain]; + // If the user hasn't configured an object store, set one up for them if (nil == _manager.objectStore) { _manager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:RKDefaultSeedDatabaseFileName]; } - + // Delete any existing persistent store - [_manager.objectStore deletePersistantStore]; - } - - return self; + [_manager.objectStore deletePersistentStore]; + } + + return self; } - (void)dealloc { - [_manager release]; - [super dealloc]; + [_manager release]; + [super dealloc]; } - (NSString*)pathToSeedDatabase { @@ -94,12 +94,12 @@ - (NSString*)pathToSeedDatabase { - (void)seedObjectsFromFiles:(NSString*)firstFileName, ... { va_list args; va_start(args, firstFileName); - NSMutableArray* fileNames = [NSMutableArray array]; + NSMutableArray* fileNames = [NSMutableArray array]; for (NSString* fileName = firstFileName; fileName != nil; fileName = va_arg(args, id)) { [fileNames addObject:fileName]; } va_end(args); - + for (NSString* fileName in fileNames) { [self seedObjectsFromFile:fileName withObjectMapping:nil]; } @@ -111,16 +111,16 @@ - (void)seedObjectsFromFile:(NSString*)fileName withObjectMapping:(RKObjectMappi - (void)seedObjectsFromFile:(NSString *)fileName withObjectMapping:(RKObjectMapping *)nilOrObjectMapping bundle:(NSBundle *)nilOrBundle { - NSError* error = nil; - - if (nilOrBundle == nil) { - nilOrBundle = [NSBundle mainBundle]; - } - - NSString* filePath = [nilOrBundle pathForResource:fileName ofType:nil]; - NSString* payload = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error]; - - if (payload) { + NSError* error = nil; + + if (nilOrBundle == nil) { + nilOrBundle = [NSBundle mainBundle]; + } + + NSString* filePath = [nilOrBundle pathForResource:fileName ofType:nil]; + NSString* payload = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error]; + + if (payload) { NSString* MIMEType = [fileName MIMETypeForPathExtension]; if (MIMEType == nil) { // Default the MIME type to the value of the Accept header if we couldn't detect it... @@ -128,9 +128,9 @@ - (void)seedObjectsFromFile:(NSString *)fileName withObjectMapping:(RKObjectMapp } id parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:MIMEType]; NSAssert1(parser, @"Could not find a parser for the MIME Type '%@'", MIMEType); - id parsedData = [parser objectFromString:payload error:&error]; + id parsedData = [parser objectFromString:payload error:&error]; NSAssert(parsedData, @"Cannot perform object load without data for mapping"); - + RKObjectMappingProvider* mappingProvider = nil; if (nilOrObjectMapping) { mappingProvider = [[RKObjectMappingProvider new] autorelease]; @@ -138,46 +138,46 @@ - (void)seedObjectsFromFile:(NSString *)fileName withObjectMapping:(RKObjectMapp } else { mappingProvider = _manager.mappingProvider; } - + RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:parsedData mappingProvider:mappingProvider]; RKObjectMappingResult* result = [mapper performMapping]; if (result == nil) { RKLogError(@"Database seeding from file '%@' failed due to object mapping errors: %@", fileName, mapper.errors); return; } - + NSArray* mappedObjects = [result asCollection]; - NSAssert1([mappedObjects isKindOfClass:[NSArray class]], @"Expected an NSArray of objects, got %@", mappedObjects); - + NSAssert1([mappedObjects isKindOfClass:[NSArray class]], @"Expected an NSArray of objects, got %@", mappedObjects); + // Inform the delegate if (self.delegate) { for (NSManagedObject* object in mappedObjects) { [self.delegate didSeedObject:object fromFile:fileName]; } } - - RKLogInfo(@"Seeded %lu objects from %@...", (unsigned long) [mappedObjects count], [NSString stringWithFormat:@"%@", fileName]); - } else { - RKLogError(@"Unable to read file %@: %@", fileName, [error localizedDescription]); - } + + RKLogInfo(@"Seeded %lu objects from %@...", (unsigned long) [mappedObjects count], [NSString stringWithFormat:@"%@", fileName]); + } else { + RKLogError(@"Unable to read file %@: %@", fileName, [error localizedDescription]); + } } - (void)finalizeSeedingAndExit { - NSError *error = nil; + NSError *error = nil; BOOL success = [[_manager objectStore] save:&error]; - if (! success) { - RKLogError(@"[RestKit] RKManagedObjectSeeder: Error saving object context: %@", [error localizedDescription]); - } - - NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString* basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil; - NSString* storeFileName = [[_manager objectStore] storeFilename]; - NSString* destinationPath = [basePath stringByAppendingPathComponent:storeFileName]; - RKLogInfo(@"A seeded database has been generated at '%@'. " - @"Please execute `open \"%@\"` in your Terminal and copy %@ to your app. Be sure to add the seed database to your \"Copy Resources\" build phase.", + if (! success) { + RKLogError(@"[RestKit] RKManagedObjectSeeder: Error saving object context: %@", [error localizedDescription]); + } + + NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString* basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil; + NSString* storeFileName = [[_manager objectStore] storeFilename]; + NSString* destinationPath = [basePath stringByAppendingPathComponent:storeFileName]; + RKLogInfo(@"A seeded database has been generated at '%@'. " + @"Please execute `open \"%@\"` in your Terminal and copy %@ to your app. Be sure to add the seed database to your \"Copy Resources\" build phase.", destinationPath, basePath, storeFileName); - - exit(1); + + exit(1); } @end diff --git a/Code/CoreData/RKManagedObjectStore.h b/Code/CoreData/RKManagedObjectStore.h index 18a8bb4415..75bfd8e64f 100644 --- a/Code/CoreData/RKManagedObjectStore.h +++ b/Code/CoreData/RKManagedObjectStore.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/22/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,7 +20,7 @@ #import #import "RKManagedObjectMapping.h" -#import "RKManagedObjectMappingCache.h" +#import "RKManagedObjectCaching.h" @class RKManagedObjectStore; @@ -47,11 +47,11 @@ extern NSString* const RKManagedObjectStoreDidFailSaveNotification; /////////////////////////////////////////////////////////////////// @interface RKManagedObjectStore : NSObject { - NSObject* _delegate; - NSString* _storeFilename; - NSString* _pathToStoreFile; + NSObject* _delegate; + NSString* _storeFilename; + NSString* _pathToStoreFile; NSManagedObjectModel* _managedObjectModel; - NSPersistentStoreCoordinator* _persistentStoreCoordinator; + NSPersistentStoreCoordinator* _persistentStoreCoordinator; } // The delegate for this object store @@ -100,7 +100,7 @@ extern NSString* const RKManagedObjectStoreDidFailSaveNotification; /** */ -@property (nonatomic, retain) NSObject *cacheStrategy; +@property (nonatomic, retain) NSObject *cacheStrategy; /** * Initialize a new managed object store with a SQLite database with the filename specified @@ -137,19 +137,19 @@ extern NSString* const RKManagedObjectStoreDidFailSaveNotification; /** * This deletes and recreates the managed object context and - * persistant store, effectively clearing all data + * persistent store, effectively clearing all data */ -- (void)deletePersistantStoreUsingSeedDatabaseName:(NSString *)seedFile; -- (void)deletePersistantStore; +- (void)deletePersistentStoreUsingSeedDatabaseName:(NSString *)seedFile; +- (void)deletePersistentStore; /** - * Retrieves a model object from the appropriate context using the objectId + * Retrieves a model object from the appropriate context using the objectId */ - (NSManagedObject*)objectWithID:(NSManagedObjectID*)objectID; /** - * Retrieves a array of model objects from the appropriate context using - * an array of NSManagedObjectIDs + * Retrieves a array of model objects from the appropriate context using + * an array of NSManagedObjectIDs */ - (NSArray*)objectsWithIDs:(NSArray*)objectIDs; diff --git a/Code/CoreData/RKManagedObjectStore.m b/Code/CoreData/RKManagedObjectStore.m index 8acc6671ad..fcc614ceb0 100644 --- a/Code/CoreData/RKManagedObjectStore.m +++ b/Code/CoreData/RKManagedObjectStore.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/22/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -68,14 +68,14 @@ + (void)setDefaultObjectStore:(RKManagedObjectStore *)objectStore { [objectStore retain]; [defaultObjectStore release]; defaultObjectStore = objectStore; - + [NSManagedObjectContext setDefaultContext:objectStore.primaryManagedObjectContext]; } + (void)deleteStoreAtPath:(NSString *)path { NSURL* storeURL = [NSURL fileURLWithPath:path]; - NSError* error = nil; + NSError* error = nil; if ([[NSFileManager defaultManager] fileExistsAtPath:storeURL.path]) { if (! [[NSFileManager defaultManager] removeItemAtPath:storeURL.path error:&error]) { NSAssert(NO, @"Managed object store failed to delete persistent store : %@", error); @@ -104,28 +104,32 @@ + (RKManagedObjectStore*)objectStoreWithStoreFilename:(NSString *)storeFilename } - (id)initWithStoreFilename:(NSString*)storeFilename { - return [self initWithStoreFilename:storeFilename inDirectory:nil usingSeedDatabaseName:nil managedObjectModel:nil delegate:nil]; + return [self initWithStoreFilename:storeFilename inDirectory:nil usingSeedDatabaseName:nil managedObjectModel:nil delegate:nil]; } - (id)initWithStoreFilename:(NSString *)storeFilename inDirectory:(NSString *)nilOrDirectoryPath usingSeedDatabaseName:(NSString *)nilOrNameOfSeedDatabaseInMainBundle managedObjectModel:(NSManagedObjectModel*)nilOrManagedObjectModel delegate:(id)delegate { self = [self init]; - if (self) { - _storeFilename = [storeFilename retain]; - - if (nilOrDirectoryPath == nil) { - nilOrDirectoryPath = [RKDirectory applicationDataDirectory]; - } else { - BOOL isDir; - NSAssert1([[NSFileManager defaultManager] fileExistsAtPath:nilOrDirectoryPath isDirectory:&isDir] && isDir == YES, @"Specified storage directory exists", nilOrDirectoryPath); - } - _pathToStoreFile = [[nilOrDirectoryPath stringByAppendingPathComponent:_storeFilename] retain]; + if (self) { + _storeFilename = [storeFilename retain]; + + if (nilOrDirectoryPath == nil) { + // If initializing into Application Data directory, ensure the directory exists + nilOrDirectoryPath = [RKDirectory applicationDataDirectory]; + [RKDirectory ensureDirectoryExistsAtPath:nilOrDirectoryPath error:nil]; + } else { + // If path given, caller is responsible for directory's existence + BOOL isDir; + NSAssert1([[NSFileManager defaultManager] fileExistsAtPath:nilOrDirectoryPath isDirectory:&isDir] && isDir == YES, @"Specified storage directory exists", nilOrDirectoryPath); + } + _pathToStoreFile = [[nilOrDirectoryPath stringByAppendingPathComponent:_storeFilename] retain]; if (nilOrManagedObjectModel == nil) { // NOTE: allBundles permits Core Data setup in unit tests - nilOrManagedObjectModel = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]]; + nilOrManagedObjectModel = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]]; } - NSMutableArray* allManagedObjectModels = [NSMutableArray arrayWithObject:nilOrManagedObjectModel]; - _managedObjectModel = [[NSManagedObjectModel modelByMergingModels:allManagedObjectModels] retain]; + NSMutableArray* allManagedObjectModels = [NSMutableArray arrayWithObject:nilOrManagedObjectModel]; + _managedObjectModel = [[NSManagedObjectModel modelByMergingModels:allManagedObjectModels] retain]; + _delegate = delegate; _delegate = delegate; @@ -133,22 +137,21 @@ - (id)initWithStoreFilename:(NSString *)storeFilename inDirectory:(NSString *)ni [self createStoreIfNecessaryUsingSeedDatabase:nilOrNameOfSeedDatabaseInMainBundle]; } - [self createPersistentStoreCoordinator]; - self.primaryManagedObjectContext = [self newManagedObjectContext]; - + [self createPersistentStoreCoordinator]; + self.primaryManagedObjectContext = [[self newManagedObjectContext] autorelease]; _cacheStrategy = [RKInMemoryManagedObjectCache new]; // Ensure there is a search word observer [RKSearchWordObserver sharedObserver]; - + // Hydrate the defaultObjectStore if (! defaultObjectStore) { [RKManagedObjectStore setDefaultObjectStore:self]; } - } + } - return self; + return self; } - (void)setThreadLocalObject:(id)value forKey:(id)key { @@ -193,24 +196,24 @@ - (void)clearThreadLocalStorage { } - (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; [self clearThreadLocalStorage]; - [_storeFilename release]; - _storeFilename = nil; - [_pathToStoreFile release]; - _pathToStoreFile = nil; + [_storeFilename release]; + _storeFilename = nil; + [_pathToStoreFile release]; + _pathToStoreFile = nil; [_managedObjectModel release]; - _managedObjectModel = nil; + _managedObjectModel = nil; [_persistentStoreCoordinator release]; - _persistentStoreCoordinator = nil; + _persistentStoreCoordinator = nil; [_cacheStrategy release]; _cacheStrategy = nil; [primaryManagedObjectContext release]; primaryManagedObjectContext = nil; - - [super dealloc]; + + [super dealloc]; } /** @@ -218,75 +221,75 @@ - (void)dealloc { message to the application's managed object context. */ - (BOOL)save:(NSError **)error { - NSManagedObjectContext* moc = [self managedObjectContextForCurrentThread]; + NSManagedObjectContext* moc = [self managedObjectContextForCurrentThread]; NSError *localError = nil; - - @try { - if (![moc save:&localError]) { - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToSaveContext:error:exception:)]) { - [self.delegate managedObjectStore:self didFailToSaveContext:moc error:localError exception:nil]; - } - - NSDictionary* userInfo = [NSDictionary dictionaryWithObject:localError forKey:@"error"]; - [[NSNotificationCenter defaultCenter] postNotificationName:RKManagedObjectStoreDidFailSaveNotification object:self userInfo:userInfo]; - - if ([[localError domain] isEqualToString:@"NSCocoaErrorDomain"]) { - NSDictionary *userInfo = [localError userInfo]; - NSArray *errors = [userInfo valueForKey:@"NSDetailedErrors"]; - if (errors) { - for (NSError *detailedError in errors) { - NSDictionary *subUserInfo = [detailedError userInfo]; - RKLogError(@"Core Data Save Error\n \ - NSLocalizedDescription:\t\t%@\n \ - NSValidationErrorKey:\t\t\t%@\n \ - NSValidationErrorPredicate:\t%@\n \ - NSValidationErrorObject:\n%@\n", - [subUserInfo valueForKey:@"NSLocalizedDescription"], - [subUserInfo valueForKey:@"NSValidationErrorKey"], - [subUserInfo valueForKey:@"NSValidationErrorPredicate"], - [subUserInfo valueForKey:@"NSValidationErrorObject"]); - } - } - else { - RKLogError(@"Core Data Save Error\n \ - NSLocalizedDescription:\t\t%@\n \ - NSValidationErrorKey:\t\t\t%@\n \ - NSValidationErrorPredicate:\t%@\n \ - NSValidationErrorObject:\n%@\n", - [userInfo valueForKey:@"NSLocalizedDescription"], - [userInfo valueForKey:@"NSValidationErrorKey"], - [userInfo valueForKey:@"NSValidationErrorPredicate"], - [userInfo valueForKey:@"NSValidationErrorObject"]); - } - } - + + @try { + if (![moc save:&localError]) { + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToSaveContext:error:exception:)]) { + [self.delegate managedObjectStore:self didFailToSaveContext:moc error:localError exception:nil]; + } + + NSDictionary* userInfo = [NSDictionary dictionaryWithObject:localError forKey:@"error"]; + [[NSNotificationCenter defaultCenter] postNotificationName:RKManagedObjectStoreDidFailSaveNotification object:self userInfo:userInfo]; + + if ([[localError domain] isEqualToString:@"NSCocoaErrorDomain"]) { + NSDictionary *userInfo = [localError userInfo]; + NSArray *errors = [userInfo valueForKey:@"NSDetailedErrors"]; + if (errors) { + for (NSError *detailedError in errors) { + NSDictionary *subUserInfo = [detailedError userInfo]; + RKLogError(@"Core Data Save Error\n \ + NSLocalizedDescription:\t\t%@\n \ + NSValidationErrorKey:\t\t\t%@\n \ + NSValidationErrorPredicate:\t%@\n \ + NSValidationErrorObject:\n%@\n", + [subUserInfo valueForKey:@"NSLocalizedDescription"], + [subUserInfo valueForKey:@"NSValidationErrorKey"], + [subUserInfo valueForKey:@"NSValidationErrorPredicate"], + [subUserInfo valueForKey:@"NSValidationErrorObject"]); + } + } + else { + RKLogError(@"Core Data Save Error\n \ + NSLocalizedDescription:\t\t%@\n \ + NSValidationErrorKey:\t\t\t%@\n \ + NSValidationErrorPredicate:\t%@\n \ + NSValidationErrorObject:\n%@\n", + [userInfo valueForKey:@"NSLocalizedDescription"], + [userInfo valueForKey:@"NSValidationErrorKey"], + [userInfo valueForKey:@"NSValidationErrorPredicate"], + [userInfo valueForKey:@"NSValidationErrorObject"]); + } + } + if (error) { *error = localError; } - - return NO; - } - } - @catch (NSException* e) { - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToSaveContext:error:exception:)]) { - [self.delegate managedObjectStore:self didFailToSaveContext:moc error:nil exception:e]; - } - else { - @throw; - } - } - - return YES; + + return NO; + } + } + @catch (NSException* e) { + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToSaveContext:error:exception:)]) { + [self.delegate managedObjectStore:self didFailToSaveContext:moc error:nil exception:e]; + } + else { + @throw; + } + } + + return YES; } - (NSManagedObjectContext *)newManagedObjectContext { - NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] init]; - [managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator]; - [managedObjectContext setUndoManager:nil]; - [managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy]; + NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] init]; + [managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator]; + [managedObjectContext setUndoManager:nil]; + [managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy]; managedObjectContext.managedObjectStore = self; - - return managedObjectContext; + + return managedObjectContext; } - (void)createStoreIfNecessaryUsingSeedDatabase:(NSString*)seedDatabase { @@ -295,13 +298,13 @@ - (void)createStoreIfNecessaryUsingSeedDatabase:(NSString*)seedDatabase { NSAssert1(seedDatabasePath, @"Unable to find seed database file '%@' in the Main Bundle, aborting...", seedDatabase); RKLogInfo(@"No existing database found, copying from seed path '%@'", seedDatabasePath); - NSError* error; + NSError* error; if (![[NSFileManager defaultManager] copyItemAtPath:seedDatabasePath toPath:self.pathToStoreFile error:&error]) { - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToCopySeedDatabase:error:)]) { - [self.delegate managedObjectStore:self didFailToCopySeedDatabase:seedDatabase error:error]; - } else { - RKLogError(@"Encountered an error during seed database copy: %@", [error localizedDescription]); - } + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToCopySeedDatabase:error:)]) { + [self.delegate managedObjectStore:self didFailToCopySeedDatabase:seedDatabase error:error]; + } else { + RKLogError(@"Encountered an error during seed database copy: %@", [error localizedDescription]); + } } NSAssert1([[NSFileManager defaultManager] fileExistsAtPath:seedDatabasePath], @"Seed database not found at path '%@'!", seedDatabasePath); } @@ -310,28 +313,28 @@ - (void)createStoreIfNecessaryUsingSeedDatabase:(NSString*)seedDatabase { - (void)createPersistentStoreCoordinator { NSAssert(_managedObjectModel, @"Cannot create persistent store coordinator without a managed object model"); NSAssert(!_persistentStoreCoordinator, @"Cannot create persistent store coordinator: one already exists."); - NSURL *storeURL = [NSURL fileURLWithPath:self.pathToStoreFile]; + NSURL *storeURL = [NSURL fileURLWithPath:self.pathToStoreFile]; - NSError *error; + NSError *error; _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_managedObjectModel]; - // Allow inferred migration from the original version of the application. - NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: - [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, - [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil]; - - if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) { - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToCreatePersistentStoreCoordinatorWithError:)]) { - [self.delegate managedObjectStore:self didFailToCreatePersistentStoreCoordinatorWithError:error]; - } else { - NSAssert(NO, @"Managed object store failed to create persistent store coordinator: %@", error); - } + // Allow inferred migration from the original version of the application. + NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, + [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil]; + + if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) { + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToCreatePersistentStoreCoordinatorWithError:)]) { + [self.delegate managedObjectStore:self didFailToCreatePersistentStoreCoordinatorWithError:error]; + } else { + NSAssert(NO, @"Managed object store failed to create persistent store coordinator: %@", error); + } } } -- (void)deletePersistantStoreUsingSeedDatabaseName:(NSString *)seedFile { - NSURL* storeURL = [NSURL fileURLWithPath:self.pathToStoreFile]; - NSError* error = nil; +- (void)deletePersistentStoreUsingSeedDatabaseName:(NSString *)seedFile { + NSURL* storeURL = [NSURL fileURLWithPath:self.pathToStoreFile]; + NSError* error = nil; if ([[NSFileManager defaultManager] fileExistsAtPath:storeURL.path]) { if (![[NSFileManager defaultManager] removeItemAtPath:storeURL.path error:&error]) { if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToDeletePersistentStore:error:)]) { @@ -345,57 +348,57 @@ - (void)deletePersistantStoreUsingSeedDatabaseName:(NSString *)seedFile { RKLogWarning(@"Asked to delete persistent store but no store file exists at path: %@", storeURL.path); } - [_persistentStoreCoordinator release]; - _persistentStoreCoordinator = nil; + [_persistentStoreCoordinator release]; + _persistentStoreCoordinator = nil; - if (seedFile) { + if (seedFile) { [self createStoreIfNecessaryUsingSeedDatabase:seedFile]; } - [self createPersistentStoreCoordinator]; -} + [self createPersistentStoreCoordinator]; -- (void)deletePersistantStore { - [self deletePersistantStoreUsingSeedDatabaseName:nil]; - // Recreate the MOC - self.primaryManagedObjectContext = [self newManagedObjectContext]; + self.primaryManagedObjectContext = [[self newManagedObjectContext] autorelease]; +} + +- (void)deletePersistentStore { + [self deletePersistentStoreUsingSeedDatabaseName:nil]; } - (NSManagedObjectContext *)managedObjectContextForCurrentThread { if ([NSThread isMainThread]) { return self.primaryManagedObjectContext; } - + // Background threads leverage thread-local storage - NSManagedObjectContext* managedObjectContext = [self threadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryContextKey]; - if (!managedObjectContext) { - managedObjectContext = [self newManagedObjectContext]; + NSManagedObjectContext* managedObjectContext = [self threadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryContextKey]; + if (!managedObjectContext) { + managedObjectContext = [self newManagedObjectContext]; // Store into thread local storage dictionary [self setThreadLocalObject:managedObjectContext forKey:RKManagedObjectStoreThreadDictionaryContextKey]; - [managedObjectContext release]; + [managedObjectContext release]; // If we are a background Thread MOC, we need to inform the main thread on save [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:managedObjectContext]; - } - - return managedObjectContext; + } + + return managedObjectContext; } - (void)mergeChangesOnMainThreadWithNotification:(NSNotification*)notification { - assert([NSThread isMainThread]); - [self.primaryManagedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) - withObject:notification - waitUntilDone:YES]; + assert([NSThread isMainThread]); + [self.primaryManagedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) + withObject:notification + waitUntilDone:YES]; } - (void)mergeChanges:(NSNotification *)notification { - // Merge changes into the main context on the main thread - [self performSelectorOnMainThread:@selector(mergeChangesOnMainThreadWithNotification:) withObject:notification waitUntilDone:YES]; + // Merge changes into the main context on the main thread + [self performSelectorOnMainThread:@selector(mergeChangesOnMainThreadWithNotification:) withObject:notification waitUntilDone:YES]; } #pragma mark - @@ -403,18 +406,18 @@ - (void)mergeChanges:(NSNotification *)notification { - (NSManagedObject*)objectWithID:(NSManagedObjectID *)objectID { NSAssert(objectID, @"Cannot fetch a managedObject with a nil objectID"); - return [[self managedObjectContextForCurrentThread] objectWithID:objectID]; + return [[self managedObjectContextForCurrentThread] objectWithID:objectID]; } - (NSArray*)objectsWithIDs:(NSArray*)objectIDs { - NSMutableArray* objects = [[NSMutableArray alloc] init]; - for (NSManagedObjectID* objectID in objectIDs) { - [objects addObject:[self objectWithID:objectID]]; - } - NSArray* objectArray = [NSArray arrayWithArray:objects]; - [objects release]; - - return objectArray; + NSMutableArray* objects = [[NSMutableArray alloc] init]; + for (NSManagedObjectID* objectID in objectIDs) { + [objects addObject:[self objectWithID:objectID]]; + } + NSArray* objectArray = [NSArray arrayWithArray:objects]; + [objects release]; + + return objectArray; } @end diff --git a/Code/CoreData/RKManagedObjectThreadSafeInvocation.h b/Code/CoreData/RKManagedObjectThreadSafeInvocation.h index 36660fa2dc..8d259715ab 100644 --- a/Code/CoreData/RKManagedObjectThreadSafeInvocation.h +++ b/Code/CoreData/RKManagedObjectThreadSafeInvocation.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/12/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/CoreData/RKManagedObjectThreadSafeInvocation.m b/Code/CoreData/RKManagedObjectThreadSafeInvocation.m index a81c172dd6..d4d805c5e1 100644 --- a/Code/CoreData/RKManagedObjectThreadSafeInvocation.m +++ b/Code/CoreData/RKManagedObjectThreadSafeInvocation.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/12/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,19 +29,32 @@ + (RKManagedObjectThreadSafeInvocation*)invocationWithMethodSignature:(NSMethodS } - (void)setManagedObjectKeyPaths:(NSSet*)keyPaths forArgument:(NSInteger)index { - if (nil == _argumentKeyPaths) { + if (nil == _argumentKeyPaths) { _argumentKeyPaths = [[NSMutableDictionary alloc] init]; } - + NSNumber* argumentIndex = [NSNumber numberWithInteger:index]; [_argumentKeyPaths setObject:keyPaths forKey:argumentIndex]; } +- (void)setValue:(id)value forKeyPathOrKey:(NSString *)keyPath object:(id)object { + [object setValue:value forKeyPath:keyPath]; + + id testValue = [object valueForKeyPath:keyPath]; + if (![value isEqual:testValue]) { + [object setValue:value forKey:keyPath]; + testValue = [object valueForKeyPath:keyPath]; + + NSAssert([value isEqual:testValue], @"Could not set value"); + } +} + - (void)serializeManagedObjectsForArgument:(id)argument withKeyPaths:(NSSet*)keyPaths { for (NSString* keyPath in keyPaths) { id value = [argument valueForKeyPath:keyPath]; if ([value isKindOfClass:[NSManagedObject class]]) { - [argument setValue:[(NSManagedObject*)value objectID] forKeyPath:keyPath]; + NSManagedObjectID *objectID = [(NSManagedObject*)value objectID]; + [self setValue:objectID forKeyPathOrKey:keyPath object:argument]; } else if ([value respondsToSelector:@selector(allObjects)]) { id collection = [[[[[value class] alloc] init] autorelease] mutableCopy]; for (id subObject in value) { @@ -51,21 +64,21 @@ - (void)serializeManagedObjectsForArgument:(id)argument withKeyPaths:(NSSet*)key [collection addObject:subObject]; } } - - [argument setValue:collection forKeyPath:keyPath]; + + [self setValue:collection forKeyPathOrKey:keyPath object:argument]; [collection release]; } } } -- (void)deserializeManagedObjectIDsForArgument:(id)argument withKeyPaths:(NSSet*)keyPaths { +- (void)deserializeManagedObjectIDsForArgument:(id)argument withKeyPaths:(NSSet*)keyPaths { for (NSString* keyPath in keyPaths) { id value = [argument valueForKeyPath:keyPath]; if ([value isKindOfClass:[NSManagedObjectID class]]) { NSAssert(self.objectStore, @"Object store cannot be nil"); NSManagedObject* managedObject = [self.objectStore objectWithID:(NSManagedObjectID*)value]; NSAssert(managedObject, @"Expected managed object for ID %@, got nil", value); - [argument setValue:managedObject forKeyPath:keyPath]; + [self setValue:managedObject forKeyPathOrKey:keyPath object:argument]; } else if ([value respondsToSelector:@selector(allObjects)]) { id collection = [[[[[value class] alloc] init] autorelease] mutableCopy]; for (id subObject in value) { @@ -77,15 +90,14 @@ - (void)deserializeManagedObjectIDsForArgument:(id)argument withKeyPaths:(NSSet* [collection addObject:subObject]; } } - - [argument setValue:collection forKeyPath:keyPath]; + + [self setValue:collection forKeyPathOrKey:keyPath object:argument]; [collection release]; } } } - - (void)serializeManagedObjects { - for (NSNumber* argumentIndex in _argumentKeyPaths) { + for (NSNumber* argumentIndex in _argumentKeyPaths) { NSSet* managedKeyPaths = [_argumentKeyPaths objectForKey:argumentIndex]; id argument = nil; [self getArgument:&argument atIndex:[argumentIndex intValue]]; @@ -96,7 +108,7 @@ - (void)serializeManagedObjects { } - (void)deserializeManagedObjects { - for (NSNumber* argumentIndex in _argumentKeyPaths) { + for (NSNumber* argumentIndex in _argumentKeyPaths) { NSSet* managedKeyPaths = [_argumentKeyPaths objectForKey:argumentIndex]; id argument = nil; [self getArgument:&argument atIndex:[argumentIndex intValue]]; diff --git a/Code/CoreData/RKObjectMappingProvider+CoreData.h b/Code/CoreData/RKObjectMappingProvider+CoreData.h index 8db36f2d28..3cb5e47421 100644 --- a/Code/CoreData/RKObjectMappingProvider+CoreData.h +++ b/Code/CoreData/RKObjectMappingProvider+CoreData.h @@ -45,7 +45,7 @@ typedef NSFetchRequest *(^RKObjectMappingProviderFetchRequestBlock)(NSString *re /** Retrieves the NSFetchRequest object that will retrieve cached objects for a given resourcePath. - + @param resourcePath A resourcePath to retrieve the fetch request for. @return An NSFetchRequest object for fetching objects for the given resource path or nil. */ diff --git a/Code/CoreData/RKObjectMappingProvider+CoreData.m b/Code/CoreData/RKObjectMappingProvider+CoreData.m index ceb46ef1fd..efdd421de2 100644 --- a/Code/CoreData/RKObjectMappingProvider+CoreData.m +++ b/Code/CoreData/RKObjectMappingProvider+CoreData.m @@ -15,7 +15,7 @@ @implementation RKObjectMappingProvider (CoreData) - (void)setObjectMapping:(RKObjectMappingDefinition *)objectMapping forResourcePathPattern:(NSString *)resourcePath withFetchRequestBlock:(RKObjectMappingProviderFetchRequestBlock)fetchRequestBlock { [self setEntry:[RKObjectMappingProviderContextEntry contextEntryWithMapping:objectMapping - userData:[fetchRequestBlock copy]] forResourcePathPattern:resourcePath]; + userData:Block_copy(fetchRequestBlock)] forResourcePathPattern:resourcePath]; } - (NSFetchRequest *)fetchRequestForResourcePath:(NSString *)resourcePath { diff --git a/Code/CoreData/RKObjectPropertyInspector+CoreData.h b/Code/CoreData/RKObjectPropertyInspector+CoreData.h index 101d6f03e7..c04d05dc55 100644 --- a/Code/CoreData/RKObjectPropertyInspector+CoreData.h +++ b/Code/CoreData/RKObjectPropertyInspector+CoreData.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 8/14/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/CoreData/RKObjectPropertyInspector+CoreData.m b/Code/CoreData/RKObjectPropertyInspector+CoreData.m index e57f5083c8..9767b6924a 100644 --- a/Code/CoreData/RKObjectPropertyInspector+CoreData.m +++ b/Code/CoreData/RKObjectPropertyInspector+CoreData.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 8/14/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -35,10 +35,10 @@ @implementation RKObjectPropertyInspector (CoreData) - (NSDictionary *)propertyNamesAndTypesForEntity:(NSEntityDescription*)entity { NSMutableDictionary* propertyNamesAndTypes = [_cachedPropertyNamesAndTypes objectForKey:[entity name]]; - if (propertyNamesAndTypes) { - return propertyNamesAndTypes; - } - + if (propertyNamesAndTypes) { + return propertyNamesAndTypes; + } + propertyNamesAndTypes = [NSMutableDictionary dictionary]; for (NSString* name in [entity attributesByName]) { NSAttributeDescription* attributeDescription = [[entity attributesByName] valueForKey:name]; @@ -63,7 +63,7 @@ - (NSDictionary *)propertyNamesAndTypesForEntity:(NSEntityDescription*)entity { } } } - + for (NSString* name in [entity relationshipsByName]) { NSRelationshipDescription* relationshipDescription = [[entity relationshipsByName] valueForKey:name]; if ([relationshipDescription isToMany]) { @@ -74,7 +74,7 @@ - (NSDictionary *)propertyNamesAndTypesForEntity:(NSEntityDescription*)entity { [propertyNamesAndTypes setValue:destinationClass forKey:name]; } } - + [_cachedPropertyNamesAndTypes setObject:propertyNamesAndTypes forKey:[entity name]]; RKLogDebug(@"Cached property names and types for Entity '%@': %@", entity, propertyNamesAndTypes); return propertyNamesAndTypes; diff --git a/Code/CoreData/RKSearchableManagedObject.h b/Code/CoreData/RKSearchableManagedObject.h index 31f8197fab..1b77767e2e 100644 --- a/Code/CoreData/RKSearchableManagedObject.h +++ b/Code/CoreData/RKSearchableManagedObject.h @@ -28,7 +28,7 @@ that are searchable using the RKManagedObjectSearchEngine interface. The collection of search words is maintained by the RKSearchWordObserver singleton at managed object context save time. - + @see RKSearchWord @see RKSearchWordObserver @see RKManagedObjectSearchEngine @@ -44,9 +44,9 @@ build the set of search words for entities with the type of the receiver. Subclasses must provide an implementation for indexing to occur as the base implementation returns an empty array. - + @warning *NOTE*: May only include attributes property names, not key paths. - + @return An array of attribute names containing searchable textual content for entities with the type of the receiver. @see RKSearchWordObserver @see searchWords @@ -58,10 +58,10 @@ ///----------------------------------------------------------------------------- /** - A predicate that will search for the specified text with the specified mode. Mode can be + A predicate that will search for the specified text with the specified mode. Mode can be configured to be RKSearchModeAnd or RKSearchModeOr. - - @return A predicate that will search for the specified text with the specified mode. + + @return A predicate that will search for the specified text with the specified mode. @see RKSearchMode */ @@ -79,7 +79,7 @@ /** Rebuilds the set of tokenized search words associated with the receiver by processing the searchable attributes and tokenizing the contents into RKSearchWord instances. - + @see [RKSearchableManagedObject searchableAttributes] */ - (void)refreshSearchWords; @@ -90,28 +90,28 @@ /** Adds a search word object to the receiver's set of search words. - + @param searchWord The search word to be added to the set of search words. */ - (void)addSearchWordsObject:(RKSearchWord *)searchWord; /** Removes a search word object from the receiver's set of search words. - + @param searchWord The search word to be removed from the receiver's set of search words. */ - (void)removeSearchWordsObject:(RKSearchWord *)searchWord; /** Adds a set of search word objects to the receiver's set of search words. - + @param searchWords The set of search words to be added to receiver's the set of search words. */ - (void)addSearchWords:(NSSet *)searchWords; /** Removes a set of search word objects from the receiver's set of search words. - + @param searchWords The set of search words to be removed from receiver's the set of search words. */ - (void)removeSearchWords:(NSSet *)searchWords; diff --git a/Code/CoreData/RKSearchableManagedObject.m b/Code/CoreData/RKSearchableManagedObject.m index 97822e00e4..df667e91c3 100644 --- a/Code/CoreData/RKSearchableManagedObject.m +++ b/Code/CoreData/RKSearchableManagedObject.m @@ -32,18 +32,18 @@ @implementation RKSearchableManagedObject + (NSArray *)searchableAttributes { - return [NSArray array]; + return [NSArray array]; } -+ (NSPredicate *)predicateForSearchWithText:(NSString *)searchText searchMode:(RKSearchMode)mode ++ (NSPredicate *)predicateForSearchWithText:(NSString *)searchText searchMode:(RKSearchMode)mode { - if (searchText == nil) { - return nil; - } else { + if (searchText == nil) { + return nil; + } else { RKManagedObjectSearchEngine *searchEngine = [RKManagedObjectSearchEngine searchEngine]; searchEngine.mode = mode; - return [searchEngine predicateForSearch:searchText]; - } + return [searchEngine predicateForSearch:searchText]; + } } - (void)refreshSearchWords @@ -58,9 +58,9 @@ - (void)refreshSearchWords RKLogTrace(@"Generating search words for searchable attribute: %@", searchableAttribute); NSArray *attributeValueWords = [RKManagedObjectSearchEngine tokenizedNormalizedString:attributeValue]; for (NSString *word in attributeValueWords) { - if (word && [word length] > 0) { - RKSearchWord *searchWord = [RKSearchWord findFirstByAttribute:RKSearchWordPrimaryKeyAttribute - withValue:word + if (word && [word length] > 0) { + RKSearchWord *searchWord = [RKSearchWord findFirstByAttribute:RKSearchWordPrimaryKeyAttribute + withValue:word inContext:self.managedObjectContext]; if (! searchWord) { searchWord = [RKSearchWord createInContext:self.managedObjectContext]; @@ -75,7 +75,7 @@ - (void)refreshSearchWords self.searchWords = searchWords; RKLogTrace(@"Generating searchWords: %@", [searchWords valueForKey:RKSearchWordPrimaryKeyAttribute]); - [pool drain]; + [pool drain]; } @end diff --git a/Code/Network/NSData+RKAdditions.h b/Code/Network/NSData+RKAdditions.h index 825cdcd9de..b33524a578 100644 --- a/Code/Network/NSData+RKAdditions.h +++ b/Code/Network/NSData+RKAdditions.h @@ -4,13 +4,13 @@ // // Created by Jeff Arena on 4/4/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,7 +25,7 @@ /** Returns a string of the MD5 sum of the receiver. - + @return A new string containing the MD5 sum of the receiver. */ - (NSString *)MD5; diff --git a/Code/Network/NSData+RKAdditions.m b/Code/Network/NSData+RKAdditions.m index de64b2e90d..5ae5fa3ee2 100644 --- a/Code/Network/NSData+RKAdditions.m +++ b/Code/Network/NSData+RKAdditions.m @@ -4,13 +4,13 @@ // // Created by Jeff Arena on 4/4/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -27,19 +27,19 @@ @implementation NSData (RKAdditions) - (NSString *)MD5 { - // Create byte array of unsigned chars - unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH]; + // Create byte array of unsigned chars + unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH]; - // Create 16 byte MD5 hash value, store in buffer - CC_MD5(self.bytes, (CC_LONG) self.length, md5Buffer); + // Create 16 byte MD5 hash value, store in buffer + CC_MD5(self.bytes, (CC_LONG) self.length, md5Buffer); - // Convert unsigned char buffer to NSString of hex values - NSMutableString* output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2]; - for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) { - [output appendFormat:@"%02x",md5Buffer[i]]; - } + // Convert unsigned char buffer to NSString of hex values + NSMutableString* output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2]; + for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) { + [output appendFormat:@"%02x",md5Buffer[i]]; + } - return output; + return output; } @end diff --git a/Code/Network/NSDictionary+RKRequestSerialization.h b/Code/Network/NSDictionary+RKRequestSerialization.h index 4612bc1e54..a2b5505186 100644 --- a/Code/Network/NSDictionary+RKRequestSerialization.h +++ b/Code/Network/NSDictionary+RKRequestSerialization.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 7/28/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,11 +23,11 @@ /* Extends NSDictionary to enable usage as the params of an RKRequest. - + This category provides for the serialization of the receiving NSDictionary into a URL - encoded string representation (MIME Type application/x-www-form-urlencoded). This + encoded string representation (MIME Type application/x-www-form-urlencoded). This enables NSDictionary objects to act as the params for an RKRequest. - + @see RKRequestSerializable @see [RKRequest params] @class NSDictionary (RKRequestSerialization) diff --git a/Code/Network/NSDictionary+RKRequestSerialization.m b/Code/Network/NSDictionary+RKRequestSerialization.m index a4139dfdc3..0c5c19fb9b 100644 --- a/Code/Network/NSDictionary+RKRequestSerialization.m +++ b/Code/Network/NSDictionary+RKRequestSerialization.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 7/28/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,12 +29,12 @@ @implementation NSDictionary (RKRequestSerialization) - (NSString *)HTTPHeaderValueForContentType { - return RKMIMETypeFormURLEncoded; + return RKMIMETypeFormURLEncoded; } - (NSData *)HTTPBody { - return [[self URLEncodedString] dataUsingEncoding:NSUTF8StringEncoding]; + return [[self URLEncodedString] dataUsingEncoding:NSUTF8StringEncoding]; } @end diff --git a/Code/Network/NSObject+URLEncoding.m b/Code/Network/NSObject+URLEncoding.m index be5f1cef6f..54c8a927c3 100644 --- a/Code/Network/NSObject+URLEncoding.m +++ b/Code/Network/NSObject+URLEncoding.m @@ -12,13 +12,13 @@ @implementation NSObject (URLEncoding) - (NSString*)URLEncodedString { - NSString *string = [NSString stringWithFormat:@"%@", self]; - NSString *encodedString = (NSString*)CFURLCreateStringByAddingPercentEscapes(NULL, - (CFStringRef)string, - NULL, - (CFStringRef)@"!*'();:@&=+$,/?%#[]", - kCFStringEncodingUTF8); - return [encodedString autorelease]; + NSString *string = [NSString stringWithFormat:@"%@", self]; + NSString *encodedString = (NSString*)CFURLCreateStringByAddingPercentEscapes(NULL, + (CFStringRef)string, + NULL, + (CFStringRef)@"!*'();:@&=+$,/?%#[]", + kCFStringEncodingUTF8); + return [encodedString autorelease]; } @end diff --git a/Code/Network/Network.h b/Code/Network/Network.h index 7591b4714a..46162f2ee9 100644 --- a/Code/Network/Network.h +++ b/Code/Network/Network.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/30/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/Network/RKClient.h b/Code/Network/RKClient.h index 17d88408d7..596481099f 100644 --- a/Code/Network/RKClient.h +++ b/Code/Network/RKClient.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 7/28/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -33,16 +33,16 @@ RKClient exposes the low level client interface for working with HTTP servers and RESTful services. It wraps the request/response cycle with a clean, simple interface. - + RKClient can be thought of as analogous to a web browser or other HTTP user agent. The client's primary purpose is to configure and dispatch requests to a remote service for processing in a global way. When working with the Network layer, a user will generally construct and dispatch all RKRequest objects via the interfaces exposed by RKClient. - - + + ### Base URL and Resource Paths - + Core to an effective utilization of RKClient is an understanding of the Base URL and Resource Path concepts. The Base URL forms the common beginning part of a complete URL string that is used to access a remote web service. RKClient @@ -50,73 +50,74 @@ the client will be sent to a URL consisting of the base URL plus the resource path specified. The resource path is simply the remaining part of the URL once all common text is removed. - + For example, given a remote web service at `http://restkit.org` and RESTful services at `http://restkit.org/services` and `http://restkit.org/objects`, our base URL would be `http://restkit.org` and we would have resource paths of `/services` and `/objects`. - + Base URLs simplify interaction with remote services by freeing us from having to interpolate strings and construct NSURL objects to get work done. We are also able to quickly retarget an entire application to a different server or API version by changing the base URL. This is commonly done via conditional compilation to create builds against a staging and production server, for example. - - + + ### Memory Management - + Note that memory management of requests sent via RKClient instances are automatically managed for you. When sent, the request is retained by the requestQueue and is released when all request processing has completed. Generally speaking this means that you can dispatch requests and work with the response in the delegate methods without regard for memory management. - - + + ### Request Serialization - + RKClient and RKRequest support the serialization of objects into payloads to be sent as the body of a request. This functionality is commonly used to provide a dictionary of simple values to be encoded and sent as a form encoding with POST and PUT operations. It is worth noting however that this functionality is provided via the RKRequestSerializable protocol and is not specific to NSDictionary objects. - + ### Sending Asynchronous Requests - + A handful of methods are provided as a convenience to cover the common asynchronous request tasks. All other request needs should instantiate a request via [RKClient requestWithResourcePath:] and work with the RKRequest object directly. - + @see RKRequest @see RKResponse @see RKRequestQueue @see RKRequestSerializable */ @interface RKClient : NSObject { - RKURL *_baseURL; + RKURL *_baseURL; RKRequestAuthenticationType _authenticationType; - NSString *_username; - NSString *_password; + NSString *_username; + NSString *_password; NSString *_OAuth1ConsumerKey; NSString *_OAuth1ConsumerSecret; NSString *_OAuth1AccessToken; NSString *_OAuth1AccessTokenSecret; NSString *_OAuth2AccessToken; NSString *_OAuth2RefreshToken; - NSMutableDictionary *_HTTPHeaders; - RKReachabilityObserver *_reachabilityObserver; - NSString *_serviceUnavailableAlertTitle; - NSString *_serviceUnavailableAlertMessage; - BOOL _serviceUnavailableAlertEnabled; + NSMutableDictionary *_HTTPHeaders; + RKReachabilityObserver *_reachabilityObserver; + NSString *_serviceUnavailableAlertTitle; + NSString *_serviceUnavailableAlertMessage; + BOOL _serviceUnavailableAlertEnabled; RKRequestQueue *_requestQueue; - RKRequestCache *_requestCache; - RKRequestCachePolicy _cachePolicy; + RKRequestCache *_requestCache; + RKRequestCachePolicy _cachePolicy; NSMutableSet *_additionalRootCertificates; BOOL _disableCertificateValidation; NSStringEncoding _defaultHTTPEncoding; - + NSString *_runLoopMode; + // Queue suspension flags BOOL _awaitingReachabilityDetermination; } @@ -128,7 +129,7 @@ /** Returns a client scoped to a particular base URL. - + If the singleton client is nil, the return client is set as the singleton. @see baseURL @@ -140,9 +141,9 @@ /** Returns a client scoped to a particular base URL. - + If the singleton client is nil, the return client is set as the singleton. - + @see baseURL @param baseURLString The string to use to construct the NSURL to set the baseURL. All requests will be relative to this base URL. @@ -152,10 +153,10 @@ /** Returns a Rest client scoped to a particular base URL with a set of HTTP AUTH - credentials. - + credentials. + If the singleton client is nil, the return client is set as the singleton. - + @bug **DEPRECATED** in version 0.9.4: Use [RKClient clientWithBaseURLString:] and set username and password afterwards. @param baseURL The baseURL to set for the client. All requests will be relative @@ -169,7 +170,7 @@ /** Returns a client scoped to a particular base URL. If the singleton client is nil, the return client is set as the singleton. - + @see baseURL @param baseURL The baseURL to set for the client. All requests will be relative to this base URL. @@ -180,7 +181,7 @@ /** Returns a client scoped to a particular base URL. If the singleton client is nil, the return client is set as the singleton. - + @see baseURL @param baseURLString The string to use to construct the NSURL to set the baseURL. All requests will be relative to this base URL. @@ -197,10 +198,10 @@ The base URL all resources are nested underneath. All requests created through the client will their URL built by appending a resourcePath to the baseURL to form a complete URL. - + Changing the baseURL has the side-effect of causing the requestCache instance to be rebuilt. Caches are maintained a per-host basis. - + @see requestCache */ @property (nonatomic, retain) RKURL *baseURL; @@ -208,25 +209,33 @@ /** A dictionary of headers to be sent with each request */ -@property (nonatomic, readonly) NSMutableDictionary *HTTPHeaders; +@property (nonatomic, retain, readonly) NSMutableDictionary *HTTPHeaders; /** An optional timeout interval within which the request should be cancelled. - + This is passed along to RKRequest if set. If it isn't set, it will default to RKRequest's default timeoutInterval. - + *Default*: Falls through to RKRequest's timeoutInterval */ @property (nonatomic, assign) NSTimeInterval timeoutInterval; /** The request queue to push asynchronous requests onto. - + *Default*: A new request queue is instantiated for you during init. */ @property (nonatomic, retain) RKRequestQueue *requestQueue; +/** + The run loop mode under which the underlying NSURLConnection is performed + + *Default*: NSRunLoopCommonModes + */ +@property (nonatomic, copy) NSString *runLoopMode; + + /** The default value used to decode HTTP body content when HTTP headers received do not provide information on the content. This encoding will be used by the RKResponse when creating the body content @@ -235,7 +244,7 @@ /** Adds an HTTP header to each request dispatched through the client - + @param value The string value to set for the HTTP header @param header The HTTP header to add @see HTTPHeaders @@ -249,9 +258,9 @@ /** Flag for disabling SSL certificate validation. - + *Default*: NO - + @warning **WARNING**: This is a potential security exposure and should be used **ONLY while debugging** in a controlled environment. */ @@ -262,11 +271,11 @@ A set of additional certificates to be used in evaluating server SSL certificates. */ -@property (nonatomic, readonly) NSSet *additionalRootCertificates; +@property (nonatomic, retain, readonly) NSSet *additionalRootCertificates; /** Adds an additional certificate that will be used to evaluate server SSL certs. - + @param cert The SecCertificateRef to add to the list of additional SSL certs. @see additionalRootCertificates */ @@ -279,9 +288,9 @@ /** The type of authentication to use for this request. - + This must be assigned one of the following: - + - `RKRequestAuthenticationTypeNone`: Disable the use of authentication - `RKRequestAuthenticationTypeHTTP`: Use NSURLConnection's HTTP AUTH auto-negotiation @@ -294,7 +303,7 @@ and OAuth1AccessTokenSecret must be set. - `RKRequestAuthenticationTypeOAuth2`: Enable the use of OAuth 2.0 authentication. OAuth2AccessToken must be set. - + **Default**: RKRequestAuthenticationTypeNone */ @@ -302,20 +311,20 @@ /** The username to use for authentication via HTTP AUTH. - + Used to respond to an authentication challenge when authenticationType is RKRequestAuthenticationTypeHTTP or RKRequestAuthenticationTypeHTTPBasic. - + @see authenticationType */ @property (nonatomic, retain) NSString *username; /** The password to use for authentication via HTTP AUTH. - + Used to respond to an authentication challenge when authenticationType is RKRequestAuthenticationTypeHTTP or RKRequestAuthenticationTypeHTTPBasic. - + @see authenticationType */ @property (nonatomic, retain) NSString *password; @@ -327,40 +336,40 @@ /** The OAuth 1.0 consumer key - + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth1 - + @see authenticationType */ @property (nonatomic, retain) NSString *OAuth1ConsumerKey; /** The OAuth 1.0 consumer secret - + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth1 - + @see authenticationType */ @property (nonatomic, retain) NSString *OAuth1ConsumerSecret; /** The OAuth 1.0 access token - + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth1 - + @see authenticationType */ @property (nonatomic, retain) NSString *OAuth1AccessToken; /** The OAuth 1.0 access token secret - + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth1 - + @see authenticationType */ @property (nonatomic, retain) NSString *OAuth1AccessTokenSecret; @@ -372,23 +381,23 @@ /** The OAuth 2.0 access token - + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth2 - + @see authenticationType */ @property (nonatomic, retain) NSString *OAuth2AccessToken; /** The OAuth 2.0 refresh token - + Used to retrieve a new access token before expiration and to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth2 - + @bug **NOT IMPLEMENTED**: This functionality is not yet implemented. - + @see authenticationType */ @property (nonatomic, retain) NSString *OAuth2RefreshToken; @@ -398,17 +407,17 @@ ///----------------------------------------------------------------------------- /** - An instance of RKReachabilityObserver used for determining the availability of + An instance of RKReachabilityObserver used for determining the availability of network access. - + Initialized using [RKReachabilityObserver reachabilityObserverForInternet] to monitor connectivity to the Internet. Can be changed to directly monitor a remote hostname/IP address or the local WiFi interface instead. - + @warning **WARNING**: Changing the reachability observer has the side-effect of temporarily suspending the requestQueue until reachability to the new host can be established. - + @see RKReachabilityObserver */ @property (nonatomic, retain) RKReachabilityObserver *reachabilityObserver; @@ -416,7 +425,7 @@ /** The title to use in the alert shown when a request encounters a ServiceUnavailable (503) response. - + *Default*: _"Service Unavailable"_ */ @property (nonatomic, retain) NSString *serviceUnavailableAlertTitle; @@ -424,7 +433,7 @@ /** The message to use in the alert shown when a request encounters a ServiceUnavailable (503) response. - + *Default*: _"The remote resource is unavailable. Please try again later."_ */ @property (nonatomic, retain) NSString *serviceUnavailableAlertMessage; @@ -432,7 +441,7 @@ /** Flag that determines whether the Service Unavailable alert is shown in response to a ServiceUnavailable (503) response. - + *Default*: _NO_ */ @property (nonatomic, assign) BOOL serviceUnavailableAlertEnabled; @@ -445,9 +454,9 @@ /** Convenience method for returning the current reachability status from the reachabilityObserver. - + Equivalent to executing `[RKClient isNetworkReachable]` on the sharedClient - + @see RKReachabilityObserver @return YES if the remote host is accessible */ @@ -456,8 +465,8 @@ /** Convenience method for returning the current reachability status from the reachabilityObserver. - - @bug **DEPRECATED** in v0.9.4: Use [RKClient isNetworkReachable] + + @bug **DEPRECATED** in v0.10.0: Use [RKClient isNetworkReachable] @see RKReachabilityObserver @return YES if the remote host is accessible */ @@ -471,8 +480,8 @@ /** An instance of the request cache used to store/load cacheable responses for requests sent through this client - - @bug **DEPRECATED** in v0.9.4: Use requestCache instead. + + @bug **DEPRECATED** in v0.10.0: Use requestCache instead. */ @property (nonatomic, retain) RKRequestCache *cache DEPRECATED_ATTRIBUTE; @@ -485,7 +494,7 @@ /** The timeout interval within which the requests should not be sent and the cached response should be used. - + This is only used if the cache policy includes RKRequestCachePolicyTimeout. */ @property (nonatomic, assign) NSTimeInterval cacheTimeoutInterval; @@ -494,7 +503,7 @@ The default cache policy to apply for all requests sent through this client This must be assigned one of the following: - + - `RKRequestCachePolicyNone`: Never use the cache. - `RKRequestCachePolicyLoadIfOffline`: Load from the cache when offline. - `RKRequestCachePolicyLoadOnError`: Load from the cache if an error is @@ -505,14 +514,14 @@ stored. - `RKRequestCachePolicyTimeout`: Load from the cache if the cacheTimeoutInterval is reached before the server responds. - + @see RKRequest */ @property (nonatomic, assign) RKRequestCachePolicy cachePolicy; /** The path used to store response data for this client's request cache. - + The path that is used is the device's cache directory with `RKClientRequestCache-host` appended. */ @@ -530,7 +539,7 @@ /** Sets the shared instance of the client, releasing the current instance (if any) - + @param client An RKClient instance to configure as the new shared instance */ + (void)setSharedClient:(RKClient *)client; @@ -542,11 +551,11 @@ /** Return a request object targetted at a resource path relative to the base URL. - + By default the method is set to GET. All headers set on the client will automatically be applied to the request as well. - - @bug **DEPRECATED** in v0.9.4: Use [RKClient requestWithResourcePath:] instead. + + @bug **DEPRECATED** in v0.10.0: Use [RKClient requestWithResourcePath:] instead. @param resourcePath The resource path to configure the request for. @param delegate A delegate to inform of events in the request lifecycle. @return A fully configured RKRequest instance ready for sending. @@ -556,10 +565,10 @@ /** Return a request object targeted at a resource path relative to the base URL. - + By default the method is set to GET. All headers set on the client will automatically be applied to the request as well. - + @param resourcePath The resource path to configure the request for. @return A fully configured RKRequest instance ready for sending. @see RKRequestDelegate @@ -574,7 +583,7 @@ /** Perform an asynchronous GET request for a resource and inform a delegate of the results. - + @param resourcePath The resourcePath to target the request at @param delegate A delegate object to inform of the results @return The RKRequest object built and sent to the remote system @@ -583,11 +592,11 @@ /** Fetch a resource via an HTTP GET with a dictionary of params. - + This request _only_ allows NSDictionary objects as the params. The dictionary will be coerced into a URL encoded string and then appended to the resourcePath as the query string of the request. - + @param resourcePath The resourcePath to target the request at @param queryParameters A dictionary of query parameters to append to the resourcePath. Assumes that resourcePath does not contain a query string. @@ -598,7 +607,7 @@ /** Fetches a resource via an HTTP GET after executing a given a block using the configured request object. - + @param resourcePath The resourcePath to target the request at @param block The block to execute with the request before sending it for processing. */ @@ -606,10 +615,10 @@ /** Create a resource via an HTTP POST with a set of form parameters. - + The form parameters passed here must conform to RKRequestSerializable, such as an instance of RKParams. - + @see RKParams @param resourcePath The resourcePath to target the request at @param params A RKRequestSerializable object to use as the body of the request @@ -621,7 +630,7 @@ /** Creates a resource via an HTTP POST after executing a given a block using the configured request object. - + @param resourcePath The resourcePath to target the request at @param block The block to execute with the request before sending it for processing. */ @@ -632,9 +641,9 @@ The form parameters passed here must conform to RKRequestSerializable, such as an instance of RKParams. - + @see RKParams - + @param resourcePath The resourcePath to target the request at @param params A RKRequestSerializable object to use as the body of the request @param delegate A delegate object to inform of the results @@ -645,7 +654,7 @@ /** Updates a resource via an HTTP PUT after executing a given a block using the configured request object. - + @param resourcePath The resourcePath to target the request at @param block The block to execute with the request before sending it for processing. */ @@ -653,7 +662,7 @@ /** Destroy a resource via an HTTP DELETE. - + @param resourcePath The resourcePath to target the request at @param delegate A delegate object to inform of the results @return The RKRequest object built and sent to the remote system @@ -662,7 +671,7 @@ /** Destroys a resource via an HTTP DELETE after executing a given a block using the configured request object. - + @param resourcePath The resourcePath to target the request at @param block The block to execute with the request before sending it for processing. */ @@ -674,9 +683,9 @@ /** Returns a NSURL by adding a resource path to the base URL - - @bug **DEPRECATED** in v0.9.4: Use [RKURL URLByAppendingResourcePath:] - + + @bug **DEPRECATED** in v0.10.0: Use [RKURL URLByAppendingResourcePath:] + @param resourcePath The resource path to build a URL against @return An NSURL constructed by concatenating the baseURL and the resourcePath */ @@ -684,9 +693,9 @@ /** Returns an NSString by adding a resource path to the base URL - + @bug **DEPRECATED**: Use `[RKURL URLByAppendingResourcePath:] absoluteString` - + @param resourcePath The resource path to build a URL against @return A string URL constructed by concatenating the baseURL and the resourcePath. @@ -696,17 +705,17 @@ /** Returns a resource path with a dictionary of query parameters URL encoded and appended - + This is a convenience method for constructing a new resource path that includes a query. For example, when given a resourcePath of /contacts and a dictionary of parameters containing foo=bar and color=red, will return /contacts?foo=bar&color=red - + @warning **NOTE**: This assumes that the resource path does not already contain any query parameters. - + @bug **DEPRECATED**: Use [RKURL URLByAppendingQueryParameters:] - + @param resourcePath The resource path to append the query parameters onto @param queryParams A dictionary of query parameters to be URL encoded and appended to the resource path. @@ -717,17 +726,17 @@ /** Returns a NSURL by adding a resource path to the base URL and appending a URL encoded set of query parameters - + This is a convenience method for constructing a new resource path that includes a query. For example, when given a resourcePath of /contacts and a dictionary of parameters containing foo=bar and color=red, will return /contacts?foo=bar&color=red - + @warning **NOTE**: Assumes that the resource path does not already contain any query parameters. - + @bug **DEPRECATED**: Use [RKURL URLByAppendingResourcePath:queryParameters:] - + @param resourcePath The resource path to append the query parameters onto @param queryParams A dictionary of query parameters to be URL encoded and appended to the resource path. @@ -746,10 +755,10 @@ /** Returns an NSURL with the specified resource path appended to the base URL that the shared RKClient instance is configured with. - + Shortcut for calling `[[RKClient sharedClient] URLForResourcePath:@"/some/path"]` - - @bug **DEPRECATED** in v0.9.4: Use [[RKClient sharedClient].baseURL + + @bug **DEPRECATED** in v0.10.0: Use [[RKClient sharedClient].baseURL URLByAppendingResourcePath:] @param resourcePath The resource path to append to the baseURL of the `[RKClient sharedClient]` @@ -761,11 +770,11 @@ NSURL *RKMakeURL(NSString *resourcePath) DEPRECATED_ATTRIBUTE; /** Returns an NSString with the specified resource path appended to the base URL that the shared RKClient instance is configured with - + Shortcut for calling `[[RKClient sharedClient] URLPathForResourcePath:@"/some/path"]` - - @bug **DEPRECATED** in v0.9.4: Use + + @bug **DEPRECATED** in v0.10.0: Use [[[RKClient sharedClient].baseURL URLByAppendingResourcePath:] absoluteString] @param resourcePath The resource path to append to the baseURL of the `[RKClient sharedClient]` @@ -780,15 +789,15 @@ NSString *RKMakeURLPath(NSString *resourcePath) DEPRECATED_ATTRIBUTE; values of the properties specified and returns the generated path. Defaults to adding escapes. If desired, turn them off with RKMakePathWithObjectAddingEscapes. - + For example, given an 'article' object with an 'articleID' property of 12345 and a 'name' of Blake, RKMakePathWithObject(@"articles/:articleID/:name", article) would generate @"articles/12345/Blake" - + This functionality is the basis for resource path generation in the Router. - - @bug **DEPRECATED** in v0.9.4: Use [NSString interpolateWithObject:] - + + @bug **DEPRECATED** in v0.10.0: Use [NSString interpolateWithObject:] + @param path The colon encoded path pattern string to use for interpolation. @param object The object containing the properties needed for interpolation. @return A new path string, replacing the pattern's parameters with the object's @@ -801,14 +810,14 @@ NSString *RKMakePathWithObject(NSString *path, id object) DEPRECATED_ATTRIBUTE; Convenience method for generating a path against the properties of an object. Takes a string with property names encoded with colons and interpolates the values of the properties specified and returns the generated path. - + For example, given an 'article' object with an 'articleID' property of 12345 and a 'code' of "This/That", `RKMakePathWithObjectAddingEscapes(@"articles/:articleID/:code", article, YES)` would generate @"articles/12345/This%2FThat" - + This functionality is the basis for resource path generation in the Router. - - @bug **DEPRECATED** in v0.9.4: Use [NSString interpolateWithObject:addingEscapes:] + + @bug **DEPRECATED** in v0.10.0: Use [NSString interpolateWithObject:addingEscapes:] @param path The colon encoded path pattern string to use for interpolation. @param object The object containing the properties needed for interpolation. @param addEscapes Conditionally add percent escapes to the interpolated @@ -821,18 +830,18 @@ NSString *RKMakePathWithObjectAddingEscapes(NSString *pattern, id object, BOOL a /** Returns a resource path with a dictionary of query parameters URL encoded and appended. - + This is a convenience method for constructing a new resource path that includes a query. For example, when given a resourcePath of /contacts and a dictionary of parameters containing `foo=bar` and `color=red`, will return `/contacts?foo=bar&color=red`. - + @warning This assumes that the resource path does not already contain any query parameters. - - @bug **DEPRECATED** in v0.9.4: Use [NSString stringByAppendingQueryParameters:] + + @bug **DEPRECATED** in v0.10.0: Use [NSString stringByAppendingQueryParameters:] instead - + @param resourcePath The resource path to append the query parameters onto @param queryParams A dictionary of query parameters to be URL encoded and appended to the resource path. diff --git a/Code/Network/RKClient.m b/Code/Network/RKClient.m index ea3a9cd14b..70eb174e65 100644 --- a/Code/Network/RKClient.m +++ b/Code/Network/RKClient.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 7/28/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -65,6 +65,11 @@ /////////////////////////////////////////////////////////////////////////////////////////////////// +@interface RKClient () +@property (nonatomic, retain, readwrite) NSMutableDictionary *HTTPHeaders; +@property (nonatomic, retain, readwrite) NSSet *additionalRootCertificates; +@end + @implementation RKClient @synthesize baseURL = _baseURL; @@ -90,14 +95,15 @@ @implementation RKClient @synthesize timeoutInterval = _timeoutInterval; @synthesize defaultHTTPEncoding = _defaultHTTPEncoding; @synthesize cacheTimeoutInterval = _cacheTimeoutInterval; +@synthesize runLoopMode = _runLoopMode; + (RKClient *)sharedClient { - return sharedClient; + return sharedClient; } + (void)setSharedClient:(RKClient *)client { - [sharedClient release]; - sharedClient = [client retain]; + [sharedClient release]; + sharedClient = [client retain]; } + (RKClient *)clientWithBaseURLString:(NSString *)baseURLString { @@ -105,58 +111,58 @@ + (RKClient *)clientWithBaseURLString:(NSString *)baseURLString { } + (RKClient *)clientWithBaseURL:(NSURL *)baseURL { - RKClient *client = [[[self alloc] initWithBaseURL:baseURL] autorelease]; - return client; + RKClient *client = [[[self alloc] initWithBaseURL:baseURL] autorelease]; + return client; } + (RKClient *)clientWithBaseURL:(NSString *)baseURL username:(NSString *)username password:(NSString *)password { - RKClient *client = [RKClient clientWithBaseURLString:baseURL]; + RKClient *client = [RKClient clientWithBaseURLString:baseURL]; client.authenticationType = RKRequestAuthenticationTypeHTTPBasic; - client.username = username; - client.password = password; - return client; + client.username = username; + client.password = password; + return client; } - (id)init { self = [super init]; - if (self) { - _HTTPHeaders = [[NSMutableDictionary alloc] init]; - _additionalRootCertificates = [[NSMutableSet alloc] init]; - _defaultHTTPEncoding = NSUTF8StringEncoding; + if (self) { + self.HTTPHeaders = [NSMutableDictionary dictionary]; + self.additionalRootCertificates = [NSMutableSet set]; + self.defaultHTTPEncoding = NSUTF8StringEncoding; self.cacheTimeoutInterval = 0; - self.serviceUnavailableAlertEnabled = NO; - self.serviceUnavailableAlertTitle = NSLocalizedString(@"Service Unavailable", nil); - self.serviceUnavailableAlertMessage = NSLocalizedString(@"The remote resource is unavailable. Please try again later.", nil); - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(serviceDidBecomeUnavailableNotification:) - name:RKServiceDidBecomeUnavailableNotification + self.runLoopMode = NSRunLoopCommonModes; + self.requestQueue = [RKRequestQueue requestQueue]; + self.serviceUnavailableAlertEnabled = NO; + self.serviceUnavailableAlertTitle = NSLocalizedString(@"Service Unavailable", nil); + self.serviceUnavailableAlertMessage = NSLocalizedString(@"The remote resource is unavailable. Please try again later.", nil); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(serviceDidBecomeUnavailableNotification:) + name:RKServiceDidBecomeUnavailableNotification object:nil]; - - // Configure reachability and queue + + // Configure observers [self addObserver:self forKeyPath:@"reachabilityObserver" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; - self.requestQueue = [RKRequestQueue requestQueue]; - [self addObserver:self forKeyPath:@"baseURL" options:NSKeyValueObservingOptionNew context:nil]; - [self addObserver:self forKeyPath:@"requestQueue" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; - } + [self addObserver:self forKeyPath:@"requestQueue" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial context:nil]; + } - return self; + return self; } - (id)initWithBaseURL:(NSURL *)baseURL { self = [self init]; - if (self) { + if (self) { self.cachePolicy = RKRequestCachePolicyDefault; self.baseURL = [RKURL URLWithBaseURL:baseURL]; - + if (sharedClient == nil) { [RKClient setSharedClient:self]; - + // Initialize Logging as soon as a client is created RKLogInitialize(); } } - + return self; } @@ -164,28 +170,29 @@ - (id)initWithBaseURLString:(NSString *)baseURLString { return [self initWithBaseURL:[RKURL URLWithString:baseURLString]]; } -- (void)dealloc { +- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; - + // Allow KVO to fire self.reachabilityObserver = nil; self.baseURL = nil; self.requestQueue = nil; - + [self removeObserver:self forKeyPath:@"reachabilityObserver"]; [self removeObserver:self forKeyPath:@"baseURL"]; [self removeObserver:self forKeyPath:@"requestQueue"]; - + self.username = nil; self.password = nil; self.serviceUnavailableAlertTitle = nil; self.serviceUnavailableAlertMessage = nil; self.requestCache = nil; + self.runLoopMode = nil; [_HTTPHeaders release]; [_additionalRootCertificates release]; if (sharedClient == self) sharedClient = nil; - + [super dealloc]; } @@ -197,38 +204,39 @@ - (NSString *)cachePath { } - (BOOL)isNetworkReachable { - BOOL isNetworkReachable = YES; - if (self.reachabilityObserver) { - isNetworkReachable = [self.reachabilityObserver isNetworkReachable]; - } - - return isNetworkReachable; + BOOL isNetworkReachable = YES; + if (self.reachabilityObserver) { + isNetworkReachable = [self.reachabilityObserver isNetworkReachable]; + } + + return isNetworkReachable; } - (void)configureRequest:(RKRequest *)request { - request.additionalHTTPHeaders = _HTTPHeaders; + request.additionalHTTPHeaders = _HTTPHeaders; request.authenticationType = self.authenticationType; - request.username = self.username; - request.password = self.password; - request.cachePolicy = self.cachePolicy; + request.username = self.username; + request.password = self.password; + request.cachePolicy = self.cachePolicy; request.cache = self.requestCache; request.queue = self.requestQueue; request.reachabilityObserver = self.reachabilityObserver; request.defaultHTTPEncoding = self.defaultHTTPEncoding; - + request.additionalRootCertificates = self.additionalRootCertificates; request.disableCertificateValidation = self.disableCertificateValidation; - + request.runLoopMode = self.runLoopMode; + // If a timeoutInterval was set on the client, we'll pass it on to the request. // Otherwise, we'll let the request default to its own timeout interval. if (self.timeoutInterval) { request.timeoutInterval = self.timeoutInterval; } - + if (self.cacheTimeoutInterval) { request.cacheTimeoutInterval = self.cacheTimeoutInterval; } - + // OAuth 1 Parameters request.OAuth1AccessToken = self.OAuth1AccessToken; request.OAuth1AccessTokenSecret = self.OAuth1AccessTokenSecret; @@ -241,7 +249,7 @@ - (void)configureRequest:(RKRequest *)request { } - (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)header { - [_HTTPHeaders setValue:value forKey:header]; + [_HTTPHeaders setValue:value forKey:header]; } - (void)addRootCertificate:(SecCertificateRef)cert { @@ -251,31 +259,31 @@ - (void)addRootCertificate:(SecCertificateRef)cert { - (void)reachabilityObserverDidChange:(NSDictionary *)change { RKReachabilityObserver *oldReachabilityObserver = [change objectForKey:NSKeyValueChangeOldKey]; RKReachabilityObserver *newReachabilityObserver = [change objectForKey:NSKeyValueChangeNewKey]; - + if (! [oldReachabilityObserver isEqual:[NSNull null]]) { RKLogDebug(@"Reachability observer changed for RKClient %@, disposing of previous instance: %@", self, oldReachabilityObserver); // Cleanup if changed immediately after client init [[NSNotificationCenter defaultCenter] removeObserver:self name:RKReachabilityWasDeterminedNotification object:oldReachabilityObserver]; } - + if (! [newReachabilityObserver isEqual:[NSNull null]]) { // Suspend the queue until reachability to our new hostname is established if (! [newReachabilityObserver isReachabilityDetermined]) { self.requestQueue.suspended = YES; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(reachabilityWasDetermined:) - name:RKReachabilityWasDeterminedNotification + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reachabilityWasDetermined:) + name:RKReachabilityWasDeterminedNotification object:newReachabilityObserver]; - - RKLogDebug(@"Reachability observer changed for client %@, suspending queue %@ until reachability to host '%@' can be determined", + + RKLogDebug(@"Reachability observer changed for client %@, suspending queue %@ until reachability to host '%@' can be determined", self, self.requestQueue, newReachabilityObserver.host); - + // Maintain a flag for Reachability determination status. This ensures that we can do the right thing in the // event that the requestQueue is changed while we are in an inderminate suspension state _awaitingReachabilityDetermination = YES; } else { self.requestQueue.suspended = NO; - RKLogDebug(@"Reachability observer changed for client %@, unsuspending queue %@ as new observer already has determined reachability to %@", + RKLogDebug(@"Reachability observer changed for client %@, unsuspending queue %@ as new observer already has determined reachability to %@", self, self.requestQueue, newReachabilityObserver.host); _awaitingReachabilityDetermination = NO; } @@ -284,14 +292,14 @@ - (void)reachabilityObserverDidChange:(NSDictionary *)change { - (void)baseURLDidChange:(NSDictionary *)change { RKURL *newBaseURL = [change objectForKey:NSKeyValueChangeNewKey]; - + // Don't crash if baseURL is nil'd out (i.e. dealloc) if (! [newBaseURL isEqual:[NSNull null]]) { // Configure a cache for the new base URL [_requestCache release]; _requestCache = [[RKRequestCache alloc] initWithPath:[self cachePath] storagePolicy:RKRequestCacheStoragePolicyPermanently]; - + // Determine reachability strategy (if user has not already done so) if (self.reachabilityObserver == nil) { NSString *hostName = [newBaseURL host]; @@ -308,12 +316,12 @@ - (void)requestQueueDidChange:(NSDictionary *)change { if (! _awaitingReachabilityDetermination) { return; } - + // If we are awaiting reachability determination, suspend the new queue RKRequestQueue *newQueue = [change objectForKey:NSKeyValueChangeNewKey]; - + if (! [newQueue isEqual:[NSNull null]]) { - // The request queue has changed while we were awaiting reachability. + // The request queue has changed while we were awaiting reachability. // Suspend the queue until reachability is determined newQueue.suspended = !self.reachabilityObserver.reachabilityDetermined; } @@ -331,16 +339,16 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N - (RKRequest *)requestWithResourcePath:(NSString *)resourcePath { RKRequest *request = [[RKRequest alloc] initWithURL:[self.baseURL URLByAppendingResourcePath:resourcePath]]; - [self configureRequest:request]; - [request autorelease]; - - return request; + [self configureRequest:request]; + [request autorelease]; + + return request; } - (RKRequest *)requestWithResourcePath:(NSString *)resourcePath delegate:(NSObject *)delegate { - RKRequest *request = [self requestWithResourcePath:resourcePath]; + RKRequest *request = [self requestWithResourcePath:resourcePath]; request.delegate = delegate; - + return request; } @@ -349,43 +357,43 @@ - (RKRequest *)requestWithResourcePath:(NSString *)resourcePath delegate:(NSObje /////////////////////////////////////////////////////////////////////////////////////////////////////////// - (RKRequest *)load:(NSString *)resourcePath method:(RKRequestMethod)method params:(NSObject *)params delegate:(id)delegate { - RKURL* resourcePathURL = nil; - if (method == RKRequestMethodGET) { + RKURL* resourcePathURL = nil; + if (method == RKRequestMethodGET) { resourcePathURL = [self.baseURL URLByAppendingResourcePath:resourcePath queryParameters:(NSDictionary *)params]; - } else { - resourcePathURL = [self.baseURL URLByAppendingResourcePath:resourcePath]; - } - RKRequest *request = [RKRequest requestWithURL:resourcePathURL]; + } else { + resourcePathURL = [self.baseURL URLByAppendingResourcePath:resourcePath]; + } + RKRequest *request = [RKRequest requestWithURL:resourcePathURL]; request.delegate = delegate; - [self configureRequest:request]; - request.method = method; - if (method != RKRequestMethodGET) { - request.params = params; - } - + [self configureRequest:request]; + request.method = method; + if (method != RKRequestMethodGET) { + request.params = params; + } + [request send]; - return request; + return request; } - (RKRequest *)get:(NSString *)resourcePath delegate:(id)delegate { - return [self load:resourcePath method:RKRequestMethodGET params:nil delegate:delegate]; + return [self load:resourcePath method:RKRequestMethodGET params:nil delegate:delegate]; } - (RKRequest *)get:(NSString *)resourcePath queryParameters:(NSDictionary *)queryParameters delegate:(id)delegate { - return [self load:resourcePath method:RKRequestMethodGET params:queryParameters delegate:delegate]; + return [self load:resourcePath method:RKRequestMethodGET params:queryParameters delegate:delegate]; } - (RKRequest *)post:(NSString *)resourcePath params:(NSObject *)params delegate:(id)delegate { - return [self load:resourcePath method:RKRequestMethodPOST params:params delegate:delegate]; + return [self load:resourcePath method:RKRequestMethodPOST params:params delegate:delegate]; } - (RKRequest *)put:(NSString *)resourcePath params:(NSObject *)params delegate:(id)delegate { - return [self load:resourcePath method:RKRequestMethodPUT params:params delegate:delegate]; + return [self load:resourcePath method:RKRequestMethodPUT params:params delegate:delegate]; } - (RKRequest *)delete:(NSString *)resourcePath delegate:(id)delegate { - return [self load:resourcePath method:RKRequestMethodDELETE params:nil delegate:delegate]; + return [self load:resourcePath method:RKRequestMethodDELETE params:nil delegate:delegate]; } - (void)serviceDidBecomeUnavailableNotification:(NSNotification *)notification { @@ -397,7 +405,7 @@ - (void)serviceDidBecomeUnavailableNotification:(NSNotification *)notification { - (void)reachabilityWasDetermined:(NSNotification *)notification { RKReachabilityObserver *observer = (RKReachabilityObserver *) [notification object]; NSAssert(observer == self.reachabilityObserver, @"Received unexpected reachability notification from inappropriate reachability observer"); - + RKLogDebug(@"Reachability to host '%@' determined for client %@, unsuspending queue %@", observer.host, self, self.requestQueue); _awaitingReachabilityDetermination = NO; self.requestQueue.suspended = NO; @@ -459,7 +467,7 @@ - (BOOL)isNetworkAvailable { } - (NSString *)resourcePath:(NSString *)resourcePath withQueryParams:(NSDictionary *)queryParams { - return RKPathAppendQueryParams(resourcePath, queryParams); + return RKPathAppendQueryParams(resourcePath, queryParams); } - (NSURL *)URLForResourcePath:(NSString *)resourcePath { @@ -467,7 +475,7 @@ - (NSURL *)URLForResourcePath:(NSString *)resourcePath { } - (NSString *)URLPathForResourcePath:(NSString *)resourcePath { - return [[self URLForResourcePath:resourcePath] absoluteString]; + return [[self URLForResourcePath:resourcePath] absoluteString]; } - (NSURL *)URLForResourcePath:(NSString *)resourcePath queryParams:(NSDictionary *)queryParams { diff --git a/Code/Network/RKNotifications.h b/Code/Network/RKNotifications.h index 74fb98ef46..a96c480b5d 100644 --- a/Code/Network/RKNotifications.h +++ b/Code/Network/RKNotifications.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/24/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,7 +22,7 @@ /** Request Auditing - + RKClient exposes a set of NSNotifications that can be used to audit the request/response cycle of your application. This is useful for doing things like generating automatic logging for all your requests or sending the response diff --git a/Code/Network/RKNotifications.m b/Code/Network/RKNotifications.m index fc1ae7c543..75e9b4ee63 100644 --- a/Code/Network/RKNotifications.m +++ b/Code/Network/RKNotifications.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/24/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/Network/RKOAuthClient.h b/Code/Network/RKOAuthClient.h index a43e14afe8..1b051d945c 100644 --- a/Code/Network/RKOAuthClient.h +++ b/Code/Network/RKOAuthClient.h @@ -4,13 +4,13 @@ // // Created by Rodrigo Garcia on 7/20/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -52,7 +52,7 @@ typedef enum RKOAuthClientErrors { */ RKOAuthClientErrorUnsupportedGrantType = 3005, /** - The requested scope is invalid, unknown, malformed, or exceeds the scope + The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner. */ RKOAuthClientErrorInvalidScope = 3006, @@ -73,13 +73,13 @@ typedef enum RKOAuthClientErrors { /** An OAuth client implementation that conforms to RKRequestDelegate to handle the authentication involved with the OAuth 2 authorization code flow. - + RKOAuthClient sets up a pre-configured RKRequest and RKResponse handler to give easy access to retrieving an access token and handling errors through RKOAuthClientDelegate. - + **Example**: - + RKOAuthClient *oauthClient; oauthClient = [RKClientOAuth clientWithClientID:@"YOUR_CLIENT_ID" secret:@"YOUR_CLIENT_SECRET" @@ -88,19 +88,19 @@ typedef enum RKOAuthClientErrors { oauthClient.authorizationURL = @"https://foursquare.com/oauth2/authenticate"; oauthClient.callbackURL = @"https://example.com/callback"; [oauthClient validateAuthorizationCode]; - + From here, errors and the access token are returned through the implementation of RKOAuthClientDelegate specified. - + For more information on the OAuth 2 implementation, see http://tools.ietf.org/html/draft-ietf-oauth-v2-22 - + @see RKOAuthClientDelegate */ @interface RKOAuthClient : NSObject { - NSString *_clientID; + NSString *_clientID; NSString *_clientSecret; - NSString *_authorizationCode; + NSString *_authorizationCode; NSString *_authorizationURL; NSString *_callbackURL; NSString *_accessToken; @@ -114,7 +114,7 @@ typedef enum RKOAuthClientErrors { /** Initialize a new RKOAuthClient with OAuth client credentials. - + @param clientID The ID of your application obtained from the OAuth provider. @param secret Confidential key obtained from the OAuth provider that is used to sign requests sent to the authentication server. @@ -139,11 +139,11 @@ typedef enum RKOAuthClientErrors { /** A delegate that must conform to the RKOAuthClientDelegate protocol. - + The delegate will get callbacks such as successful access token acquisitions as well as any errors that are encountered. Reference the RKOAuthClientDelegate for more information. - + @see RKOAuthClientDelegate. */ @property (nonatomic, assign) id delegate; @@ -199,7 +199,7 @@ typedef enum RKOAuthClientErrors { Fire a request to the authentication server to validate the authorization code that has been set on the authorizationCode property. All responses are handled by the delegate. - + @see RKOAuthClientDelegate */ - (void)validateAuthorizationCode; @@ -211,7 +211,7 @@ typedef enum RKOAuthClientErrors { The delegate of an RKOAuthClient object must adopt the RKOAuthClientDelegate protocol. The protocol defines all methods relating to obtaining an accessToken and handling any errors along the way. It optionally provides - callbacks for many different OAuth2 exceptions that may occur during the + callbacks for many different OAuth2 exceptions that may occur during the authorization code flow. */ @protocol RKOAuthClientDelegate @@ -223,7 +223,7 @@ typedef enum RKOAuthClientErrors { /** Sent when a new access token has been acquired - + @param client A reference to the RKOAuthClient that triggered the callback @param token A string of the access token acquired from the authentication server. @@ -237,7 +237,7 @@ typedef enum RKOAuthClientErrors { /** Sent when an access token request has failed due an invalid authorization code - + @param client A reference to the RKOAuthClient that triggered the callback @param error An NSError object containing the RKOAuthClientError that triggered the callback @@ -248,7 +248,7 @@ typedef enum RKOAuthClientErrors { /** Sent to the delegate when the OAuth client encounters any error. - + @param client A reference to the RKOAuthClient that triggered the callback @param error An NSError object containing the RKOAuthClientError that triggered the callback @@ -257,7 +257,7 @@ typedef enum RKOAuthClientErrors { /** Sent when the client isn't authorized to perform the requested action - + @param client A reference to the RKOAuthClient that triggered the callback @param error An NSError object containing the RKOAuthClientError that triggered the callback @@ -268,7 +268,7 @@ typedef enum RKOAuthClientErrors { Sent when an error is encountered with the OAuth client such as an unknown client, there is no client authentication included, or an unsupported authentication method was used. - + @param client A reference to the RKOAuthClient that triggered the callback @param error An NSError object containing the RKOAuthClientError that triggered the callback @@ -277,7 +277,7 @@ typedef enum RKOAuthClientErrors { /** Sent when the request sent to the authentication server is invalid - + @param client A reference to the RKOAuthClient that triggered the callback @param error An NSError object containing the RKOAuthClientError that triggered the callback @@ -286,7 +286,7 @@ typedef enum RKOAuthClientErrors { /** Sent when the grant type specified isn't supported by the authentication server - + @param client A reference to the RKOAuthClient that triggered the callback @param error An NSError object containing the RKOAuthClientError that triggered the callback @@ -296,17 +296,17 @@ typedef enum RKOAuthClientErrors { /** Sent when the requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner. - + @param client A reference to the RKOAuthClient that triggered the callback @param error An NSError object containing the RKOAuthClientError that triggered the callback */ - (void)OAuthClient:(RKOAuthClient *)client didFailWithInvalidScopeError:(NSError *)error; -/** +/** Sent to the delegate when an authorization code flow request failed due to a loading error somewhere within the RKRequest call - + @param client A reference to the RKOAuthClient that triggered the callback @param request A reference to the RKRequest that failed @param error An NSError object containing the RKOAuthClientError that triggered diff --git a/Code/Network/RKOAuthClient.m b/Code/Network/RKOAuthClient.m index 36c306ca17..7642f4d912 100644 --- a/Code/Network/RKOAuthClient.m +++ b/Code/Network/RKOAuthClient.m @@ -4,13 +4,13 @@ // // Created by Rodrigo Garcia on 7/20/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -45,7 +45,7 @@ - (id)initWithClientID:(NSString *)clientID secret:(NSString *)secret { _clientID = [clientID copy]; _clientSecret = [secret copy]; } - + return self; } @@ -53,7 +53,7 @@ - (void)dealloc { [_clientID release]; [_clientSecret release]; [_accessToken release]; - + [super dealloc]; } @@ -71,28 +71,28 @@ - (void)validateAuthorizationCode { - (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response { NSError *error = nil; NSString *errorResponse = nil; - + //Use the parsedBody answer in NSDictionary - + NSDictionary* oauthResponse = (NSDictionary *) [response parsedBody:&error]; if ([oauthResponse isKindOfClass:[NSDictionary class]]) { - + //Check the if an access token comes in the response _accessToken = [[oauthResponse objectForKey:@"access_token"] copy]; errorResponse = [oauthResponse objectForKey:@"error"]; - - if (_accessToken) { - // W00T We got an accessToken + + if (_accessToken) { + // W00T We got an accessToken [self.delegate OAuthClient:self didAcquireAccessToken:_accessToken]; - + return; } else if (errorResponse) { // Heads-up! There is an error in the response // The possible errors are defined in the OAuth2 Protocol - + RKOAuthClientErrorCode errorCode = RKOAuthClientErrorUnknown; NSString *errorDescription = [oauthResponse objectForKey:@"error_description"]; - + if ([errorResponse isEqualToString:@"invalid_grant"]) { errorCode = RKOAuthClientErrorInvalidGrant; } @@ -111,42 +111,42 @@ - (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response { else if([errorResponse isEqualToString:@"invalid_scope"]){ errorCode = RKOAuthClientErrorInvalidScope; } - + NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys: errorDescription, NSLocalizedDescriptionKey, nil]; NSError *error = [NSError errorWithDomain:RKErrorDomain code:errorCode userInfo:userInfo]; - - + + // Inform the delegate of what happened if ([self.delegate respondsToSelector:@selector(OAuthClient:didFailWithError:)]) { [self.delegate OAuthClient:self didFailWithError:error]; } - + // Invalid grant if (errorCode == RKOAuthClientErrorInvalidGrant && [self.delegate respondsToSelector:@selector(OAuthClient:didFailWithInvalidGrantError:)]) { [self.delegate OAuthClient:self didFailWithInvalidGrantError:error]; } - + // Unauthorized client if (errorCode == RKOAuthClientErrorUnauthorizedClient && [self.delegate respondsToSelector:@selector(OAuthClient:didFailWithUnauthorizedClientError:)]) { [self.delegate OAuthClient:self didFailWithUnauthorizedClientError:error]; } - + // Invalid client if (errorCode == RKOAuthClientErrorInvalidClient && [self.delegate respondsToSelector:@selector(OAuthClient:didFailWithInvalidClientError:)]) { [self.delegate OAuthClient:self didFailWithInvalidClientError:error]; } - + // Invalid request if (errorCode == RKOAuthClientErrorInvalidRequest && [self.delegate respondsToSelector:@selector(OAuthClient:didFailWithInvalidRequestError:)]) { [self.delegate OAuthClient:self didFailWithInvalidRequestError:error]; } - + // Unsupported grant type if (errorCode == RKOAuthClientErrorUnsupportedGrantType && [self.delegate respondsToSelector:@selector(OAuthClient:didFailWithUnsupportedGrantTypeError:)]) { [self.delegate OAuthClient:self didFailWithUnsupportedGrantTypeError:error]; } - + // Invalid scope if (errorCode == RKOAuthClientErrorInvalidScope && [self.delegate respondsToSelector:@selector(OAuthClient:didFailWithInvalidScopeError:)]) { [self.delegate OAuthClient:self didFailWithInvalidScopeError:error]; @@ -169,7 +169,7 @@ - (void)request:(RKRequest *)request didFailLoadWithError:(NSError *)error { if ([self.delegate respondsToSelector:@selector(OAuthClient:didFailLoadingRequest:withError:)]) { [self.delegate OAuthClient:self didFailLoadingRequest:request withError:clientError]; } - + if ([self.delegate respondsToSelector:@selector(OAuthClient:didFailWithError:)]) { [self.delegate OAuthClient:self didFailWithError:clientError]; } diff --git a/Code/Network/RKParams.h b/Code/Network/RKParams.h index c20e98f3df..0fcc609b1d 100644 --- a/Code/Network/RKParams.h +++ b/Code/Network/RKParams.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 8/3/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,27 +25,27 @@ /** This helper class implements the RKRequestSerializable protocol to provide support for creating the multi-part request body for RKRequest objects. - + RKParams enables simple support for file uploading from NSData objects and files stored locally. RKParams will serialize these objects into a multi-part form representation that is suitable for submission to a remote web server for processing. After creating the RKParams object, use [RKClient post:params:delegate:] as the example below does. - + **Example**: - + RKParams *params = [RKParams params]; NSData *imageData = UIImagePNGRepresentation([_imageView image]); [params setData:imageData MIMEType:@"image/png" forParam:@"image1"]; - + UIImage *image = [UIImage imageNamed:@"RestKit.png"]; imageData = UIImagePNGRepresentation(image); [params setData:imageData MIMEType:@"image/png" forParam:@"image2"]; - + [_client post:@"/RKParamsExample" params:params delegate:self]; - + It is also used internally by RKRequest for its OAuth1 implementation. - + */ @interface RKParams : NSInputStream { @private @@ -65,7 +65,7 @@ /** Creates and returns an RKParams object that is ready for population. - + @return An RKParams object to be populated. */ + (RKParams *)params; @@ -73,7 +73,7 @@ /** Creates and returns an RKParams object created from a dictionary of key/value pairs. - + @param dictionary NSDictionary of key/value pairs to add as RKParamsAttachment objects. @return An RKParams object with the key/value pairs of the dictionary. @@ -82,7 +82,7 @@ /** Initalize an RKParams object from a dictionary of key/value pairs - + @param dictionary NSDictionary of key/value pairs to add as RKParamsAttachment objects. @return An RKParams object with the key/value pairs of the dictionary. @@ -112,7 +112,7 @@ /** Creates a new RKParamsAttachment for a named parameter with the data contained in the file at the given path and adds it to the attachments array. - + @param filePath String of the path to the file to be attached @param param Key name of the attachment to add @return the new RKParamsAttachment that was added to the attachments array @@ -122,9 +122,9 @@ /** Creates a new RKParamsAttachment for a named parameter with the data given and adds it to the attachments array. - + A default MIME type of application/octet-stream will be used. - + @param data NSData object of the data to be attached @param param Key name of the attachment to add @return the new RKParamsAttachment that was added to the attachments array @@ -134,7 +134,7 @@ /** Creates a new RKParamsAttachment for a named parameter with the data given and the MIME type specified and adds it to the attachments array. - + @param data NSData object of the data to be attached @param MIMEType String of the MIME type of the data @param param Key name of the attachment to add @@ -145,10 +145,10 @@ /** Creates a new RKParamsAttachment and sets the value for a named parameter to a data object with the specified MIME Type and attachment file name. - + @bug **DEPRECATED**: Use [RKParams setData:MIMEType:forParam:] and set the fileName on the returned RKParamsAttachment instead. - + @param data NSData object of the data to be attached @param MIMEType String of the MIME type of the data @param fileName String of the attachment file name @@ -161,10 +161,10 @@ Creates a new RKParamsAttachment and sets the value for a named parameter to the data contained in a file at the given path with the specified MIME Type and attachment file name. - + @bug **DEPRECATED**: Use [RKParams setFile:forParam:] and set the MIMEType and fileName on the returned RKParamsAttachment instead. - + @param filePath String of the path to the file to be attached @param MIMEType String of the MIME type of the data @param fileName String of the attachment file name @@ -176,18 +176,18 @@ /** Get the dictionary of params which are plain text as specified by [RFC 5849](http://tools.ietf.org/html/rfc5849#section-3.4.1.3). - + This is largely used for RKClient's OAuth1 implementation. - + The params in this dictionary include those where: - + - The entity-body is single-part. - The entity-body follows the encoding requirements of the "application/x-www-form-urlencoded" content-type as defined by [W3C.REC-html40-19980424]. - The HTTP request entity-header includes the "Content-Type" header field set to "application/x-www-form-urlencoded". - + @return NSDictionary of key/values extracting from the RKParamsAttachment objects that meet the plain text criteria */ diff --git a/Code/Network/RKParams.m b/Code/Network/RKParams.m index 4b09bd599c..8b90fc8665 100644 --- a/Code/Network/RKParams.m +++ b/Code/Network/RKParams.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 8/3/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -39,54 +39,54 @@ @implementation RKParams + (RKParams*)params { - RKParams* params = [[[RKParams alloc] init] autorelease]; - return params; + RKParams* params = [[[RKParams alloc] init] autorelease]; + return params; } + (RKParams*)paramsWithDictionary:(NSDictionary*)dictionary { - RKParams* params = [[[RKParams alloc] initWithDictionary:dictionary] autorelease]; - return params; + RKParams* params = [[[RKParams alloc] initWithDictionary:dictionary] autorelease]; + return params; } - (id)init { self = [super init]; - if (self) { - _attachments = [NSMutableArray new]; - _footer = [[[NSString stringWithFormat:@"--%@--\r\n", kRKStringBoundary] dataUsingEncoding:NSUTF8StringEncoding] retain]; - _footerLength = [_footer length]; - } - - return self; + if (self) { + _attachments = [NSMutableArray new]; + _footer = [[[NSString stringWithFormat:@"--%@--\r\n", kRKStringBoundary] dataUsingEncoding:NSUTF8StringEncoding] retain]; + _footerLength = [_footer length]; + } + + return self; } - (void)dealloc { - [_attachments release]; + [_attachments release]; [_footer release]; - - [super dealloc]; + + [super dealloc]; } - (RKParams *)initWithDictionary:(NSDictionary *)dictionary { self = [self init]; - if (self) { + if (self) { // NOTE: We sort the keys to try and ensure given identical dictionaries we'll wind up // with matching MD5 checksums. NSArray *sortedKeys = [[dictionary allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; - for (NSString *key in sortedKeys) { - id value = [dictionary objectForKey:key]; - [self setValue:value forParam:key]; - } - } - - return self; + for (NSString *key in sortedKeys) { + id value = [dictionary objectForKey:key]; + [self setValue:value forParam:key]; + } + } + + return self; } - (RKParamsAttachment *)setValue:(id )value forParam:(NSString *)param { - RKParamsAttachment *attachment = [[RKParamsAttachment alloc] initWithName:param value:value]; - [_attachments addObject:attachment]; - [attachment release]; - - return attachment; + RKParamsAttachment *attachment = [[RKParamsAttachment alloc] initWithName:param value:value]; + [_attachments addObject:attachment]; + [attachment release]; + + return attachment; } - (NSDictionary *)dictionaryOfPlainTextParams { @@ -98,62 +98,73 @@ - (NSDictionary *)dictionaryOfPlainTextParams { } - (RKParamsAttachment *)setFile:(NSString *)filePath forParam:(NSString *)param { - RKParamsAttachment *attachment = [[RKParamsAttachment alloc] initWithName:param file:filePath]; - [_attachments addObject:attachment]; - [attachment release]; - - return attachment; + NSParameterAssert(filePath); + NSParameterAssert(param); + RKParamsAttachment *attachment = [[RKParamsAttachment alloc] initWithName:param file:filePath]; + [_attachments addObject:attachment]; + [attachment release]; + + return attachment; } - (RKParamsAttachment *)setData:(NSData *)data forParam:(NSString *)param { - RKParamsAttachment *attachment = [[RKParamsAttachment alloc] initWithName:param data:data]; - [_attachments addObject:attachment]; - [attachment release]; - - return attachment; + NSParameterAssert(data); + NSParameterAssert(param); + RKParamsAttachment *attachment = [[RKParamsAttachment alloc] initWithName:param data:data]; + [_attachments addObject:attachment]; + [attachment release]; + + return attachment; } - (RKParamsAttachment *)setData:(NSData *)data MIMEType:(NSString *)MIMEType forParam:(NSString *)param { - RKParamsAttachment *attachment = [self setData:data forParam:param]; - if (MIMEType != nil) { - attachment.MIMEType = MIMEType; - } - - return attachment; + NSParameterAssert(data); + NSParameterAssert(MIMEType); + NSParameterAssert(param); + RKParamsAttachment *attachment = [self setData:data forParam:param]; + if (MIMEType != nil) { + attachment.MIMEType = MIMEType; + } + + return attachment; } - (RKParamsAttachment *)setData:(NSData *)data MIMEType:(NSString *)MIMEType fileName:(NSString *)fileName forParam:(NSString *)param { - RKParamsAttachment *attachment = [self setData:data forParam:param]; - if (MIMEType != nil) { - attachment.MIMEType = MIMEType; - } - if (fileName != nil) { - attachment.fileName = fileName; - } - - return attachment; + NSParameterAssert(data); + NSParameterAssert(param); + RKParamsAttachment *attachment = [self setData:data forParam:param]; + if (MIMEType) { + attachment.MIMEType = MIMEType; + } + if (fileName) { + attachment.fileName = fileName; + } + + return attachment; } - (RKParamsAttachment *)setFile:(NSString *)filePath MIMEType:(NSString *)MIMEType fileName:(NSString *)fileName forParam:(NSString *)param { - RKParamsAttachment *attachment = [self setFile:filePath forParam:param]; - if (MIMEType != nil) { - attachment.MIMEType = MIMEType; - } - if (fileName != nil) { - attachment.fileName = fileName; - } - - return attachment; + NSParameterAssert(filePath); + NSParameterAssert(param); + RKParamsAttachment *attachment = [self setFile:filePath forParam:param]; + if (MIMEType) { + attachment.MIMEType = MIMEType; + } + if (fileName) { + attachment.fileName = fileName; + } + + return attachment; } #pragma mark RKRequestSerializable methods - (NSString *)HTTPHeaderValueForContentType { - return [NSString stringWithFormat:@"multipart/form-data; boundary=%@", kRKStringBoundary]; + return [NSString stringWithFormat:@"multipart/form-data; boundary=%@", kRKStringBoundary]; } - (NSUInteger)HTTPHeaderValueForContentLength { - return _length; + return _length; } - (void)reset { @@ -162,59 +173,59 @@ - (void)reset { _streamStatus = NSStreamStatusNotOpen; } -- (NSInputStream*)HTTPBodyStream { - // Open each of our attachments - [_attachments makeObjectsPerformSelector:@selector(open)]; - - // Calculate the length of the stream - _length = _footerLength; - for (RKParamsAttachment *attachment in _attachments) { - _length += [attachment length]; - } - - return (NSInputStream*)self; +- (NSInputStream *)HTTPBodyStream { + // Open each of our attachments + [_attachments makeObjectsPerformSelector:@selector(open)]; + + // Calculate the length of the stream + _length = _footerLength; + for (RKParamsAttachment *attachment in _attachments) { + _length += [attachment length]; + } + + return (NSInputStream*)self; } #pragma mark NSInputStream methods - (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)maxLength { NSUInteger bytesSentInThisRead = 0, bytesRead; - NSUInteger lengthOfAttachments = (_length - _footerLength); - - // Proxy the read through to our attachments + NSUInteger lengthOfAttachments = (_length - _footerLength); + + // Proxy the read through to our attachments _streamStatus = NSStreamStatusReading; while (_bytesDelivered < _length && bytesSentInThisRead < maxLength && _currentPart < [_attachments count]) { if ((bytesRead = [[_attachments objectAtIndex:_currentPart] read:buffer + bytesSentInThisRead maxLength:maxLength - bytesSentInThisRead]) == 0) { _currentPart ++; continue; } - + bytesSentInThisRead += bytesRead; _bytesDelivered += bytesRead; } - - // If we have sent all the attachments data, begin emitting the boundary footer + + // If we have sent all the attachments data, begin emitting the boundary footer if ((_bytesDelivered >= lengthOfAttachments) && (bytesSentInThisRead < maxLength)) { - NSUInteger footerBytesSent, footerBytesRemaining, bytesRemainingInBuffer; - - // Calculate our position in the stream & buffer - footerBytesSent = _bytesDelivered - lengthOfAttachments; - footerBytesRemaining = _footerLength - footerBytesSent; - bytesRemainingInBuffer = maxLength - bytesSentInThisRead; - - // Send the entire footer back if there is room - bytesRead = (footerBytesRemaining < bytesRemainingInBuffer) ? footerBytesRemaining : bytesRemainingInBuffer; + NSUInteger footerBytesSent, footerBytesRemaining, bytesRemainingInBuffer; + + // Calculate our position in the stream & buffer + footerBytesSent = _bytesDelivered - lengthOfAttachments; + footerBytesRemaining = _footerLength - footerBytesSent; + bytesRemainingInBuffer = maxLength - bytesSentInThisRead; + + // Send the entire footer back if there is room + bytesRead = (footerBytesRemaining < bytesRemainingInBuffer) ? footerBytesRemaining : bytesRemainingInBuffer; [_footer getBytes:buffer + bytesSentInThisRead range:NSMakeRange(footerBytesSent, bytesRead)]; - + bytesSentInThisRead += bytesRead; _bytesDelivered += bytesRead; } - + return bytesSentInThisRead; } - (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len { - return NO; + return NO; } - (BOOL)hasBytesAvailable { @@ -229,9 +240,9 @@ - (void)open { - (void)close { if (_streamStatus != NSStreamStatusClosed) { _streamStatus = NSStreamStatusClosed; - - RKLogTrace(@"RKParams stream closed. Releasing self."); - + + RKLogTrace(@"RKParams stream closed. Releasing self."); + #if TARGET_OS_IPHONE // NOTE: When we are assigned to the URL request, we get // retained. We release ourselves here to ensure the retain @@ -250,7 +261,7 @@ - (NSStreamStatus)streamStatus { if (_streamStatus != NSStreamStatusClosed && _bytesDelivered >= _length) { _streamStatus = NSStreamStatusAtEnd; } - + return _streamStatus; } @@ -263,7 +274,7 @@ - (NSString *)MD5 { for (RKParamsAttachment *attachment in self.attachments) { [attachmentsMD5 appendString:[attachment MD5]]; } - + return [attachmentsMD5 MD5]; } diff --git a/Code/Network/RKParamsAttachment.h b/Code/Network/RKParamsAttachment.h index 81103b7c3e..675c1a02cc 100644 --- a/Code/Network/RKParamsAttachment.h +++ b/Code/Network/RKParamsAttachment.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 8/6/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,22 +24,22 @@ Models an individual part of a multi-part MIME document. These attachments are stacked together within the RKParams document to allow for uploading files via HTTP. - + Typically, interactions with the RKParamsAttachment are accomplished through the RKParams class and there shouldn't be much need to deal directly with this class. */ @interface RKParamsAttachment : NSObject { NSString *_name; - NSString *_fileName; + NSString *_fileName; NSString *_MIMEType; - @private + @private NSString *_filePath; NSData *_body; NSInputStream *_bodyStream; NSData *_MIMEHeader; - NSUInteger _MIMEHeaderLength; + NSUInteger _MIMEHeaderLength; NSUInteger _bodyLength; NSUInteger _length; NSUInteger _delivered; @@ -53,7 +53,7 @@ /** Returns a newly initialized attachment with a given parameter name and value. - + @param name The parameter name of this attachment in the multi-part document. @param value A value that is used to create the attachment body @return An initialized attachment with the given name and value. @@ -63,7 +63,7 @@ /** Returns a newly initialized attachment with a given parameter name and the data stored in an NSData object. - + @param name The parameter name of this attachment in the multi-part document. @param data The data that is used to create the attachment body. @return An initialized attachment with the given name and data. @@ -73,7 +73,7 @@ /** Returns a newly initialized attachment with a given parameter name and the data stored on disk at the given file path. - + @param name The parameter name of this attachment in the multi-part document. @param filePath The complete path of a file to use its data contents as the attachment body. @@ -95,7 +95,7 @@ /** The MIME type of the attached file in the MIME stream. MIME Type will be auto-detected from the file extension of the attached file. - + **Default**: nil */ @property (nonatomic, retain) NSString *MIMEType; @@ -112,7 +112,7 @@ /** The name of the attached file in the MIME stream - + **Default**: The name of the file attached or nil if there is not one. */ @property (nonatomic, retain) NSString *fileName; @@ -130,14 +130,14 @@ /** The length of the entire attachment including the MIME header and the body. - + @return Unsigned integer of the MIME header and the body. */ - (NSUInteger)length; /** Calculate and return an MD5 checksum for the body of this attachment. - + This works for simple values, NSData structures in memory, or by efficiently streaming a file and calculating an MD5. */ @@ -150,12 +150,12 @@ /** Read the attachment body in a streaming fashion for NSInputStream. - + @param buffer A data buffer. The buffer must be large enough to contain the number of bytes specified by len. @param len The maximum number of bytes to read. @return A number indicating the outcome of the operation: - + - A positive number indicates the number of bytes read; - 0 indicates that the end of the buffer was reached; - A negative number means that the operation failed. diff --git a/Code/Network/RKParamsAttachment.m b/Code/Network/RKParamsAttachment.m index fb06dcbb80..e62413f623 100644 --- a/Code/Network/RKParamsAttachment.m +++ b/Code/Network/RKParamsAttachment.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 8/6/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -43,68 +43,68 @@ @implementation RKParamsAttachment - (id)initWithName:(NSString *)name { self = [self init]; - if (self) { + if (self) { self.name = name; self.fileName = name; - } - - return self; + } + + return self; } - (id)initWithName:(NSString *)name value:(id)value { - if ((self = [self initWithName:name])) { - if ([value respondsToSelector:@selector(dataUsingEncoding:)]) { + if ((self = [self initWithName:name])) { + if ([value respondsToSelector:@selector(dataUsingEncoding:)]) { _body = [[(NSString*)value dataUsingEncoding:NSUTF8StringEncoding] retain]; - } else { - _body = [[[NSString stringWithFormat:@"%@", value] dataUsingEncoding:NSUTF8StringEncoding] retain]; - } - - _bodyStream = [[NSInputStream alloc] initWithData:_body]; - _bodyLength = [_body length]; + } else { + _body = [[[NSString stringWithFormat:@"%@", value] dataUsingEncoding:NSUTF8StringEncoding] retain]; + } + + _bodyStream = [[NSInputStream alloc] initWithData:_body]; + _bodyLength = [_body length]; _value = [value retain]; - } - - return self; + } + + return self; } - (id)initWithName:(NSString*)name data:(NSData*)data { self = [self initWithName:name]; - if (self) { + if (self) { _body = [data retain]; - _bodyStream = [[NSInputStream alloc] initWithData:data]; - _bodyLength = [data length]; - } - - return self; + _bodyStream = [[NSInputStream alloc] initWithData:data]; + _bodyLength = [data length]; + } + + return self; } - (id)initWithName:(NSString*)name file:(NSString*)filePath { self = [self initWithName:name]; - if (self) { - NSAssert1([[NSFileManager defaultManager] fileExistsAtPath:filePath], @"Expected file to exist at path: %@", filePath); + if (self) { + NSAssert1([[NSFileManager defaultManager] fileExistsAtPath:filePath], @"Expected file to exist at path: %@", filePath); _filePath = [filePath retain]; _fileName = [[filePath lastPathComponent] retain]; NSString *MIMEType = [filePath MIMETypeForPathExtension]; - if (! MIMEType) MIMEType = @"application/octet-stream"; - _MIMEType = [MIMEType retain]; - _bodyStream = [[NSInputStream alloc] initWithFileAtPath:filePath]; - - NSError* error; - NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error]; - if (attributes) { - _bodyLength = [[attributes objectForKey:NSFileSize] unsignedIntegerValue]; - } - else { - RKLogError(@"Encountered an error while determining file size: %@", error); - } - } - - return self; + if (! MIMEType) MIMEType = @"application/octet-stream"; + _MIMEType = [MIMEType retain]; + _bodyStream = [[NSInputStream alloc] initWithFileAtPath:filePath]; + + NSError* error; + NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error]; + if (attributes) { + _bodyLength = [[attributes objectForKey:NSFileSize] unsignedIntegerValue]; + } + else { + RKLogError(@"Encountered an error while determining file size: %@", error); + } + } + + return self; } - (void)dealloc { [_value release]; - [_name release]; + [_name release]; [_body release]; [_filePath release]; [_fileName release]; @@ -121,35 +121,35 @@ - (void)dealloc { } - (NSString*)MIMEBoundary { - return kRKStringBoundary; + return kRKStringBoundary; } #pragma mark NSStream methods - (void)open { - // Generate the MIME header for this part - if (self.fileName && self.MIMEType) { - // Typical for file attachments - _MIMEHeader = [[[NSString stringWithFormat:@"--%@\r\nContent-Disposition: form-data; name=\"%@\"; " - @"filename=\"%@\"\r\nContent-Type: %@\r\n\r\n", - [self MIMEBoundary], self.name, self.fileName, self.MIMEType] dataUsingEncoding:NSUTF8StringEncoding] retain]; - } else if (self.MIMEType) { - // Typical for data values - _MIMEHeader = [[[NSString stringWithFormat:@"--%@\r\nContent-Disposition: form-data; name=\"%@\"\r\n" - @"Content-Type: %@\r\n\r\n", - [self MIMEBoundary], self.name, self.MIMEType] dataUsingEncoding:NSUTF8StringEncoding] retain]; - } else { - // Typical for raw values - _MIMEHeader = [[[NSString stringWithFormat:@"--%@\r\nContent-Disposition: form-data; name=\"%@\"\r\n\r\n", - [self MIMEBoundary], self.name] - dataUsingEncoding:NSUTF8StringEncoding] retain]; - } - - // Calculate lengths - _MIMEHeaderLength = [_MIMEHeader length]; - _length = _MIMEHeaderLength + _bodyLength + 2; // \r\n is the + 2 - - // Open the stream + // Generate the MIME header for this part + if (self.fileName && self.MIMEType) { + // Typical for file attachments + _MIMEHeader = [[[NSString stringWithFormat:@"--%@\r\nContent-Disposition: form-data; name=\"%@\"; " + @"filename=\"%@\"\r\nContent-Type: %@\r\n\r\n", + [self MIMEBoundary], self.name, self.fileName, self.MIMEType] dataUsingEncoding:NSUTF8StringEncoding] retain]; + } else if (self.MIMEType) { + // Typical for data values + _MIMEHeader = [[[NSString stringWithFormat:@"--%@\r\nContent-Disposition: form-data; name=\"%@\"\r\n" + @"Content-Type: %@\r\n\r\n", + [self MIMEBoundary], self.name, self.MIMEType] dataUsingEncoding:NSUTF8StringEncoding] retain]; + } else { + // Typical for raw values + _MIMEHeader = [[[NSString stringWithFormat:@"--%@\r\nContent-Disposition: form-data; name=\"%@\"\r\n\r\n", + [self MIMEBoundary], self.name] + dataUsingEncoding:NSUTF8StringEncoding] retain]; + } + + // Calculate lengths + _MIMEHeaderLength = [_MIMEHeader length]; + _length = _MIMEHeaderLength + _bodyLength + 2; // \r\n is the + 2 + + // Open the stream [_bodyStream open]; } @@ -159,50 +159,50 @@ - (NSUInteger)length { - (NSUInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)maxLength { NSUInteger sent = 0, read; - - // We are done with the read + + // We are done with the read if (_delivered >= _length) { return 0; } - - // First we send back the MIME headers + + // First we send back the MIME headers if (_delivered < _MIMEHeaderLength && sent < maxLength) { - NSUInteger headerBytesRemaining, bytesRemainingInBuffer; - - headerBytesRemaining = _MIMEHeaderLength - _delivered; - bytesRemainingInBuffer = maxLength; - - // Send the entire header if there is room + NSUInteger headerBytesRemaining, bytesRemainingInBuffer; + + headerBytesRemaining = _MIMEHeaderLength - _delivered; + bytesRemainingInBuffer = maxLength; + + // Send the entire header if there is room read = (headerBytesRemaining < bytesRemainingInBuffer) ? headerBytesRemaining : bytesRemainingInBuffer; [_MIMEHeader getBytes:buffer range:NSMakeRange(_delivered, read)]; - + sent += read; _delivered += sent; } - - // Read the attachment body out of our underlying stream + + // Read the attachment body out of our underlying stream while (_delivered >= _MIMEHeaderLength && _delivered < (_length - 2) && sent < maxLength) { if ((read = [_bodyStream read:(buffer + sent) maxLength:(maxLength - sent)]) == 0) { break; } - + sent += read; _delivered += read; } - - // Append the \r\n + + // Append the \r\n if (_delivered >= (_length - 2) && sent < maxLength) { if (_delivered == (_length - 2)) { *(buffer + sent) = '\r'; sent ++; - _delivered ++; + _delivered ++; } - + *(buffer + sent) = '\n'; sent ++; - _delivered ++; + _delivered ++; } - + return sent; } @@ -210,7 +210,7 @@ - (NSString *)MD5 { if (_body) { return [_body MD5]; } else if (_filePath) { - CFStringRef fileAttachmentMD5 = FileMD5HashCreateWithPath((CFStringRef)_filePath, + CFStringRef fileAttachmentMD5 = FileMD5HashCreateWithPath((CFStringRef)_filePath, FileHashDefaultChunkSizeForReadingData); return [(NSString *)fileAttachmentMD5 autorelease]; } else { diff --git a/Code/Network/RKReachabilityObserver.h b/Code/Network/RKReachabilityObserver.h index aa0c959bc0..390d71a812 100755 --- a/Code/Network/RKReachabilityObserver.h +++ b/Code/Network/RKReachabilityObserver.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/14/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -45,41 +45,41 @@ typedef enum { /** Network reachability not yet known */ - RKReachabilityIndeterminate, + RKReachabilityIndeterminate, /** Network is not reachable */ - RKReachabilityNotReachable, + RKReachabilityNotReachable, /** Network is reachable via a WiFi connection */ - RKReachabilityReachableViaWiFi, + RKReachabilityReachableViaWiFi, /** Network is reachable via a "wireless wide area network" (WWAN). i.e. GPRS, Edge, 3G, etc. */ - RKReachabilityReachableViaWWAN + RKReachabilityReachableViaWWAN } RKReachabilityNetworkStatus; /** Provides a notification based interface for monitoring changes to network status. - + When initialized, creates an SCReachabilityReg and schedules it for callback notifications on the main dispatch queue. As notifications are intercepted from SystemConfiguration, the observer will update its state and emit `[RKReachabilityDidChangeNotifications](RKReachabilityDidChangeNotification)` to inform listeners about state changes. - + Portions of this software are derived from the Apple Reachability code sample: http://developer.apple.com/library/ios/#samplecode/Reachability/Listings/Classes_Reachability_m.html */ @interface RKReachabilityObserver : NSObject { NSString *_host; - SCNetworkReachabilityRef _reachabilityRef; - BOOL _reachabilityDetermined; + SCNetworkReachabilityRef _reachabilityRef; + BOOL _reachabilityDetermined; BOOL _monitoringLocalWiFi; - SCNetworkReachabilityFlags _reachabilityFlags; + SCNetworkReachabilityFlags _reachabilityFlags; } ///----------------------------------------------------------------------------- @@ -90,12 +90,12 @@ typedef enum { Creates and returns a RKReachabilityObserver instance observing reachability changes to the hostname or IP address referenced in a given string. The observer will monitor the ability to reach the specified remote host and emit - notifications when its reachability status changes. - + notifications when its reachability status changes. + The hostNameOrIPAddress will be introspected to determine if it contains an IP address encoded into a string or a DNS name. The observer will be configured appropriately based on the contents of the string. - + @bug Note that iOS 5 has known issues with hostname based reachability @param hostNameOrIPAddress An NSString containing a hostname or IP address to be observed. @@ -107,7 +107,7 @@ typedef enum { /** Creates and returns a reachabilityObserverForInternet instance observing the reachability to the Internet in general. - + @return A reachability observer targeting INADDR_ANY or nil if it could not be observed. */ @@ -117,7 +117,7 @@ typedef enum { Creates and returns a reachabilityObserverForInternet instance observing the reachability to the Internet via the local WiFi interface. Internet access available via the WWAN (3G, Edge, etc) will not be considered reachable. - + @return A reachability observer targeting IN_LINKLOCALNETNUM or nil if it could not be observed. */ @@ -126,7 +126,7 @@ typedef enum { /** Creates and returns a RKReachabilityObserver instance observing reachability changes to the sockaddr address provided. - + @param address A socket address to determine reachability for. @return A reachability observer targeting the given socket address or nil if it could not be observed. @@ -135,8 +135,8 @@ typedef enum { /** Creates and returns a RKReachabilityObserver instance observing reachability - changes to the IP address provided. - + changes to the IP address provided. + @param internetAddress A 32-bit integer representation of an IP address @return A reachability observer targeting the given IP address or nil if it could not be observed. @@ -147,12 +147,12 @@ typedef enum { Returns a RKReachabilityObserver instance observing reachability changes to the hostname or IP address referenced in a given string. The observer will monitor the ability to reach the specified remote host and emit notifications when its - reachability status changes. - + reachability status changes. + The hostNameOrIPAddress will be introspected to determine if it contains an IP address encoded into a string or a DNS name. The observer will be configured appropriately based on the contents of the string. - + @bug Note that iOS 5 has known issues with hostname based reachability @param hostNameOrIPAddress An NSString containing a hostname or IP address to be observed. @@ -164,7 +164,7 @@ typedef enum { /** Returns a RKReachabilityObserver instance observing reachability changes to the sockaddr address provided. - + @param address A socket address to determine reachability for. @return A reachability observer targeting the given socket address or nil if it could not be observed. @@ -188,13 +188,13 @@ typedef enum { /** Current state of determining reachability - + When initialized, RKReachabilityObserver instances are in an indeterminate state to indicate that reachability status has not been yet established. After the first callback is processed by the observer, the observer will answer YES for reachabilityDetermined and networkStatus will return a determinate response. - + @return YES if reachability has been determined */ @property (nonatomic, readonly, getter=isReachabilityDetermined) BOOL reachabilityDetermined; @@ -202,26 +202,26 @@ typedef enum { /** Current network status as determined by examining the state of the currently cached reachabilityFlags - + @return Status of the network as RKReachabilityNetworkStatus */ @property (nonatomic, readonly) RKReachabilityNetworkStatus networkStatus; /** Current state of the local WiFi interface's reachability - + When the local WiFi interface is being monitored, only three reachability states are possible: - + - RKReachabilityIndeterminate - RKReachabilityNotReachable - RKReachabilityReachableViaWiFi - + If the device has connectivity through a WWAN connection only it will consider the network not reachable. - + @see reachabilityObserverForLocalWifi - + @return YES if the reachability observer is monitoring the local WiFi interface */ @property (nonatomic, readonly, getter=isMonitoringLocalWiFi) BOOL monitoringLocalWiFi; @@ -229,13 +229,13 @@ typedef enum { /** The reachability flags as of the last invocation of the reachability callback - + Each time the reachability callback is invoked with an asynchronous update of reachability status the flags are cached and made accessible via the reachabilityFlags method. - + Flags can also be directly obtained via [RKReachabilityObserver getFlags] - + @see getFlags @return The most recently cached reachability flags reflecting current network status. @@ -245,7 +245,7 @@ typedef enum { /** Acquires the current network reachability flags, answering YES if successfully acquired; answering NO otherwise. - + Beware! The System Configuration framework operates synchronously by default. See Technical Q&A QA1693, Synchronous Networking On The Main Thread. Asking for flags blocks the current thread and potentially kills your @@ -261,7 +261,7 @@ typedef enum { /** Returns YES when the Internet is reachable (via WiFi or WWAN) - + @exception NSInternalInconsistencyException Raises an NSInternalInconsistencyException if called before reachability is determined */ @@ -269,7 +269,7 @@ typedef enum { /** Returns YES when we the network is reachable via WWAN - + @exception NSInternalInconsistencyException Raises an NSInternalInconsistencyException if called before reachability is determined */ @@ -277,7 +277,7 @@ typedef enum { /** Returns YES when we the network is reachable via WiFi - + @exception NSInternalInconsistencyException Raises an NSInternalInconsistencyException if called before reachability is determined */ @@ -285,7 +285,7 @@ typedef enum { /** Returns YES when WWAN may be available, but not active until a connection has been established. - + @exception NSInternalInconsistencyException Raises an NSInternalInconsistencyException if called before reachability is determined */ @@ -293,7 +293,7 @@ typedef enum { /** Returns YES if a dynamic, on-demand connection is available - + @exception NSInternalInconsistencyException Raises an NSInternalInconsistencyException if called before reachability is determined */ @@ -301,7 +301,7 @@ typedef enum { /** Returns YES if user intervention is required to initiate a connection - + @exception NSInternalInconsistencyException Raises an NSInternalInconsistencyException if called before reachability is determined */ @@ -309,7 +309,7 @@ typedef enum { /** Returns a string representation of the currently cached reachabilityFlags for inspection - + @return A string containing single character representations of the bits in an SCNetworkReachabilityFlags */ diff --git a/Code/Network/RKReachabilityObserver.m b/Code/Network/RKReachabilityObserver.m index c19a1b431c..b2956f57f2 100755 --- a/Code/Network/RKReachabilityObserver.m +++ b/Code/Network/RKReachabilityObserver.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/14/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -48,12 +48,12 @@ - (void)unscheduleObserver; NSString* const RKReachabilityWasDeterminedNotification = @"RKReachabilityWasDeterminedNotification"; static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info) { - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - - RKReachabilityObserver *observer = (RKReachabilityObserver *) info; - observer.reachabilityFlags = flags; - - [pool release]; + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + RKReachabilityObserver *observer = (RKReachabilityObserver *) info; + observer.reachabilityFlags = flags; + + [pool release]; } #pragma mark - @@ -66,24 +66,24 @@ @implementation RKReachabilityObserver @synthesize monitoringLocalWiFi = _monitoringLocalWiFi; + (RKReachabilityObserver *)reachabilityObserverForAddress:(const struct sockaddr *)address { - return [[[self alloc] initWithAddress:address] autorelease]; + return [[[self alloc] initWithAddress:address] autorelease]; } + (RKReachabilityObserver *)reachabilityObserverForInternetAddress:(in_addr_t)internetAddress { - struct sockaddr_in address; - bzero(&address, sizeof(address)); - address.sin_len = sizeof(address); - address.sin_family = AF_INET; - address.sin_addr.s_addr = htonl(internetAddress); - return [self reachabilityObserverForAddress:(struct sockaddr *)&address]; + struct sockaddr_in address; + bzero(&address, sizeof(address)); + address.sin_len = sizeof(address); + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(internetAddress); + return [self reachabilityObserverForAddress:(struct sockaddr *)&address]; } + (RKReachabilityObserver *)reachabilityObserverForInternet { - return [self reachabilityObserverForInternetAddress:INADDR_ANY]; + return [self reachabilityObserverForInternetAddress:INADDR_ANY]; } + (RKReachabilityObserver *)reachabilityObserverForLocalWifi { - return [self reachabilityObserverForInternetAddress:IN_LINKLOCALNETNUM]; + return [self reachabilityObserverForInternetAddress:IN_LINKLOCALNETNUM]; } + (RKReachabilityObserver *)reachabilityObserverForHost:(NSString *)hostNameOrIPAddress { @@ -92,76 +92,76 @@ + (RKReachabilityObserver *)reachabilityObserverForHost:(NSString *)hostNameOrIP - (id)initWithAddress:(const struct sockaddr *)address { self = [super init]; - if (self) { - _reachabilityRef = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, address); - if (_reachabilityRef == NULL) { + if (self) { + _reachabilityRef = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, address); + if (_reachabilityRef == NULL) { RKLogWarning(@"Unable to initialize reachability reference"); - [self release]; - self = nil; - } else { - // For technical details regarding link-local connections, please - // see the following source file at Apple's open-source site. - // - // http://www.opensource.apple.com/source/bootp/bootp-89/IPConfiguration.bproj/linklocal.c - // - _monitoringLocalWiFi = address->sa_len == sizeof(struct sockaddr_in) && address->sa_family == AF_INET && IN_LINKLOCAL(ntohl(((const struct sockaddr_in *)address)->sin_addr.s_addr)); - + [self release]; + self = nil; + } else { + // For technical details regarding link-local connections, please + // see the following source file at Apple's open-source site. + // + // http://www.opensource.apple.com/source/bootp/bootp-89/IPConfiguration.bproj/linklocal.c + // + _monitoringLocalWiFi = address->sa_len == sizeof(struct sockaddr_in) && address->sa_family == AF_INET && IN_LINKLOCAL(ntohl(((const struct sockaddr_in *)address)->sin_addr.s_addr)); + // Save the IP address char str[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, &((const struct sockaddr_in *)address)->sin_addr, str, INET_ADDRSTRLEN); + inet_ntop(AF_INET, &((const struct sockaddr_in *)address)->sin_addr, str, INET_ADDRSTRLEN); _host = [[NSString alloc] initWithCString:str encoding:NSUTF8StringEncoding]; - + if (_monitoringLocalWiFi) { RKLogInfo(@"Reachability observer initialized for Local Wifi"); - } else if (address->sa_len == sizeof(struct sockaddr_in) && address->sa_family == AF_INET) { + } else if (address->sa_len == sizeof(struct sockaddr_in) && address->sa_family == AF_INET) { RKLogInfo(@"Reachability observer initialized with IP address: %@.", _host); } - + // We can immediately determine reachability to an IP address dispatch_async(dispatch_get_main_queue(), ^{ // Obtain the flags after giving other objects a chance to observe us [self getFlags]; }); - + // Schedule the observer [self scheduleObserver]; - } - } - return self; + } + } + return self; } - (id)initWithHost:(NSString *)hostNameOrIPAddress { // Determine if the string contains a hostname or IP address struct sockaddr_in sa; char *hostNameOrIPAddressCString = (char *) [hostNameOrIPAddress UTF8String]; - int result = inet_pton(AF_INET, hostNameOrIPAddressCString, &(sa.sin_addr)); + int result = inet_pton(AF_INET, hostNameOrIPAddressCString, &(sa.sin_addr)); if (result != 0) { // IP Address struct sockaddr_in remote_saddr; - + bzero(&remote_saddr, sizeof(struct sockaddr_in)); remote_saddr.sin_len = sizeof(struct sockaddr_in); remote_saddr.sin_family = AF_INET; inet_aton(hostNameOrIPAddressCString, &(remote_saddr.sin_addr)); - + return [self initWithAddress:(struct sockaddr *) &remote_saddr]; } - + // Hostname - self = [self init]; + self = [self init]; if (self) { - _host = [hostNameOrIPAddress retain]; + _host = [hostNameOrIPAddress retain]; _reachabilityRef = SCNetworkReachabilityCreateWithName(CFAllocatorGetDefault(), hostNameOrIPAddressCString); RKLogInfo(@"Reachability observer initialized with hostname %@", hostNameOrIPAddress); if (_reachabilityRef == NULL) { RKLogWarning(@"Unable to initialize reachability reference"); - [self release]; - self = nil; - } else { + [self release]; + self = nil; + } else { [self scheduleObserver]; } } - + return self; } @@ -173,8 +173,8 @@ - (void)dealloc { if (_reachabilityRef) { CFRelease(_reachabilityRef); } - [_host release]; - + [_host release]; + [super dealloc]; } @@ -190,29 +190,29 @@ - (NSString *)stringFromNetworkStatus:(RKReachabilityNetworkStatus)status { case RKReachabilityIndeterminate: return @"RKReachabilityIndeterminate"; break; - + case RKReachabilityNotReachable: return @"RKReachabilityNotReachable"; break; - + case RKReachabilityReachableViaWiFi: return @"RKReachabilityReachableViaWiFi"; - break; - + break; + case RKReachabilityReachableViaWWAN: return @"RKReachabilityReachableViaWWAN"; break; - + default: break; } - + return nil; } - (NSString *)reachabilityFlagsDescription { return [NSString stringWithFormat:@"%c%c %c%c%c%c%c%c%c", - #if TARGET_OS_IPHONE + #if TARGET_OS_IPHONE (_reachabilityFlags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-', #else // If we are not on iOS, always output a dash for WWAN @@ -229,59 +229,59 @@ - (NSString *)reachabilityFlagsDescription { } - (RKReachabilityNetworkStatus)networkStatus { - NSAssert(_reachabilityRef != NULL, @"currentNetworkStatus called with NULL reachabilityRef"); - RKReachabilityNetworkStatus status = RKReachabilityNotReachable; - - if (!self.reachabilityDetermined) { + NSAssert(_reachabilityRef != NULL, @"currentNetworkStatus called with NULL reachabilityRef"); + RKReachabilityNetworkStatus status = RKReachabilityNotReachable; + + if (!self.reachabilityDetermined) { RKLogTrace(@"Reachability observer %@ has not yet established reachability. networkStatus = %@", self, @"RKReachabilityIndeterminate"); - return RKReachabilityIndeterminate; - } - + return RKReachabilityIndeterminate; + } + RKLogTrace(@"Reachability Flags: %@\n", [self reachabilityFlagsDescription]); - + // If we are observing WiFi, we are only reachable via WiFi when flags are direct - if (self.isMonitoringLocalWiFi) { - if ((_reachabilityFlags & kSCNetworkReachabilityFlagsReachable) && (_reachabilityFlags & kSCNetworkReachabilityFlagsIsDirect)) { - // <-- reachable AND direct - status = RKReachabilityReachableViaWiFi; - } else { - // <-- NOT reachable OR NOT direct - status = RKReachabilityNotReachable; - } - } else { - if ((_reachabilityFlags & kSCNetworkReachabilityFlagsReachable)) { - // <-- reachable + if (self.isMonitoringLocalWiFi) { + if ((_reachabilityFlags & kSCNetworkReachabilityFlagsReachable) && (_reachabilityFlags & kSCNetworkReachabilityFlagsIsDirect)) { + // <-- reachable AND direct + status = RKReachabilityReachableViaWiFi; + } else { + // <-- NOT reachable OR NOT direct + status = RKReachabilityNotReachable; + } + } else { + if ((_reachabilityFlags & kSCNetworkReachabilityFlagsReachable)) { + // <-- reachable #if TARGET_OS_IPHONE - if ((_reachabilityFlags & kSCNetworkReachabilityFlagsIsWWAN)) { - // <-- reachable AND is wireless wide-area network (iOS only) - status = RKReachabilityReachableViaWWAN; - } else { + if ((_reachabilityFlags & kSCNetworkReachabilityFlagsIsWWAN)) { + // <-- reachable AND is wireless wide-area network (iOS only) + status = RKReachabilityReachableViaWWAN; + } else { #endif - // <-- reachable AND is NOT wireless wide-area network (iOS only) - if ((_reachabilityFlags & kSCNetworkReachabilityFlagsConnectionOnTraffic) || (_reachabilityFlags & kSCNetworkReachabilityFlagsConnectionOnDemand)) { - // <-- reachable, on-traffic OR on-demand connection - if ((_reachabilityFlags & kSCNetworkReachabilityFlagsInterventionRequired)) { - // <-- reachable, on-traffic OR on-demand connection, intervention required - status = (_reachabilityFlags & kSCNetworkReachabilityFlagsConnectionRequired) ? RKReachabilityNotReachable : RKReachabilityReachableViaWiFi; - } else { - // <-- reachable, on-traffic OR on-demand connection, intervention NOT required - status = RKReachabilityReachableViaWiFi; - } - } else { - // <-- reachable, NOT on-traffic OR on-demand connection - status = (_reachabilityFlags & kSCNetworkReachabilityFlagsConnectionRequired) ? RKReachabilityNotReachable : RKReachabilityReachableViaWiFi; - } + // <-- reachable AND is NOT wireless wide-area network (iOS only) + if ((_reachabilityFlags & kSCNetworkReachabilityFlagsConnectionOnTraffic) || (_reachabilityFlags & kSCNetworkReachabilityFlagsConnectionOnDemand)) { + // <-- reachable, on-traffic OR on-demand connection + if ((_reachabilityFlags & kSCNetworkReachabilityFlagsInterventionRequired)) { + // <-- reachable, on-traffic OR on-demand connection, intervention required + status = (_reachabilityFlags & kSCNetworkReachabilityFlagsConnectionRequired) ? RKReachabilityNotReachable : RKReachabilityReachableViaWiFi; + } else { + // <-- reachable, on-traffic OR on-demand connection, intervention NOT required + status = RKReachabilityReachableViaWiFi; + } + } else { + // <-- reachable, NOT on-traffic OR on-demand connection + status = (_reachabilityFlags & kSCNetworkReachabilityFlagsConnectionRequired) ? RKReachabilityNotReachable : RKReachabilityReachableViaWiFi; + } #if TARGET_OS_IPHONE - } + } #endif - } else { - // <-- NOT reachable - status = RKReachabilityNotReachable; - } - } - + } else { + // <-- NOT reachable + status = RKReachabilityNotReachable; + } + } + RKLogTrace(@"Reachability observer %@ determined networkStatus = %@", self, [self stringFromNetworkStatus:status]); - return status; + return status; } #pragma Reachability Flag Introspection @@ -295,15 +295,15 @@ - (BOOL)isNetworkReachable { [self validateIntrospection]; BOOL reachable = (RKReachabilityNotReachable != [self networkStatus]); RKLogDebug(@"Reachability observer %@ determined isNetworkReachable = %d", self, reachable); - return reachable; + return reachable; } - (BOOL)isConnectionRequired { - [self validateIntrospection]; + [self validateIntrospection]; BOOL required = (_reachabilityFlags & kSCNetworkReachabilityFlagsConnectionRequired); - + RKLogDebug(@"Reachability observer %@ determined isConnectionRequired = %d", self, required); - return required; + return required; } - (BOOL)isReachableViaWWAN { @@ -319,9 +319,9 @@ - (BOOL)isReachableViaWiFi { - (BOOL)isConnectionOnDemand { [self validateIntrospection]; return ((_reachabilityFlags & kSCNetworkReachabilityFlagsConnectionRequired) && - (_reachabilityFlags & (kSCNetworkReachabilityFlagsConnectionOnTraffic | + (_reachabilityFlags & (kSCNetworkReachabilityFlagsConnectionOnTraffic | kSCNetworkReachabilityFlagsConnectionOnDemand))); - + } - (BOOL)isInterventionRequired { @@ -335,11 +335,11 @@ - (BOOL)isInterventionRequired { - (void)scheduleObserver { SCNetworkReachabilityContext context = { .info = self }; RKLogDebug(@"Scheduling reachability observer %@ in main dispatch queue", self); - if (! SCNetworkReachabilitySetCallback(_reachabilityRef, ReachabilityCallback, &context)) { + if (! SCNetworkReachabilitySetCallback(_reachabilityRef, ReachabilityCallback, &context)) { RKLogWarning(@"%@: SCNetworkReachabilitySetCallback() failed: %s", self, SCErrorString(SCError())); return; } - + if (! SCNetworkReachabilitySetDispatchQueue(_reachabilityRef, dispatch_get_main_queue())) { RKLogWarning("%@: SCNetworkReachabilitySetDispatchQueue() failed: %s", self, SCErrorString(SCError())); return; @@ -347,36 +347,36 @@ - (void)scheduleObserver { } - (void)unscheduleObserver { - if (_reachabilityRef) { + if (_reachabilityRef) { RKLogDebug(@"%@: Unscheduling reachability observer from main dispatch queue", self); if (! SCNetworkReachabilitySetDispatchQueue(_reachabilityRef, NULL)) { - RKLogWarning("%@: SCNetworkReachabilitySetDispatchQueue() failed: %s\n", self, SCErrorString(SCError())); - return; - } - } else { + RKLogWarning("%@: SCNetworkReachabilitySetDispatchQueue() failed: %s\n", self, SCErrorString(SCError())); + return; + } + } else { RKLogDebug(@"%@: Failed to unschedule reachability observer %@: reachability reference is nil.", self, _reachabilityRef); } } - + - (void)setReachabilityFlags:(SCNetworkReachabilityFlags)reachabilityFlags { // Save the reachability flags _reachabilityFlags = reachabilityFlags; - + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithUnsignedInt:reachabilityFlags] forKey:RKReachabilityFlagsUserInfoKey]; - + if (! self.reachabilityDetermined) { _reachabilityDetermined = YES; - RKLogInfo(@"Network availability has been determined for reachability observer %@", self); + RKLogInfo(@"Network availability has been determined for reachability observer %@", self); [[NSNotificationCenter defaultCenter] postNotificationName:RKReachabilityWasDeterminedNotification object:self userInfo:userInfo]; } - + // Post a notification to notify the client that the network reachability changed. [[NSNotificationCenter defaultCenter] postNotificationName:RKReachabilityDidChangeNotification object:self userInfo:userInfo]; } - (NSString *)description { - return [NSString stringWithFormat:@"<%@: %p host=%@ isReachabilityDetermined=%@ isMonitoringLocalWiFi=%d reachabilityFlags=%@>", - NSStringFromClass([self class]), self, self.host, self.isReachabilityDetermined ? @"YES" : @"NO", + return [NSString stringWithFormat:@"<%@: %p host=%@ isReachabilityDetermined=%@ isMonitoringLocalWiFi=%d reachabilityFlags=%@>", + NSStringFromClass([self class]), self, self.host, self.isReachabilityDetermined ? @"YES" : @"NO", self.isMonitoringLocalWiFi ? @"YES" : @"NO", [self reachabilityFlagsDescription]]; } diff --git a/Code/Network/RKRequest.h b/Code/Network/RKRequest.h index 07008e51fc..b81ac5d8a2 100644 --- a/Code/Network/RKRequest.h +++ b/Code/Network/RKRequest.h @@ -4,13 +4,13 @@ // // Created by Jeremy Ellison on 7/27/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -82,7 +82,7 @@ typedef enum { #if TARGET_OS_IPHONE /** Background Request Policy - + On iOS 4.x and higher, UIKit provides support for continuing activities for a limited amount of time in the background. RestKit provides simple support for continuing a request when in the background. @@ -110,7 +110,7 @@ typedef enum RKRequestBackgroundPolicy { /** Authentication type for the request - + Based on the authentication type that is selected, authentication functionality is triggered and other options may be required. */ @@ -122,10 +122,10 @@ typedef enum { /** Use NSURLConnection's HTTP AUTH auto-negotiation */ - RKRequestAuthenticationTypeHTTP, + RKRequestAuthenticationTypeHTTP, /** Force the use of HTTP Basic authentication. - + This will supress AUTH challenges as RestKit will add an Authorization header establishing login via HTTP basic. This is an optimization that skips the challenge portion of the request. @@ -133,14 +133,14 @@ typedef enum { RKRequestAuthenticationTypeHTTPBasic, /** Enable the use of OAuth 1.0 authentication. - + OAuth1ConsumerKey, OAuth1ConsumerSecret, OAuth1AccessToken, and OAuth1AccessTokenSecret must be set when using this type. */ RKRequestAuthenticationTypeOAuth1, /** Enable the use of OAuth 2.0 authentication. - + OAuth2AccessToken must be set when using this type. */ RKRequestAuthenticationTypeOAuth2 @@ -159,47 +159,20 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); Models the request portion of an HTTP request/response cycle. */ @interface RKRequest : NSObject { - NSURL *_URL; - NSMutableURLRequest *_URLRequest; - NSURLConnection *_connection; - NSDictionary *_additionalHTTPHeaders; - NSObject *_params; - NSObject *_delegate; - NSObject *_configurationDelegate; - id _userData; - RKRequestAuthenticationType _authenticationType; - NSString *_username; - NSString *_password; - NSString *_OAuth1ConsumerKey; - NSString *_OAuth1ConsumerSecret; - NSString *_OAuth1AccessToken; - NSString *_OAuth1AccessTokenSecret; - NSString *_OAuth2AccessToken; - NSString *_OAuth2RefreshToken; - RKRequestMethod _method; - BOOL _isLoading; - BOOL _isLoaded; - RKRequestCachePolicy _cachePolicy; BOOL _sentSynchronously; - RKRequestCache *_cache; - NSTimeInterval _cacheTimeoutInterval; - RKRequestQueue *_queue; - RKReachabilityObserver *_reachabilityObserver; + NSURLConnection *_connection; + id _delegate; NSTimer *_timeoutTimer; - NSStringEncoding _defaultHTTPEncoding; - - NSSet *_additionalRootCertificates; - BOOL _disableCertificateValidation; - - #if TARGET_OS_IPHONE - RKRequestBackgroundPolicy _backgroundPolicy; - UIBackgroundTaskIdentifier _backgroundTaskIdentifier; - #endif + RKRequestCachePolicy _cachePolicy; RKRequestDidLoadResponseBlock _onDidLoadResponse; RKRequestDidFailLoadWithErrorBlock _onDidFailLoadWithError; -} +#if TARGET_OS_IPHONE + RKRequestBackgroundPolicy _backgroundPolicy; + UIBackgroundTaskIdentifier _backgroundTaskIdentifier; +#endif +} ///----------------------------------------------------------------------------- /// @name Creating a Request @@ -208,7 +181,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Creates and returns a RKRequest object initialized to load content from a provided URL. - + @param URL The remote URL to load @return An autoreleased RKRequest object initialized with URL. */ @@ -216,7 +189,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Initializes a RKRequest object to load from a provided URL - + @param URL The remote URL to load @return An RKRequest object initialized with URL. */ @@ -225,8 +198,8 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Creates and returns a RKRequest object initialized to load content from a provided URL with a specified delegate. - - @bug **DEPRECATED** in v0.9.4: Use [RKRequest requestWithURL:] instead + + @bug **DEPRECATED** in v0.10.0: Use [RKRequest requestWithURL:] instead @param URL The remote URL to load @param delegate The delegate that will handle the response callbacks. @return An autoreleased RKRequest object initialized with URL. @@ -236,7 +209,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Initializes a RKRequest object to load from a provided URL - @bug **DEPRECATED** in v0.9.4: Use [RKRequest initWithURL:] instead + @bug **DEPRECATED** in v0.10.0: Use [RKRequest initWithURL:] instead @param URL The remote URL to load @param delegate The delegate that will handle the response callbacks. @return An RKRequest object initialized with URL. @@ -260,20 +233,25 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** The HTTP verb in which the request is sent - + **Default**: RKRequestMethodGET */ @property (nonatomic, assign) RKRequestMethod method; /** Returns HTTP method as a string used for this request. - + This should be set through the method property using an RKRequestMethod type. - + @see [RKRequest method] */ @property (nonatomic, readonly) NSString *HTTPMethod; +/** + The response returned when the receiver was sent. + */ +@property (nonatomic, retain, readonly) RKResponse *response; + /** A serializable collection of parameters sent as the HTTP body of the request */ @@ -285,7 +263,14 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); @property (nonatomic, retain) NSDictionary *additionalHTTPHeaders; /** - An opaque pointer to associate user defined data with the request. + The run loop mode under which the underlying NSURLConnection is performed + + *Default*: NSRunLoopCommonModes + */ +@property (nonatomic, copy) NSString *runLoopMode; + +/** + * An opaque pointer to associate user defined data with the request. */ @property (nonatomic, retain) id userData; @@ -308,7 +293,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); Sets the request body using the provided NSDictionary after passing the NSDictionary through serialization using the currently configured parser for the provided MIMEType. - + @param body An NSDictionary of key/value pairs to be serialized and sent as the HTTP body. @param MIMEType The MIMEType for the parser to use for the dictionary. @@ -317,7 +302,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** The HTTP body as a NSData used for this request - */ + */ @property (nonatomic, retain) NSData *HTTPBody; /** @@ -332,7 +317,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** The delegate to inform when the request is completed - + If the object implements the RKRequestDelegate protocol, it will receive request lifecycle event messages. */ @@ -342,12 +327,12 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); A delegate responsible for configuring the request. Centralizes common configuration data (such as HTTP headers, authentication information, etc) for re-use. - + RKClient and RKObjectManager conform to the RKConfigurationDelegate protocol. Request and object loader instances built through these objects will have a reference to their parent client/object manager assigned as the configuration delegate. - + **Default**: nil @see RKClient @see RKObjectManager @@ -361,18 +346,24 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** A block to invoke when the receiver has loaded a response. - + @see [RKRequestDelegate request:didLoadResponse:] */ @property (nonatomic, copy) RKRequestDidLoadResponseBlock onDidLoadResponse; /** A block to invoke when the receuver has failed loading due to an error. - + @see [RKRequestDelegate request:didFailLoadWithError:] */ @property (nonatomic, copy) RKRequestDidFailLoadWithErrorBlock onDidFailLoadWithError; +/** + Whether this request should follow server redirects or not. + + @default YES + */ +@property (nonatomic, assign) BOOL followRedirect; #if TARGET_OS_IPHONE ///----------------------------------------------------------------------------- @@ -381,7 +372,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** The policy to take on transition to the background (iOS 4.x and higher only) - + **Default:** RKRequestBackgroundPolicyCancel */ @property (nonatomic, assign) RKRequestBackgroundPolicy backgroundPolicy; @@ -399,9 +390,9 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** The type of authentication to use for this request. - + This must be assigned one of the following: - + - `RKRequestAuthenticationTypeNone`: Disable the use of authentication - `RKRequestAuthenticationTypeHTTP`: Use NSURLConnection's HTTP AUTH auto-negotiation @@ -414,27 +405,27 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); and OAuth1AccessTokenSecret must be set. - `RKRequestAuthenticationTypeOAuth2`: Enable the use of OAuth 2.0 authentication. OAuth2AccessToken must be set. - + **Default**: RKRequestAuthenticationTypeNone */ @property (nonatomic, assign) RKRequestAuthenticationType authenticationType; /** The username to use for authentication via HTTP AUTH. - + Used to respond to an authentication challenge when authenticationType is RKRequestAuthenticationTypeHTTP or RKRequestAuthenticationTypeHTTPBasic. - + @see authenticationType */ @property (nonatomic, retain) NSString *username; /** The password to use for authentication via HTTP AUTH. - + Used to respond to an authentication challenge when authenticationType is RKRequestAuthenticationTypeHTTP or RKRequestAuthenticationTypeHTTPBasic. - + @see authenticationType */ @property (nonatomic, retain) NSString *password; @@ -446,40 +437,40 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** The OAuth 1.0 consumer key - + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth1 - + @see authenticationType */ @property (nonatomic, retain) NSString *OAuth1ConsumerKey; /** The OAuth 1.0 consumer secret - + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth1 - + @see authenticationType */ @property (nonatomic, retain) NSString *OAuth1ConsumerSecret; /** The OAuth 1.0 access token - + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth1 - + @see authenticationType */ @property (nonatomic, retain) NSString *OAuth1AccessToken; /** The OAuth 1.0 access token secret - + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth1 - + @see authenticationType */ @property (nonatomic, retain) NSString *OAuth1AccessTokenSecret; @@ -491,10 +482,10 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** The OAuth 2.0 access token - + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth2 - + @see authenticationType */ @property (nonatomic, retain) NSString *OAuth2AccessToken; @@ -502,13 +493,13 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** The OAuth 2.0 refresh token - + Used to retrieve a new access token before expiration and to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth2 - + @bug **NOT IMPLEMENTED**: This functionality is not yet implemented. - + @see authenticationType */ @property (nonatomic, retain) NSString *OAuth2RefreshToken; @@ -521,7 +512,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Returns the cache key for getting/setting the cache entry for this request in the cache. - + The cacheKey is an MD5 value computed by hashing a combination of the destination URL, the HTTP verb, and the request body (when possible). */ @@ -534,11 +525,11 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** The request cache to store and load responses for this request. - + Generally configured by the RKClient instance that minted this request - + This must be assigned one of the following: - + - `RKRequestCachePolicyNone`: Never use the cache. - `RKRequestCachePolicyLoadIfOffline`: Load from the cache when offline. - `RKRequestCachePolicyLoadOnError`: Load from the cache if an error is @@ -554,7 +545,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Returns YES if the request is cacheable - + Only GET requests are considered cacheable (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html). */ - (BOOL)isCacheable; @@ -573,11 +564,11 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Flag for disabling SSL certificate validation. - + When YES, SSL certificates will not be validated. - + *Default*: NO - + @warning **WARNING**: This is a potential security exposure and should be used **ONLY while debugging** in a controlled environment. */ @@ -595,9 +586,9 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); ///----------------------------------------------------------------------------- /** Setup the NSURLRequest. - + The request must be prepared right before dispatching. - + @return A boolean for the success of the URL preparation. */ - (BOOL)prepareURLRequest; @@ -620,7 +611,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Send the request synchronously and return a hydrated response object. - + @return An RKResponse object with the result of the request. */ - (RKResponse *)sendSynchronously; @@ -634,12 +625,12 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Cancels the underlying URL connection. - + This will call the requestDidCancel: delegate method if your delegate responds to it. This does not subsequently set the the request's delegate to nil. However, it's good practice to cancel the RKRequest and immediately set the delegate property to nil within the delegate's dealloc method. - + @see NSURLConnection:cancel */ - (void)cancel; @@ -647,10 +638,10 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** The reachability observer to consult for network status. Used for performing offline cache loads. - + Generally configured by the RKClient instance that minted this request. */ -@property (nonatomic, assign) RKReachabilityObserver *reachabilityObserver; +@property (nonatomic, retain) RKReachabilityObserver *reachabilityObserver; ///----------------------------------------------------------------------------- @@ -670,7 +661,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Callback performed to notify the request that the underlying NSURLConnection has failed with an error. - + @param error An NSError object containing the RKRestKitError that triggered the callback. */ @@ -679,7 +670,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Callback performed to notify the request that the underlying NSURLConnection has completed with a response. - + @param response An RKResponse object with the result of the request. */ - (void)didFinishLoad:(RKResponse *)response; @@ -692,24 +683,24 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** The timeout interval within which the request should be cancelled if no data has been received. - + The timeout timer is cancelled as soon as we start receiving data and are expecting the request to finish. - + **Default**: 120.0 seconds */ @property (nonatomic, assign) NSTimeInterval timeoutInterval; /** Creates a timeoutTimer to trigger the timeout method - + This is mainly used so we can test that the timer is only being created once. */ - (void)createTimeoutTimer; /** Cancels request due to connection timeout exceeded. - + This method is invoked by the timeoutTimer upon its expiration and will return an RKRequestConnectionTimeoutError via didFailLoadWithError: */ @@ -717,7 +708,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Invalidates the timeout timer. - + Called by RKResponse when the NSURLConnection begins receiving data. */ - (void)invalidateTimeoutTimer; @@ -755,12 +746,12 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Returns YES when this request is in-progress */ -- (BOOL)isLoading; +@property (nonatomic, assign, readonly, getter = isLoading) BOOL loading; /** Returns YES when this request has been completed */ -- (BOOL)isLoaded; +@property (nonatomic, assign, readonly, getter = isLoaded) BOOL loaded; /** Returns YES when this request has not yet been sent @@ -769,14 +760,14 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Returns YES when the request was sent to the specified resource path - + @param resourcePath A string of the resource path that we want to check against */ - (BOOL)wasSentToResourcePath:(NSString *)resourcePath; /** Returns YES when the receiver was sent to the specified resource path with a given request method. - + @param resourcePath A string of the resource path that we want to check against @param method The HTTP method to confirm the request was sent with. */ @@ -797,21 +788,29 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Tells the delegate the request is about to be prepared for sending to the remote host. - + @param request The RKRequest object that is about to be sent. */ - (void)requestWillPrepareForSend:(RKRequest *)request; +/** + Sent when a request has received a response from the remote host. + + @param request The RKRequest object that received a response. + @param response The RKResponse object for the HTTP response that was received. + */ +- (void)request:(RKRequest *)request didReceiveResponse:(RKResponse *)response; + /** Sent when a request has started loading - + @param request The RKRequest object that has begun loading. */ - (void)requestDidStartLoad:(RKRequest *)request; /** Sent when a request has uploaded data to the remote site - + @param request The RKRequest object that is handling the loading. @param bytesWritten An integer of the bytes of the chunk just sent to the remote site. @@ -824,7 +823,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Sent when request has received data from remote site - + @param request The RKRequest object that is handling the loading. @param bytesReceived An integer of the bytes of the chunk just received from the remote site. @@ -842,7 +841,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Sent when a request has finished loading - + @param request The RKRequest object that was handling the loading. @param response The RKResponse object containing the result of the request. */ @@ -855,7 +854,7 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Sent when a request has failed due to an error - + @param request The RKRequest object that was handling the loading. @param error An NSError object containing the RKRestKitError that triggered the callback. @@ -864,15 +863,15 @@ typedef void(^RKRequestDidFailLoadWithErrorBlock)(NSError *error); /** Sent to the delegate when a request was cancelled - + @param request The RKRequest object that was cancelled. */ - (void)requestDidCancelLoad:(RKRequest *)request; /** - Sent to the delegate when a request has timed out. This is sent when a + Sent to the delegate when a request has timed out. This is sent when a backgrounded request expired before completion. - + @param request The RKRequest object that timed out. */ - (void)requestDidTimeout:(RKRequest *)request; diff --git a/Code/Network/RKRequest.m b/Code/Network/RKRequest.m index 8be2631a4f..3cd1089647 100644 --- a/Code/Network/RKRequest.m +++ b/Code/Network/RKRequest.m @@ -4,13 +4,13 @@ // // Created by Jeremy Ellison on 7/27/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -85,6 +85,13 @@ RKRequestMethod RKRequestMethodTypeFromName(NSString *methodName) { #undef RKLogComponent #define RKLogComponent lcl_cRestKitNetwork +@interface RKRequest () +@property (nonatomic, assign, readwrite, getter = isLoaded) BOOL loaded; +@property (nonatomic, assign, readwrite, getter = isLoading) BOOL loading; +@property (nonatomic, assign, readwrite, getter = isCancelled) BOOL cancelled; +@property (nonatomic, retain, readwrite) RKResponse *response; +@end + @implementation RKRequest @class GCOAuth; @@ -116,33 +123,41 @@ @implementation RKRequest @synthesize onDidFailLoadWithError; @synthesize additionalRootCertificates = _additionalRootCertificates; @synthesize disableCertificateValidation = _disableCertificateValidation; +@synthesize followRedirect = _followRedirect; +@synthesize runLoopMode = _runLoopMode; +@synthesize loaded = _loaded; +@synthesize loading = _loading; +@synthesize response = _response; @synthesize cancelled = _cancelled; #if TARGET_OS_IPHONE -@synthesize backgroundPolicy = _backgroundPolicy, backgroundTaskIdentifier = _backgroundTaskIdentifier; +@synthesize backgroundPolicy = _backgroundPolicy; +@synthesize backgroundTaskIdentifier = _backgroundTaskIdentifier; #endif + (RKRequest*)requestWithURL:(NSURL*)URL { - return [[[RKRequest alloc] initWithURL:URL] autorelease]; + return [[[RKRequest alloc] initWithURL:URL] autorelease]; } - (id)initWithURL:(NSURL*)URL { self = [self init]; - if (self) { - _URL = [URL retain]; + if (self) { + _URL = [URL retain]; [self reset]; _authenticationType = RKRequestAuthenticationTypeNone; - _cachePolicy = RKRequestCachePolicyDefault; + _cachePolicy = RKRequestCachePolicyDefault; _cacheTimeoutInterval = 0; _timeoutInterval = 120.0; _defaultHTTPEncoding = NSUTF8StringEncoding; - } - return self; + _followRedirect = YES; + } + return self; } - (id)init { self = [super init]; if (self) { + self.runLoopMode = NSRunLoopCommonModes; #if TARGET_OS_IPHONE _backgroundPolicy = RKRequestBackgroundPolicyNone; _backgroundTaskIdentifier = 0; @@ -157,7 +172,7 @@ - (id)init { } - (void)reset { - if (_isLoading) { + if (self.isLoading) { RKLogWarning(@"Request was reset while loading: %@. Canceling.", self); [self cancel]; } @@ -166,9 +181,9 @@ - (void)reset { [_URLRequest setCachePolicy:NSURLRequestReloadIgnoringCacheData]; [_connection release]; _connection = nil; - _isLoading = NO; - _isLoaded = NO; - _cancelled = NO; + self.loading = NO; + self.loaded = NO; + self.cancelled = NO; } - (void)cleanupBackgroundTask { @@ -180,8 +195,8 @@ - (void)cleanupBackgroundTask { UIApplication* app = [UIApplication sharedApplication]; if ([app respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)]) { - [app endBackgroundTask:_backgroundTaskIdentifier]; - _backgroundTaskIdentifier = UIBackgroundTaskInvalid; + [app endBackgroundTask:_backgroundTaskIdentifier]; + _backgroundTaskIdentifier = UIBackgroundTaskInvalid; } #endif } @@ -189,31 +204,35 @@ - (void)cleanupBackgroundTask { - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; - self.delegate = nil; + self.delegate = nil; if (_onDidLoadResponse) Block_release(_onDidLoadResponse); if (_onDidFailLoadWithError) Block_release(_onDidFailLoadWithError); - - _delegate = nil; + + _delegate = nil; _configurationDelegate = nil; - [_connection cancel]; - [_connection release]; - _connection = nil; - [_userData release]; - _userData = nil; - [_URL release]; - _URL = nil; - [_URLRequest release]; - _URLRequest = nil; - [_params release]; - _params = nil; - [_additionalHTTPHeaders release]; - _additionalHTTPHeaders = nil; - [_username release]; - _username = nil; - [_password release]; - _password = nil; + [_reachabilityObserver release]; + _reachabilityObserver = nil; + [_connection cancel]; + [_connection release]; + _connection = nil; + [_response release]; + _response = nil; + [_userData release]; + _userData = nil; + [_URL release]; + _URL = nil; + [_URLRequest release]; + _URLRequest = nil; + [_params release]; + _params = nil; + [_additionalHTTPHeaders release]; + _additionalHTTPHeaders = nil; + [_username release]; + _username = nil; + [_password release]; + _password = nil; [_cache release]; - _cache = nil; + _cache = nil; [_OAuth1ConsumerKey release]; _OAuth1ConsumerKey = nil; [_OAuth1ConsumerSecret release]; @@ -233,7 +252,9 @@ - (void)dealloc { [self invalidateTimeoutTimer]; [_timeoutTimer release]; _timeoutTimer = nil; - + [_runLoopMode release]; + _runLoopMode = nil; + // Cleanup a background task if there is any [self cleanupBackgroundTask]; @@ -245,16 +266,16 @@ - (BOOL)shouldSendParams { } - (void)setRequestBody { - if ([self shouldSendParams]) { - // Prefer the use of a stream over a raw body - if ([_params respondsToSelector:@selector(HTTPBodyStream)]) { + if ([self shouldSendParams]) { + // Prefer the use of a stream over a raw body + if ([_params respondsToSelector:@selector(HTTPBodyStream)]) { // NOTE: This causes the stream to be retained. For RKParams, this will // cause a leak unless the stream is released. See [RKParams close] - [_URLRequest setHTTPBodyStream:[_params HTTPBodyStream]]; - } else { - [_URLRequest setHTTPBody:[_params HTTPBody]]; - } - } + [_URLRequest setHTTPBodyStream:[_params HTTPBodyStream]]; + } else { + [_URLRequest setHTTPBody:[_params HTTPBody]]; + } + } } - (NSData*)HTTPBody { @@ -274,25 +295,25 @@ - (void)setHTTPBodyString:(NSString *)HTTPBodyString { } - (void)addHeadersToRequest { - NSString *header = nil; - for (header in _additionalHTTPHeaders) { - [_URLRequest setValue:[_additionalHTTPHeaders valueForKey:header] forHTTPHeaderField:header]; - } - - if ([self shouldSendParams]) { - // Temporarily support older RKRequestSerializable implementations - if ([_params respondsToSelector:@selector(HTTPHeaderValueForContentType)]) { - [_URLRequest setValue:[_params HTTPHeaderValueForContentType] forHTTPHeaderField:@"Content-Type"]; - } else if ([_params respondsToSelector:@selector(ContentTypeHTTPHeader)]) { - [_URLRequest setValue:[_params performSelector:@selector(ContentTypeHTTPHeader)] forHTTPHeaderField:@"Content-Type"]; - } - if ([_params respondsToSelector:@selector(HTTPHeaderValueForContentLength)]) { - [_URLRequest setValue:[NSString stringWithFormat:@"%d", [_params HTTPHeaderValueForContentLength]] forHTTPHeaderField:@"Content-Length"]; - } - } else { + NSString *header = nil; + for (header in _additionalHTTPHeaders) { + [_URLRequest setValue:[_additionalHTTPHeaders valueForKey:header] forHTTPHeaderField:header]; + } + + if ([self shouldSendParams]) { + // Temporarily support older RKRequestSerializable implementations + if ([_params respondsToSelector:@selector(HTTPHeaderValueForContentType)]) { + [_URLRequest setValue:[_params HTTPHeaderValueForContentType] forHTTPHeaderField:@"Content-Type"]; + } else if ([_params respondsToSelector:@selector(ContentTypeHTTPHeader)]) { + [_URLRequest setValue:[_params performSelector:@selector(ContentTypeHTTPHeader)] forHTTPHeaderField:@"Content-Type"]; + } + if ([_params respondsToSelector:@selector(HTTPHeaderValueForContentLength)]) { + [_URLRequest setValue:[NSString stringWithFormat:@"%d", [_params HTTPHeaderValueForContentLength]] forHTTPHeaderField:@"Content-Length"]; + } + } else { [_URLRequest setValue:@"0" forHTTPHeaderField:@"Content-Length"]; } - + // Add authentication headers so we don't have to deal with an extra cycle for each message requiring basic auth. if (self.authenticationType == RKRequestAuthenticationTypeHTTPBasic && _username && _password) { CFHTTPMessageRef dummyRequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, (CFStringRef)[self HTTPMethod], (CFURLRef)[self URL], kCFHTTPVersion1_1); @@ -306,24 +327,24 @@ - (void)addHeadersToRequest { CFRelease(dummyRequest); } } - - // Add OAuth headers if is need it + + // Add OAuth headers if necessary // OAuth 1 - if(self.authenticationType == RKRequestAuthenticationTypeOAuth1){ + if(self.authenticationType == RKRequestAuthenticationTypeOAuth1){ NSURLRequest *echo = nil; - + // use the suitable parameters dict NSDictionary *parameters = nil; if ([self.params isKindOfClass:[RKParams class]]) parameters = [(RKParams *)self.params dictionaryOfPlainTextParams]; - else + else parameters = [_URL queryParameters]; - + if (self.method == RKRequestMethodPUT) echo = [GCOAuth URLRequestForPath:[_URL path] PUTParameters:parameters scheme:[_URL scheme] - host:[_URL host] + host:[_URL hostAndPort] consumerKey:self.OAuth1ConsumerKey consumerSecret:self.OAuth1ConsumerSecret accessToken:self.OAuth1AccessToken @@ -332,7 +353,7 @@ - (void)addHeadersToRequest { echo = [GCOAuth URLRequestForPath:[_URL path] POSTParameters:parameters scheme:[_URL scheme] - host:[_URL host] + host:[_URL hostAndPort] consumerKey:self.OAuth1ConsumerKey consumerSecret:self.OAuth1ConsumerSecret accessToken:self.OAuth1AccessToken @@ -341,7 +362,7 @@ - (void)addHeadersToRequest { echo = [GCOAuth URLRequestForPath:[_URL path] GETParameters:[_URL queryParameters] scheme:[_URL scheme] - host:[_URL host] + host:[_URL hostAndPort] consumerKey:self.OAuth1ConsumerKey consumerSecret:self.OAuth1ConsumerSecret accessToken:self.OAuth1AccessToken @@ -350,13 +371,13 @@ - (void)addHeadersToRequest { [_URLRequest setValue:[echo valueForHTTPHeaderField:@"Accept-Encoding"] forHTTPHeaderField:@"Accept-Encoding"]; [_URLRequest setValue:[echo valueForHTTPHeaderField:@"User-Agent"] forHTTPHeaderField:@"User-Agent"]; } - + // OAuth 2 valid request if(self.authenticationType == RKRequestAuthenticationTypeOAuth2) { NSString *authorizationString = [NSString stringWithFormat:@"OAuth2 %@",self.OAuth2AccessToken]; [_URLRequest setValue:authorizationString forHTTPHeaderField:@"Authorization"]; } - + if (self.cachePolicy & RKRequestCachePolicyEtag) { NSString* etag = [self.cache etagForRequest:self]; if (etag) { @@ -368,62 +389,42 @@ - (void)addHeadersToRequest { // Setup the NSURLRequest. The request must be prepared right before dispatching - (BOOL)prepareURLRequest { - [_URLRequest setHTTPMethod:[self HTTPMethod]]; - + [_URLRequest setHTTPMethod:[self HTTPMethod]]; + if ([self.delegate respondsToSelector:@selector(requestWillPrepareForSend:)]) { [self.delegate requestWillPrepareForSend:self]; } - - [self setRequestBody]; - [self addHeadersToRequest]; + + [self setRequestBody]; + [self addHeadersToRequest]; NSString* body = [[NSString alloc] initWithData:[_URLRequest HTTPBody] encoding:NSUTF8StringEncoding]; RKLogTrace(@"Prepared %@ URLRequest '%@'. HTTP Headers: %@. HTTP Body: %@.", [self HTTPMethod], _URLRequest, [_URLRequest allHTTPHeaderFields], body); - [body release]; + [body release]; return YES; } - (void)cancelAndInformDelegate:(BOOL)informDelegate { - _cancelled = YES; - [_connection cancel]; - [_connection release]; - _connection = nil; + self.cancelled = YES; + [_connection cancel]; + [_connection release]; + _connection = nil; [self invalidateTimeoutTimer]; - _isLoading = NO; - - if (informDelegate && [_delegate respondsToSelector:@selector(requestDidCancelLoad:)]) { - [_delegate requestDidCancelLoad:self]; - } -} - -- (NSString*)HTTPMethod { - switch (_method) { - case RKRequestMethodGET: - return @"GET"; - break; - case RKRequestMethodPOST: - return @"POST"; - break; - case RKRequestMethodPUT: - return @"PUT"; - break; - case RKRequestMethodDELETE: - return @"DELETE"; - break; - case RKRequestMethodHEAD: - return @"HEAD"; - break; - default: - return nil; - break; - } + self.loading = NO; + + if (informDelegate && [_delegate respondsToSelector:@selector(requestDidCancelLoad:)]) { + [_delegate requestDidCancelLoad:self]; + } } -// TODO: We may want to eliminate the coupling between the request queue and individual request instances. -// We could factor the knowledge about the queue out of RKRequest entirely, but it will break behavior. +- (NSString *)HTTPMethod { + return RKRequestMethodNameFromType(self.method); +} + +// NOTE: We could factor the knowledge about the queue out of RKRequest entirely, but it will break behavior. - (void)send { - NSAssert(NO == _isLoading || NO == _isLoaded, @"Cannot send a request that is loading or loaded without resetting it first."); + NSAssert(NO == self.isLoading || NO == self.isLoaded, @"Cannot send a request that is loading or loaded without resetting it first."); if (self.queue) { [self.queue addRequest:self]; } else { @@ -434,19 +435,21 @@ - (void)send { - (void)fireAsynchronousRequest { RKLogDebug(@"Sending asynchronous %@ request to URL %@.", [self HTTPMethod], [[self URL] absoluteString]); if (![self prepareURLRequest]) { - // TODO: Logging + RKLogWarning(@"Failed to send request asynchronously: prepareURLRequest returned NO."); return; } - _isLoading = YES; + self.loading = YES; if ([self.delegate respondsToSelector:@selector(requestDidStartLoad:)]) { [self.delegate requestDidStartLoad:self]; } RKResponse* response = [[[RKResponse alloc] initWithRequest:self] autorelease]; - - _connection = [[NSURLConnection connectionWithRequest:_URLRequest delegate:response] retain]; + + _connection = [[[[NSURLConnection alloc] initWithRequest:_URLRequest delegate:response startImmediately:NO] autorelease] retain]; + [_connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:self.runLoopMode]; + [_connection start]; [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestSentNotification object:self userInfo:nil]; } @@ -474,16 +477,16 @@ - (BOOL)shouldDispatchRequest { if (nil == self.reachabilityObserver || NO == [self.reachabilityObserver isReachabilityDetermined]) { return YES; } - + return [self.reachabilityObserver isNetworkReachable]; } - (void)sendAsynchronously { - NSAssert(NO == _isLoading || NO == _isLoaded, @"Cannot send a request that is loading or loaded without resetting it first."); + NSAssert(NO == self.loading || NO == self.loaded, @"Cannot send a request that is loading or loaded without resetting it first."); _sentSynchronously = NO; if ([self shouldLoadFromCache]) { RKResponse* response = [self loadResponseFromCache]; - _isLoading = YES; + self.loading = YES; [self performSelector:@selector(didFinishLoad:) withObject:response afterDelay:0]; } else if ([self shouldDispatchRequest]) { [self createTimeoutTimer]; @@ -524,59 +527,57 @@ - (void)sendAsynchronously { #else [self fireAsynchronousRequest]; #endif - } else { + } else { RKLogTrace(@"Declined to dispatch request %@: reachability observer reported the network is not available.", self); - if (_cachePolicy & RKRequestCachePolicyLoadIfOffline && - [self.cache hasResponseForRequest:self]) { - - _isLoading = YES; - + if (_cachePolicy & RKRequestCachePolicyLoadIfOffline && + [self.cache hasResponseForRequest:self]) { + self.loading = YES; [self didFinishLoad:[self loadResponseFromCache]]; + } else { + self.loading = YES; - } else { RKLogError(@"Failed to send request to %@ due to unreachable network. Reachability observer = %@", [[self URL] absoluteString], self.reachabilityObserver); NSString* errorMessage = [NSString stringWithFormat:@"The client is unable to contact the resource at %@", [[self URL] absoluteString]]; - NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: - errorMessage, NSLocalizedDescriptionKey, - nil]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: + errorMessage, NSLocalizedDescriptionKey, + nil]; NSError* error = [NSError errorWithDomain:RKErrorDomain code:RKRequestBaseURLOfflineError userInfo:userInfo]; - _isLoading = YES; [self performSelector:@selector(didFailLoadWithError:) withObject:error afterDelay:0]; } - } + } } - (RKResponse*)sendSynchronously { - NSAssert(NO == _isLoading || NO == _isLoaded, @"Cannot send a request that is loading or loaded without resetting it first."); - NSHTTPURLResponse* URLResponse = nil; - NSError* error; - NSData* payload = nil; - RKResponse* response = nil; + NSAssert(NO == self.loading || NO == self.loaded, @"Cannot send a request that is loading or loaded without resetting it first."); + NSHTTPURLResponse* URLResponse = nil; + NSError* error; + NSData* payload = nil; + RKResponse* response = nil; _sentSynchronously = YES; - if ([self shouldLoadFromCache]) { + if ([self shouldLoadFromCache]) { response = [self loadResponseFromCache]; - _isLoading = YES; + self.loading = YES; [self didFinishLoad:response]; } else if ([self shouldDispatchRequest]) { RKLogDebug(@"Sending synchronous %@ request to URL %@.", [self HTTPMethod], [[self URL] absoluteString]); - + if (![self prepareURLRequest]) { - // TODO: Logging + RKLogWarning(@"Failed to send request synchronously: prepareURLRequest returned NO."); return nil; } - [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestSentNotification object:self userInfo:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestSentNotification object:self userInfo:nil]; - _isLoading = YES; + self.loading = YES; if ([self.delegate respondsToSelector:@selector(requestDidStartLoad:)]) { [self.delegate requestDidStartLoad:self]; } _URLRequest.timeoutInterval = _timeoutInterval; payload = [NSURLConnection sendSynchronousRequest:_URLRequest returningResponse:&URLResponse error:&error]; - + if (payload != nil) error = nil; response = [[[RKResponse alloc] initWithSynchronousRequest:self URLResponse:URLResponse body:payload error:error] autorelease]; @@ -589,24 +590,24 @@ - (RKResponse*)sendSynchronously { [self didFinishLoad:response]; } - } else { - if (_cachePolicy & RKRequestCachePolicyLoadIfOffline && - [self.cache hasResponseForRequest:self]) { + } else { + if (_cachePolicy & RKRequestCachePolicyLoadIfOffline && + [self.cache hasResponseForRequest:self]) { - response = [self loadResponseFromCache]; + response = [self loadResponseFromCache]; - } else { - NSString* errorMessage = [NSString stringWithFormat:@"The client is unable to contact the resource at %@", [[self URL] absoluteString]]; - NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: - errorMessage, NSLocalizedDescriptionKey, - nil]; - error = [NSError errorWithDomain:RKErrorDomain code:RKRequestBaseURLOfflineError userInfo:userInfo]; - [self didFailLoadWithError:error]; - response = [[[RKResponse alloc] initWithSynchronousRequest:self URLResponse:URLResponse body:payload error:error] autorelease]; - } - } + } else { + NSString* errorMessage = [NSString stringWithFormat:@"The client is unable to contact the resource at %@", [[self URL] absoluteString]]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: + errorMessage, NSLocalizedDescriptionKey, + nil]; + error = [NSError errorWithDomain:RKErrorDomain code:RKRequestBaseURLOfflineError userInfo:userInfo]; + [self didFailLoadWithError:error]; + response = [[[RKResponse alloc] initWithSynchronousRequest:self URLResponse:URLResponse body:payload error:error] autorelease]; + } + } - return response; + return response; } - (void)cancel { @@ -634,27 +635,28 @@ - (void)invalidateTimeoutTimer { } - (void)didFailLoadWithError:(NSError*)error { - if (_cachePolicy & RKRequestCachePolicyLoadOnError && - [self.cache hasResponseForRequest:self]) { + if (_cachePolicy & RKRequestCachePolicyLoadOnError && + [self.cache hasResponseForRequest:self]) { - [self didFinishLoad:[self loadResponseFromCache]]; - } else { - _isLoading = NO; + [self didFinishLoad:[self loadResponseFromCache]]; + } else { + self.loaded = YES; + self.loading = NO; + + if ([_delegate respondsToSelector:@selector(request:didFailLoadWithError:)]) { + [_delegate request:self didFailLoadWithError:error]; + } - if ([_delegate respondsToSelector:@selector(request:didFailLoadWithError:)]) { - [_delegate request:self didFailLoadWithError:error]; - } - if (self.onDidFailLoadWithError) { self.onDidFailLoadWithError(error); } - + NSDictionary* userInfo = [NSDictionary dictionaryWithObject:error forKey:RKRequestDidFailWithErrorNotificationUserInfoErrorKey]; - [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidFailWithErrorNotification + [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidFailWithErrorNotification object:self userInfo:userInfo]; - } + } // NOTE: This notification must be posted last as the request queue releases the request when it // receives the notification @@ -667,38 +669,37 @@ - (void)updateInternalCacheDate { [self.cache setCacheDate:date forRequest:self]; } -- (void)didFinishLoad:(RKResponse*)response { - _isLoading = NO; - _isLoaded = YES; +- (void)didFinishLoad:(RKResponse *)response { + self.loading = NO; + self.loaded = YES; RKLogInfo(@"Status Code: %ld", (long) [response statusCode]); RKLogDebug(@"Body: %@", [response bodyAsString]); - RKResponse* finalResponse = response; + self.response = response; - if ((_cachePolicy & RKRequestCachePolicyEtag) && [response isNotModified]) { - finalResponse = [self loadResponseFromCache]; + if ((_cachePolicy & RKRequestCachePolicyEtag) && [response isNotModified]) { + self.response = [self loadResponseFromCache]; [self updateInternalCacheDate]; - } + } - if (![response wasLoadedFromCache] && [response isSuccessful] && (_cachePolicy != RKRequestCachePolicyNone)) { - [self.cache storeResponse:response forRequest:self]; - } + if (![response wasLoadedFromCache] && [response isSuccessful] && (_cachePolicy != RKRequestCachePolicyNone)) { + [self.cache storeResponse:response forRequest:self]; + } + + if ([_delegate respondsToSelector:@selector(request:didLoadResponse:)]) { + [_delegate request:self didLoadResponse:self.response]; + } - if ([_delegate respondsToSelector:@selector(request:didLoadResponse:)]) { - [_delegate request:self didLoadResponse:finalResponse]; - } - if (self.onDidLoadResponse) { - self.onDidLoadResponse(finalResponse); + self.onDidLoadResponse(self.response); } - if ([response isServiceUnavailable]) { [[NSNotificationCenter defaultCenter] postNotificationName:RKServiceDidBecomeUnavailableNotification object:self]; } - - NSDictionary* userInfo = [NSDictionary dictionaryWithObject:finalResponse + + NSDictionary* userInfo = [NSDictionary dictionaryWithObject:self.response forKey:RKRequestDidLoadResponseNotificationUserInfoResponseKey]; [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidLoadResponseNotification object:self @@ -710,44 +711,36 @@ - (void)didFinishLoad:(RKResponse*)response { } - (BOOL)isGET { - return _method == RKRequestMethodGET; + return _method == RKRequestMethodGET; } - (BOOL)isPOST { - return _method == RKRequestMethodPOST; + return _method == RKRequestMethodPOST; } - (BOOL)isPUT { - return _method == RKRequestMethodPUT; + return _method == RKRequestMethodPUT; } - (BOOL)isDELETE { - return _method == RKRequestMethodDELETE; + return _method == RKRequestMethodDELETE; } - (BOOL)isHEAD { - return _method == RKRequestMethodHEAD; -} - -- (BOOL)isLoading { - return _isLoading; -} - -- (BOOL)isLoaded { - return _isLoaded; + return _method == RKRequestMethodHEAD; } - (BOOL)isUnsent { - return _isLoading == NO && _isLoaded == NO; + return self.loading == NO && self.loaded == NO; } - (NSString*)resourcePath { - NSString* resourcePath = nil; - if ([self.URL isKindOfClass:[RKURL class]]) { - RKURL* url = (RKURL*)self.URL; - resourcePath = url.resourcePath; - } - return resourcePath; + NSString* resourcePath = nil; + if ([self.URL isKindOfClass:[RKURL class]]) { + RKURL* url = (RKURL*)self.URL; + resourcePath = url.resourcePath; + } + return resourcePath; } - (void)setURL:(NSURL *)URL { @@ -757,16 +750,16 @@ - (void)setURL:(NSURL *)URL { _URLRequest.URL = URL; } -- (void)setResourcePath:(NSString *)resourcePath { +- (void)setResourcePath:(NSString *)resourcePath { if ([self.URL isKindOfClass:[RKURL class]]) { self.URL = [(RKURL *)self.URL URLByReplacingResourcePath:resourcePath]; - } else { + } else { self.URL = [RKURL URLWithBaseURL:self.URL resourcePath:resourcePath]; } } - (BOOL)wasSentToResourcePath:(NSString*)resourcePath { - return [[self resourcePath] isEqualToString:resourcePath]; + return [[self resourcePath] isEqualToString:resourcePath]; } - (BOOL)wasSentToResourcePath:(NSString *)resourcePath method:(RKRequestMethod)method { @@ -794,7 +787,7 @@ - (NSString*)cacheKey { if (! [self isCacheable]) { return nil; } - + // Use [_params HTTPBody] because the URLRequest body may not have been set up yet. NSString* compositeCacheKey = nil; if (_params) { @@ -812,12 +805,12 @@ - (NSString*)cacheKey { - (void)setBody:(NSDictionary *)body forMIMEType:(NSString *)MIMEType { id parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:MIMEType]; - + NSError *error = nil; NSString* parsedValue = [parser stringFromObject:body error:&error]; - + RKLogTrace(@"parser=%@, error=%@, parsedValue=%@", parser, error, parsedValue); - + if (error == nil && parsedValue) { self.params = [RKRequestSerialization serializationWithData:[parsedValue dataUsingEncoding:NSUTF8StringEncoding] MIMEType:MIMEType]; @@ -826,15 +819,15 @@ - (void)setBody:(NSDictionary *)body forMIMEType:(NSString *)MIMEType { // Deprecations + (RKRequest*)requestWithURL:(NSURL*)URL delegate:(id)delegate { - return [[[RKRequest alloc] initWithURL:URL delegate:delegate] autorelease]; + return [[[RKRequest alloc] initWithURL:URL delegate:delegate] autorelease]; } - (id)initWithURL:(NSURL*)URL delegate:(id)delegate { self = [self initWithURL:URL]; - if (self) { - _delegate = delegate; - } - return self; + if (self) { + _delegate = delegate; + } + return self; } @end diff --git a/Code/Network/RKRequestCache.h b/Code/Network/RKRequestCache.h index 08085a34b1..b2bb937471 100644 --- a/Code/Network/RKRequestCache.h +++ b/Code/Network/RKRequestCache.h @@ -4,13 +4,13 @@ // // Created by Jeff Arena on 4/4/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -43,20 +43,30 @@ typedef enum { /** Location of session specific cache files within the Caches path. */ -NSString * const RKRequestCacheSessionCacheDirectory; +extern NSString * const RKRequestCacheSessionCacheDirectory; /** Location of permanent cache files within the Caches path. */ -NSString * const RKRequestCachePermanentCacheDirectory; +extern NSString * const RKRequestCachePermanentCacheDirectory; /** - */ -NSString * const RKRequestCacheHeadersExtension; -NSString * const RKRequestCacheDateHeaderKey; -NSString * const RKRequestCacheStatusCodeKey; -NSString * const RKRequestCacheMIMETypeKey; -NSString * const RKRequestCacheURLKey; + @constant RKRequestCache Header Keys + + Constants for accessing cache specific X-RESTKIT + headers used to store cache metadata within the cache entry. +*/ +/** The key for accessing the date the entry was cached. **/ +extern NSString * const RKRequestCacheDateHeaderKey; + +/** The key for accessing the status code of the cached request. **/ +extern NSString * const RKRequestCacheStatusCodeHeadersKey; + +/** The key for accessing the MIME Type of the cached request. **/ +extern NSString * const RKRequestCacheMIMETypeHeadersKey; + +/** The key for accessing the URL of the cached request. **/ +extern NSString * const RKRequestCacheURLHeadersKey; /** Stores and retrieves cache entries for RestKit request objects. @@ -72,7 +82,7 @@ NSString * const RKRequestCacheURLKey; /** Initializes the receiver with a cache at a given path and storage policy. - + @param cachePath The path to store cached data in. @param storagePolicy The storage policy to use for cached data. @return An initialized request cache object. @@ -90,7 +100,7 @@ NSString * const RKRequestCacheURLKey; /** Returns the cache path for the specified request. - + @param request An RKRequest object to determine the cache path. @return A string of the cache path for the specified request. */ @@ -98,7 +108,7 @@ NSString * const RKRequestCacheURLKey; /** Determine if a response exists for a request. - + @param request An RKRequest object that is looking for cached content. @return A boolean value for if a response exists in the cache. */ @@ -111,7 +121,7 @@ NSString * const RKRequestCacheURLKey; /** Store a request's response in the cache. - + @param response The response to be stored in the cache. @param request The request that retrieved the response. */ @@ -119,7 +129,7 @@ NSString * const RKRequestCacheURLKey; /** Set the cache date for a request. - + @param date The date the response for a request was cached. @param request The request to store the cache date for. */ @@ -131,7 +141,7 @@ NSString * const RKRequestCacheURLKey; /** Returns a dictionary of cached headers for a cached request. - + @param request The request to retrieve cached headers for. @return An NSDictionary of the cached headers that were stored for the specified request. @@ -140,7 +150,7 @@ NSString * const RKRequestCacheURLKey; /** Returns an ETag for a request if it is stored in the cached headers. - + @param request The request that an ETag is to be determined for. @return A string of the ETag value stored for the specified request. */ @@ -148,7 +158,7 @@ NSString * const RKRequestCacheURLKey; /** Returns the date of the cached request. - + @param request The request that needs a cache date returned. @return A date object for the cached request. */ @@ -156,7 +166,7 @@ NSString * const RKRequestCacheURLKey; /** Returns the cached response for a given request. - + @param request The request used to find the cached response. @return An RKResponse object that was cached for a given request. */ @@ -173,14 +183,14 @@ NSString * const RKRequestCacheURLKey; /** Invalidate the cache for a given request. - + @param request The request that needs its cache invalidated. */ - (void)invalidateRequest:(RKRequest *)request; /** Invalidate any caches that fall under the given storage policy. - + @param storagePolicy The RKRequestCacheStorePolicy used to determine which caches need to be invalidated. */ diff --git a/Code/Network/RKRequestCache.m b/Code/Network/RKRequestCache.m index ce18b13357..d0d376ed0e 100644 --- a/Code/Network/RKRequestCache.m +++ b/Code/Network/RKRequestCache.m @@ -4,13 +4,13 @@ // // Created by Jeff Arena on 4/4/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,9 +29,9 @@ NSString * const RKRequestCachePermanentCacheDirectory = @"PermanentStore"; NSString * const RKRequestCacheHeadersExtension = @"headers"; NSString * const RKRequestCacheDateHeaderKey = @"X-RESTKIT-CACHEDATE"; -NSString * const RKRequestCacheStatusCodeKey = @"X-RESTKIT-CACHED-RESPONSE-CODE"; -NSString * const RKRequestCacheMIMETypeKey = @"X-RESTKIT-CACHED-MIME-TYPE"; -NSString * const RKRequestCacheURLKey = @"X-RESTKIT-CACHED-URL"; +NSString * const RKRequestCacheStatusCodeHeadersKey = @"X-RESTKIT-CACHED-RESPONSE-CODE"; +NSString * const RKRequestCacheMIMETypeHeadersKey = @"X-RESTKIT-CACHED-MIME-TYPE"; +NSString * const RKRequestCacheURLHeadersKey = @"X-RESTKIT-CACHED-URL"; static NSDateFormatter* __rfc1123DateFormatter; @@ -40,39 +40,39 @@ @implementation RKRequestCache @synthesize storagePolicy = _storagePolicy; + (NSDateFormatter*)rfc1123DateFormatter { - if (__rfc1123DateFormatter == nil) { - __rfc1123DateFormatter = [[NSDateFormatter alloc] init]; - [__rfc1123DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; - [__rfc1123DateFormatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss 'GMT'"]; - } - return __rfc1123DateFormatter; + if (__rfc1123DateFormatter == nil) { + __rfc1123DateFormatter = [[NSDateFormatter alloc] init]; + [__rfc1123DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + [__rfc1123DateFormatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss 'GMT'"]; + } + return __rfc1123DateFormatter; } - (id)initWithPath:(NSString*)cachePath storagePolicy:(RKRequestCacheStoragePolicy)storagePolicy { self = [super init]; - if (self) { + if (self) { _cache = [[RKCache alloc] initWithPath:cachePath subDirectories: [NSArray arrayWithObjects:RKRequestCacheSessionCacheDirectory, RKRequestCachePermanentCacheDirectory, nil]]; - self.storagePolicy = storagePolicy; - } + self.storagePolicy = storagePolicy; + } - return self; + return self; } - (void)dealloc { - [_cache release]; - _cache = nil; - [super dealloc]; + [_cache release]; + _cache = nil; + [super dealloc]; } - (NSString*)path { - return _cache.cachePath; + return _cache.cachePath; } - (NSString*)pathForRequest:(RKRequest*)request { - NSString* pathForRequest = nil; + NSString* pathForRequest = nil; NSString* requestCacheKey = [request cacheKey]; if (requestCacheKey) { if (_storagePolicy == RKRequestCacheStoragePolicyForDurationOfSession) { @@ -85,69 +85,69 @@ - (NSString*)pathForRequest:(RKRequest*)request { } else { RKLogTrace(@"Failed to find cacheKey for %@ due to nil cacheKey", request); } - return pathForRequest; + return pathForRequest; } - (BOOL)hasResponseForRequest:(RKRequest*)request { - BOOL hasEntryForRequest = NO; - NSString* cacheKey = [self pathForRequest:request]; + BOOL hasEntryForRequest = NO; + NSString* cacheKey = [self pathForRequest:request]; if (cacheKey) { hasEntryForRequest = ([_cache hasEntry:cacheKey] && [_cache hasEntry:[cacheKey stringByAppendingPathExtension:RKRequestCacheHeadersExtension]]); } RKLogTrace(@"Determined hasResponseForRequest: %@ => %@", request, hasEntryForRequest ? @"YES" : @"NO"); - return hasEntryForRequest; + return hasEntryForRequest; } - (void)storeResponse:(RKResponse*)response forRequest:(RKRequest*)request { - if ([self hasResponseForRequest:request]) { - [self invalidateRequest:request]; - } + if ([self hasResponseForRequest:request]) { + [self invalidateRequest:request]; + } - if (_storagePolicy != RKRequestCacheStoragePolicyDisabled) { - NSString* cacheKey = [self pathForRequest:request]; - if (cacheKey) { + if (_storagePolicy != RKRequestCacheStoragePolicyDisabled) { + NSString* cacheKey = [self pathForRequest:request]; + if (cacheKey) { [_cache writeData:response.body withCacheKey:cacheKey]; - NSMutableDictionary* headers = [response.allHeaderFields mutableCopy]; - if (headers) { + NSMutableDictionary* headers = [response.allHeaderFields mutableCopy]; + if (headers) { // TODO: expose this? NSHTTPURLResponse* urlResponse = [response valueForKey:@"_httpURLResponse"]; // Cache Loaded Time - [headers setObject:[[RKRequestCache rfc1123DateFormatter] stringFromDate:[NSDate date]] - forKey:RKRequestCacheDateHeaderKey]; + [headers setObject:[[RKRequestCache rfc1123DateFormatter] stringFromDate:[NSDate date]] + forKey:RKRequestCacheDateHeaderKey]; // Cache status code [headers setObject:[NSNumber numberWithInteger:urlResponse.statusCode] - forKey:RKRequestCacheStatusCodeKey]; + forKey:RKRequestCacheStatusCodeHeadersKey]; // Cache MIME Type [headers setObject:urlResponse.MIMEType - forKey:RKRequestCacheMIMETypeKey]; + forKey:RKRequestCacheMIMETypeHeadersKey]; // Cache URL [headers setObject:[urlResponse.URL absoluteString] - forKey:RKRequestCacheURLKey]; + forKey:RKRequestCacheURLHeadersKey]; // Save [_cache writeDictionary:headers withCacheKey:[cacheKey stringByAppendingPathExtension:RKRequestCacheHeadersExtension]]; - } - [headers release]; - } - } + } + [headers release]; + } + } } - (RKResponse*)responseForRequest:(RKRequest*)request { - RKResponse* response = nil; - NSString* cacheKey = [self pathForRequest:request]; - if (cacheKey) { - NSData* responseData = [_cache dataForCacheKey:cacheKey]; + RKResponse* response = nil; + NSString* cacheKey = [self pathForRequest:request]; + if (cacheKey) { + NSData* responseData = [_cache dataForCacheKey:cacheKey]; NSDictionary* responseHeaders = [_cache dictionaryForCacheKey:[cacheKey stringByAppendingPathExtension:RKRequestCacheHeadersExtension]]; - response = [[[RKResponse alloc] initWithRequest:request body:responseData headers:responseHeaders] autorelease]; - } + response = [[[RKResponse alloc] initWithRequest:request body:responseData headers:responseHeaders] autorelease]; + } RKLogDebug(@"Found cached RKResponse '%@' for '%@'", response, request); - return response; + return response; } - (NSDictionary*)headersForRequest:(RKRequest*)request { NSDictionary* headers = nil; - NSString* cacheKey = [self pathForRequest:request]; + NSString* cacheKey = [self pathForRequest:request]; if (cacheKey) { NSString* headersCacheKey = [cacheKey stringByAppendingPathExtension:RKRequestCacheHeadersExtension]; headers = [_cache dictionaryForCacheKey:headersCacheKey]; @@ -163,7 +163,7 @@ - (NSDictionary*)headersForRequest:(RKRequest*)request { } - (NSString*)etagForRequest:(RKRequest*)request { - NSString* etag = nil; + NSString* etag = nil; NSDictionary* responseHeaders = [self headersForRequest:request]; if (responseHeaders) { @@ -174,11 +174,11 @@ - (NSString*)etagForRequest:(RKRequest*)request { } } RKLogDebug(@"Found cached ETag '%@' for '%@'", etag, request); - return etag; + return etag; } - (void)setCacheDate:(NSDate*)date forRequest:(RKRequest*)request { - NSString* cacheKey = [self pathForRequest:request]; + NSString* cacheKey = [self pathForRequest:request]; if (cacheKey) { NSMutableDictionary* responseHeaders = [[self headersForRequest:request] mutableCopy]; @@ -191,7 +191,7 @@ - (void)setCacheDate:(NSDate*)date forRequest:(RKRequest*)request { } - (NSDate*)cacheDateForRequest:(RKRequest*)request { - NSDate* date = nil; + NSDate* date = nil; NSString* dateString = nil; NSDictionary* responseHeaders = [self headersForRequest:request]; @@ -204,27 +204,27 @@ - (NSDate*)cacheDateForRequest:(RKRequest*)request { } date = [[RKRequestCache rfc1123DateFormatter] dateFromString:dateString]; RKLogDebug(@"Found cached date '%@' for '%@'", date, request); - return date; + return date; } - (void)invalidateRequest:(RKRequest*)request { RKLogDebug(@"Invalidating cache entry for '%@'", request); - NSString* cacheKey = [self pathForRequest:request]; - if (cacheKey) { + NSString* cacheKey = [self pathForRequest:request]; + if (cacheKey) { [_cache invalidateEntry:cacheKey]; [_cache invalidateEntry:[cacheKey stringByAppendingPathExtension:RKRequestCacheHeadersExtension]]; RKLogTrace(@"Removed cache entry at path '%@' for '%@'", cacheKey, request); - } + } } - (void)invalidateWithStoragePolicy:(RKRequestCacheStoragePolicy)storagePolicy { - if (storagePolicy != RKRequestCacheStoragePolicyDisabled) { - if (storagePolicy == RKRequestCacheStoragePolicyForDurationOfSession) { + if (storagePolicy != RKRequestCacheStoragePolicyDisabled) { + if (storagePolicy == RKRequestCacheStoragePolicyForDurationOfSession) { [_cache invalidateSubDirectory:RKRequestCacheSessionCacheDirectory]; - } else { + } else { [_cache invalidateSubDirectory:RKRequestCachePermanentCacheDirectory]; - } - } + } + } } - (void)invalidateAll { @@ -234,11 +234,11 @@ - (void)invalidateAll { } - (void)setStoragePolicy:(RKRequestCacheStoragePolicy)storagePolicy { - [self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyForDurationOfSession]; - if (storagePolicy == RKRequestCacheStoragePolicyDisabled) { - [self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; - } - _storagePolicy = storagePolicy; + [self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyForDurationOfSession]; + if (storagePolicy == RKRequestCacheStoragePolicyDisabled) { + [self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; + } + _storagePolicy = storagePolicy; } @end diff --git a/Code/Network/RKRequestQueue.h b/Code/Network/RKRequestQueue.h index 018b828de9..f7308f0690 100644 --- a/Code/Network/RKRequestQueue.h +++ b/Code/Network/RKRequestQueue.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 12/1/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -46,7 +46,7 @@ /** Creates and returns a new request queue. - + @return An autoreleased RKRequestQueue object. */ + (id)requestQueue; @@ -54,7 +54,7 @@ /** Returns a new retained request queue with the given name. If there is already an existing queue with the given name, nil will be returned. - + @param name A symbolic name for the queue. @return A new retained RKRequestQueue with the given name or nil if one already exists with the given name. @@ -69,7 +69,7 @@ /** Returns queue with the specified name. If no queue is found with the name provided, a new queue will be initialized and returned. - + @param name A symbolic name for the queue. @return An existing RKRequestQueue with the given name or a new queue if none currently exist. @@ -82,7 +82,7 @@ /** A symbolic name for the queue. - + Used to return existing queue references via [RKRequestQueue requestQueueWithName:] */ @@ -90,7 +90,7 @@ /** Determine if a queue exists with a given name. - + @param name The queue name to search against. @return YES when there is a queue with the given name. */ @@ -103,7 +103,7 @@ /** The delegate to inform when the request queue state machine changes. - + If the object implements the RKRequestQueueDelegate protocol, it will receive request lifecycle event messages. */ @@ -116,14 +116,14 @@ /** The number of concurrent requests supported by this queue. - + **Default**: 5 concurrent requests */ @property (nonatomic) NSUInteger concurrentRequestsLimit; /** Request timeout value used by the queue. - + **Default**: 5 minutes (300 seconds) */ @property (nonatomic, assign) NSUInteger requestTimeout; @@ -135,21 +135,21 @@ /** Add an asynchronous request to the queue and send it as as soon as possible. - + @param request The request to be added to the queue. */ - (void)addRequest:(RKRequest *)request; /** Cancel a request that is in progress. - + @param request The request to be cancelled. */ - (void)cancelRequest:(RKRequest *)request; /** Cancel all requests with a given delegate. - + @param delegate The delegate assigned to the requests to be cancelled. */ - (void)cancelRequestsWithDelegate:(id)delegate; @@ -159,7 +159,7 @@ reference and canceling the request. Useful when an object that acts as the delegate for one or more requests - is being deallocated and all outstanding requests should be canceled + is being deallocated and all outstanding requests should be cancelled without generating any further delegate callbacks. @param delegate The object acting as the delegate for all enqueued requests that are to be aborted. @@ -173,7 +173,7 @@ /** Determine if a given request is currently in this queue. - + @param request The request to check the queue for. @return YES if the specified request is in this queue. */ @@ -192,7 +192,7 @@ /** Sets the flag that determines if new load requests are allowed to reach the network. - + Because network requests tend to slow down performance, this property can be used to temporarily delay them. All requests made while suspended are queued, and when suspended becomes false again they are executed. @@ -207,10 +207,10 @@ #if TARGET_OS_IPHONE /** Sets the flag for showing the network activity indicatory. - + When YES, this queue will spin the network activity in the menu bar when it is processing requests. - + **Default**: NO */ @property (nonatomic) BOOL showsNetworkActivityIndicatorWhenBusy; @@ -223,10 +223,10 @@ /** Returns the global queue - - @bug **DEPRECATED** in v0.9.4: All RKClient instances now own their own + + @bug **DEPRECATED** in v0.10.0: All RKClient instances now own their own individual request queues. - + @see [RKClient requestQueue] @return Global request queue. */ @@ -234,10 +234,10 @@ /** Sets the global queue - - @bug **DEPRECATED** in v0.9.4: All RKClient instances now own their own + + @bug **DEPRECATED** in v0.10.0: All RKClient instances now own their own individual request queues. - + @see [RKClient requestQueue] @param requestQueue The request queue to assign as the global queue. */ @@ -258,28 +258,28 @@ /** Sent when the queue transitions from an empty state to processing requests. - - @param queue The queue that began processing requests. + + @param queue The queue that began processing requests. */ - (void)requestQueueDidBeginLoading:(RKRequestQueue *)queue; /** Sent when queue transitions from a processing state to an empty start. - + @param queue The queue that finished processing requests. */ - (void)requestQueueDidFinishLoading:(RKRequestQueue *)queue; /** Sent when the queue has been suspended and request processing has been halted. - + @param queue The request queue that has been suspended. */ - (void)requestQueueWasSuspended:(RKRequestQueue *)queue; /** Sent when the queue has been unsuspended and request processing has resumed. - + @param queue The request queue that has resumed processing. */ - (void)requestQueueWasUnsuspended:(RKRequestQueue *)queue; @@ -291,7 +291,7 @@ /** Sent before queue sends a request. - + @param queue The queue that will process the request. @param request The request to be processed. */ @@ -299,7 +299,7 @@ /** Sent after queue has sent a request. - + @param queue The queue that processed the request. @param request The processed request. */ @@ -307,15 +307,15 @@ /** Sent when queue received a response for a request. - + @param queue The queue that received the response. @param response The response that was received. */ - (void)requestQueue:(RKRequestQueue *)queue didLoadResponse:(RKResponse *)response; /** - Sent when queue has canceled a request. - + Sent when queue has cancelled a request. + @param queue The queue that cancelled the request. @param request The cancelled request. */ @@ -323,7 +323,7 @@ /** Sent when an attempted request fails. - + @param queue The queue in which the request failed from. @param request The failed request. @param error An NSError object containing the RKRestKitError that caused the @@ -337,7 +337,7 @@ /** A category on UIApplication to allow for jointly managing the network activity indicator. - + Adopted from 'iOS Recipes' book: http://pragprog.com/book/cdirec/ios-recipes */ @interface UIApplication (RKNetworkActivity) diff --git a/Code/Network/RKRequestQueue.m b/Code/Network/RKRequestQueue.m index dbbd20f544..88eb637360 100644 --- a/Code/Network/RKRequestQueue.m +++ b/Code/Network/RKRequestQueue.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 12/1/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -72,25 +72,25 @@ + (id)requestQueue { + (id)newRequestQueueWithName:(NSString*)name { if (RKRequestQueueInstances == nil) { - RKRequestQueueInstances = [NSMutableArray new]; + RKRequestQueueInstances = [NSMutableArray new]; } - + if ([self requestQueueExistsWithName:name]) { return nil; } - + RKRequestQueue* queue = [self new]; queue.name = name; [RKRequestQueueInstances addObject:[NSValue valueWithNonretainedObject:queue]]; - + return queue; } + (id)requestQueueWithName:(NSString *)name { if (RKRequestQueueInstances == nil) { - RKRequestQueueInstances = [NSMutableArray new]; + RKRequestQueueInstances = [NSMutableArray new]; } - + // Find existing reference NSArray *requestQueueInstances = [RKRequestQueueInstances copy]; RKRequestQueue *namedQueue = nil; @@ -102,13 +102,13 @@ + (id)requestQueueWithName:(NSString *)name { } } [requestQueueInstances release]; - + if (namedQueue == nil) { namedQueue = [self requestQueue]; namedQueue.name = name; [RKRequestQueueInstances addObject:[NSValue valueWithNonretainedObject:namedQueue]]; } - + return namedQueue; } @@ -125,17 +125,17 @@ + (BOOL)requestQueueExistsWithName:(NSString*)name { } [requestQueueInstances release]; } - + return queueExists; } - + - (id)init { - if ((self = [super init])) { - _requests = [[NSMutableArray alloc] init]; + if ((self = [super init])) { + _requests = [[NSMutableArray alloc] init]; _loadingRequests = [[NSMutableSet alloc] init]; - _suspended = YES; - _concurrentRequestsLimit = 5; - _requestTimeout = 300; + _suspended = YES; + _concurrentRequestsLimit = 5; + _requestTimeout = 300; _showsNetworkActivityIndicatorWhenBusy = NO; #if TARGET_OS_IPHONE @@ -151,8 +151,8 @@ - (id)init { object:nil]; } #endif - } - return self; + } + return self; } - (void)removeFromNamedQueues { @@ -170,7 +170,7 @@ - (void)removeFromNamedQueues { - (void)dealloc { RKLogDebug(@"Queue instance is being deallocated: %@", self); [[NSNotificationCenter defaultCenter] removeObserver:self]; - + [self removeFromNamedQueues]; [_queueTimer invalidate]; @@ -187,8 +187,8 @@ - (NSUInteger)count { } - (NSString*)description { - return [NSString stringWithFormat:@"<%@: %p name=%@ suspended=%@ requestCount=%d loadingCount=%d/%d>", - NSStringFromClass([self class]), self, self.name, self.suspended ? @"YES" : @"NO", + return [NSString stringWithFormat:@"<%@: %p name=%@ suspended=%@ requestCount=%d loadingCount=%d/%d>", + NSStringFromClass([self class]), self, self.name, self.suspended ? @"YES" : @"NO", self.count, self.loadingCount, self.concurrentRequestsLimit]; } @@ -239,14 +239,14 @@ - (void)removeLoadingRequest:(RKRequest*)request { } - (void)loadNextInQueueDelayed { - if (!_queueTimer) { - _queueTimer = [NSTimer scheduledTimerWithTimeInterval:kFlushDelay - target:self - selector:@selector(loadNextInQueue) - userInfo:nil - repeats:NO]; + if (!_queueTimer) { + _queueTimer = [NSTimer scheduledTimerWithTimeInterval:kFlushDelay + target:self + selector:@selector(loadNextInQueue) + userInfo:nil + repeats:NO]; RKLogTrace(@"Timer initialized with delay %f for queue %@", kFlushDelay, self); - } + } } - (RKRequest*)nextRequest { @@ -267,19 +267,19 @@ - (void)loadNextInQueue { [self performSelectorOnMainThread:@selector(loadNextInQueue) withObject:nil waitUntilDone:NO]; return; } - - // Make sure that the Request Queue does not fire off any requests until the Reachability state has been determined. - if (self.suspended) { - _queueTimer = nil; - [self loadNextInQueueDelayed]; + + // Make sure that the Request Queue does not fire off any requests until the Reachability state has been determined. + if (self.suspended) { + _queueTimer = nil; + [self loadNextInQueueDelayed]; RKLogTrace(@"Deferring request loading for queue %@ due to suspension", self); - return; - } + return; + } + + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + _queueTimer = nil; - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - _queueTimer = nil; - @synchronized(self) { RKRequest* request = [self nextRequest]; while (request && self.loadingCount < _concurrentRequestsLimit) { @@ -300,11 +300,11 @@ - (void)loadNextInQueue { } } - if (_requests.count && !_suspended) { - [self loadNextInQueueDelayed]; - } + if (_requests.count && !_suspended) { + [self loadNextInQueueDelayed]; + } - [pool drain]; + [pool drain]; } - (void)setSuspended:(BOOL)isSuspended { @@ -326,14 +326,14 @@ - (void)setSuspended:(BOOL)isSuspended { } } - _suspended = isSuspended; + _suspended = isSuspended; - if (!_suspended) { - [self loadNextInQueue]; - } else if (_queueTimer) { - [_queueTimer invalidate]; - _queueTimer = nil; - } + if (!_suspended) { + [self loadNextInQueue]; + } else if (_queueTimer) { + [_queueTimer invalidate]; + _queueTimer = nil; + } } - (void)addRequest:(RKRequest*)request { @@ -358,7 +358,7 @@ - (void)addRequest:(RKRequest*)request { name:RKRequestDidFailWithErrorNotification object:request]; - [self loadNextInQueue]; + [self loadNextInQueue]; } - (BOOL)removeRequest:(RKRequest*)request { @@ -382,12 +382,14 @@ - (BOOL)removeRequest:(RKRequest*)request { } - (BOOL)containsRequest:(RKRequest*)request { - return [_requests containsObject:request]; + @synchronized(self) { + return [_requests containsObject:request]; + } } - (void)cancelRequest:(RKRequest*)request loadNext:(BOOL)loadNext { if ([request isUnsent]) { - RKLogDebug(@"Canceled undispatched request %@ and removed from queue %@", request, self); + RKLogDebug(@"Cancelled undispatched request %@ and removed from queue %@", request, self); [self removeRequest:request]; request.delegate = nil; @@ -396,10 +398,10 @@ - (void)cancelRequest:(RKRequest*)request loadNext:(BOOL)loadNext { [_delegate requestQueue:self didCancelRequest:request]; } } else if ([self containsRequest:request] && [request isLoading]) { - RKLogDebug(@"Canceled loading request %@ and removed from queue %@", request, self); + RKLogDebug(@"Cancelled loading request %@ and removed from queue %@", request, self); - [request cancel]; - request.delegate = nil; + [request cancel]; + request.delegate = nil; if ([_delegate respondsToSelector:@selector(requestQueue:didCancelRequest:)]) { [_delegate requestQueue:self didCancelRequest:request]; @@ -407,52 +409,52 @@ - (void)cancelRequest:(RKRequest*)request loadNext:(BOOL)loadNext { [self removeRequest:request]; - if (loadNext) { - [self loadNextInQueue]; - } - } + if (loadNext) { + [self loadNextInQueue]; + } + } } - (void)cancelRequest:(RKRequest*)request { - [self cancelRequest:request loadNext:YES]; + [self cancelRequest:request loadNext:YES]; } - (void)cancelRequestsWithDelegate:(NSObject*)delegate { RKLogDebug(@"Cancelling all request in queue %@ with delegate %p", self, delegate); - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - NSArray* requestsCopy = [NSArray arrayWithArray:_requests]; - for (RKRequest* request in requestsCopy) { - if (request.delegate && request.delegate == delegate) { - [self cancelRequest:request]; - } - } - [pool drain]; + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + NSArray* requestsCopy = [NSArray arrayWithArray:_requests]; + for (RKRequest* request in requestsCopy) { + if (request.delegate && request.delegate == delegate) { + [self cancelRequest:request]; + } + } + [pool drain]; } - (void)abortRequestsWithDelegate:(NSObject*)delegate { RKLogDebug(@"Aborting all request in queue %@ with delegate %p", self, delegate); - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - NSArray* requestsCopy = [NSArray arrayWithArray:_requests]; - for (RKRequest* request in requestsCopy) { - if (request.delegate && request.delegate == delegate) { + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + NSArray* requestsCopy = [NSArray arrayWithArray:_requests]; + for (RKRequest* request in requestsCopy) { + if (request.delegate && request.delegate == delegate) { request.delegate = nil; - [self cancelRequest:request]; - } - } - [pool drain]; + [self cancelRequest:request]; + } + } + [pool drain]; } - (void)cancelAllRequests { RKLogDebug(@"Cancelling all request in queue %@", self); - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - NSArray* requestsCopy = [NSArray arrayWithArray:_requests]; - for (RKRequest* request in requestsCopy) { - [self cancelRequest:request loadNext:NO]; - } - [pool drain]; + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + NSArray* requestsCopy = [NSArray arrayWithArray:_requests]; + for (RKRequest* request in requestsCopy) { + [self cancelRequest:request loadNext:NO]; + } + [pool drain]; } - (void)start { @@ -462,7 +464,7 @@ - (void)start { - (void)processRequestDidLoadResponseNotification:(NSNotification *)notification { NSAssert([notification.object isKindOfClass:[RKRequest class]], @"Notification expected to contain an RKRequest, got a %@", NSStringFromClass([notification.object class])); - RKLogTrace(@"Received notification: %@", notification); + RKLogTrace(@"Received notification: %@", notification); RKRequest* request = (RKRequest*)notification.object; NSDictionary* userInfo = [notification userInfo]; @@ -481,7 +483,7 @@ - (void)processRequestDidLoadResponseNotification:(NSNotification *)notification - (void)processRequestDidFailWithErrorNotification:(NSNotification *)notification { NSAssert([notification.object isKindOfClass:[RKRequest class]], @"Notification expected to contain an RKRequest, got a %@", NSStringFromClass([notification.object class])); - RKLogTrace(@"Received notification: %@", notification); + RKLogTrace(@"Received notification: %@", notification); RKRequest* request = (RKRequest*)notification.object; NSDictionary* userInfo = [notification userInfo]; @@ -510,7 +512,7 @@ - (void)processRequestDidFailWithErrorNotification:(NSNotification *)notificatio */ - (void)processRequestDidFinishLoadingNotification:(NSNotification *)notification { NSAssert([notification.object isKindOfClass:[RKRequest class]], @"Notification expected to contain an RKRequest, got a %@", NSStringFromClass([notification.object class])); - RKLogTrace(@"Received notification: %@", notification); + RKLogTrace(@"Received notification: %@", notification); RKRequest* request = (RKRequest*)notification.object; if ([self containsRequest:request]) { diff --git a/Code/Network/RKRequestSerializable.h b/Code/Network/RKRequestSerializable.h index 4e7e9602fc..62fbbb776f 100644 --- a/Code/Network/RKRequestSerializable.h +++ b/Code/Network/RKRequestSerializable.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 8/3/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,10 +22,10 @@ This protocol is implemented by objects that can be serialized into a representation suitable for transmission over a REST request. Suitable serializations are x-www-form-urlencoded and multipart/form-data. - + @warning One of the following methods MUST be implemented for your serializable implementation to be complete: - + - (NSData *)HTTPBody - If you are allowing serialization of a small in-memory data structure, implement HTTPBody as it is much simpler. - (NSInputStream *)HTTPBodyStream - This provides support for streaming a large @@ -41,7 +41,7 @@ /** The value of the Content-Type header for the HTTP Body representation of the serialization. - + @return A string value of the Content-Type header for the HTTP body. */ - (NSString *)HTTPHeaderValueForContentType; @@ -55,7 +55,7 @@ /** An NSData representing the HTTP Body serialization of the object implementing the protocol. - + @return An NSData object respresenting the HTTP body serialization. */ - (NSData *)HTTPBody; @@ -63,7 +63,7 @@ /** Returns an input stream for reading the serialization as a stream used to provide support for handling large HTTP payloads. - + @return An input stream for reading the serialization as a stream. */ - (NSInputStream *)HTTPBodyStream; @@ -75,7 +75,7 @@ /** Returns the length of the HTTP Content-Length header. - + @return Unsigned integer length of the HTTP Content-Length header. */ - (NSUInteger)HTTPHeaderValueForContentLength; @@ -83,8 +83,8 @@ /** The value of the Content-Type header for the HTTP Body representation of the serialization. - - @bug **DEPRECATED** in v0.9.4: Implement [RKRequestSerializable HTTPHeaderValueForContentType] + + @bug **DEPRECATED** in v0.10.0: Implement [RKRequestSerializable HTTPHeaderValueForContentType] instead. @return A string value of the Content-Type header for the HTTP body. */ diff --git a/Code/Network/RKRequestSerialization.h b/Code/Network/RKRequestSerialization.h index 5fbc5593d9..62317f28b3 100644 --- a/Code/Network/RKRequestSerialization.h +++ b/Code/Network/RKRequestSerialization.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/18/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,7 +24,7 @@ A simple implementation of the RKRequestSerializable protocol suitable for wrapping a MIME Type string and HTTP Body into a format that can be sent as the params of an RKRequest. - + @see RKRequestSerializable */ @interface RKRequestSerialization : NSObject { @@ -40,7 +40,7 @@ /** Creates and returns a new serialization enclosing an NSData object with the specified MIME type. - + @param data An NSData object to initialize the serialization with. @param MIMEType A string of the MIME type of the provided data. @return An autoreleased RKRequestSerialization object with the data and MIME @@ -51,7 +51,7 @@ /** Returns a new serialization enclosing an NSData object with the specified MIME type. - + @param data An NSData object to initialize the serialization with. @param MIMEType A string of the MIME type of the provided data. @return An RKRequestSerialization object with the data and MIME type set. diff --git a/Code/Network/RKRequestSerialization.m b/Code/Network/RKRequestSerialization.m index 839e51c3ac..f00c12d2ee 100644 --- a/Code/Network/RKRequestSerialization.m +++ b/Code/Network/RKRequestSerialization.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/18/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -28,13 +28,13 @@ @implementation RKRequestSerialization - (id)initWithData:(NSData *)data MIMEType:(NSString *)MIMEType { NSAssert(data, @"Cannot create a request serialization without Data"); NSAssert(MIMEType, @"Cannot create a request serialization without a MIME Type"); - + self = [super init]; if (self) { _data = [data retain]; _MIMEType = [MIMEType retain]; } - + return self; } @@ -45,7 +45,7 @@ + (id)serializationWithData:(NSData *)data MIMEType:(NSString *)MIMEType { - (void)dealloc { [_data release]; [_MIMEType release]; - + [super dealloc]; } diff --git a/Code/Network/RKRequest_Internals.h b/Code/Network/RKRequest_Internals.h index 3404ab30ed..1184188d55 100644 --- a/Code/Network/RKRequest_Internals.h +++ b/Code/Network/RKRequest_Internals.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/31/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,4 +21,5 @@ @interface RKRequest (Internals) - (BOOL)prepareURLRequest; - (void)didFailLoadWithError:(NSError*)error; +- (void)finalizeLoad:(BOOL)successful; @end diff --git a/Code/Network/RKResponse.h b/Code/Network/RKResponse.h index 9026975954..2ce426f39a 100644 --- a/Code/Network/RKResponse.h +++ b/Code/Network/RKResponse.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 7/28/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,12 +25,12 @@ Models the response portion of an HTTP request/response cycle */ @interface RKResponse : NSObject { - RKRequest *_request; - NSHTTPURLResponse *_httpURLResponse; - NSMutableData *_body; - NSError *_failureError; - BOOL _loading; - NSDictionary *_responseHeaders; + RKRequest *_request; + NSHTTPURLResponse *_httpURLResponse; + NSMutableData *_body; + NSError *_failureError; + BOOL _loading; + NSDictionary *_responseHeaders; } @@ -40,7 +40,7 @@ /** Initializes a new response object for a REST request. - + @param request The request that the response being created belongs to. @return An RKResponse object with the request parameter set. */ @@ -48,7 +48,7 @@ /** Initializes a new response object from a cached request. - + @param request The request that the response being created belongs to. @param body The data of the body of the response. @param headers A dictionary of the response's headers. @@ -58,7 +58,7 @@ /** Initializes a response object from the results of a synchronous request. - + @param request The request that the response being created belongs to. @param URLResponse The response from the NSURLConnection call containing the headers and HTTP status code. @@ -77,7 +77,7 @@ /** The request that generated this response. */ -@property (nonatomic, readonly) RKRequest *request; +@property (nonatomic, assign, readonly) RKRequest *request; /** The URL the response was loaded from. @@ -147,13 +147,13 @@ /** Returns the response body parsed as JSON into an object - @bug **DEPRECATED** in v0.9.4 + @bug **DEPRECATED** in v0.10.0 */ - (id)bodyAsJSON DEPRECATED_ATTRIBUTE; /** Returns the response body parsed as JSON into an object - + @param error An NSError to populate if something goes wrong while parsing the body JSON into an object. */ @@ -171,14 +171,14 @@ /** Determines if there is an error object and uses it's localized message - + @return A string of the localized error message. */ - (NSString *)failureErrorDescription; /** Indicates whether the response was loaded from RKCache - + @return YES if the response was loaded from the cache */ - (BOOL)wasLoadedFromCache; @@ -191,62 +191,62 @@ /** Indicates that the connection failed to reach the remote server. The details of the failure are available on the failureError reader. - + @return YES if the connection failed to reach the remote server. */ - (BOOL)isFailure; /** Indicates an invalid HTTP response code less than 100 or greater than 600 - + @return YES if the HTTP response code is less than 100 or greater than 600 */ - (BOOL)isInvalid; /** Indicates an informational HTTP response code between 100 and 199 - + @return YES if the HTTP response code is between 100 and 199 */ - (BOOL)isInformational; /** Indicates an HTTP response code between 200 and 299. - + Confirms that the server received, understood, accepted and processed the request successfully. - + @return YES if the HTTP response code is between 200 and 299 */ - (BOOL)isSuccessful; /** Indicates an HTTP response code between 300 and 399. - + This class of status code indicates that further action needs to be taken by the user agent in order to fulfil the request. The action required may be carried out by the user agent without interaction with the user if and only if the method used in the second request is GET or HEAD. - + @return YES if the HTTP response code is between 300 and 399. */ - (BOOL)isRedirection; /** Indicates an HTTP response code between 400 and 499. - + This status code is indented for cases in which the client seems to have erred. - + @return YES if the HTTP response code is between 400 and 499. */ - (BOOL)isClientError; /** Indicates an HTTP response code between 500 and 599. - + This state code occurs when the server failed to fulfill an apparently valid request. - + @return YES if the HTTP response code is between 500 and 599. */ - (BOOL)isServerError; @@ -258,7 +258,7 @@ /** Indicates that the response is either a server or a client error. - + @return YES if the response is either a server or client error, with a response code between 400 and 599. */ @@ -266,91 +266,91 @@ /** Indicates an HTTP response code of 200. - + @return YES if the response is 200 OK. */ - (BOOL)isOK; /** Indicates an HTTP response code of 201. - + @return YES if the response is 201 Created. */ - (BOOL)isCreated; /** Indicates an HTTP response code of 204. - + @return YES if the response is 204 No Content. */ - (BOOL)isNoContent; /** Indicates an HTTP response code of 304. - + @return YES if the response is 304 Not Modified. */ - (BOOL)isNotModified; /** Indicates an HTTP response code of 401. - + @return YES if the response is 401 Unauthorized. */ - (BOOL)isUnauthorized; /** Indicates an HTTP response code of 403. - + @return YES if the response is 403 Forbidden. */ - (BOOL)isForbidden; /** Indicates an HTTP response code of 404. - + @return YES if the response is 404 Not Found. */ - (BOOL)isNotFound; /** Indicates an HTTP response code of 409. - + @return YES if the response is 409 Conflict. */ - (BOOL)isConflict; /** Indicates an HTTP response code of 410. - + @return YES if the response is 410 Gone. */ - (BOOL)isGone; /** Indicates an HTTP response code of 422. - + @return YES if the response is 422 Unprocessable Entity. */ - (BOOL)isUnprocessableEntity; /** Indicates an HTTP response code of 301, 302, 303 or 307. - + @return YES if the response requires a redirect to finish processing. */ - (BOOL)isRedirect; /** Indicates an empty HTTP response code of 201, 204, or 304 - + @return YES if the response body is empty. */ - (BOOL)isEmpty; /** Indicates an HTTP response code of 503 - + @return YES if the response is 503 Service Unavailable. */ - (BOOL)isServiceUnavailable; @@ -367,28 +367,28 @@ /** True when the server turned an HTML response. - + @return YES when the MIME type is text/html. */ - (BOOL)isHTML; /** True when the server turned an XHTML response - + @return YES when the MIME type is application/xhtml+xml. */ - (BOOL)isXHTML; /** True when the server turned an XML response - + @return YES when the MIME type is application/xml. */ - (BOOL)isXML; /** True when the server turned an JSON response - + @return YES when the MIME type is application/json. */ - (BOOL)isJSON; diff --git a/Code/Network/RKResponse.m b/Code/Network/RKResponse.m index 074afcaf6d..5a1bde7a22 100644 --- a/Code/Network/RKResponse.m +++ b/Code/Network/RKResponse.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 7/28/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -36,66 +36,68 @@ @implementation RKResponse -@synthesize body = _body, request = _request, failureError = _failureError; +@synthesize body = _body; +@synthesize request = _request; +@synthesize failureError = _failureError; - (id)init { self = [super init]; - if (self) { - _body = [[NSMutableData alloc] init]; - _failureError = nil; - _loading = NO; - _responseHeaders = nil; - } + if (self) { + _body = [[NSMutableData alloc] init]; + _failureError = nil; + _loading = NO; + _responseHeaders = nil; + } - return self; + return self; } -- (id)initWithRequest:(RKRequest*)request { +- (id)initWithRequest:(RKRequest *)request { self = [self init]; - if (self) { - // We don't retain here as we're letting RKRequestQueue manage - // request ownership - _request = request; - } + if (self) { + // We don't retain here as we're letting RKRequestQueue manage + // request ownership + _request = request; + } - return self; + return self; } - (id)initWithRequest:(RKRequest*)request body:(NSData*)body headers:(NSDictionary*)headers { - self = [self initWithRequest:request]; - if (self) { - [_body release]; + self = [self initWithRequest:request]; + if (self) { + [_body release]; _body = [[NSMutableData dataWithData:body] retain]; - _responseHeaders = [headers retain]; - } + _responseHeaders = [headers retain]; + } - return self; + return self; } - (id)initWithSynchronousRequest:(RKRequest*)request URLResponse:(NSHTTPURLResponse*)URLResponse body:(NSData*)body error:(NSError*)error { self = [super init]; - if (self) { - _request = request; - _httpURLResponse = [URLResponse retain]; - _failureError = [error retain]; + if (self) { + _request = request; + _httpURLResponse = [URLResponse retain]; + _failureError = [error retain]; _body = [[NSMutableData dataWithData:body] retain]; - _loading = NO; - } + _loading = NO; + } - return self; + return self; } - (void)dealloc { _request = nil; - [_httpURLResponse release]; - _httpURLResponse = nil; - [_body release]; - _body = nil; - [_failureError release]; - _failureError = nil; - [_responseHeaders release]; - _responseHeaders = nil; - [super dealloc]; + [_httpURLResponse release]; + _httpURLResponse = nil; + [_body release]; + _body = nil; + [_failureError release]; + _failureError = nil; + [_responseHeaders release]; + _responseHeaders = nil; + [super dealloc]; } - (BOOL)hasCredentials { @@ -104,21 +106,21 @@ - (BOOL)hasCredentials { - (BOOL)isServerTrusted:(SecTrustRef)trust { BOOL proceed = NO; - + if (_request.disableCertificateValidation) { proceed = YES; } else if ([_request.additionalRootCertificates count] > 0 ) { CFArrayRef rootCerts = (CFArrayRef)[_request.additionalRootCertificates allObjects]; SecTrustResultType result; OSStatus returnCode; - + if (rootCerts && CFArrayGetCount(rootCerts)) { // this could fail, but the trust evaluation will proceed (it's likely to fail, of course) SecTrustSetAnchorCertificates(trust, rootCerts); } - + returnCode = SecTrustEvaluate(trust, &result); - + if (returnCode == errSecSuccess) { proceed = (result == kSecTrustResultProceed || result == kSecTrustResultConfirm || result == kSecTrustResultUnspecified); if (result == kSecTrustResultRecoverableTrustFailure) { @@ -127,7 +129,7 @@ - (BOOL)isServerTrusted:(SecTrustRef)trust { } } } - + return proceed; } @@ -135,55 +137,65 @@ - (BOOL)isServerTrusted:(SecTrustRef)trust { - (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { RKResponseIgnoreDelegateIfCancelled(); RKLogDebug(@"Received authentication challenge"); - - if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { - SecTrustRef trust = [[challenge protectionSpace] serverTrust]; - if ([self isServerTrusted:trust]) { - [challenge.sender useCredential:[NSURLCredential credentialForTrust:trust] forAuthenticationChallenge:challenge]; - } else { - [[challenge sender] cancelAuthenticationChallenge:challenge]; - } - return; - } - - if ([challenge previousFailureCount] == 0) { - NSURLCredential *newCredential; - newCredential=[NSURLCredential credentialWithUser:[NSString stringWithFormat:@"%@", _request.username] - password:[NSString stringWithFormat:@"%@", _request.password] + + if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { + SecTrustRef trust = [[challenge protectionSpace] serverTrust]; + if ([self isServerTrusted:trust]) { + [challenge.sender useCredential:[NSURLCredential credentialForTrust:trust] forAuthenticationChallenge:challenge]; + } else { + [[challenge sender] cancelAuthenticationChallenge:challenge]; + } + return; + } + + if ([challenge previousFailureCount] == 0) { + NSURLCredential *newCredential; + newCredential=[NSURLCredential credentialWithUser:[NSString stringWithFormat:@"%@", _request.username] + password:[NSString stringWithFormat:@"%@", _request.password] persistence:NSURLCredentialPersistenceNone]; - [[challenge sender] useCredential:newCredential - forAuthenticationChallenge:challenge]; - } else { - RKLogWarning(@"Failed authentication challenge after %ld failures", (long) [challenge previousFailureCount]); - [[challenge sender] cancelAuthenticationChallenge:challenge]; - } + [[challenge sender] useCredential:newCredential + forAuthenticationChallenge:challenge]; + } else { + RKLogWarning(@"Failed authentication challenge after %ld failures", (long) [challenge previousFailureCount]); + [[challenge sender] cancelAuthenticationChallenge:challenge]; + } } - (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)space { RKResponseIgnoreDelegateIfCancelled(NO); RKLogDebug(@"Asked if canAuthenticateAgainstProtectionSpace: with authenticationMethod = %@", [space authenticationMethod]); - if ([[space authenticationMethod] isEqualToString:NSURLAuthenticationMethodServerTrust]) { - // server is using an SSL certificate that the OS can't validate - // see whether the client settings allow validation here - if (_request.disableCertificateValidation || [_request.additionalRootCertificates count] > 0) { - return YES; - } else { - return NO; - } - } - + if ([[space authenticationMethod] isEqualToString:NSURLAuthenticationMethodServerTrust]) { + // server is using an SSL certificate that the OS can't validate + // see whether the client settings allow validation here + if (_request.disableCertificateValidation || [_request.additionalRootCertificates count] > 0) { + return YES; + } else { + return NO; + } + } + // Handle non-SSL challenges BOOL hasCredentials = [self hasCredentials]; if (! hasCredentials) { RKLogWarning(@"Received an authentication challenge without any credentials to satisfy the request."); } - + return hasCredentials; } +- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response { + if (nil == response || _request.followRedirect) { + RKLogDebug(@"Proceeding with request to %@", request); + return request; + } else { + RKLogDebug(@"Not following redirect to %@", request); + return nil; + } +} + - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { RKResponseIgnoreDelegateIfCancelled(); - [_body appendData:data]; + [_body appendData:data]; [_request invalidateTimeoutTimer]; if ([[_request delegate] respondsToSelector:@selector(request:didReceiveData:totalBytesReceived:totalBytesExpectedToReceive:)]) { [[_request delegate] request:_request didReceiveData:[data length] totalBytesReceived:[_body length] totalBytesExpectedToReceive:_httpURLResponse.expectedContentLength]; @@ -194,14 +206,17 @@ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLRe RKResponseIgnoreDelegateIfCancelled(); RKLogDebug(@"NSHTTPURLResponse Status Code: %ld", (long) [response statusCode]); RKLogDebug(@"Headers: %@", [response allHeaderFields]); - _httpURLResponse = [response retain]; + _httpURLResponse = [response retain]; [_request invalidateTimeoutTimer]; + if ([[_request delegate] respondsToSelector:@selector(request:didReceiveResponse:)]) { + [[_request delegate] request:_request didReceiveResponse:self]; + } } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { RKResponseIgnoreDelegateIfCancelled(); - RKLogTrace(@"Read response body: %@", [self bodyAsString]); - [_request didFinishLoad:self]; + RKLogTrace(@"Read response body: %@", [self bodyAsString]); + [_request didFinishLoad:self]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { @@ -226,26 +241,26 @@ - (NSInputStream *)connection:(NSURLConnection *)connection needNewBodyStream:(N - (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite { RKResponseIgnoreDelegateIfCancelled(); [_request invalidateTimeoutTimer]; - - if ([[_request delegate] respondsToSelector:@selector(request:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite:)]) { - [[_request delegate] request:_request didSendBodyData:bytesWritten totalBytesWritten:totalBytesWritten totalBytesExpectedToWrite:totalBytesExpectedToWrite]; - } + + if ([[_request delegate] respondsToSelector:@selector(request:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite:)]) { + [[_request delegate] request:_request didSendBodyData:bytesWritten totalBytesWritten:totalBytesWritten totalBytesExpectedToWrite:totalBytesExpectedToWrite]; + } } - (NSString*)localizedStatusCodeString { - return [NSHTTPURLResponse localizedStringForStatusCode:[self statusCode]]; + return [NSHTTPURLResponse localizedStringForStatusCode:[self statusCode]]; } - (NSData *)body { - return _body; + return _body; } - (NSString *)bodyEncodingName { - return [_httpURLResponse textEncodingName]; + return [_httpURLResponse textEncodingName]; } - (NSStringEncoding)bodyEncoding { - CFStringEncoding cfEncoding = kCFStringEncodingInvalidId; + CFStringEncoding cfEncoding = kCFStringEncodingInvalidId; NSString *textEncodingName = [self bodyEncodingName]; if (textEncodingName) { cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef) textEncodingName); @@ -254,7 +269,7 @@ - (NSStringEncoding)bodyEncoding { } - (NSString *)bodyAsString { - return [[[NSString alloc] initWithData:self.body encoding:[self bodyEncoding]] autorelease]; + return [[[NSString alloc] initWithData:self.body encoding:[self bodyEncoding]] autorelease]; } - (id)bodyAsJSON { @@ -279,107 +294,107 @@ - (id)parsedBody:(NSError**)error { } - (NSString*)failureErrorDescription { - if ([self isFailure]) { - return [_failureError localizedDescription]; - } else { - return nil; - } + if ([self isFailure]) { + return [_failureError localizedDescription]; + } else { + return nil; + } } - (BOOL)wasLoadedFromCache { - return (_responseHeaders != nil); + return (_responseHeaders != nil); } - (NSURL*)URL { if ([self wasLoadedFromCache]) { - return [NSURL URLWithString:[_responseHeaders valueForKey:RKRequestCacheURLKey]]; + return [NSURL URLWithString:[_responseHeaders valueForKey:RKRequestCacheURLHeadersKey]]; } - return [_httpURLResponse URL]; + return [_httpURLResponse URL]; } - (NSString*)MIMEType { if ([self wasLoadedFromCache]) { - return [_responseHeaders valueForKey:RKRequestCacheMIMETypeKey]; + return [_responseHeaders valueForKey:RKRequestCacheMIMETypeHeadersKey]; } - return [_httpURLResponse MIMEType]; + return [_httpURLResponse MIMEType]; } - (NSInteger)statusCode { if ([self wasLoadedFromCache]) { - return [[_responseHeaders valueForKey:RKRequestCacheStatusCodeKey] intValue]; + return [[_responseHeaders valueForKey:RKRequestCacheStatusCodeHeadersKey] intValue]; } return ([_httpURLResponse respondsToSelector:@selector(statusCode)] ? [_httpURLResponse statusCode] : 200); } - (NSDictionary*)allHeaderFields { - if ([self wasLoadedFromCache]) { - return _responseHeaders; - } + if ([self wasLoadedFromCache]) { + return _responseHeaders; + } return ([_httpURLResponse respondsToSelector:@selector(allHeaderFields)] ? [_httpURLResponse allHeaderFields] : nil); } - (NSArray*)cookies { - return [NSHTTPCookie cookiesWithResponseHeaderFields:self.allHeaderFields forURL:self.URL]; + return [NSHTTPCookie cookiesWithResponseHeaderFields:self.allHeaderFields forURL:self.URL]; } - (BOOL)isFailure { - return (nil != _failureError); + return (nil != _failureError); } - (BOOL)isInvalid { - return ([self statusCode] < 100 || [self statusCode] > 600); + return ([self statusCode] < 100 || [self statusCode] > 600); } - (BOOL)isInformational { - return ([self statusCode] >= 100 && [self statusCode] < 200); + return ([self statusCode] >= 100 && [self statusCode] < 200); } - (BOOL)isSuccessful { - return (([self statusCode] >= 200 && [self statusCode] < 300) || ([self wasLoadedFromCache])); + return (([self statusCode] >= 200 && [self statusCode] < 300) || ([self wasLoadedFromCache])); } - (BOOL)isRedirection { - return ([self statusCode] >= 300 && [self statusCode] < 400); + return ([self statusCode] >= 300 && [self statusCode] < 400); } - (BOOL)isClientError { - return ([self statusCode] >= 400 && [self statusCode] < 500); + return ([self statusCode] >= 400 && [self statusCode] < 500); } - (BOOL)isServerError { - return ([self statusCode] >= 500 && [self statusCode] < 600); + return ([self statusCode] >= 500 && [self statusCode] < 600); } - (BOOL)isError { - return ([self isClientError] || [self isServerError]); + return ([self isClientError] || [self isServerError]); } - (BOOL)isOK { - return ([self statusCode] == 200); + return ([self statusCode] == 200); } - (BOOL)isCreated { - return ([self statusCode] == 201); + return ([self statusCode] == 201); } - (BOOL)isNoContent { - return ([self statusCode] == 204); + return ([self statusCode] == 204); } - (BOOL)isNotModified { - return ([self statusCode] == 304); + return ([self statusCode] == 304); } - (BOOL)isUnauthorized { - return ([self statusCode] == 401); + return ([self statusCode] == 401); } - (BOOL)isForbidden { - return ([self statusCode] == 403); + return ([self statusCode] == 403); } - (BOOL)isNotFound { - return ([self statusCode] == 404); + return ([self statusCode] == 404); } - (BOOL)isConflict { @@ -391,59 +406,59 @@ - (BOOL)isGone { } - (BOOL)isUnprocessableEntity { - return ([self statusCode] == 422); + return ([self statusCode] == 422); } - (BOOL)isRedirect { - return ([self statusCode] == 301 || [self statusCode] == 302 || [self statusCode] == 303 || [self statusCode] == 307); + return ([self statusCode] == 301 || [self statusCode] == 302 || [self statusCode] == 303 || [self statusCode] == 307); } - (BOOL)isEmpty { - return ([self statusCode] == 201 || [self statusCode] == 204 || [self statusCode] == 304); + return ([self statusCode] == 201 || [self statusCode] == 204 || [self statusCode] == 304); } - (BOOL)isServiceUnavailable { - return ([self statusCode] == 503); + return ([self statusCode] == 503); } - (NSString*)contentType { - return ([[self allHeaderFields] objectForKey:@"Content-Type"]); + return ([[self allHeaderFields] objectForKey:@"Content-Type"]); } - (NSString*)contentLength { - return ([[self allHeaderFields] objectForKey:@"Content-Length"]); + return ([[self allHeaderFields] objectForKey:@"Content-Length"]); } - (NSString*)location { - return ([[self allHeaderFields] objectForKey:@"Location"]); + return ([[self allHeaderFields] objectForKey:@"Location"]); } - (BOOL)isHTML { - NSString* contentType = [self contentType]; - return (contentType && ([contentType rangeOfString:@"text/html" - options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0 || - [self isXHTML])); + NSString* contentType = [self contentType]; + return (contentType && ([contentType rangeOfString:@"text/html" + options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0 || + [self isXHTML])); } - (BOOL)isXHTML { - NSString* contentType = [self contentType]; - return (contentType && - [contentType rangeOfString:@"application/xhtml+xml" - options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0); + NSString* contentType = [self contentType]; + return (contentType && + [contentType rangeOfString:@"application/xhtml+xml" + options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0); } - (BOOL)isXML { - NSString* contentType = [self contentType]; - return (contentType && - [contentType rangeOfString:@"application/xml" - options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0); + NSString* contentType = [self contentType]; + return (contentType && + [contentType rangeOfString:@"application/xml" + options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0); } - (BOOL)isJSON { - NSString* contentType = [self contentType]; - return (contentType && - [contentType rangeOfString:@"application/json" - options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0); + NSString* contentType = [self contentType]; + return (contentType && + [contentType rangeOfString:@"application/json" + options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0); } @end diff --git a/Code/Network/RKURL.h b/Code/Network/RKURL.h index efc531e885..8fca84acf9 100644 --- a/Code/Network/RKURL.h +++ b/Code/Network/RKURL.h @@ -4,13 +4,13 @@ // // Created by Jeff Arena on 10/18/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,19 +24,19 @@ framework. RKURL is immutable, but provides numerous methods for constructing new RKURL instances where the received becomes the baseURL of the RKURL instance. - + Instances of RKURL are aware of: - + - the baseURL they were constructed against, if any - the resource path that was appended to that baseURL - any query parameters present in the URL - + ### Example - + NSDictionary *queryParams; queryParams = [NSDictionary dictionaryWithObjectsAndKeys:@"pitbull", @"username", @"pickles", @"password", nil]; - + RKURL *URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"/test" queryParameters:queryParams]; @@ -49,7 +49,7 @@ /** Creates and returns an RKURL object intialized with a provided base URL. - + @param baseURL The URL object with which to initialize the RKURL object. @return An RKURL object initialized with baseURL. */ @@ -58,7 +58,7 @@ /** Creates and returns an RKURL object intialized with a provided base URL and resource path. - + @param baseURL The URL object with which to initialize the RKURL object. @param resourcePath The resource path for the RKURL object. @return An RKURL object initialized with baseURL and resourcePath. @@ -68,7 +68,7 @@ /** Creates and returns an RKURL object intialized with a provided base URL, resource path, and a dictionary of query parameters. - + @param baseURL The URL object with which to initialize the RKURL object. @param resourcePath The resource path for the RKURL object. @param queryParameters The query parameters for the RKURL object. @@ -80,7 +80,7 @@ /** Creates and returns an RKURL object intialized with a base URL constructed from the specified base URL string. - + @param baseURLString The string with which to initialize the RKURL object. @return An RKURL object initialized with baseURLString. */ @@ -89,7 +89,7 @@ /** Creates and returns an RKURL object intialized with a base URL constructed from the specified base URL string and resource path. - + @param baseURLString The string with which to initialize the RKURL object. @param resourcePath The resource path for the RKURL object. @return An RKURL object initialized with baseURLString and resourcePath. @@ -100,7 +100,7 @@ Creates and returns an RKURL object intialized with a base URL constructed from the specified base URL string, resource path and a dictionary of query parameters. - + @param baseURLString The string with which to initialize the RKURL object. @param resourcePath The resource path for the RKURL object. @param queryParameters The query parameters for the RKURL object. @@ -112,9 +112,9 @@ /** Initializes an RKURL object with a base URL, a resource path string, and a dictionary of query parameters. - + `initWithBaseURL:resourcePath:queryParameters:` is the designated initializer. - + @param theBaseURL The NSURL with which to initialize the RKURL object. @param theResourcePath The resource path for the RKURL object. @param theQueryParameters The query parameters for the RKURL object. @@ -129,7 +129,7 @@ /** Returns the base URL of the receiver. - + The base URL includes everything up to the resource path, typically the portion that is repeated in every API call. */ @@ -137,7 +137,7 @@ /** Returns the resource path of the receiver. - + The resource path is the path portion of the complete URL beyond that contained in the baseURL. */ @@ -145,7 +145,7 @@ /** Returns the query component of a URL conforming to RFC 1808 as a dictionary. - + If the receiver does not conform to RFC 1808, returns nil just as `NSURL query` does. */ @@ -158,7 +158,7 @@ /** Returns a new RKURL object with a new resource path appended to its path. - + @param theResourcePath The resource path to append to the receiver's path. @return A new RKURL that refers to a new resource at theResourcePath. */ @@ -167,7 +167,7 @@ /** Returns a new RKURL object with a new resource path appended to its path and a dictionary of query parameters merged with the existing query component. - + @param theResourcePath The resource path to append to the receiver's path. @param theQueryParameters A dictionary of query parameters to merge with any existing query parameters. @@ -179,7 +179,7 @@ /** Returns a new RKURL object with a dictionary of query parameters merged with the existing query component. - + @param theQueryParameters A dictionary of query parameters to merge with any existing query parameters. @return A new RKURL that refers to the same resource as the receiver with a new @@ -190,7 +190,7 @@ /** Returns a new RKURL object with the baseURL of the receiver and a new resourcePath. - + @param newResourcePath The resource path to replace the value of resourcePath in the new RKURL object. @return An RKURL object with newResourcePath appended to the receiver's baseURL. @@ -200,28 +200,28 @@ /** Returns a new RKURL object with its resource path processed as a pattern and evaluated against the specified object. - + Resource paths may contain pattern strings prefixed by colons (":") that refer to key-value coding accessible properties on the provided object. - + For example: - + // Given an RKURL initialized as: - RKURL *myURL = [RKURL URLWithBaseURLString:@"http://restkit.org" + RKURL *myURL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"/paginate?per_page=:perPage&page=:page"]; - + // And a dictionary containing values: NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:@"25", @"perPage", @"5", @"page", nil]; - + // A new RKURL can be constructed by interpolating the dictionary with the original URL RKURL *interpolatedURL = [myURL URLByInterpolatingResourcePathWithObject:dictionary]; - + The absoluteString of this new URL would be: `http://restkit.org/paginate?per_page=25&page=5` - + @see RKPathMatcher - + @param object The object to call methods on for the pattern strings in the resource path. @return A new RKURL object with its resource path evaluated as a pattern and diff --git a/Code/Network/RKURL.m b/Code/Network/RKURL.m index db2547ba07..bcced9c11c 100644 --- a/Code/Network/RKURL.m +++ b/Code/Network/RKURL.m @@ -4,13 +4,13 @@ // // Created by Jeff Arena on 10/18/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -55,7 +55,7 @@ + (id)URLWithBaseURLString:(NSString *)baseURLString resourcePath:(NSString *)re return [self URLWithBaseURLString:baseURLString resourcePath:resourcePath queryParameters:nil]; } -+ (id)URLWithBaseURLString:(NSString *)baseURLString resourcePath:(NSString *)resourcePath queryParameters:(NSDictionary *)queryParameters { ++ (id)URLWithBaseURLString:(NSString *)baseURLString resourcePath:(NSString *)resourcePath queryParameters:(NSDictionary *)queryParameters { return [self URLWithBaseURL:[NSURL URLWithString:baseURLString] resourcePath:resourcePath queryParameters:queryParameters]; } @@ -67,14 +67,14 @@ - (id)initWithBaseURL:(NSURL *)theBaseURL resourcePath:(NSString *)theResourcePa NSMutableDictionary *mergedQueryParameters = [NSMutableDictionary dictionaryWithDictionary:[theBaseURL queryParameters]]; [mergedQueryParameters addEntriesFromDictionary:resourcePathQueryParameters]; [mergedQueryParameters addEntriesFromDictionary:theQueryParameters]; - + // Build the new URL path NSRange queryCharacterRange = [theResourcePath rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"?"]]; NSString *resourcePathWithoutQueryString = (queryCharacterRange.location == NSNotFound) ? theResourcePath : [theResourcePath substringToIndex:queryCharacterRange.location]; NSString *baseURLPath = [[theBaseURL path] isEqualToString:@"/"] ? @"" : [[theBaseURL path] stringByStandardizingPath]; NSString *completePath = resourcePathWithoutQueryString ? [baseURLPath stringByAppendingString:resourcePathWithoutQueryString] : baseURLPath; NSString* completePathWithQuery = [completePath stringByAppendingQueryParameters:mergedQueryParameters]; - + // NOTE: You can't safely use initWithString:relativeToURL: in a NSURL subclass, see http://www.openradar.me/9729706 // So we unfortunately convert into an NSURL before going back into an NSString -> RKURL NSURL* completeURL = [NSURL URLWithString:completePathWithQuery relativeToURL:theBaseURL]; @@ -83,23 +83,23 @@ - (id)initWithBaseURL:(NSURL *)theBaseURL resourcePath:(NSString *)theResourcePa [self release]; return nil; } - + self = [self initWithString:[completeURL absoluteString]]; if (self) { self.baseURL = theBaseURL; self.resourcePath = theResourcePath; } - + return self; } - (void)dealloc { - [baseURL release]; - baseURL = nil; - [resourcePath release]; - resourcePath = nil; - - [super dealloc]; + [baseURL release]; + baseURL = nil; + [resourcePath release]; + resourcePath = nil; + + [super dealloc]; } - (NSDictionary *)queryParameters { @@ -145,7 +145,7 @@ - (id)initWithString:(NSString *)URLString { if (self) { self.baseURL = self; } - + return self; } diff --git a/Code/ObjectMapping/ObjectMapping.h b/Code/ObjectMapping/ObjectMapping.h index b74383eb10..293fe2e6c7 100644 --- a/Code/ObjectMapping/ObjectMapping.h +++ b/Code/ObjectMapping/ObjectMapping.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/30/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/ObjectMapping/RKConfigurationDelegate.h b/Code/ObjectMapping/RKConfigurationDelegate.h index 18b920cc79..cc70f66364 100644 --- a/Code/ObjectMapping/RKConfigurationDelegate.h +++ b/Code/ObjectMapping/RKConfigurationDelegate.h @@ -14,7 +14,7 @@ RKObjectLoader configuration. An object conforming to the protocol can be used to set headers, authentication credentials, etc. - + RKClient and RKObjectManager conform to RKConfigurationDelegate to configure request and object loader instances they build. */ @@ -24,14 +24,14 @@ /** Configure a request before it is utilized - + @param request A request object being configured for dispatch */ - (void)configureRequest:(RKRequest *)request; /** Configure an object loader before it is utilized - + @param request An object loader being configured for dispatch */ - (void)configureObjectLoader:(RKObjectLoader *)objectLoader; diff --git a/Code/ObjectMapping/RKDynamicObjectMapping.h b/Code/ObjectMapping/RKDynamicObjectMapping.h index 23e09f00bc..c0c70b3cd5 100644 --- a/Code/ObjectMapping/RKDynamicObjectMapping.h +++ b/Code/ObjectMapping/RKDynamicObjectMapping.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 7/28/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -51,7 +51,7 @@ typedef RKObjectMapping *(^RKDynamicObjectMappingDelegateBlock)(id); /** A delegate to call back to determine the appropriate concrete object mapping to apply to the mappable data. - + @see RKDynamicObjectMappingDelegate */ @property (nonatomic, assign) id delegate; @@ -82,11 +82,11 @@ typedef RKObjectMapping *(^RKDynamicObjectMappingDelegateBlock)(id); /** Defines a dynamic mapping rule stating that when the value of the key property matches the specified value, the objectMapping should be used. - + For example, suppose that we have a JSON fragment for a person that we want to map differently based on the gender of the person. When the gender is 'male', we want to use the Boy class and when then the gender is 'female' we want to use the Girl class. We might define our dynamic mapping like so: - + RKDynamicObjectMapping* mapping = [RKDynamicObjectMapping dynamicMapping]; [mapping setObjectMapping:boyMapping whenValueOfKeyPath:@"gender" isEqualTo:@"male"]; [mapping setObjectMapping:boyMapping whenValueOfKeyPath:@"gender" isEqualTo:@"female"]; @@ -103,7 +103,7 @@ typedef RKObjectMapping *(^RKDynamicObjectMappingDelegateBlock)(id); /** Define an alias for the old class name for compatibility - + @deprecated */ @interface RKObjectDynamicMapping : RKDynamicObjectMapping diff --git a/Code/ObjectMapping/RKDynamicObjectMapping.m b/Code/ObjectMapping/RKDynamicObjectMapping.m index 9f6b2ec4d8..1ed3d39652 100644 --- a/Code/ObjectMapping/RKDynamicObjectMapping.m +++ b/Code/ObjectMapping/RKDynamicObjectMapping.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 7/28/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -55,7 +55,7 @@ - (id)init { if (self) { _matchers = [NSMutableArray new]; } - + return self; } @@ -74,9 +74,9 @@ - (void)setObjectMapping:(RKObjectMapping*)objectMapping whenValueOfKeyPath:(NSS - (RKObjectMapping*)objectMappingForDictionary:(NSDictionary*)data { NSAssert([data isKindOfClass:[NSDictionary class]], @"Dynamic object mapping can only be performed on NSDictionary mappables, got %@", NSStringFromClass([data class])); RKObjectMapping* mapping = nil; - + RKLogTrace(@"Performing dynamic object mapping for mappable data: %@", data); - + // Consult the declarative matchers first for (RKDynamicObjectMappingMatcher* matcher in _matchers) { if ([matcher isMatchForData:data]) { @@ -84,7 +84,7 @@ - (RKObjectMapping*)objectMappingForDictionary:(NSDictionary*)data { return matcher.objectMapping; } } - + // Otherwise consult the delegates if (self.delegate) { mapping = [self.delegate objectMappingForData:data]; @@ -93,14 +93,14 @@ - (RKObjectMapping*)objectMappingForDictionary:(NSDictionary*)data { return mapping; } } - - if (self.objectMappingForDataBlock) { + + if (self.objectMappingForDataBlock) { mapping = self.objectMappingForDataBlock(data); if (mapping) { RKLogTrace(@"Found dynamic delegateBlock match. objectMappingForDataBlock = %@", self.objectMappingForDataBlock); } } - + return mapping; } diff --git a/Code/ObjectMapping/RKErrorMessage.h b/Code/ObjectMapping/RKErrorMessage.h index 3541902ce4..5a20a993fa 100644 --- a/Code/ObjectMapping/RKErrorMessage.h +++ b/Code/ObjectMapping/RKErrorMessage.h @@ -4,13 +4,13 @@ // // Created by Jeremy Ellison on 5/10/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/ObjectMapping/RKErrorMessage.m b/Code/ObjectMapping/RKErrorMessage.m index f704255d03..6df33abada 100644 --- a/Code/ObjectMapping/RKErrorMessage.m +++ b/Code/ObjectMapping/RKErrorMessage.m @@ -4,13 +4,13 @@ // // Created by Jeremy Ellison on 5/10/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/ObjectMapping/RKMappingOperationQueue.h b/Code/ObjectMapping/RKMappingOperationQueue.h index cb5a3c2273..7d3b5c6acf 100644 --- a/Code/ObjectMapping/RKMappingOperationQueue.h +++ b/Code/ObjectMapping/RKMappingOperationQueue.h @@ -13,11 +13,11 @@ operation until the entire aggregate operation has completed. This is used by Core Data to connect all object relationships once the entire object graph has been mapped, rather than as each object is encountered. - + Designed as a lightweight workalike for NSOperationQueue, which was not usable do to its reliance on threading for concurrent operations. The threading was causing problems with managed objects due to MOC being thread specific. - + This class is not intended to be thread-safe and is used for queueing non-concurrent operations that will be executed within the object mapper only. It is not a general purpose work queue. @@ -29,28 +29,28 @@ /** Adds an NSOperation to the queue for later execution - + @param op The operation to enqueue */ - (void)addOperation:(NSOperation *)op; /** Adds an NSBlockOperation to the queue configured to executed the block passed - + @param block A block to wrap into an operation for later execution */ - (void)addOperationWithBlock:(void (^)(void))block; /** Returns the collection of operations in the queue - + @return A new aray containing the NSOperation objects in the order in which they were added to the queue */ - (NSArray *)operations; /** Returns the number of operations in the queue - + @return The number of operations in the queue. */ - (NSUInteger)operationCount; diff --git a/Code/ObjectMapping/RKMappingOperationQueue.m b/Code/ObjectMapping/RKMappingOperationQueue.m index 0beff6d49d..7f36a3eb0a 100644 --- a/Code/ObjectMapping/RKMappingOperationQueue.m +++ b/Code/ObjectMapping/RKMappingOperationQueue.m @@ -15,7 +15,7 @@ - (id)init { if (self) { _operations = [NSMutableArray new]; } - + return self; } diff --git a/Code/ObjectMapping/RKObjectAttributeMapping.h b/Code/ObjectMapping/RKObjectAttributeMapping.h index 49a054ea20..93e859ba23 100644 --- a/Code/ObjectMapping/RKObjectAttributeMapping.h +++ b/Code/ObjectMapping/RKObjectAttributeMapping.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 4/30/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -36,11 +36,11 @@ /** Returns YES if this attribute mapping targets the key of a nested dictionary. - + When an object mapping is configured to target mapping of nested content via [RKObjectMapping mapKeyOfNestedDictionaryToAttribute:], a special attribute mapping is defined that targets the key of the nested dictionary rather than a value within in. This method will return YES if this attribute mapping is configured in such a way. - + @see [RKObjectMapping mapKeyOfNestedDictionaryToAttribute:] @return YES if this attribute mapping targets a nesting key path */ diff --git a/Code/ObjectMapping/RKObjectAttributeMapping.m b/Code/ObjectMapping/RKObjectAttributeMapping.m index 883e82d191..d92dcee9ed 100644 --- a/Code/ObjectMapping/RKObjectAttributeMapping.m +++ b/Code/ObjectMapping/RKObjectAttributeMapping.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 4/30/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -38,7 +38,7 @@ - (id)initWithSourceKeyPath:(NSString *)sourceKeyPath andDestinationKeyPath:(NSS _sourceKeyPath = [sourceKeyPath retain]; _destinationKeyPath = [destinationKeyPath retain]; } - + return self; } @@ -50,7 +50,7 @@ - (id)copyWithZone:(NSZone *)zone { - (void)dealloc { [_sourceKeyPath release]; [_destinationKeyPath release]; - + [super dealloc]; } diff --git a/Code/ObjectMapping/RKObjectLoader.h b/Code/ObjectMapping/RKObjectLoader.h index 75c2e0433a..e80453f2b1 100644 --- a/Code/ObjectMapping/RKObjectLoader.h +++ b/Code/ObjectMapping/RKObjectLoader.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 8/8/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -36,10 +36,10 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction /** The delegate of an RKObjectLoader object must adopt the RKObjectLoaderDelegate protocol. Optional methods of the protocol allow the delegate to handle asynchronous object mapping operations performed - by the object loader. Also note that the RKObjectLoaderDelegate protocol incorporates the + by the object loader. Also note that the RKObjectLoaderDelegate protocol incorporates the RKRequestDelegate protocol and the delegate may provide implementations of methods from RKRequestDelegate as well. - + @see RKRequestDelegate */ @protocol RKObjectLoaderDelegate @@ -61,7 +61,7 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction - (void)objectLoader:(RKObjectLoader *)objectLoader didLoadObjects:(NSArray *)objects; /** - When implemented, sent to the delegate when the object loader has completed succesfully. + When implemented, sent to the delegate when the object loader has completed succesfully. If the load resulted in a collection of objects being mapped, only the first object in the collection will be sent with this delegate method. This method simplifies things when you know you are working with a single object reference. @@ -83,7 +83,7 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction /** Informs the delegate that the object loader has serialized the source object into a serializable representation for sending to the remote system. The serialization can be modified to allow customization of the request payload independent of mapping. - + @param objectLoader The object loader performing the serialization. @param sourceObject The object that was serialized. @param serialization The serialization of sourceObject to be sent to the remote backend for processing. @@ -92,25 +92,25 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction /** Sent when an object loader encounters a response status code or MIME Type that RestKit does not know how to handle. - + Response codes in the 2xx, 4xx, and 5xx range are all handled as you would expect. 2xx (successful) response codes are considered a successful content load and object mapping will be attempted. 4xx and 5xx are interpretted as errors and RestKit will attempt to object map an error out of the payload (provided the MIME Type is mappable) and will invoke objectLoader:didFailWithError: after constructing an NSError. Any other status code is considered - unexpected and will cause objectLoaderDidLoadUnexpectedResponse: to be invoked provided that you have provided + unexpected and will cause objectLoaderDidLoadUnexpectedResponse: to be invoked provided that you have provided an implementation in your delegate class. - + RestKit will also invoke objectLoaderDidLoadUnexpectedResponse: in the event that content is loaded, but there is not a parser registered to handle the MIME Type of the payload. This often happens when the remote backend system RestKit is talking to generates an HTML error page on failure. If your remote system returns content in a MIME Type other than application/json or application/xml, you must register the MIME Type and an appropriate parser with the [RKParserRegistry sharedParser] instance. - - Also note that in the event RestKit encounters an unexpected status code or MIME Type response an error will be - constructed and sent to the delegate via objectLoader:didFailsWithError: unless your delegate provides an + + Also note that in the event RestKit encounters an unexpected status code or MIME Type response an error will be + constructed and sent to the delegate via objectLoader:didFailsWithError: unless your delegate provides an implementation of objectLoaderDidLoadUnexpectedResponse:. It is recommended that you provide an implementation and attempt to handle common unexpected MIME types (particularly text/html and text/plain). - + @optional */ - (void)objectLoaderDidLoadUnexpectedResponse:(RKObjectLoader *)objectLoader; @@ -120,7 +120,7 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction to extract data from the parsed payload that is not object mapped, but is interesting for one reason or another. The mappableData will be made mutable via mutableCopy before the delegate method is invoked. - + Note that the mappable data is a pointer to a pointer to allow you to replace the mappable data with a new object to be mapped. You must dereference it to access the value. */ @@ -132,31 +132,25 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction * Wraps a request/response cycle and loads a remote object representation into local domain objects * * NOTE: When Core Data is linked into the application, the object manager will return instances of - * RKManagedObjectLoader instead of RKObjectLoader. RKManagedObjectLoader is a descendent class that + * RKManagedObjectLoader instead of RKObjectLoader. RKManagedObjectLoader is a descendent class that * includes Core Data specific mapping logic. */ -@interface RKObjectLoader : RKRequest { - RKObjectMappingProvider *_mappingProvider; - RKResponse* _response; - RKObjectMapping* _objectMapping; - RKObjectMappingResult* _result; - RKObjectMapping* _serializationMapping; - NSString* _serializationMIMEType; - NSObject* _sourceObject; - NSObject* _targetObject; +@interface RKObjectLoader : RKRequest { + id _sourceObject; + id _targetObject; dispatch_queue_t _mappingQueue; } /** The object that acts as the delegate of the receiving object loader. - + @see RKRequestDelegate */ @property (nonatomic, assign) id delegate; /** The block to invoke when the object loader fails due to an error. - + @see [RKObjectLoaderDelegate objectLoader:didFailWithError:] */ @property (nonatomic, copy) RKObjectLoaderDidFailWithErrorBlock onDidFailWithError; @@ -164,7 +158,7 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction /** The block to invoke when the object loader has completed object mapping and the consumer wishes to retrieve a single object from the mapping result. - + @see [RKObjectLoaderDelegate objectLoader:didLoadObject:] @see RKObjectMappingResult */ @@ -173,7 +167,7 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction /** The block to invoke when the object loader has completed object mapping and the consumer wishes to retrieve an collections of objects from the mapping result. - + @see [RKObjectLoaderDelegate objectLoader:didLoadObjects:] @see RKObjectMappingResult */ @@ -184,7 +178,7 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction wishes to retrieve the entire mapping result as a dictionary. Each key within the dictionary will correspond to a mapped keyPath within the source JSON/XML and the value will be the object mapped result. - + @see [RKObjectLoaderDelegate objectLoader:didLoadObjects:] @see RKObjectMappingResult */ @@ -205,7 +199,7 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction /** A mapping provider containing object mapping configurations for mapping remote object representations into local domain objects. - + @see RKObjectMappingProvider */ @property (nonatomic, retain) RKObjectMappingProvider *mappingProvider; @@ -213,7 +207,7 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction /** * The underlying response object for this loader */ -@property (nonatomic, readonly) RKResponse *response; +@property (nonatomic, retain, readonly) RKResponse *response; /** * The mapping result that was produced after the request finished loading and @@ -245,7 +239,7 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction /** The object being serialized for transport. This object will be transformed into a serialization in the serializationMIMEType using the serializationMapping. - + @see RKObjectSerializer */ @property (nonatomic, retain) NSObject *sourceObject; @@ -268,7 +262,7 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction /** Initialize and return an autoreleased object loader targeting a remote URL using a mapping provider - + @param URL A RestKit RKURL targetting a particular baseURL and resourcePath @param mappingProvider A mapping provider containing object mapping configurations for processing loaded payloads */ @@ -276,7 +270,7 @@ typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *diction /** Initialize and return an autoreleased object loader targeting a remote URL using a mapping provider - + @param URL A RestKit RKURL targetting a particular baseURL and resourcePath @param mappingProvider A mapping provider containing object mapping configurations for processing loaded payloads */ diff --git a/Code/ObjectMapping/RKObjectLoader.m b/Code/ObjectMapping/RKObjectLoader.m index 4710ce4d69..eed310762f 100644 --- a/Code/ObjectMapping/RKObjectLoader.m +++ b/Code/ObjectMapping/RKObjectLoader.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 8/8/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -37,19 +37,29 @@ - (void)updateInternalCacheDate; - (void)postRequestDidFailWithErrorNotification:(NSError *)error; @end +@interface RKObjectLoader () +@property (nonatomic, assign, readwrite, getter = isLoaded) BOOL loaded; +@property (nonatomic, assign, readwrite, getter = isLoading) BOOL loading; +@property (nonatomic, retain, readwrite) RKResponse *response; +@end + @implementation RKObjectLoader -@synthesize mappingProvider = _mappingProvider, response = _response; -@synthesize targetObject = _targetObject, objectMapping = _objectMapping; +@synthesize mappingProvider = _mappingProvider; +@synthesize targetObject = _targetObject; +@synthesize objectMapping = _objectMapping; @synthesize result = _result; @synthesize serializationMapping = _serializationMapping; @synthesize serializationMIMEType = _serializationMIMEType; @synthesize sourceObject = _sourceObject; @synthesize mappingQueue = _mappingQueue; -@synthesize onDidFailWithError; -@synthesize onDidLoadObject; -@synthesize onDidLoadObjects; -@synthesize onDidLoadObjectsDictionary; +@synthesize onDidFailWithError = _onDidFailWithError; +@synthesize onDidLoadObject = _onDidLoadObject; +@synthesize onDidLoadObjects = _onDidLoadObjects; +@synthesize onDidLoadObjectsDictionary = _onDidLoadObjectsDictionary; +@dynamic loaded; +@dynamic loading; +@dynamic response; + (id)loaderWithURL:(RKURL *)URL mappingProvider:(RKObjectMappingProvider *)mappingProvider { return [[[self alloc] initWithURL:URL mappingProvider:mappingProvider] autorelease]; @@ -61,7 +71,7 @@ - (id)initWithURL:(RKURL *)URL mappingProvider:(RKObjectMappingProvider *)mappin _mappingProvider = [mappingProvider retain]; _mappingQueue = [RKObjectManager defaultMappingQueue]; } - + return self; } @@ -70,41 +80,37 @@ - (void)dealloc { _mappingProvider = nil; [_sourceObject release]; _sourceObject = nil; - [_targetObject release]; - _targetObject = nil; - [_response release]; - _response = nil; - [_objectMapping release]; - _objectMapping = nil; + [_targetObject release]; + _targetObject = nil; + [_objectMapping release]; + _objectMapping = nil; [_result release]; _result = nil; [_serializationMIMEType release]; _serializationMIMEType = nil; [_serializationMapping release]; - _serializationMapping = nil; - [onDidFailWithError release]; - onDidFailWithError = nil; - [onDidLoadObject release]; - onDidLoadObject = nil; - [onDidLoadObjects release]; - onDidLoadObjects = nil; - [onDidLoadObjectsDictionary release]; - onDidLoadObjectsDictionary = nil; - - [super dealloc]; + _serializationMapping = nil; + [_onDidFailWithError release]; + _onDidFailWithError = nil; + [_onDidLoadObject release]; + _onDidLoadObject = nil; + [_onDidLoadObjects release]; + _onDidLoadObjects = nil; + [_onDidLoadObjectsDictionary release]; + _onDidLoadObjectsDictionary = nil; + + [super dealloc]; } - (void)reset { [super reset]; - [_response release]; - _response = nil; [_result release]; _result = nil; } - (void)informDelegateOfError:(NSError *)error { [(NSObject*)_delegate objectLoader:self didFailWithError:error]; - + if (self.onDidFailWithError) { self.onDidFailWithError(error); } @@ -115,8 +121,8 @@ - (void)informDelegateOfError:(NSError *)error { // NOTE: This method is significant because the notifications posted are used by // RKRequestQueue to remove requests from the queue. All requests need to be finalized. - (void)finalizeLoad:(BOOL)successful { - _isLoading = NO; - _isLoaded = successful; + self.loading = NO; + self.loaded = successful; if ([self.delegate respondsToSelector:@selector(objectLoaderDidFinishLoading:)]) { [(NSObject*)self.delegate performSelectorOnMainThread:@selector(objectLoaderDidFinishLoading:) @@ -129,37 +135,37 @@ - (void)finalizeLoad:(BOOL)successful { // Invoked on the main thread. Inform the delegate. - (void)informDelegateOfObjectLoadWithResultDictionary:(NSDictionary*)resultDictionary { NSAssert([NSThread isMainThread], @"RKObjectLoaderDelegate callbacks must occur on the main thread"); - - RKObjectMappingResult* result = [RKObjectMappingResult mappingResultWithDictionary:resultDictionary]; - + + RKObjectMappingResult* result = [RKObjectMappingResult mappingResultWithDictionary:resultDictionary]; + // Dictionary callback if ([self.delegate respondsToSelector:@selector(objectLoader:didLoadObjectDictionary:)]) { [(NSObject*)self.delegate objectLoader:self didLoadObjectDictionary:[result asDictionary]]; } - + if (self.onDidLoadObjectsDictionary) { self.onDidLoadObjectsDictionary([result asDictionary]); } - + // Collection callback if ([self.delegate respondsToSelector:@selector(objectLoader:didLoadObjects:)]) { [(NSObject*)self.delegate objectLoader:self didLoadObjects:[result asCollection]]; } - + if (self.onDidLoadObjects) { self.onDidLoadObjects([result asCollection]); } - + // Singular object callback if ([self.delegate respondsToSelector:@selector(objectLoader:didLoadObject:)]) { [(NSObject*)self.delegate objectLoader:self didLoadObject:[result asObject]]; } - + if (self.onDidLoadObject) { self.onDidLoadObject([result asObject]); } - - [self finalizeLoad:YES]; + + [self finalizeLoad:YES]; } #pragma mark - Subclass Hooks @@ -184,13 +190,13 @@ - (RKObjectMappingResult*)mapResponseWithMappingProvider:(RKObjectMappingProvide // with the appropriate MIME Type with no content (such as for a successful PUT or DELETE). Make sure we don't generate an error // in these cases id bodyAsString = [self.response bodyAsString]; - RKLogTrace(@"bodyAsString: %@", bodyAsString); + RKLogTrace(@"bodyAsString: %@", bodyAsString); if (bodyAsString == nil || [[bodyAsString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length] == 0) { RKLogDebug(@"Mapping attempted on empty response body..."); if (self.targetObject) { return [RKObjectMappingResult mappingResultWithDictionary:[NSDictionary dictionaryWithObject:self.targetObject forKey:@""]]; } - + return [RKObjectMappingResult mappingResultWithDictionary:[NSDictionary dictionary]]; } @@ -230,20 +236,20 @@ - (RKObjectMappingDefinition *)configuredObjectMapping { if (self.objectMapping) { return self.objectMapping; } - + return [self.mappingProvider objectMappingForResourcePath:self.resourcePath]; } - (RKObjectMappingResult*)performMapping:(NSError**)error { NSAssert(_sentSynchronously || ![NSThread isMainThread], @"Mapping should occur on a background thread"); - + RKObjectMappingProvider* mappingProvider; RKObjectMappingDefinition *configuredObjectMapping = [self configuredObjectMapping]; if (configuredObjectMapping) { mappingProvider = [RKObjectMappingProvider mappingProvider]; NSString *rootKeyPath = configuredObjectMapping.rootKeyPath ? configuredObjectMapping.rootKeyPath : @""; [mappingProvider setMapping:configuredObjectMapping forKeyPath:rootKeyPath]; - + // Copy the error mapping from our configured mappingProvider mappingProvider.errorMapping = self.mappingProvider.errorMapping; } else { @@ -287,11 +293,11 @@ - (BOOL)isResponseMappable { [[NSNotificationCenter defaultCenter] postNotificationName:RKServiceDidBecomeUnavailableNotification object:self]; } - if ([self.response isFailure]) { + if ([self.response isFailure]) { [self informDelegateOfError:self.response.failureError]; - + [self didFailLoadWithError:self.response.failureError]; - return NO; + return NO; } else if ([self.response isNoContent]) { // The No Content (204) response will never have a message body or a MIME Type. id resultDictionary = nil; @@ -304,7 +310,7 @@ - (BOOL)isResponseMappable { } [self informDelegateOfObjectLoadWithResultDictionary:resultDictionary]; return NO; - } else if (NO == [self canParseMIMEType:[self.response MIMEType]]) { + } else if (NO == [self canParseMIMEType:[self.response MIMEType]]) { // We can't parse the response, it's unmappable regardless of the status code RKLogWarning(@"Encountered unexpected response with status code: %ld (MIME Type: %@ -> URL: %@)", (long) self.response.statusCode, self.response.MIMEType, self.URL); NSError* error = [NSError errorWithDomain:RKErrorDomain code:RKObjectLoaderUnexpectedResponseError userInfo:nil]; @@ -322,10 +328,10 @@ - (BOOL)isResponseMappable { } else if ([self.response isError]) { // This is an error and we can map the MIME Type of the response [self handleResponseError]; - return NO; + return NO; } - return YES; + return YES; } - (void)handleResponseError { @@ -359,7 +365,7 @@ - (BOOL)prepareURLRequest { [self didFailLoadWithError:error]; return NO; } - + if ([self.delegate respondsToSelector:@selector(objectLoader:didSerializeSourceObject:toSerialization:)]) { [self.delegate objectLoader:self didSerializeSourceObject:self.sourceObject toSerialization:¶ms]; } @@ -381,15 +387,15 @@ - (void)didFailLoadWithError:(NSError *)error { NSParameterAssert(error); NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - if (_cachePolicy & RKRequestCachePolicyLoadOnError && - [self.cache hasResponseForRequest:self]) { + if (_cachePolicy & RKRequestCachePolicyLoadOnError && + [self.cache hasResponseForRequest:self]) { - [self didFinishLoad:[self.cache responseForRequest:self]]; - } else { + [self didFinishLoad:[self.cache responseForRequest:self]]; + } else { if ([_delegate respondsToSelector:@selector(request:didFailLoadWithError:)]) { [_delegate request:self didFailLoadWithError:error]; } - + if (self.onDidFailLoadWithError) { self.onDidFailLoadWithError(error); } @@ -401,7 +407,10 @@ - (void)didFailLoadWithError:(NSError *)error { object:self userInfo:userInfo]; } - [self informDelegateOfError:error]; + + if (! self.isCancelled) { + [self informDelegateOfError:error]; + } [self finalizeLoad:NO]; } @@ -412,36 +421,34 @@ - (void)didFailLoadWithError:(NSError *)error { // NOTE: We do NOT call super here. We are overloading the default behavior from RKRequest - (void)didFinishLoad:(RKResponse*)response { NSAssert([NSThread isMainThread], @"RKObjectLoaderDelegate callbacks must occur on the main thread"); - _response = [response retain]; + self.response = response; - if ((_cachePolicy & RKRequestCachePolicyEtag) && [response isNotModified]) { - [_response release]; - _response = nil; - _response = [[self.cache responseForRequest:self] retain]; - NSAssert(_response, @"Unexpectedly loaded nil response from cache"); + if ((_cachePolicy & RKRequestCachePolicyEtag) && [response isNotModified]) { + self.response = [self.cache responseForRequest:self]; + NSAssert(self.response, @"Unexpectedly loaded nil response from cache"); [self updateInternalCacheDate]; - } + } - if (![_response wasLoadedFromCache] && [_response isSuccessful] && (_cachePolicy != RKRequestCachePolicyNone)) { - [self.cache storeResponse:_response forRequest:self]; - } + if (![self.response wasLoadedFromCache] && [self.response isSuccessful] && (_cachePolicy != RKRequestCachePolicyNone)) { + [self.cache storeResponse:self.response forRequest:self]; + } if ([_delegate respondsToSelector:@selector(request:didLoadResponse:)]) { - [_delegate request:self didLoadResponse:_response]; + [_delegate request:self didLoadResponse:self.response]; } - + if (self.onDidLoadResponse) { - self.onDidLoadResponse(_response); + self.onDidLoadResponse(self.response); } // Post the notification - NSDictionary* userInfo = [NSDictionary dictionaryWithObject:_response + NSDictionary* userInfo = [NSDictionary dictionaryWithObject:self.response forKey:RKRequestDidLoadResponseNotificationUserInfoResponseKey]; [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidLoadResponseNotification object:self userInfo:userInfo]; - if ([self isResponseMappable]) { + if ([self isResponseMappable]) { // Determine if we are synchronous here or not. if (_sentSynchronously) { NSError* error = nil; @@ -454,7 +461,7 @@ - (void)didFinishLoad:(RKResponse*)response { } else { [self performMappingInDispatchQueue]; } - } + } } - (void)setMappingQueue:(dispatch_queue_t)newMappingQueue { @@ -487,13 +494,13 @@ + (id)loaderWithResourcePath:(NSString*)resourcePath objectManager:(RKObjectMana return [[[self alloc] initWithResourcePath:resourcePath objectManager:objectManager delegate:delegate] autorelease]; } -- (id)initWithResourcePath:(NSString*)resourcePath objectManager:(RKObjectManager*)objectManager delegate:(id)theDelegate { - if ((self = [self initWithURL:[objectManager.baseURL URLByAppendingResourcePath:resourcePath] mappingProvider:objectManager.mappingProvider])) { +- (id)initWithResourcePath:(NSString*)resourcePath objectManager:(RKObjectManager*)objectManager delegate:(id)theDelegate { + if ((self = [self initWithURL:[objectManager.baseURL URLByAppendingResourcePath:resourcePath] mappingProvider:objectManager.mappingProvider])) { [objectManager.client configureRequest:self]; _delegate = theDelegate; - } - - return self; + } + + return self; } @end diff --git a/Code/ObjectMapping/RKObjectLoader_Internals.h b/Code/ObjectMapping/RKObjectLoader_Internals.h index f6593f7991..7f745bed8b 100644 --- a/Code/ObjectMapping/RKObjectLoader_Internals.h +++ b/Code/ObjectMapping/RKObjectLoader_Internals.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/13/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/ObjectMapping/RKObjectManager.h b/Code/ObjectMapping/RKObjectManager.h index 5bdcd54cf9..fc6f3adc23 100644 --- a/Code/ObjectMapping/RKObjectManager.h +++ b/Code/ObjectMapping/RKObjectManager.h @@ -4,13 +4,13 @@ // // Created by Jeremy Ellison on 8/14/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -40,9 +40,9 @@ extern NSString* const RKObjectManagerDidBecomeOfflineNotification; extern NSString* const RKObjectManagerDidBecomeOnlineNotification; typedef enum { - RKObjectManagerNetworkStatusUnknown, - RKObjectManagerNetworkStatusOffline, - RKObjectManagerNetworkStatusOnline + RKObjectManagerNetworkStatusUnknown, + RKObjectManagerNetworkStatusOffline, + RKObjectManagerNetworkStatusOnline } RKObjectManagerNetworkStatus; @class RKManagedObjectStore; @@ -53,44 +53,44 @@ typedef enum { domain objects via the RKObjectMapper. It is also capable of serializing local objects and sending them to a remote system for processing. The object manager strives to hide the developer from the details of configuring an RKRequest, processing an RKResponse, parsing any data returned by the remote system, and - running the parsed data through the object mapper. - + running the parsed data through the object mapper. +

Shared Manager Instance

- + Multiple instances of RKObjectManager may be used in parallel, but the first instance initialized is automatically configured as the sharedManager instance. The shared instance can be changed at runtime if so desired. See sharedManager and setSharedManager for details. - +

Configuring the Object Manager

- + The object mapper must be configured before object can be loaded from or transmitted to your remote backend system. Configuration consists of specifying the desired MIME types to be used during loads and serialization, registering object mappings to use for mapping and serialization, registering routes, and optionally configuring an instance of the managed object store (for Core Data). - +

MIME Types

- + MIME Types are used for two purposes within RestKit: - + 1. Content Negotiation. RestKit leverages the HTTP Accept header to specify the desired representation of content when contacting a remote web service. You can specify the MIME Type to use via the acceptMIMEType method. The default MIME Type is RKMIMETypeJSON (application/json). If the remote web service responds with content in a different MIME Type than specified, RestKit will attempt to parse it by consulting the [parser registry][RKParserRegistry parserForMIMEType:]. - Failure to find a parser for the returned content will result in an unexpected response invocation of + Failure to find a parser for the returned content will result in an unexpected response invocation of [RKObjectLoaderDelegate objectLoaderDidLoadUnexpectedResponse]. 1. Serialization. RestKit can be used to transport local object representation back to the remote web server for processing by serializing them into an RKRequestSerializable representation. The desired serialization format is configured by setting the serializationMIMEType property. RestKit currently supports serialization to RKMIMETypeFormURLEncoded and RKMIMETypeJSON. The serialization rules themselves are expressed via an instance of RKObjectMapping. - +

The Mapping Provider

- + RestKit determines how to map and serialize objects by consulting the mappingProvider. The mapping provider is responsible for providing instances of RKObjectMapper with object mappings that should be used for transforming mappable data into object representations. When you ask the object manager to load or send objects for you, the mappingProvider instance will be used for the object mapping operations constructed for you. In this way, the mappingProvider is the central registry for the knowledge about how objects in your application are mapped. - + Mappings are registered by constructing instances of RKObjectMapping and registering them with the provider: ` RKObjectManager* manager = [RKObjectManager managerWithBaseURL:myBaseURL]; @@ -98,22 +98,22 @@ typedef enum { [mapping mapAttributes:@"title", @"body", @"publishedAt", nil]; [manager.mappingProvider setObjectMapping:articleMapping forKeyPath:@"article"]; - // Generate an inverse mapping for transforming Article -> NSMutableDictionary. + // Generate an inverse mapping for transforming Article -> NSMutableDictionary. [manager.mappingProvider setSerializationMapping:[articleMapping inverseMapping] forClass:[Article class]];` - +

Configuring Routes

- + Routing is the process of transforming objects and actions (as defined by HTTP verbs) into resource paths. RestKit ships - +

Initializing a Core Data Object Store

Loading Remote Objects

- +

Routing & Object Serialization

- +

Default Error Mapping

- + When an instance of RKObjectManager is configured, the RKObjectMappingProvider - instance configured + instance configured */ @interface RKObjectManager : NSObject @@ -163,7 +163,7 @@ typedef enum { /** Initializes a newly created object manager with a specified baseURL. - + @param baseURL A baseURL to initialize the underlying client instance with @return The newly initialized RKObjectManager object */ @@ -180,7 +180,7 @@ typedef enum { The base URL of the underlying RKClient instance. Object loader and paginator instances built through the object manager are relative to this URL. - + @see RKClient @return The baseURL of the client. */ @@ -255,17 +255,17 @@ typedef enum { Returns the class of object loader instances built through the manager. When Core Data has been configured, instances of RKManagedObjectLoader will be emitted by the manager. Otherwise RKObjectLoader is used. - + @return RKObjectLoader OR RKManagedObjectLoader */ - (Class)objectLoaderClass; /** Creates and returns an RKObjectLoader or RKManagedObjectLoader instance targeting the specified resourcePath. - + The object loader instantiated will be initialized with an RKURL built by appending the resourcePath to the baseURL of the client. The loader will then be configured with object mapping configuration from the manager and request configuration from the client. - + @param resourcePath A resource to use when building the URL to initialize the object loader instance. @return The newly created object loader instance. @see RKURL @@ -275,10 +275,10 @@ typedef enum { /** Creates and returns an RKObjectLoader or RKManagedObjectLoader instance targeting the specified URL. - + The object loader instantiated will be initialized with URL and will then be configured with object mapping configuration from the manager and request configuration from the client. - + @param URL The URL with which to initialize the object loader. @return The newly created object loader instance. @see RKURL @@ -288,13 +288,13 @@ typedef enum { /** Creates and returns an RKObjectLoader or RKManagedObjectLoader instance for an object instance. - + The object loader instantiated will be initialized with a URL built by evaluating the object with the router to construct a resource path and then appending that resource path to the baseURL of the client. - The loader will then be configured with object mapping configuration from the manager and request + The loader will then be configured with object mapping configuration from the manager and request configuration from the client. The specified object will be the target of the object loader and will have any returned content mapped back onto the instance. - + @param object The object with which to initialize the object loader. @return The newly created object loader instance. @see RKObjectLoader @@ -304,10 +304,10 @@ typedef enum { /** Creates and returns an RKObjectPaginator instance targeting the specified resource path pattern. - - The paginator instantiated will be initialized with an RKURL built by appending the resourcePathPattern to the - baseURL of the client. - + + The paginator instantiated will be initialized with an RKURL built by appending the resourcePathPattern to the + baseURL of the client. + @return The newly created paginator instance. @see RKObjectMappingProvider @see RKObjectPaginator @@ -322,7 +322,7 @@ typedef enum { the mapping of complex payloads spanning multiple types (i.e. a search operation returning Articles & Comments in one payload). Ruby on Rails JSON serialization is an example of such a conformant system. */ - + /** Create and send an asynchronous GET request to load the objects at the resource path and call back the delegate with the loaded objects. Remote objects will be mapped to local objects by consulting the keyPath registrations @@ -334,7 +334,7 @@ typedef enum { /// @name Mappable Object Loaders /** - Fetch the data for a mappable object by performing an HTTP GET. + Fetch the data for a mappable object by performing an HTTP GET. */ - (void)getObject:(id)object delegate:(id)delegate; @@ -362,9 +362,9 @@ typedef enum { Load the objects at the specified resource path and perform object mapping on the response payload. Prior to sending the object loader, the block will be invoked to allow you to configure the object loader as you see fit. This can be used to change the response type, set custom parameters, choose an object mapping, etc. - + For example: - + - (void)loadObjectUsingBlockExample { [[RKObjectManager sharedManager] loadObjectsAtResourcePath:@"/monkeys.json" usingBlock:^(RKObjectLoader* loader) { loader.objectMapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[Monkey class]]; @@ -376,9 +376,9 @@ typedef enum { /* Configure and send an object loader after yielding it to a block for configuration. This allows for very succinct on-the-fly configuration of the request without obtaining an object reference via objectLoaderForObject: and then sending it yourself. - + For example: - + - (BOOL)changePassword:(NSString*)newPassword error:(NSError**)error { if ([self validatePassword:newPassword error:error]) { self.password = newPassword; @@ -399,28 +399,28 @@ typedef enum { /** GET a remote object instance and yield the object loader to the block before sending - + @see sendObject:method:delegate:block */ - (void)getObject:(id)object usingBlock:(RKObjectLoaderBlock)block; /** POST a remote object instance and yield the object loader to the block before sending - + @see sendObject:method:delegate:block */ - (void)postObject:(id)object usingBlock:(RKObjectLoaderBlock)block; /** PUT a remote object instance and yield the object loader to the block before sending - + @see sendObject:method:delegate:block */ - (void)putObject:(id)object usingBlock:(RKObjectLoaderBlock)block; /** DELETE a remote object instance and yield the object loader to the block before sending - + @see sendObject:method:delegate:block */ - (void)deleteObject:(id)object usingBlock:(RKObjectLoaderBlock)block; @@ -440,7 +440,7 @@ typedef enum { /* NOTE: - + The mapResponseWith: family of methods have been deprecated by the support for object mapping selection using resourcePath's */ diff --git a/Code/ObjectMapping/RKObjectManager.m b/Code/ObjectMapping/RKObjectManager.m index a55527094f..846ffb3318 100644 --- a/Code/ObjectMapping/RKObjectManager.m +++ b/Code/ObjectMapping/RKObjectManager.m @@ -4,13 +4,13 @@ // // Created by Jeremy Ellison on 8/14/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -36,6 +36,10 @@ /////////////////////////////////// +@interface RKObjectManager () +@property (nonatomic, assign, readwrite) RKObjectManagerNetworkStatus networkStatus; +@end + @implementation RKObjectManager @synthesize client = _client; @@ -66,46 +70,54 @@ + (void)setDefaultMappingQueue:(dispatch_queue_t)newDefaultMappingQueue { } } -- (id)initWithBaseURL:(RKURL *)baseURL { +- (id)init { self = [super init]; - if (self) { + if (self) { _mappingProvider = [RKObjectMappingProvider new]; _router = [RKObjectRouter new]; - _client = [[RKClient alloc] initWithBaseURL:baseURL]; _networkStatus = RKObjectManagerNetworkStatusUnknown; - - self.acceptMIMEType = RKMIMETypeJSON; + self.serializationMIMEType = RKMIMETypeFormURLEncoded; self.mappingQueue = [RKObjectManager defaultMappingQueue]; - + // Setup default error message mappings RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[RKErrorMessage class]]; errorMapping.rootKeyPath = @"errors"; [errorMapping mapKeyPath:@"" toAttribute:@"errorMessage"]; _mappingProvider.errorMapping = errorMapping; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(reachabilityChanged:) - name:RKReachabilityDidChangeNotification - object:_client.reachabilityObserver]; - + [self addObserver:self + forKeyPath:@"client.reachabilityObserver" + options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial + context:nil]; + // Set shared manager if nil if (nil == sharedManager) { [RKObjectManager setSharedManager:self]; } - } - - return self; + } + + return self; +} + +- (id)initWithBaseURL:(RKURL *)baseURL { + self = [self init]; + if (self) { + self.client = [RKClient clientWithBaseURL:baseURL]; + self.acceptMIMEType = RKMIMETypeJSON; + } + + return self; } + (RKObjectManager *)sharedManager { - return sharedManager; + return sharedManager; } + (void)setSharedManager:(RKObjectManager *)manager { - [manager retain]; - [sharedManager release]; - sharedManager = manager; + [manager retain]; + [sharedManager release]; + sharedManager = manager; } + (RKObjectManager *)managerWithBaseURLString:(NSString *)baseURLString { @@ -113,45 +125,79 @@ + (RKObjectManager *)managerWithBaseURLString:(NSString *)baseURLString { } + (RKObjectManager *)managerWithBaseURL:(NSURL *)baseURL { - RKObjectManager *manager = [[[self alloc] initWithBaseURL:baseURL] autorelease]; + RKObjectManager *manager = [[[self alloc] initWithBaseURL:baseURL] autorelease]; return manager; } - (void)dealloc { + [self removeObserver:self forKeyPath:@"client.reachabilityObserver"]; [[NSNotificationCenter defaultCenter] removeObserver:self]; - + [_router release]; _router = nil; - [_client release]; - _client = nil; + self.client = nil; [_objectStore release]; _objectStore = nil; [_serializationMIMEType release]; _serializationMIMEType = nil; [_mappingProvider release]; _mappingProvider = nil; - + [super dealloc]; } - (BOOL)isOnline { - return (_networkStatus == RKObjectManagerNetworkStatusOnline); + return (_networkStatus == RKObjectManagerNetworkStatusOnline); } - (BOOL)isOffline { - return (_networkStatus == RKObjectManagerNetworkStatusOffline); + return (_networkStatus == RKObjectManagerNetworkStatusOffline); +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if ([keyPath isEqualToString:@"client.reachabilityObserver"]) { + [self reachabilityObserverDidChange:change]; + } +} + +- (void)reachabilityObserverDidChange:(NSDictionary *)change { + RKReachabilityObserver *oldReachabilityObserver = [change objectForKey:NSKeyValueChangeOldKey]; + RKReachabilityObserver *newReachabilityObserver = [change objectForKey:NSKeyValueChangeNewKey]; + + if (! [oldReachabilityObserver isEqual:[NSNull null]]) { + RKLogDebug(@"Reachability observer changed for RKClient %@ of RKObjectManager %@, stopping observing reachability changes", self.client, self); + [[NSNotificationCenter defaultCenter] removeObserver:self name:RKReachabilityDidChangeNotification object:oldReachabilityObserver]; + } + + if (! [newReachabilityObserver isEqual:[NSNull null]]) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reachabilityChanged:) + name:RKReachabilityDidChangeNotification + object:newReachabilityObserver]; + + RKLogDebug(@"Reachability observer changed for client %@ of object manager %@, starting observing reachability changes", self.client, self); + } + + // Initialize current Network Status + if ([self.client.reachabilityObserver isReachabilityDetermined]) { + BOOL isNetworkReachable = [self.client.reachabilityObserver isNetworkReachable]; + self.networkStatus = isNetworkReachable ? RKObjectManagerNetworkStatusOnline : RKObjectManagerNetworkStatusOffline; + } else { + self.networkStatus = RKObjectManagerNetworkStatusUnknown; + } } - (void)reachabilityChanged:(NSNotification *)notification { - BOOL isHostReachable = [self.client.reachabilityObserver isNetworkReachable]; + BOOL isHostReachable = [self.client.reachabilityObserver isNetworkReachable]; - _networkStatus = isHostReachable ? RKObjectManagerNetworkStatusOnline : RKObjectManagerNetworkStatusOffline; + _networkStatus = isHostReachable ? RKObjectManagerNetworkStatusOnline : RKObjectManagerNetworkStatusOffline; - if (isHostReachable) { - [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOnlineNotification object:self]; - } else { - [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOfflineNotification object:self]; - } + if (isHostReachable) { + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOnlineNotification object:self]; + } else { + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOfflineNotification object:self]; + } } - (void)setAcceptMIMEType:(NSString *)MIMEType { @@ -170,7 +216,7 @@ - (Class)objectLoaderClass { if (self.objectStore && managedObjectLoaderClass) { return managedObjectLoaderClass; } - + return [RKObjectLoader class]; } @@ -186,7 +232,7 @@ - (id)loaderWithURL:(RKURL *)URL { [(RKManagedObjectLoader *)loader setObjectStore:self.objectStore]; } [self configureObjectLoader:loader]; - + return loader; } @@ -196,7 +242,7 @@ - (NSURL *)baseURL { - (RKObjectPaginator *)paginatorWithResourcePathPattern:(NSString *)resourcePathPattern { RKURL *patternURL = [[self baseURL] URLByAppendingResourcePath:resourcePathPattern]; - RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:self.mappingProvider]; paginator.configurationDelegate = self; return paginator; @@ -209,50 +255,50 @@ - (id)loaderForObject:(id)object method:(RKRequestMethod)method { loader.sourceObject = object; loader.serializationMIMEType = self.serializationMIMEType; loader.serializationMapping = [self.mappingProvider serializationMappingForClass:[object class]]; - + RKObjectMappingDefinition *objectMapping = resourcePath ? [self.mappingProvider objectMappingForResourcePath:resourcePath] : nil; if (objectMapping == nil || ([objectMapping isKindOfClass:[RKObjectMapping class]] && [object isMemberOfClass:[(RKObjectMapping *)objectMapping objectClass]])) { loader.targetObject = object; } else { loader.targetObject = nil; } - - return loader; + + return loader; } - (void)loadObjectsAtResourcePath:(NSString *)resourcePath delegate:(id)delegate { - RKObjectLoader *loader = [self loaderWithResourcePath:resourcePath]; + RKObjectLoader *loader = [self loaderWithResourcePath:resourcePath]; loader.delegate = delegate; - loader.method = RKRequestMethodGET; + loader.method = RKRequestMethodGET; - [loader send]; + [loader send]; } ///////////////////////////////////////////////////////////// #pragma mark - Object Instance Loaders - (void)getObject:(id)object delegate:(id)delegate { - RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodGET]; + RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodGET]; loader.delegate = delegate; - [loader send]; + [loader send]; } - (void)postObject:(id)object delegate:(id)delegate { - RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodPOST]; + RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodPOST]; loader.delegate = delegate; - [loader send]; + [loader send]; } - (void)putObject:(id)object delegate:(id)delegate { - RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodPUT]; + RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodPUT]; loader.delegate = delegate; - [loader send]; + [loader send]; } - (void)deleteObject:(id)object delegate:(id)delegate { - RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodDELETE]; + RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodDELETE]; loader.delegate = delegate; - [loader send]; + [loader send]; } #if NS_BLOCKS_AVAILABLE @@ -260,13 +306,13 @@ - (void)deleteObject:(id)object delegate:(id)d #pragma mark - Block Configured Object Loaders - (void)loadObjectsAtResourcePath:(NSString*)resourcePath usingBlock:(void(^)(RKObjectLoader *))block { - RKObjectLoader* loader = [self loaderWithResourcePath:resourcePath]; - loader.method = RKRequestMethodGET; - + RKObjectLoader* loader = [self loaderWithResourcePath:resourcePath]; + loader.method = RKRequestMethodGET; + // Yield to the block for setup block(loader); - - [loader send]; + + [loader send]; } - (void)sendObject:(id)object toResourcePath:(NSString *)resourcePath usingBlock:(void(^)(RKObjectLoader *))block { @@ -274,12 +320,12 @@ - (void)sendObject:(id)object toResourcePath:(NSString *)resourcePath loader.URL = [self.baseURL URLByAppendingResourcePath:resourcePath]; // Yield to the block for setup block(loader); - + [loader send]; } - (void)sendObject:(id)object method:(RKRequestMethod)method usingBlock:(void(^)(RKObjectLoader *))block { - NSString *resourcePath = [self.router resourcePathForObject:object method:method]; + NSString *resourcePath = [self.router resourcePathForObject:object method:method]; [self sendObject:object toResourcePath:resourcePath usingBlock:^(RKObjectLoader *loader) { loader.method = method; block(loader); @@ -314,21 +360,21 @@ - (void)getObject:(id)object mapResponseWith:(RKObjectMapping *)object } - (void)postObject:(id)object mapResponseWith:(RKObjectMapping *)objectMapping delegate:(id)delegate { - [self sendObject:object method:RKRequestMethodPOST usingBlock:^(RKObjectLoader *loader) { + [self sendObject:object method:RKRequestMethodPOST usingBlock:^(RKObjectLoader *loader) { loader.delegate = delegate; loader.objectMapping = objectMapping; }]; } - (void)putObject:(id)object mapResponseWith:(RKObjectMapping *)objectMapping delegate:(id)delegate { - [self sendObject:object method:RKRequestMethodPUT usingBlock:^(RKObjectLoader *loader) { + [self sendObject:object method:RKRequestMethodPUT usingBlock:^(RKObjectLoader *loader) { loader.delegate = delegate; loader.objectMapping = objectMapping; }]; } - (void)deleteObject:(id)object mapResponseWith:(RKObjectMapping *)objectMapping delegate:(id)delegate { - [self sendObject:object method:RKRequestMethodDELETE usingBlock:^(RKObjectLoader *loader) { + [self sendObject:object method:RKRequestMethodDELETE usingBlock:^(RKObjectLoader *loader) { loader.delegate = delegate; loader.objectMapping = objectMapping; }]; @@ -372,13 +418,13 @@ + (RKObjectManager *)objectManagerWithBaseURLString:(NSString *)baseURLString { } + (RKObjectManager *)objectManagerWithBaseURL:(NSURL *)baseURL { - return [self managerWithBaseURL:baseURL]; + return [self managerWithBaseURL:baseURL]; } - (RKObjectLoader *)objectLoaderWithResourcePath:(NSString *)resourcePath delegate:(id)delegate { RKObjectLoader* loader = [self loaderWithResourcePath:resourcePath]; loader.delegate = delegate; - + return loader; } @@ -389,12 +435,12 @@ - (RKObjectLoader*)objectLoaderForObject:(id)object method:(RKRequestM } - (void)loadObjectsAtResourcePath:(NSString *)resourcePath objectMapping:(RKObjectMapping *)objectMapping delegate:(id)delegate { - RKObjectLoader *loader = [self loaderWithResourcePath:resourcePath]; + RKObjectLoader *loader = [self loaderWithResourcePath:resourcePath]; loader.delegate = delegate; - loader.method = RKRequestMethodGET; + loader.method = RKRequestMethodGET; loader.objectMapping = objectMapping; - - [loader send]; + + [loader send]; } @end diff --git a/Code/ObjectMapping/RKObjectMapper.h b/Code/ObjectMapping/RKObjectMapper.h index 65ac1a2550..d7fc361c6c 100644 --- a/Code/ObjectMapping/RKObjectMapper.h +++ b/Code/ObjectMapping/RKObjectMapper.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/6/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -44,7 +44,7 @@ @end /** - + */ @interface RKObjectMapper : NSObject { @protected diff --git a/Code/ObjectMapping/RKObjectMapper.m b/Code/ObjectMapping/RKObjectMapper.m index 5daafe4123..cc97b1b92d 100644 --- a/Code/ObjectMapping/RKObjectMapper.m +++ b/Code/ObjectMapping/RKObjectMapper.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/6/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -131,7 +131,7 @@ - (id)mapObject:(id)mappableObject atKeyPath:(NSString *)keyPath usingMapping:(R objectMapping = (RKObjectMapping *)mapping; } else { NSAssert(objectMapping, @"Encountered unknown mapping type '%@'", NSStringFromClass([mapping class])); - } + } if (NO == [[self.targetObject class] isSubclassOfClass:objectMapping.objectClass]) { NSString *errorMessage = [NSString stringWithFormat: @"Expected an object mapping for class of type '%@', provider returned one for '%@'", @@ -142,7 +142,7 @@ - (id)mapObject:(id)mappableObject atKeyPath:(NSString *)keyPath usingMapping:(R } else { destinationObject = [self objectWithMapping:mapping andData:mappableObject]; } - + if (mapping && destinationObject) { BOOL success = [self mapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping]; if (success) { @@ -185,13 +185,13 @@ - (NSArray *)mapCollection:(NSArray *)mappableObjects atKeyPath:(NSString *)keyP [self addErrorWithCode:RKObjectMapperErrorObjectMappingTypeMismatch message:errorMessage keyPath:keyPath userInfo:nil]; return nil; } - + for (id mappableObject in objectsToMap) { id destinationObject = [self objectWithMapping:mapping andData:mappableObject]; - if (! destinationObject) { + if (! destinationObject) { continue; } - + BOOL success = [self mapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping]; if (success) { [mappedObjects addObject:destinationObject]; @@ -203,7 +203,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:(RKObjectMappingDefinition *)mapping { - NSAssert(destinationObject != nil, @"Cannot map without a target object to assign the results to"); + NSAssert(destinationObject != nil, @"Cannot map without a target object to assign the results to"); NSAssert(mappableObject != nil, @"Cannot map without a collection of attributes"); NSAssert(mapping != nil, @"Cannot map without an mapping"); @@ -211,14 +211,14 @@ - (BOOL)mapFromObject:(id)mappableObject toObject:(id)destinationObject atKeyPat if ([self.delegate respondsToSelector:@selector(objectMapper:willMapFromObject:toObject:atKeyPath:usingMapping:)]) { [self.delegate objectMapper:self willMapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping]; } - + NSError *error = nil; - - RKObjectMappingOperation *operation = [RKObjectMappingOperation mappingOperationFromObject:mappableObject - toObject:destinationObject - withMapping:mapping]; + + RKObjectMappingOperation *operation = [RKObjectMappingOperation mappingOperationFromObject:mappableObject + toObject:destinationObject + withMapping:mapping]; operation.queue = operationQueue; - BOOL success = [operation performMapping:&error]; + BOOL success = [operation performMapping:&error]; if (success) { if ([self.delegate respondsToSelector:@selector(objectMapper:didMapFromObject:toObject:atKeyPath:usingMapping:)]) { [self.delegate objectMapper:self didMapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping]; @@ -230,7 +230,7 @@ - (BOOL)mapFromObject:(id)mappableObject toObject:(id)destinationObject atKeyPat [self addError:error]; } - + return success; } @@ -247,11 +247,11 @@ - (id)objectWithMapping:(RKObjectMappingDefinition *)mapping andData:(id)mappabl } else { NSAssert(objectMapping, @"Encountered unknown mapping type '%@'", NSStringFromClass([mapping class])); } - + if (objectMapping) { return [objectMapping mappableObjectForData:mappableData]; } - + return nil; } @@ -300,33 +300,33 @@ - (NSMutableDictionary *)performKeyPathMappingUsingMappingDictionary:(NSDictiona if ([self.delegate respondsToSelector:@selector(objectMapper:didFindMappableObject:atKeyPath:withMapping:)]) { [self.delegate objectMapper:self didFindMappableObject:mappableValue atKeyPath:keyPath withMapping:mapping]; } - + mappingResult = [self performMappingForObject:mappableValue atKeyPath:keyPath usingMapping:mapping]; if (mappingResult) { [results setObject:mappingResult forKey:keyPath]; } } - + if (NO == foundMappable) return nil; return results; } -// Primary entry point for the mapper. +// Primary entry point for the mapper. - (RKObjectMappingResult *)performMapping { NSAssert(self.sourceObject != nil, @"Cannot perform object mapping without a source object to map from"); NSAssert(self.mappingProvider != nil, @"Cannot perform object mapping without an object mapping provider"); - + RKLogDebug(@"Performing object mapping sourceObject: %@\n and targetObject: %@", self.sourceObject, self.targetObject); - + if ([self.delegate respondsToSelector:@selector(objectMapperWillBeginMapping:)]) { [self.delegate objectMapperWillBeginMapping:self]; } - + // Perform the mapping BOOL foundMappable = NO; NSMutableDictionary *results = nil; - + // Handle mapping selection for context id mappingsForContext = [self.mappingProvider valueForContext:context]; if ([mappingsForContext isKindOfClass:[NSDictionary class]]) { @@ -339,18 +339,18 @@ - (RKObjectMappingResult *)performMapping { mappableData = [self.sourceObject valueForKeyPath:rootKeyPath]; RKLogDebug(@"Selected object mapping has rootKeyPath. Apply valueForKeyPath to mappable data: %@", rootKeyPath); } - + if (mappableData) { id mappingResult = [self performMappingForObject:mappableData atKeyPath:@"" usingMapping:mappingsForContext]; - foundMappable = YES; + foundMappable = YES; results = [NSDictionary dictionaryWithObject:mappingResult forKey:@""]; } } - + // Allow any queued operations to complete RKLogDebug(@"The following operations are in the queue: %@", operationQueue.operations); [operationQueue waitUntilAllOperationsAreFinished]; - + if ([self.delegate respondsToSelector:@selector(objectMapperDidFinishMapping:)]) { [self.delegate objectMapperDidFinishMapping:self]; } diff --git a/Code/ObjectMapping/RKObjectMapperError.h b/Code/ObjectMapping/RKObjectMapperError.h index 86ddb5b95a..bfe29eafa1 100644 --- a/Code/ObjectMapping/RKObjectMapperError.h +++ b/Code/ObjectMapping/RKObjectMapperError.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/31/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/ObjectMapping/RKObjectMapper_Private.h b/Code/ObjectMapping/RKObjectMapper_Private.h index 42681a4982..d78f2f86f4 100644 --- a/Code/ObjectMapping/RKObjectMapper_Private.h +++ b/Code/ObjectMapping/RKObjectMapper_Private.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/9/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/ObjectMapping/RKObjectMapping.h b/Code/ObjectMapping/RKObjectMapping.h index 311ca42715..7c0f51f557 100644 --- a/Code/ObjectMapping/RKObjectMapping.h +++ b/Code/ObjectMapping/RKObjectMapping.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 4/30/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -100,8 +100,8 @@ relationship. Relationships are processed using an object mapping as well. @property (nonatomic, assign) BOOL setNilForMissingRelationships; /** - When YES, RestKit will invoke key-value validation at object mapping time. - + When YES, RestKit will invoke key-value validation at object mapping time. + **Default**: YES @see validateValue:forKey:error: */ @@ -125,10 +125,10 @@ relationship. Relationships are processed using an object mapping as well. into NSDate attributes on the target objectClass. Each date formatter will be invoked with the string value being mapped until one of the date formatters does not return nil. - + Defaults to the application-wide collection of date formatters configured via: [RKObjectMapping setDefaultDateFormatters:] - + @see [RKObjectMapping defaultDateFormatters] */ @property (nonatomic, retain) NSArray *dateFormatters; @@ -138,10 +138,10 @@ relationship. Relationships are processed using an object mapping as well. and time configuration. This date formatter will be used when generating string representations of NSDate attributes (i.e. during serialization to URL form encoded or JSON format). - + Defaults to the application-wide preferred date formatter configured via: [RKObjectMapping setPreferredDateFormatter:] - + @see [RKObjectMapping preferredDateFormatter] */ @property (nonatomic, retain) NSFormatter *preferredDateFormatter; @@ -172,10 +172,10 @@ relationship. Relationships are processed using an object mapping as well. Returns an object mapping targeting the specified class. The RKObjectMapping instance will be yieled to the block so that you can perform on the fly configuration without having to obtain a reference variable for the mapping. - - For example, consider we have a one-off request that will load a few attributes for our object. + + For example, consider we have a one-off request that will load a few attributes for our object. Using blocks, this is very succinct: - + [[RKObjectManager sharedManager] postObject:self usingBlock:^(RKObjectLoader* loader) { loader.objectMapping = [RKObjectMapping mappingForClass:[Person class] usingBlock:^(RKObjectMapping* mapping) { [mapping mapAttributes:@"email", @"first_name", nil]; @@ -188,10 +188,10 @@ relationship. Relationships are processed using an object mapping as well. Returns serialization mapping for encoding a local object to a dictionary for transport. The RKObjectMapping instance will be yieled to the block so that you can perform on the fly configuration without having to obtain a reference variable for the mapping. - + For example, consider we have a one-off request within which we want to post a subset of our object data. Using blocks, this is very succinct: - + - (BOOL)changePassword:(NSString*)newPassword error:(NSError**)error { if ([self validatePassword:newPassword error:error]) { self.password = newPassword; @@ -202,7 +202,7 @@ relationship. Relationships are processed using an object mapping as well. }]; } } - + Using the block forms we are able to quickly configure and send this request on the fly. */ + (id)serializationMappingUsingBlock:(void (^)(RKObjectMapping *serializationMapping))block; @@ -242,7 +242,7 @@ relationship. Relationships are processed using an object mapping as well. /** Returns the attribute or relationship mapping for the given destination keyPath. - + @param destinationKeyPath A keyPath on the destination object that is currently */ - (id)mappingForDestinationKeyPath:(NSString *)destinationKeyPath; @@ -283,15 +283,15 @@ relationship. Relationships are processed using an object mapping as well. /** Defines an attribute mapping for each string attribute in the collection where the source keyPath and the destination attribute property have the same name. - + For example, given the transformation from a JSON dictionary: - + {"name": "My Name", "age": 28} - + To a Person class with corresponding name & age properties, we could configure the attribute mappings via: - + [mapping mapAttributesFromSet:[NSSet setWithObjects:@"name", @"age", nil]]; - + @param set A set of string attribute keyPaths to deifne mappings for */ - (void)mapAttributesFromSet:(NSSet *)set; @@ -299,15 +299,15 @@ relationship. Relationships are processed using an object mapping as well. /** Defines an attribute mapping for each string attribute in the collection where the source keyPath and the destination attribute property have the same name. - + For example, given the transformation from a JSON dictionary: - + {"name": "My Name", "age": 28} - + To a Person class with corresponding name & age properties, we could configure the attribute mappings via: - + [mapping mapAttributesFromSet:[NSArray arrayWithObjects:@"name", @"age", nil]]; - + @param array An array of string attribute keyPaths to deifne mappings for */ - (void)mapAttributesFromArray:(NSArray *)set; @@ -443,7 +443,7 @@ relationship. Relationships are processed using an object mapping as well. /** Returns the attribute mapping targeting the key of a nested dictionary in the source JSON. This attribute mapping corresponds to the attributeName configured via mapKeyOfNestedDictionaryToAttribute: - + @see mapKeyOfNestedDictionaryToAttribute: @returns An attribute mapping for the key of a nested dictionary being mapped or nil */ @@ -498,9 +498,9 @@ relationship. Relationships are processed using an object mapping as well. /** Returns the class of the attribute or relationship property of the target objectClass - + Given the name of a string property, this will return an NSString, etc. - + @param propertyName The name of the property we would like to retrieve the type of */ - (Class)classForProperty:(NSString*)propertyName; @@ -534,12 +534,12 @@ relationship. Relationships are processed using an object mapping as well. /** Returns the collection of default date formatters that will be used for all object mappings that have not been configured specifically. - + Out of the box, RestKit initializes the following default date formatters for you in the UTC time zone: * yyyy-MM-dd'T'HH:mm:ss'Z' * MM/dd/yyyy - + @return An array of NSFormatter objects used when mapping strings into NSDate attributes */ + (NSArray *)defaultDateFormatters; @@ -548,7 +548,7 @@ relationship. Relationships are processed using an object mapping as well. Sets the collection of default date formatters to the specified array. The array should contain configured instances of NSDateFormatter in the order in which you want them applied during object mapping operations. - + @param dateFormatters An array of date formatters to replace the existing defaults @see defaultDateFormatters */ @@ -556,7 +556,7 @@ relationship. Relationships are processed using an object mapping as well. /** Adds a date formatter instance to the default collection - + @param dateFormatter An NSFormatter object to append to the end of the default formatters collection @see defaultDateFormatters */ @@ -565,7 +565,7 @@ relationship. Relationships are processed using an object mapping as well. /** Convenience method for quickly constructing a date formatter and adding it to the collection of default date formatters. The locale is auto-configured to en_US_POSIX - + @param dateFormatString The dateFormat string to assign to the newly constructed NSDateFormatter instance @param nilOrTimeZone The NSTimeZone object to configure on the NSDateFormatter instance. Defaults to UTC time. @result A new NSDateFormatter will be appended to the defaultDateFormatters with the specified date format and time zone @@ -577,9 +577,9 @@ relationship. Relationships are processed using an object mapping as well. Returns the preferred date formatter to use when generating NSString representations from NSDate attributes. This type of transformation occurs when RestKit is mapping local objects into JSON or form encoded serializations that do not have a native time construct. - + Defaults to a date formatter configured for the UTC Time Zone with a format string of "yyyy-MM-dd HH:mm:ss Z" - + @return The preferred NSFormatter object to use when serializing dates into strings */ + (NSFormatter *)preferredDateFormatter; @@ -588,7 +588,7 @@ relationship. Relationships are processed using an object mapping as well. Sets the preferred date formatter to use when generating NSString representations from NSDate attributes. This type of transformation occurs when RestKit is mapping local objects into JSON or form encoded serializations that do not have a native time construct. - + @param dateFormatter The NSFormatter object to designate as the new preferred instance */ + (void)setPreferredDateFormatter:(NSFormatter *)dateFormatter; diff --git a/Code/ObjectMapping/RKObjectMapping.m b/Code/ObjectMapping/RKObjectMapping.m index 1d23f8de3c..373151f5c6 100644 --- a/Code/ObjectMapping/RKObjectMapping.m +++ b/Code/ObjectMapping/RKObjectMapping.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 4/30/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -106,8 +106,8 @@ - (id)copyWithZone:(NSZone *)zone { copy.setNilForMissingRelationships = self.setNilForMissingRelationships; copy.forceCollectionMapping = self.forceCollectionMapping; copy.performKeyValueValidation = self.performKeyValueValidation; - copy.dateFormatters = self.dateFormatters; - copy.preferredDateFormatter = self.preferredDateFormatter; + copy.dateFormatters = self.dateFormatters; + copy.preferredDateFormatter = self.preferredDateFormatter; for (RKObjectAttributeMapping *mapping in self.mappings) { [copy addAttributeMapping:mapping]; @@ -191,7 +191,7 @@ - (id)mappingForDestinationKeyPath:(NSString *)destinationKeyPath { return mapping; } } - + return nil; } @@ -204,7 +204,7 @@ - (void)mapAttributesCollection:(id)attributes { - (void)mapAttributes:(NSString*)attributeKeyPath, ... { va_list args; va_start(args, attributeKeyPath); - NSMutableSet* attributeKeyPaths = [NSMutableSet set]; + NSMutableSet* attributeKeyPaths = [NSMutableSet set]; for (NSString* keyPath = attributeKeyPath; keyPath != nil; keyPath = va_arg(args, NSString*)) { [attributeKeyPaths addObject:keyPath]; @@ -294,7 +294,7 @@ - (void)mapKeyPathsToAttributes:(NSString*)firstKeyPath, ... { va_list args; va_start(args, firstKeyPath); for (NSString* keyPath = firstKeyPath; keyPath != nil; keyPath = va_arg(args, NSString*)) { - NSString* attributeKeyPath = va_arg(args, NSString*); + NSString* attributeKeyPath = va_arg(args, NSString*); NSAssert(attributeKeyPath != nil, @"Cannot map a keyPath without a destination attribute keyPath"); [self mapKeyPath:keyPath toAttribute:attributeKeyPath]; // TODO: Raise proper exception here, argument error... @@ -364,16 +364,16 @@ @implementation RKObjectMapping (DateAndTimeFormatting) + (NSArray *)defaultDateFormatters { if (!defaultDateFormatters) { defaultDateFormatters = [[NSMutableArray alloc] initWithCapacity:2]; - + // Setup the default formatters RKISO8601DateFormatter *isoFormatter = [[RKISO8601DateFormatter alloc] init]; [self addDefaultDateFormatter:isoFormatter]; [isoFormatter release]; - + [self addDefaultDateFormatterForString:@"MM/dd/yyyy" inTimeZone:nil]; [self addDefaultDateFormatterForString:@"yyyy-MM-dd'T'HH:mm:ss'Z'" inTimeZone:nil]; } - + return defaultDateFormatters; } @@ -400,7 +400,7 @@ + (void)addDefaultDateFormatterForString:(NSString *)dateFormatString inTimeZone } else { dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; } - + [self addDefaultDateFormatter:dateFormatter]; [dateFormatter release]; @@ -414,7 +414,7 @@ + (NSFormatter *)preferredDateFormatter { preferredDateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; preferredDateFormatter.locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"] autorelease]; } - + return preferredDateFormatter; } diff --git a/Code/ObjectMapping/RKObjectMappingDefinition.h b/Code/ObjectMapping/RKObjectMappingDefinition.h index a315bbec5f..aa67b3cfcd 100644 --- a/Code/ObjectMapping/RKObjectMappingDefinition.h +++ b/Code/ObjectMapping/RKObjectMappingDefinition.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 7/31/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,21 +19,21 @@ // /** - RKObjectMappingDefinition is an abstract class for objects defining RestKit object mappings. - Its interface is common to all object mapping classes, including its concrete subclasses + RKObjectMappingDefinition is an abstract class for objects defining RestKit object mappings. + Its interface is common to all object mapping classes, including its concrete subclasses RKObjectMapping and RKDynamicObjectMapping. */ @interface RKObjectMappingDefinition : NSObject /** The root key path for the receiver. - + Root key paths are handled differently depending on the context in which the mapping is being used. If the receiver is used for object mapping, the rootKeyPath specifies a nested root dictionary that all attribute and relationship mappings will be considered relative to. When the mapping is used in a serialization context, the rootKeyPath specifies that the serialized content should be stored in a dictionary nested with the rootKeyPath as the key. - + @see RKObjectSerializer */ @property (nonatomic, copy) NSString *rootKeyPath; @@ -42,21 +42,21 @@ Forces the mapper to treat the mapped keyPath as a collection even if it does not return an array or a set of objects. This permits mapping where a dictionary identifies a collection of objects. - + When enabled, each key/value pair in the resolved dictionary will be mapped as a separate entity. This is useful when you have a JSON structure similar to: - + { "users": { "blake": { "id": 1234, "email": "blake@restkit.org" }, "rachit": { "id": 5678", "email": "rachit@restkit.org" } } } - + By enabling forceCollectionMapping, RestKit will map "blake" => attributes and "rachit" => attributes as independent objects. This can be combined with mapKeyOfNestedDictionaryToAttribute: to properly map these sorts of structures. - + @default NO @see mapKeyOfNestedDictionaryToAttribute */ diff --git a/Code/ObjectMapping/RKObjectMappingOperation.h b/Code/ObjectMapping/RKObjectMappingOperation.h index 7bb472d092..4535260ca0 100644 --- a/Code/ObjectMapping/RKObjectMappingOperation.h +++ b/Code/ObjectMapping/RKObjectMappingOperation.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 4/30/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,29 +24,75 @@ @class RKObjectMappingOperation; @class RKMappingOperationQueue; +/** + Objects acting as the delegate for RKObjectMappingOperation objects must adopt the + RKObjectMappingOperationDelegate protocol. These methods enable the delegate to be + notified of events such as the application of attribute and relationship mappings + during a mapping operation. + */ @protocol RKObjectMappingOperationDelegate @optional + +/** + Tells the delegate that an attribute or relationship mapping was found for a given key + path within the data being mapped. + + @param operation The object mapping operation being performed. + @param mapping The RKObjectAttributeMapping or RKObjectRelationshipMapping found for the key path. + @param keyPath The key path in the source object for which the mapping is to be applied. + */ - (void)objectMappingOperation:(RKObjectMappingOperation *)operation didFindMapping:(RKObjectAttributeMapping *)mapping forKeyPath:(NSString *)keyPath; + +/** + Tells the delegate that no attribute or relationships mapping was found for a given key + path within the data being mapped. + + @param operation The object mapping operation being performed. + @param keyPath The key path in the source object for which no mapping was found. + */ - (void)objectMappingOperation:(RKObjectMappingOperation *)operation didNotFindMappingForKeyPath:(NSString *)keyPath; -- (void)objectMappingOperation:(RKObjectMappingOperation *)operation didSetValue:(id)value forKeyPath:(NSString *)keyPath usingMapping:(RKObjectAttributeMapping*)mapping; -- (void)objectMappingOperation:(RKObjectMappingOperation *)operation didFailWithError:(NSError*)error; +/** + Tells the delegate that the mapping operation has set a value for a given key path with + an attribute or relationship mapping. + + @param operation The object mapping operation being performed. + @param value A new value that was set on the destination object. + @param keyPath The key path in the destination object for which a new value has been set. + @param mapping The RKObjectAttributeMapping or RKObjectRelationshipMapping found for the key path. + */ +- (void)objectMappingOperation:(RKObjectMappingOperation *)operation didSetValue:(id)value forKeyPath:(NSString *)keyPath usingMapping:(RKObjectAttributeMapping *)mapping; + +/** + Tells the delegate that the mapping operation has declined to set a value for a given + key path because the value has not changed. + + @param operation The object mapping operation being performed. + @param value A unchanged value for the key path in the destination object. + @param keyPath The key path in the destination object for which a unchanged value was not set. + @param mapping The RKObjectAttributeMapping or RKObjectRelationshipMapping found for the key path. + */ +- (void)objectMappingOperation:(RKObjectMappingOperation *)operation didNotSetUnchangedValue:(id)value forKeyPath:(NSString *)keyPath usingMapping:(RKObjectAttributeMapping *)mapping; + +/** + Tells the delegate that the object mapping operation has failed due to an error. + + @param operation The object mapping operation that has failed. + @param error An error object indicating the reason for the failure. + */ +- (void)objectMappingOperation:(RKObjectMappingOperation *)operation didFailWithError:(NSError *)error; @end /** - Performs an object mapping operation by mapping values from a dictinary of elements - and setting the mapped values onto a target object. + Instances of RKObjectMappingOperation perform transformation between object + representations according to the rules express in RKObjectMapping objects. Mapping + operations provide the foundation for the RestKit object mapping engine and + perform the work of inspecting the attributes and relationships of a source object + and determining how to map them into new representations on a destination object. + */ -@interface RKObjectMappingOperation : NSObject { - id _sourceObject; - id _destinationObject; - RKObjectMapping* _objectMapping; - id _delegate; - NSDictionary* _nestedAttributeSubstitution; - NSError* _validationError; - RKMappingOperationQueue *_queue; -} +@interface RKObjectMappingOperation : NSObject /** A dictionary of mappable elements containing simple values or nested object structures. @@ -62,7 +108,7 @@ /** The object mapping defining how values contained in the source object should be transformed to the destination object via key-value coding */ -@property (nonatomic, readonly) RKObjectMapping* objectMapping; +@property (nonatomic, readonly) RKObjectMapping *objectMapping; /** The delegate to inform of interesting events during the mapping operation @@ -71,7 +117,7 @@ /** An operation queue for deferring portions of the mapping process until later - + Defaults to nil. If this mapping operation was configured by an instance of RKObjectMapper, then an instance of the operation queue will be configured and assigned for use. If the queue is nil, the mapping operation will perform all its operations within the body of performMapping. If a queue @@ -80,24 +126,40 @@ @property (nonatomic, retain) RKMappingOperationQueue *queue; /** - Create a new mapping operation configured to transform the object representation + Creates and returns a new mapping operation configured to transform the object representation in a source object to a new destination object according to an object mapping definition. - - Note that if Core Data support is available, an instance of RKManagedObjectMappingOperation may be returned - - @return An instance of RKObjectMappingOperation or RKManagedObjectMappingOperation for performing the mapping + + Note that if Core Data support is available, an instance of RKManagedObjectMappingOperation may be returned. + + @param sourceObject The source object to be mapped. Cannot be nil. + @param destinationObject The destination object the results are to be mapped onto. May be nil, + in which case a new object will be constructed during the mapping. + @param mapping An instance of RKObjectMapping or RKDynamicObjectMapping defining how the + mapping is to be performed. + @return An instance of RKObjectMappingOperation or RKManagedObjectMappingOperation for performing the mapping. */ + (id)mappingOperationFromObject:(id)sourceObject toObject:(id)destinationObject withMapping:(RKObjectMappingDefinition *)mapping; /** - Initialize a mapping operation for an object and set of data at a particular key path with an object mapping definition + Initializes the receiver with a source and destination objects and an object mapping + definition for performing a mapping. + + @param sourceObject The source object to be mapped. Cannot be nil. + @param destinationObject The destination object the results are to be mapped onto. May be nil, + in which case a new object will be constructed during the mapping. + @param mapping An instance of RKObjectMapping or RKDynamicObjectMapping 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:(RKObjectMappingDefinition *)mapping; /** Process all mappable values from the mappable dictionary and assign them to the target object according to the rules expressed in the object mapping definition + + @param error A pointer to an NSError reference to capture any error that occurs during the mapping. May be nil. + @return A Boolean value indicating if the mapping operation was successful. */ -- (BOOL)performMapping:(NSError**)error; +- (BOOL)performMapping:(NSError **)error; @end diff --git a/Code/ObjectMapping/RKObjectMappingOperation.m b/Code/ObjectMapping/RKObjectMappingOperation.m index c024b1b1a5..711150e194 100644 --- a/Code/ObjectMapping/RKObjectMappingOperation.m +++ b/Code/ObjectMapping/RKObjectMappingOperation.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 4/30/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -36,7 +36,7 @@ BOOL RKObjectIsValueEqualToValue(id sourceValue, id destinationValue) { NSCAssert(sourceValue, @"Expected sourceValue not to be nil"); NSCAssert(destinationValue, @"Expected destinationValue not to be nil"); - + SEL comparisonSelector; if ([sourceValue isKindOfClass:[NSString class]] && [destinationValue isKindOfClass:[NSString class]]) { comparisonSelector = @selector(isEqualToString:); @@ -53,7 +53,7 @@ BOOL RKObjectIsValueEqualToValue(id sourceValue, id destinationValue) { } else { comparisonSelector = @selector(isEqual:); } - + // Comparison magic using function pointers. See this page for details: http://www.red-sweater.com/blog/320/abusing-objective-c-with-class // Original code courtesy of Greg Parker // This is necessary because isEqualToNumber will return negative integer values that aren't coercable directly to BOOL's without help [sbw] @@ -61,6 +61,11 @@ BOOL RKObjectIsValueEqualToValue(id sourceValue, id destinationValue) { return ComparisonSender(sourceValue, comparisonSelector, destinationValue); } +@interface RKObjectMappingOperation () +@property (nonatomic, retain) NSDictionary *nestedAttributeSubstitution; +@property (nonatomic, retain) NSError *validationError; +@end + @implementation RKObjectMappingOperation @synthesize sourceObject = _sourceObject; @@ -68,12 +73,14 @@ @implementation RKObjectMappingOperation @synthesize objectMapping = _objectMapping; @synthesize delegate = _delegate; @synthesize queue = _queue; +@synthesize nestedAttributeSubstitution = _nestedAttributeSubstitution; +@synthesize validationError = _validationError; + (id)mappingOperationFromObject:(id)sourceObject toObject:(id)destinationObject withMapping:(RKObjectMappingDefinition *)objectMapping { // Check for availability of ManagedObjectMappingOperation. Better approach for handling? Class targetClass = NSClassFromString(@"RKManagedObjectMappingOperation"); if (targetClass == nil) targetClass = [RKObjectMappingOperation class]; - + return [[[targetClass alloc] initWithSourceObject:sourceObject destinationObject:destinationObject mapping:objectMapping] autorelease]; } @@ -81,12 +88,12 @@ - (id)initWithSourceObject:(id)sourceObject destinationObject:(id)destinationObj NSAssert(sourceObject != nil, @"Cannot perform a mapping operation without a sourceObject object"); NSAssert(destinationObject != nil, @"Cannot perform a mapping operation without a destinationObject"); NSAssert(objectMapping != nil, @"Cannot perform a mapping operation without a mapping"); - + self = [super init]; if (self) { - _sourceObject = [sourceObject retain]; + _sourceObject = [sourceObject retain]; _destinationObject = [destinationObject retain]; - + if ([objectMapping isKindOfClass:[RKDynamicObjectMapping class]]) { _objectMapping = [[(RKDynamicObjectMapping*)objectMapping objectMappingForDictionary:_sourceObject] retain]; RKLogDebug(@"RKObjectMappingOperation was initialized with a dynamic mapping. Determined concrete mapping = %@", _objectMapping); @@ -94,8 +101,8 @@ - (id)initWithSourceObject:(id)sourceObject destinationObject:(id)destinationObj _objectMapping = (RKObjectMapping*)[objectMapping retain]; } NSAssert(_objectMapping, @"Cannot perform a mapping operation with an object mapping"); - } - + } + return self; } @@ -105,7 +112,7 @@ - (void)dealloc { [_objectMapping release]; [_nestedAttributeSubstitution release]; [_queue release]; - + [super dealloc]; } @@ -113,38 +120,38 @@ - (NSDate*)parseDateFromString:(NSString*)string { RKLogTrace(@"Transforming string value '%@' to NSDate...", string); NSDate* date = nil; - + NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; numberFormatter.numberStyle = NSNumberFormatterDecimalStyle; NSNumber *numeric = [numberFormatter numberFromString:string]; [numberFormatter release]; - + if (numeric) { date = [NSDate dateWithTimeIntervalSince1970:[numeric doubleValue]]; } else if(![string isEqualToString:@""]) { for (NSFormatter *dateFormatter in self.objectMapping.dateFormatters) { BOOL success; - @synchronized(dateFormatter) { + @synchronized(dateFormatter) { if ([dateFormatter isKindOfClass:[NSDateFormatter class]]) { RKLogTrace(@"Attempting to parse string '%@' with format string '%@' and time zone '%@'", string, [(NSDateFormatter *)dateFormatter dateFormat], [(NSDateFormatter *)dateFormatter timeZone]); } NSString *errorDescription = nil; success = [dateFormatter getObjectValue:&date forString:string errorDescription:&errorDescription]; - } - - if (success && date) { + } + + if (success && date) { if ([dateFormatter isKindOfClass:[NSDateFormatter class]]) { RKLogTrace(@"Successfully parsed string '%@' with format string '%@' and time zone '%@' and turned into date '%@'", string, [(NSDateFormatter *)dateFormatter dateFormat], [(NSDateFormatter *)dateFormatter timeZone], date); } break; - } - } - } - + } + } + } + return date; } @@ -152,7 +159,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)); Class sourceType = [value class]; Class orderedSetClass = NSClassFromString(@"NSOrderedSet"); - + if ([sourceType isSubclassOfClass:[NSString class]]) { if ([destinationType isSubclassOfClass:[NSDate class]]) { // String -> Date @@ -234,18 +241,19 @@ - (id)transformValue:(id)value atKeyPath:(NSString *)keyPath toType:(Class)desti return nil; } -- (BOOL)isValue:(id)sourceValue equalToValue:(id)destinationValue { +- (BOOL)isValue:(id)sourceValue equalToValue:(id)destinationValue { return RKObjectIsValueEqualToValue(sourceValue, destinationValue); } - (BOOL)validateValue:(id *)value atKeyPath:(NSString*)keyPath { - BOOL success = YES; - + BOOL success = YES; + if (self.objectMapping.performKeyValueValidation && [self.destinationObject respondsToSelector:@selector(validateValue:forKeyPath:error:)]) { success = [self.destinationObject validateValue:value forKeyPath:keyPath error:&_validationError]; if (!success) { if (_validationError) { - RKLogError(@"Validation failed while mapping attribute at key path %@ to value %@. Error: %@", keyPath, *value, [_validationError localizedDescription]); + RKLogError(@"Validation failed while mapping attribute at key path '%@' to value %@. Error: %@", keyPath, *value, [_validationError localizedDescription]); + RKLogValidationError(_validationError); } else { RKLogWarning(@"Destination object %@ rejected attribute value %@ for keyPath %@. Skipping...", self.destinationObject, *value, keyPath); } @@ -260,26 +268,26 @@ - (BOOL)shouldSetValue:(id *)value atKeyPath:(NSString*)keyPath { if (currentValue == [NSNull null] || [currentValue isEqual:[NSNull null]]) { currentValue = nil; } - + /* WTF - This workaround should not be necessary, but I have been unable to replicate the circumstances that trigger it in a unit test to fix elsewhere. The proper place to handle it is in transformValue:atKeyPath:toType: - + See issue & pull request: https://github.com/RestKit/RestKit/pull/436 */ if (*value == [NSNull null] || [*value isEqual:[NSNull null]]) { RKLogWarning(@"Coercing NSNull value to nil in shouldSetValue:atKeyPath: -- should be fixed."); *value = nil; } - - if (nil == currentValue && nil == *value) { - // Both are nil + + if (nil == currentValue && nil == *value) { + // Both are nil return NO; - } else if (nil == *value || nil == currentValue) { - // One is nil and the other is not + } else if (nil == *value || nil == currentValue) { + // One is nil and the other is not return [self validateValue:value atKeyPath:keyPath]; - } + } if (! [self isValue:*value equalToValue:currentValue]) { // Validate value for key @@ -330,13 +338,16 @@ - (void)applyAttributeMapping:(RKObjectAttributeMapping*)attributeMapping withVa // Ensure that the value is different if ([self shouldSetValue:&value atKeyPath:attributeMapping.destinationKeyPath]) { RKLogTrace(@"Mapped attribute value from keyPath '%@' to '%@'. Value: %@", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath, value); - + [self.destinationObject setValue:value forKeyPath:attributeMapping.destinationKeyPath]; if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didSetValue:forKeyPath:usingMapping:)]) { [self.delegate objectMappingOperation:self didSetValue:value forKeyPath:attributeMapping.destinationKeyPath usingMapping:attributeMapping]; } } else { RKLogTrace(@"Skipped mapping of attribute value from keyPath '%@ to keyPath '%@' -- value is unchanged (%@)", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath, value); + if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didNotSetUnchangedValue:forKeyPath:usingMapping:)]) { + [self.delegate objectMappingOperation:self didNotSetUnchangedValue:value forKeyPath:attributeMapping.destinationKeyPath usingMapping:attributeMapping]; + } } } @@ -344,17 +355,17 @@ - (void)applyAttributeMapping:(RKObjectAttributeMapping*)attributeMapping withVa - (BOOL)applyAttributeMappings { // If we have a nesting substitution value, we have alread BOOL appliedMappings = (_nestedAttributeSubstitution != nil); - + if (!self.objectMapping.performKeyValueValidation) { RKLogDebug(@"Key-value validation is disabled for mapping, skipping..."); } - + for (RKObjectAttributeMapping* attributeMapping in [self attributeMappings]) { if ([attributeMapping isMappingForKeyOfNestedDictionary]) { RKLogTrace(@"Skipping attribute mapping for special keyPath '%@'", attributeMapping.sourceKeyPath); continue; } - + if (self.objectMapping.ignoreUnknownKeyPaths && ![self.sourceObject respondsToSelector:NSSelectorFromString(attributeMapping.sourceKeyPath)]) { RKLogDebug(@"Source object is not key-value coding compliant for the keyPath '%@', skipping...", attributeMapping.sourceKeyPath); continue; @@ -406,7 +417,7 @@ - (BOOL)applyAttributeMappings { - (BOOL)isTypeACollection:(Class)type { Class orderedSetClass = NSClassFromString(@"NSOrderedSet"); - return (type && ([type isSubclassOfClass:[NSSet class]] || + return (type && ([type isSubclassOfClass:[NSSet class]] || [type isSubclassOfClass:[NSArray class]] || (orderedSetClass && [type isSubclassOfClass:orderedSetClass]))); } @@ -420,7 +431,7 @@ - (BOOL)mapNestedObject:(id)anObject toObject:(id)anotherObject withRealtionship NSAssert(anotherObject, @"Cannot map nested object without a destination object"); NSAssert(relationshipMapping, @"Cannot map a nested object relationship without a relationship mapping"); NSError* error = nil; - + RKLogTrace(@"Performing nested object mapping using mapping %@ for data: %@", relationshipMapping, anObject); RKObjectMappingOperation* subOperation = [RKObjectMappingOperation mappingOperationFromObject:anObject toObject:anotherObject withMapping:relationshipMapping.mapping]; subOperation.delegate = self.delegate; @@ -435,7 +446,7 @@ - (BOOL)mapNestedObject:(id)anObject toObject:(id)anotherObject withRealtionship - (BOOL)applyRelationshipMappings { BOOL appliedMappings = NO; id destinationObject = nil; - + for (RKObjectRelationshipMapping* relationshipMapping in [self relationshipMappings]) { id value = nil; @try { @@ -453,7 +464,7 @@ - (BOOL)applyRelationshipMappings { if (value == nil || value == [NSNull null] || [value isEqual:[NSNull null]]) { RKLogDebug(@"Did not find mappable relationship value keyPath '%@'", relationshipMapping.sourceKeyPath); - + // Optionally nil out the property id nilReference = nil; if ([self.objectMapping setNilForMissingRelationships] && [self shouldSetValue:&nilReference atKeyPath:relationshipMapping.destinationKeyPath]) { @@ -463,8 +474,8 @@ - (BOOL)applyRelationshipMappings { continue; } - - // Handle case where incoming content is collection represented by a dictionary + + // Handle case where incoming content is collection represented by a dictionary if (relationshipMapping.mapping.forceCollectionMapping) { // If we have forced mapping of a dictionary, map each subdictionary if ([value isKindOfClass:[NSDictionary class]]) { @@ -479,9 +490,9 @@ - (BOOL)applyRelationshipMappings { RKLogWarning(@"Collection mapping forced but mappable objects is of type '%@' rather than NSDictionary", NSStringFromClass([value class])); } } - + // Handle case where incoming content is a single object, but we want a collection - Class relationshipType = [self.objectMapping classForProperty:relationshipMapping.destinationKeyPath]; + Class relationshipType = [self.objectMapping classForProperty:relationshipMapping.destinationKeyPath]; BOOL mappingToCollection = [self isTypeACollection:relationshipType]; if (mappingToCollection && ![self isValueACollection:value]) { Class orderedSetClass = NSClassFromString(@"NSOrderedSet"); @@ -496,8 +507,8 @@ - (BOOL)applyRelationshipMappings { RKLogWarning(@"Failed to transform single object"); } } - - if ([self isValueACollection:value]) { + + if ([self isValueACollection:value]) { // One to many relationship RKLogDebug(@"Mapping one to many relationship value at keyPath '%@' to '%@'", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath); appliedMappings = YES; @@ -510,7 +521,7 @@ - (BOOL)applyRelationshipMappings { 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); } - for (id nestedObject in value) { + for (id nestedObject in value) { RKObjectMappingDefinition * mapping = relationshipMapping.mapping; RKObjectMapping* objectMapping = nil; if ([mapping isKindOfClass:[RKDynamicObjectMapping class]]) { @@ -558,11 +569,15 @@ - (BOOL)applyRelationshipMappings { RKLogTrace(@"Mapped relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, destinationObject); [self.destinationObject setValue:destinationObject forKeyPath:relationshipMapping.destinationKeyPath]; } + } else { + if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didNotSetUnchangedValue:forKeyPath:usingMapping:)]) { + [self.delegate objectMappingOperation:self didNotSetUnchangedValue:destinationObject forKeyPath:relationshipMapping.destinationKeyPath usingMapping:relationshipMapping]; + } } } else { // One to one relationship - RKLogDebug(@"Mapping one to one relationship value at keyPath '%@' to '%@'", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath); - + RKLogDebug(@"Mapping one to one relationship value at keyPath '%@' to '%@'", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath); + RKObjectMappingDefinition * mapping = relationshipMapping.mapping; RKObjectMapping* objectMapping = nil; if ([mapping isKindOfClass:[RKDynamicObjectMapping class]]) { @@ -581,14 +596,18 @@ - (BOOL)applyRelationshipMappings { appliedMappings = YES; RKLogTrace(@"Mapped relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, destinationObject); [self.destinationObject setValue:destinationObject forKey:relationshipMapping.destinationKeyPath]; + } else { + if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didNotSetUnchangedValue:forKeyPath:usingMapping:)]) { + [self.delegate objectMappingOperation:self didNotSetUnchangedValue:destinationObject forKeyPath:relationshipMapping.destinationKeyPath usingMapping:relationshipMapping]; + } } } - + // Notify the delegate if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didSetValue:forKeyPath:usingMapping:)]) { [self.delegate objectMappingOperation:self didSetValue:destinationObject forKeyPath:relationshipMapping.destinationKeyPath usingMapping:relationshipMapping]; } - + // Fail out if a validation error has occurred if (_validationError) { return NO; @@ -616,7 +635,7 @@ - (void)applyNestedMappings { - (BOOL)performMapping:(NSError**)error { RKLogDebug(@"Starting mapping operation..."); RKLogTrace(@"Performing mapping operation: %@", self); - + [self applyNestedMappings]; BOOL mappedAttributes = [self applyAttributeMappings]; BOOL mappedRelationships = [self applyRelationshipMappings]; diff --git a/Code/ObjectMapping/RKObjectMappingProvider+Contexts.h b/Code/ObjectMapping/RKObjectMappingProvider+Contexts.h index b5176b90d0..1a42c74d4a 100644 --- a/Code/ObjectMapping/RKObjectMappingProvider+Contexts.h +++ b/Code/ObjectMapping/RKObjectMappingProvider+Contexts.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 1/17/12. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/ObjectMapping/RKObjectMappingProvider.h b/Code/ObjectMapping/RKObjectMappingProvider.h index 553491f9fb..7a6cb197ba 100644 --- a/Code/ObjectMapping/RKObjectMappingProvider.h +++ b/Code/ObjectMapping/RKObjectMappingProvider.h @@ -4,13 +4,13 @@ // // Created by Jeremy Ellison on 5/6/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -37,17 +37,17 @@ typedef enum { The mapping provider is a repository of registered object mappings for use by instances of RKObjectManager and RKObjectMapper. It provides for the storage and retrieval of object mappings by keyPath and type. - + The mapping provider is responsible for: - - 1. Providing instances of RKObjectMapper with keyPaths and object mappings for use + + 1. Providing instances of RKObjectMapper with keyPaths and object mappings for use when attempting to map a parsed payload into objects. Each keyPath is examined using valueForKeyPath: to determine if any mappable data exists within the payload. If data is found, the RKObjectMapper will instantiate an RKObjectMappingOperation to perform the mapping using the RKObjectMapping or RKDynamicObjectMapping associated with the keyPath. 1. Providing the appropriate serialization mapping to instances of RKObjectManager when an object - is to be sent to the remote server using [RKObjectManager postObject:delegate:] or - [RKObjectManager postObject:delegate]. This mapping is used to serialize the object into a + is to be sent to the remote server using [RKObjectManager postObject:delegate:] or + [RKObjectManager postObject:delegate]. This mapping is used to serialize the object into a format suitable for encoding into a URL form encoded or JSON representation. 1. Providing convenient storage of RKObjectMapping references for users who are not using keyPath based mapping. Mappings can be added to the provider and retrieved by the [RKObjectMapping objectClass] @@ -59,7 +59,7 @@ typedef enum { /** Creates and returns an autoreleased RKObjectMappingProvider instance. - + @return A new autoreleased object mapping provider instance. */ + (id)mappingProvider; @@ -73,11 +73,11 @@ typedef enum { /** Configures the mapping provider to use the RKObjectMapping or RKDynamicObjectMapping provided when content is encountered at the specified keyPath. - + When an RKObjectMapper is performing its work, each registered keyPath within the mapping provider will be searched for content in the parsed payload. If mappable content is found, the object mapping configured for the keyPath will be used to perform an RKObjectMappingOperation. - + @param objectOrDynamicMapping An RKObjectMapping or RKDynamicObjectMapping to register for keyPath based mapping. @param keyPath The keyPath to register the mapping as being responsible for mapping. @see RKObjectMapper @@ -86,9 +86,9 @@ typedef enum { - (void)setObjectMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping forKeyPath:(NSString *)keyPath; /** - Returns the RKObjectMapping or RKObjectDynamic mapping configured for use + Returns the RKObjectMapping or RKObjectDynamic mapping configured for use when mappable content is encountered at keyPath - + @param keyPath A registered keyPath to retrieve the object mapping for @return The RKObjectMapping or RKDynamicObjectMapping for the specified keyPath or nil if none is registered. */ @@ -97,7 +97,7 @@ typedef enum { /** Removes the RKObjectMapping or RKDynamicObjectMapping registered at the specified keyPath from the provider. - + @param keyPath The keyPath to remove the corresponding mapping for */ - (void)removeObjectMappingForKeyPath:(NSString *)keyPath; @@ -105,7 +105,7 @@ typedef enum { /** Returns a dictionary where the keys are mappable keyPaths and the values are the RKObjectMapping or RKDynamicObjectMapping to use for mappable data that appears at the keyPath. - + @warning The returned dictionary can contain RKDynamicObjectMapping instances. Check the type if you are using dynamic mapping. @return A dictionary of all registered keyPaths and their corresponding object mapping instances @@ -114,35 +114,35 @@ typedef enum { /** Registers an object mapping as being rooted at a specific keyPath. The keyPath will be registered - and an inverse mapping for the object will be generated and used for serialization. - + and an inverse mapping for the object will be generated and used for serialization. + This is a shortcut for configuring a pair of object mappings that model a simple resource the same way when going to and from the server. - + For example, if we have a simple resource called 'person' that returns JSON in the following format: - + { "person": { "first_name": "Blake", "last_name": "Watters" } } - + We might configure a mapping like so: - + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[Person class]]; [mapping mapAttributes:@"first_name", @"last_name", nil]; - + If we want to parse the above JSON and serialize it such that using postObject: or putObject: use the same format, we can auto-generate the serialization mapping and set the whole thing up in one shot: - + [[RKObjectManager sharedManager].mappingProvider registerMapping:mapping withRootKeyPath:@"person"]; - + This will call setMapping:forKeyPath: for you, then generate a serialization mapping and set the root keyPath as well. - + If you want to manipulate the serialization mapping yourself, you can work with the mapping directly: - + RKObjectMapping* serializationMappingForPerson = [personMapping inverseMapping]; // NOTE: Serialization mapping default to a nil root keyPath and will serialize to a flat dictionary [[RKObjectManager sharedManager].mappingProvider setSerializationMapping:serializationMappingForPerson forClass:[Person class]]; - + @param objectMapping An object mapping we wish to register on the provider @param keyPath The keyPath we wish to register for the mapping and use as the rootKeyPath for serialization */ @@ -152,9 +152,9 @@ typedef enum { Adds an object mapping to the provider for later retrieval. The mapping is not bound to a particular keyPath and must be explicitly set on an instance of RKObjectLoader or RKObjectMappingOperation to be applied. This is useful in cases where the remote system does not namespace resources in a keyPath that can be used for disambiguation. - + You can retrieve mappings added to the provider by invoking objectMappingsForClass: and objectMappingForClass: - + @param objectMapping An object mapping instance we wish to register with the provider. @see objectMappingsForClass: @see objectMappingForClass: @@ -164,7 +164,7 @@ typedef enum { /** Returns all object mappings registered for a particular class on the provider. The collection of mappings is assembled by searching for all mappings added via addObjctMapping: and then consulting those registered via objectMappingForKeyPath: - + @param objectClass The class we want to retrieve the mappings for @return An array of all object mappings matching the objectClass. Can be empty. */ @@ -172,16 +172,16 @@ typedef enum { /** Returns the first object mapping for a objectClass registered in the provider. - + The objectClass is the class for a model you use to represent data retrieved in XML or JSON format. For example, if we were developing a Twitter application we might have an objectClass of RKTweet for storing Tweet data. We could retrieve - the object mapping for this model by invoking + the object mapping for this model by invoking `[mappingProvider objectMappingForClass:[RKTweet class]];` - - Mappings registered via addObjectMapping: take precedence over those registered + + Mappings registered via addObjectMapping: take precedence over those registered via setObjectMapping:forKeyPath:. - + @param objectClass The class that we want to return mappings for @return An RKObjectMapping matching objectClass or nil */ @@ -190,11 +190,11 @@ typedef enum { /** Registers an object mapping for use when serializing instances of objectClass for transport over HTTP. Used by the object manager during postObject: and putObject:. - + Serialization mappings are simply instances of RKObjectMapping that target NSMutableDictionary as the target object class. After the object is mapped into an NSMutableDictionary, it can be encoded to form encoded string, JSON, XML, etc. - + @param objectMapping The serialization mapping to register for use when serializing objectClass @param objectClass The class of the object type we are registering a serialization for @see [RKObjectMapping serializationMapping] @@ -203,8 +203,8 @@ typedef enum { /** Returns the serialization mapping for a specific object class - which has been previously registered. - + which has been previously registered. + @param objectClass The class we wish to obtain the serialization mapping for @return The RKObjectMapping instance used for mapping instances of objectClass for transport @see setSerializationMapping:forClass: @@ -214,16 +214,16 @@ typedef enum { /** Configures an object mapping to be used when during a load event where the resourcePath of the RKObjectLoader instance matches resourcePathPattern. - + The resourcePathPattern is a SOCKit pattern matching property names preceded by colons within a path. For example, if a collection of reviews for a product were loaded from a remote system - at the resourcePath @"/products/1234/reviews", object mapping could be configured to handle + at the resourcePath @"/products/1234/reviews", object mapping could be configured to handle this request with a resourcePathPattern of @"/products/:productID/reviews". - + **NOTE** that care must be taken when configuring patterns within the provider. The patterns will be evaluated in the order they are added to the provider, so more specific patterns must precede more general patterns where either would generate a match. - + @param objectMapping The object mapping to use when the resourcePath matches the specified resourcePathPattern. @param resourcePathPattern A pattern to be evaluated using an RKPathMatcher against a resourcePath @@ -237,7 +237,7 @@ typedef enum { /** Returns the first objectMapping configured in the provider with a resourcePathPattern matching the specified resourcePath. - + @param resourcePath A resource path to retrieve the first RKObjectMapping or RKDynamicObjectMapping configured with a matching pattern. @return An RKObjectMapping or RKDynamicObjectMapping for a resource path pattern matching resourcePath @@ -252,7 +252,7 @@ typedef enum { /** An object mapping used when the remote system returns an error status code and a payload with a MIME Type that RestKit is capable of parsing. - + @see RKObjectLoader @see RKParserRegistry */ @@ -262,16 +262,16 @@ typedef enum { An object mapping used when mapping pagination metadata (current page, object count, etc) during a paginated object loading operation. The objectClass of the paginationMapping must be RKObjectPaginator. - + For example, if using the popular will_paginate plugin with Ruby on Rails, we would configure our pagination mapping like so: - + // Assumes the JSON format of http://stackoverflow.com/questions/4699182/will-paginate-json-support RKObjectMapping *paginationMapping = [RKObjectMapping mappingForClass:[RKObjectPaginator class]]; [paginationMapping mapKeyPath:@"current_page" toAttribute:@"currentPage"]; [paginationMapping mapKeyPath:@"per_page" toAttribute:@"perPage"]; [paginationMapping mapKeyPath:@"total_entries" toAttribute:@"objectCount"]; - + @see RKObjectPaginator */ @property (nonatomic, retain) RKObjectMapping *paginationMapping; diff --git a/Code/ObjectMapping/RKObjectMappingProvider.m b/Code/ObjectMapping/RKObjectMappingProvider.m index a4c01d3a11..3bf5d4ecad 100644 --- a/Code/ObjectMapping/RKObjectMappingProvider.m +++ b/Code/ObjectMapping/RKObjectMappingProvider.m @@ -4,13 +4,13 @@ // // Created by Jeremy Ellison on 5/6/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -189,7 +189,7 @@ - (NSArray *)mappingsForContext:(RKObjectMappingProviderContext)context { id contextValue = [self valueForContext:context]; if (contextValue == nil) return [NSArray array]; [self assertStorageForContext:context isKindOfClass:[NSArray class]]; - + return [NSArray arrayWithArray:contextValue]; } @@ -222,12 +222,12 @@ - (void)setMapping:(RKObjectMappingDefinition *)mapping forKeyPath:(NSString *)k NSMutableDictionary *contextValue = [self valueForContext:context]; if (contextValue == nil) { contextValue = [NSMutableDictionary dictionary]; - [self setValue:contextValue forContext:context]; + [self setValue:contextValue forContext:context]; } [self assertStorageForContext:context isKindOfClass:[NSDictionary class]]; [contextValue setValue:mapping forKey:keyPath]; } - + - (void)removeMappingForKeyPath:(NSString *)keyPath context:(RKObjectMappingProviderContext)context { NSMutableDictionary *contextValue = [self valueForContext:context]; [self assertStorageForContext:context isKindOfClass:[NSDictionary class]]; @@ -238,7 +238,7 @@ - (void)setMapping:(RKObjectMappingDefinition *)mapping forPattern:(NSString *)p RKOrderedDictionary *contextValue = [self valueForContext:context]; if (contextValue == nil) { contextValue = [RKOrderedDictionary dictionary]; - [self setValue:contextValue forContext:context]; + [self setValue:contextValue forContext:context]; } [self assertStorageForContext:context isKindOfClass:[RKOrderedDictionary class]]; [contextValue insertObject:[RKObjectMappingProviderContextEntry contextEntryWithMapping:mapping] @@ -250,7 +250,7 @@ - (void)setMapping:(RKObjectMappingDefinition *)mapping forPattern:(NSString *)p RKOrderedDictionary *contextValue = [self valueForContext:context]; if (contextValue == nil) { contextValue = [RKOrderedDictionary dictionary]; - [self setValue:contextValue forContext:context]; + [self setValue:contextValue forContext:context]; } [self assertStorageForContext:context isKindOfClass:[RKOrderedDictionary class]]; [contextValue setObject:[RKObjectMappingProviderContextEntry contextEntryWithMapping:mapping] @@ -292,7 +292,7 @@ - (RKObjectMappingProviderContextEntry *)entryForPatternMatchingString:(NSString return [contextValue objectForKey:pattern]; } } - + return nil; } diff --git a/Code/ObjectMapping/RKObjectMappingProviderContextEntry.m b/Code/ObjectMapping/RKObjectMappingProviderContextEntry.m index ebec22737c..da328f183d 100644 --- a/Code/ObjectMapping/RKObjectMappingProviderContextEntry.m +++ b/Code/ObjectMapping/RKObjectMappingProviderContextEntry.m @@ -41,8 +41,7 @@ - (BOOL)isEqual:(id)object { - (NSUInteger)hash { int prime = 31; int result = 1; - result = prime * [self.mapping hash]; - result = prime * [self.userData hash]; + result = prime * [self.userData hash]? [self.mapping hash] : [self.userData hash]; return result; } @@ -53,7 +52,7 @@ + (RKObjectMappingProviderContextEntry *)contextEntryWithMapping:(RKObjectMappin } + (RKObjectMappingProviderContextEntry *)contextEntryWithMapping:(RKObjectMappingDefinition *)mapping userData:(id)userData { - RKObjectMappingProviderContextEntry * contextEntry = [RKObjectMappingProviderContextEntry contextEntryWithMapping:mapping]; + RKObjectMappingProviderContextEntry *contextEntry = [RKObjectMappingProviderContextEntry contextEntryWithMapping:mapping]; contextEntry.userData = userData; return contextEntry; } diff --git a/Code/ObjectMapping/RKObjectMappingResult.h b/Code/ObjectMapping/RKObjectMappingResult.h index 4d45228c69..07acbbf698 100644 --- a/Code/ObjectMapping/RKObjectMappingResult.h +++ b/Code/ObjectMapping/RKObjectMappingResult.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/7/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/ObjectMapping/RKObjectMappingResult.m b/Code/ObjectMapping/RKObjectMappingResult.m index 9b76e824b7..02061f3919 100644 --- a/Code/ObjectMapping/RKObjectMappingResult.m +++ b/Code/ObjectMapping/RKObjectMappingResult.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/7/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,7 +29,7 @@ - (id)initWithDictionary:(id)dictionary { if (self) { _keyPathToMappedObjects = [dictionary retain]; } - + return self; } @@ -51,14 +51,14 @@ - (NSArray*)asCollection { NSMutableArray* collection = [NSMutableArray array]; for (id object in [_keyPathToMappedObjects allValues]) { // We don't want to strip the keys off of a mapped dictionary result - + if (NO == [object isKindOfClass:[NSDictionary class]] && [object respondsToSelector:@selector(allObjects)]) { [collection addObjectsFromArray:[object allObjects]]; } else { [collection addObject:object]; } } - + return collection; } @@ -68,7 +68,7 @@ - (id)asObject { if (count == 0) { return nil; } - + if (count > 1) RKLogWarning(@"Coerced object mapping result containing %lu objects into singular result.", (unsigned long) count); return [collection objectAtIndex:0]; } @@ -83,7 +83,7 @@ - (NSError*)asError { } NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys:collection, RKObjectMapperErrorObjectsKey, description, NSLocalizedDescriptionKey, nil]; - + NSError* error = [NSError errorWithDomain:RKErrorDomain code:RKObjectMapperErrorFromMappingResult userInfo:userInfo]; return error; } diff --git a/Code/ObjectMapping/RKObjectPaginator.h b/Code/ObjectMapping/RKObjectPaginator.h index 5e6746d655..20c5564eec 100644 --- a/Code/ObjectMapping/RKObjectPaginator.h +++ b/Code/ObjectMapping/RKObjectPaginator.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 12/29/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -33,17 +33,17 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo from remote systems via HTTP. Paginators perform GET requests and use a patterned URL to construct a full URL reflecting the state of the paginator. Paginators rely on an instance of RKObjectMappingProvider to determine how to perform object mapping - on the retrieved data. Paginators can load Core Data backed models provided that an - instance of RKManagedObjectStore is assigned to the paginator. + on the retrieved data. Paginators can load Core Data backed models provided that an + instance of RKManagedObjectStore is assigned to the paginator. */ @interface RKObjectPaginator : NSObject /** Creates and returns a RKObjectPaginator object with the a provided patternURL and mappingProvider. - - @param patternURL A RKURL containing a dynamic pattern for constructing a URL to the paginated + + @param patternURL A RKURL containing a dynamic pattern for constructing a URL to the paginated resource collection. - @param mappingProvider An RKObjectMappingProvider containing object mapping configurations for mapping the + @param mappingProvider An RKObjectMappingProvider containing object mapping configurations for mapping the paginated resource collection. @see patternURL @return A paginator object initialized with patterned URL and mapping provider. @@ -52,10 +52,10 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** Initializes a RKObjectPaginator object with the a provided patternURL and mappingProvider. - - @param patternURL A RKURL containing a dynamic pattern for constructing a URL to the paginated + + @param patternURL A RKURL containing a dynamic pattern for constructing a URL to the paginated resource collection. - @param mappingProvider An RKObjectMappingProvider containing object mapping configurations for mapping the + @param mappingProvider An RKObjectMappingProvider containing object mapping configurations for mapping the paginated resource collection. @see patternURL @return The receiver, initialized with patterned URL and mapping provider. @@ -66,26 +66,26 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo A RKURL with a resource path pattern for building a complete URL from which to load the paginated resource collection. The patterned resource path will be evaluated against the state of the paginator object itself. - + For example, given a paginated collection of data at the /articles resource path, the patterned resource path may look like: - + /articles?per_page=:perPage&page_number=:currentPage - + When the pattern is evaluated against the state of the paginator, this will yield a complete resource path that can be used to load the specified page. Given a paginator configured with 100 objects per page and a current page number of 3, the resource path of the pagination URL would become: - + /articles?per_page=100&page_number=3 - + @see [RKURL URLByInterpolatingResourcePathWithObject:] */ @property (nonatomic, copy) RKURL *patternURL; /** Returns a complete RKURL to the paginated resource collection by interpolating - the state of the paginator object against the resource + the state of the paginator object against the resource */ @property (nonatomic, readonly) RKURL *URL; @@ -96,22 +96,22 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** The block to invoke when the paginator has loaded a page of objects from the collection. - + @see [RKObjectPaginatorDelegate paginator:didLoadObjects:forPage] */ @property (nonatomic, copy) RKObjectPaginatorDidLoadObjectsForPageBlock onDidLoadObjectsForPage; /** The block to invoke when the paginator has failed loading due to an error. - + @see [RKObjectPaginatorDelegate paginator:didFailWithError:objectLoader:] */ @property (nonatomic, copy) RKObjectPaginatorDidFailWithErrorBlock onDidFailWithError; /** - The object that acts as the configuration delegate for RKObjectLoader instances built + The object that acts as the configuration delegate for RKObjectLoader instances built and utilized by the paginator. - + **Default**: nil @see RKClient @see RKObjectManager @@ -142,14 +142,14 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** A Boolean value indicating if the paginator has loaded a page of objects - + @returns YES when the paginator has loaded a page of objects */ @property (nonatomic, readonly, getter = isLoaded) BOOL loaded; /** Returns the page number for the most recently loaded page of objects. - + @return The page number for the current page of objects. @exception NSInternalInconsistencyException Raised if isLoaded is NO. */ @@ -157,7 +157,7 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** Returns the number of pages in the total resource collection. - + @return A count of the number of pages in the resource collection. @exception NSInternalInconsistencyException Raised if hasPageCount is NO. */ @@ -165,7 +165,7 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** Returns the total number of objects in the collection - + @return A count of the number of objects in the resource collection. @exception NSInternalInconsistencyException Raised if hasObjectCount is NO. */ @@ -174,7 +174,7 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** Returns a Boolean value indicating if the total number of pages in the collection is known by the paginator. - + @return YES if the paginator knows the page count, otherwise NO */ - (BOOL)hasPageCount; @@ -182,14 +182,14 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** Returns a Boolean value indicating if the total number of objects in the collection is known by the paginator. - + @return YES if the paginator knows the number of objects in the paginated collection, otherwise NO. */ - (BOOL)hasObjectCount; /** Returns a Boolean value indicating if there is a next page in the collection. - + @return YES if there is a next page, otherwise NO. @exception NSInternalInconsistencyException Raised if isLoaded or hasPageCount is NO. */ @@ -197,7 +197,7 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** Returns a Boolean value indicating if there is a previous page in the collection. - + @return YES if there is a previous page, otherwise NO. @exception NSInternalInconsistencyException Raised if isLoaded is NO. */ @@ -220,7 +220,7 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** Loads a specific page of data by mutating the current page, constructing an object loader to fetch the data, and object mapping the results. - + @param pageNumber The page of objects to load from the remote backend */ - (void)loadPage:(NSUInteger)pageNumber; @@ -237,7 +237,7 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** Tells the delegate the paginator has loaded a page of objects from the collection. - + @param paginator The paginator that loaded the objects. @param objects An array of objects mapped from the remote JSON/XML representation. @param page The page number that was loaded. @@ -246,7 +246,7 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** Tells the delegate the paginator has failed loading due to an error. - + @param paginator The paginator that failed loading due to an error. @param error An NSError indicating the cause of the failure. @param loader The loader request that resulted in the failure. @@ -257,7 +257,7 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** Tells the delegate that the paginator is about to begin loading a page of objects. - + @param paginator The paginator performing the load. @param page The numeric page number being loaded. @param loader The object loader request used to load the page. @@ -266,14 +266,14 @@ typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLo /** Tells the delegate the paginator has loaded the first page of objects in the collection. - + @param paginator The paginator instance that has loaded the first page. */ - (void)paginatorDidLoadFirstPage:(RKObjectPaginator *)paginator; /** Tells the delegate the paginator has loaded the last page of objects in the collection. - + @param paginator The paginator instance that has loaded the last page. */ - (void)paginatorDidLoadLastPage:(RKObjectPaginator *)paginator; diff --git a/Code/ObjectMapping/RKObjectPaginator.m b/Code/ObjectMapping/RKObjectPaginator.m index 1dcc9ed7d2..71e10b196f 100644 --- a/Code/ObjectMapping/RKObjectPaginator.m +++ b/Code/ObjectMapping/RKObjectPaginator.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 12/29/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -62,14 +62,14 @@ - (id)initWithPatternURL:(RKURL *)aPatternURL mappingProvider:(RKObjectMappingPr perPage = RKObjectPaginatorDefaultPerPage; loaded = NO; } - + return self; } - (void)dealloc { delegate = nil; configurationDelegate = nil; - objectLoader.delegate = nil; + objectLoader.delegate = nil; [patternURL release]; patternURL = nil; [mappingProvider release]; @@ -83,8 +83,8 @@ - (void)dealloc { [onDidLoadObjectsForPage release]; onDidLoadObjectsForPage = nil; [onDidFailWithError release]; - onDidFailWithError = nil; - + onDidFailWithError = nil; + [super dealloc]; } @@ -123,7 +123,7 @@ - (NSUInteger)pageCount { - (BOOL)hasNextPage { NSAssert(self.isLoaded, @"Cannot determine hasNextPage: paginator is not loaded."); NSAssert([self hasPageCount], @"Cannot determine hasNextPage: page count is not known."); - + return self.currentPage < self.pageCount; } @@ -139,17 +139,17 @@ - (void)objectLoader:(RKObjectLoader *)objectLoader didLoadObjects:(NSArray *)ob loaded = YES; RKLogInfo(@"Loaded objects: %@", objects); [self.delegate paginator:self didLoadObjects:objects forPage:self.currentPage]; - + if (self.onDidLoadObjectsForPage) { self.onDidLoadObjectsForPage(objects, self.currentPage); } - + if ([self hasPageCount] && self.currentPage == 1) { if ([self.delegate respondsToSelector:@selector(paginatorDidLoadFirstPage:)]) { [self.delegate paginatorDidLoadFirstPage:self]; } } - + if ([self hasPageCount] && self.currentPage == self.pageCount) { if ([self.delegate respondsToSelector:@selector(paginatorDidLoadLastPage:)]) { [self.delegate paginatorDidLoadLastPage:self]; @@ -176,7 +176,7 @@ - (void)objectLoader:(RKObjectLoader *)loader willMapData:(inout id *)mappableDa } else if (self.perPage && [self hasObjectCount]) { float objectCountFloat = self.objectCount; pageCount = ceilf(objectCountFloat / self.perPage); - RKLogInfo(@"Paginator objectCount: %ld pageCount: %ld", (long) self.objectCount, (long) self.pageCount); + RKLogInfo(@"Paginator objectCount: %ld pageCount: %ld", (long) self.objectCount, (long) self.pageCount); } else { NSAssert(NO, @"Paginator perPage set is 0."); RKLogError(@"Paginator perPage set is 0."); @@ -197,23 +197,23 @@ - (void)loadPage:(NSUInteger)pageNumber { NSAssert(self.mappingProvider, @"Cannot perform a load with a nil mappingProvider."); NSAssert(! objectLoader, @"Cannot perform a load while one is already in progress."); currentPage = pageNumber; - + if (self.objectStore) { self.objectLoader = [[[RKManagedObjectLoader alloc] initWithURL:self.URL mappingProvider:self.mappingProvider objectStore:self.objectStore] autorelease]; } else { self.objectLoader = [[[RKObjectLoader alloc] initWithURL:self.URL mappingProvider:self.mappingProvider] autorelease]; } - + if ([self.configurationDelegate respondsToSelector:@selector(configureObjectLoader:)]) { [self.configurationDelegate configureObjectLoader:objectLoader]; } self.objectLoader.method = RKRequestMethodGET; self.objectLoader.delegate = self; - + if ([self.delegate respondsToSelector:@selector(paginator:willLoadPage:objectLoader:)]) { [self.delegate paginator:self willLoadPage:pageNumber objectLoader:self.objectLoader]; } - + [self.objectLoader send]; } diff --git a/Code/ObjectMapping/RKObjectPropertyInspector.h b/Code/ObjectMapping/RKObjectPropertyInspector.h index 004719ef10..1008a418e6 100644 --- a/Code/ObjectMapping/RKObjectPropertyInspector.h +++ b/Code/ObjectMapping/RKObjectPropertyInspector.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 3/4/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,7 +23,7 @@ @class NSEntityDescription; @interface RKObjectPropertyInspector : NSObject { - NSMutableDictionary* _cachedPropertyNamesAndTypes; + NSMutableDictionary* _cachedPropertyNamesAndTypes; } + (RKObjectPropertyInspector*)sharedInspector; diff --git a/Code/ObjectMapping/RKObjectPropertyInspector.m b/Code/ObjectMapping/RKObjectPropertyInspector.m index 54ca2167a2..30f501f6f8 100644 --- a/Code/ObjectMapping/RKObjectPropertyInspector.m +++ b/Code/ObjectMapping/RKObjectPropertyInspector.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 3/4/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -34,78 +34,78 @@ + (RKObjectPropertyInspector*)sharedInspector { if (sharedInspector == nil) { sharedInspector = [RKObjectPropertyInspector new]; } - + return sharedInspector; } - (id)init { - if ((self = [super init])) { - _cachedPropertyNamesAndTypes = [[NSMutableDictionary alloc] init]; - } - - return self; + if ((self = [super init])) { + _cachedPropertyNamesAndTypes = [[NSMutableDictionary alloc] init]; + } + + return self; } - (void)dealloc { - [_cachedPropertyNamesAndTypes release]; - [super dealloc]; + [_cachedPropertyNamesAndTypes release]; + [super dealloc]; } + (NSString*)propertyTypeFromAttributeString:(NSString*)attributeString { - NSString *type = [NSString string]; - NSScanner *typeScanner = [NSScanner scannerWithString:attributeString]; - [typeScanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"@"] intoString:NULL]; - - // we are not dealing with an object - if([typeScanner isAtEnd]) { - return @"NULL"; - } - [typeScanner scanCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\"@"] intoString:NULL]; - // this gets the actual object type - [typeScanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\""] intoString:&type]; - return type; + NSString *type = [NSString string]; + NSScanner *typeScanner = [NSScanner scannerWithString:attributeString]; + [typeScanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"@"] intoString:NULL]; + + // we are not dealing with an object + if([typeScanner isAtEnd]) { + return @"NULL"; + } + [typeScanner scanCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\"@"] intoString:NULL]; + // this gets the actual object type + [typeScanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\""] intoString:&type]; + return type; } - (NSDictionary *)propertyNamesAndTypesForClass:(Class)theClass { - NSMutableDictionary* propertyNames = [_cachedPropertyNamesAndTypes objectForKey:theClass]; - if (propertyNames) { - return propertyNames; - } - propertyNames = [NSMutableDictionary dictionary]; - - //include superclass properties - Class currentClass = theClass; - while (currentClass != nil) { - // Get the raw list of properties - unsigned int outCount; - objc_property_t *propList = class_copyPropertyList(currentClass, &outCount); - - // Collect the property names - int i; - NSString *propName; - for (i = 0; i < outCount; i++) { - // property_getAttributes() returns everything we need to implement this... - // See: http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW5 - objc_property_t* prop = propList + i; - NSString* attributeString = [NSString stringWithCString:property_getAttributes(*prop) encoding:NSUTF8StringEncoding]; - propName = [NSString stringWithCString:property_getName(*prop) encoding:NSUTF8StringEncoding]; - - if (![propName isEqualToString:@"_mapkit_hasPanoramaID"]) { - const char* className = [[RKObjectPropertyInspector propertyTypeFromAttributeString:attributeString] cStringUsingEncoding:NSUTF8StringEncoding]; - Class aClass = objc_getClass(className); - if (aClass) { - [propertyNames setObject:aClass forKey:propName]; - } - } - } - - free(propList); - currentClass = [currentClass superclass]; - } - - [_cachedPropertyNamesAndTypes setObject:propertyNames forKey:theClass]; + NSMutableDictionary* propertyNames = [_cachedPropertyNamesAndTypes objectForKey:theClass]; + if (propertyNames) { + return propertyNames; + } + propertyNames = [NSMutableDictionary dictionary]; + + //include superclass properties + Class currentClass = theClass; + while (currentClass != nil) { + // Get the raw list of properties + unsigned int outCount; + objc_property_t *propList = class_copyPropertyList(currentClass, &outCount); + + // Collect the property names + int i; + NSString *propName; + for (i = 0; i < outCount; i++) { + // property_getAttributes() returns everything we need to implement this... + // See: http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW5 + objc_property_t* prop = propList + i; + NSString* attributeString = [NSString stringWithCString:property_getAttributes(*prop) encoding:NSUTF8StringEncoding]; + propName = [NSString stringWithCString:property_getName(*prop) encoding:NSUTF8StringEncoding]; + + if (![propName isEqualToString:@"_mapkit_hasPanoramaID"]) { + const char* className = [[RKObjectPropertyInspector propertyTypeFromAttributeString:attributeString] cStringUsingEncoding:NSUTF8StringEncoding]; + Class aClass = objc_getClass(className); + if (aClass) { + [propertyNames setObject:aClass forKey:propName]; + } + } + } + + free(propList); + currentClass = [currentClass superclass]; + } + + [_cachedPropertyNamesAndTypes setObject:propertyNames forKey:theClass]; RKLogDebug(@"Cached property names and types for Class '%@': %@", NSStringFromClass(theClass), propertyNames); - return propertyNames; + return propertyNames; } - (Class)typeForProperty:(NSString*)propertyName ofClass:(Class)objectClass { diff --git a/Code/ObjectMapping/RKObjectRelationshipMapping.h b/Code/ObjectMapping/RKObjectRelationshipMapping.h index 657da88516..e2b4c26a1b 100644 --- a/Code/ObjectMapping/RKObjectRelationshipMapping.h +++ b/Code/ObjectMapping/RKObjectRelationshipMapping.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/4/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/ObjectMapping/RKObjectRelationshipMapping.m b/Code/ObjectMapping/RKObjectRelationshipMapping.m index b88f6f3230..555ea7eb8b 100644 --- a/Code/ObjectMapping/RKObjectRelationshipMapping.m +++ b/Code/ObjectMapping/RKObjectRelationshipMapping.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/4/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -26,7 +26,7 @@ @implementation RKObjectRelationshipMapping @synthesize reversible = _reversible; + (RKObjectRelationshipMapping*)mappingFromKeyPath:(NSString*)sourceKeyPath toKeyPath:(NSString*)destinationKeyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping reversible:(BOOL)reversible { - RKObjectRelationshipMapping* relationshipMapping = (RKObjectRelationshipMapping*) [self mappingFromKeyPath:sourceKeyPath toKeyPath:destinationKeyPath]; + RKObjectRelationshipMapping* relationshipMapping = (RKObjectRelationshipMapping*) [self mappingFromKeyPath:sourceKeyPath toKeyPath:destinationKeyPath]; relationshipMapping.reversible = reversible; relationshipMapping.mapping = objectOrDynamicMapping; return relationshipMapping; diff --git a/Code/ObjectMapping/RKObjectRouter.h b/Code/ObjectMapping/RKObjectRouter.h index 61b1ada729..a340266ceb 100644 --- a/Code/ObjectMapping/RKObjectRouter.h +++ b/Code/ObjectMapping/RKObjectRouter.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 10/18/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -31,18 +31,18 @@ * the resourcePath using a single colon delimiter, such as /users/:userID */ @interface RKObjectRouter : NSObject { - NSMutableDictionary* _routes; + NSMutableDictionary* _routes; } /** * Register a mapping from an object class to a resource path. This resourcePath can be static * (i.e. /this/is/the/path) or dynamic (i.e. /users/:userID/:username). Dynamic routes are * evaluated against the object being routed using Key-Value coding and coerced into a string. - * *NOTE* - The pattern matcher fully supports KVM, so /:key1.otherKey normally resolves as it + * *NOTE* - The pattern matcher fully supports KVM, so /:key1.otherKey normally resolves as it * would in any other KVM situation, ... otherKey is a sub-key on a the object represented by - * key1. This presents a problem in situations where you might want to build a pattern like - * /:filename.json, where the dot isn't intended as a sub-key on the dynamic "filename", but - * rather it is part of the "json" static string. In these instances, you need to escape the + * key1. This presents a problem in situations where you might want to build a pattern like + * /:filename.json, where the dot isn't intended as a sub-key on the dynamic "filename", but + * rather it is part of the "json" static string. In these instances, you need to escape the * dot with two backslashes, like so: /:filename\\.json * @see RKPathMatcher */ @@ -55,10 +55,10 @@ - (void)routeClass:(Class)objectClass toResourcePathPattern:(NSString*)resourcePathPattern forMethod:(RKRequestMethod)method; /** - * Register a mapping from an object class to a resource path for a specific HTTP method, + * Register a mapping from an object class to a resource path for a specific HTTP method, * optionally adding url escapes to the path. This urlEscape flag comes in handy when you want to provide * your own fully escaped dynamic resource path via a method/attribute on the object model. - * For example, if your Person model has a string attribute titled "polymorphicResourcePath" that returns + * For example, if your Person model has a string attribute titled "polymorphicResourcePath" that returns * @"/this/is/the/path", you should configure the route with url escapes 'off', otherwise the router will return * @"%2Fthis%2Fis%2Fthe%2Fpath". * @see RKPathMatcher diff --git a/Code/ObjectMapping/RKObjectRouter.m b/Code/ObjectMapping/RKObjectRouter.m index 61f2f4c029..3a5dd486f1 100644 --- a/Code/ObjectMapping/RKObjectRouter.m +++ b/Code/ObjectMapping/RKObjectRouter.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 10/18/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -50,7 +50,7 @@ - (void)routeClass:(Class)theClass toResourcePathPattern:(NSString *)resourcePat } NSMutableDictionary *routeEntry = [NSMutableDictionary dictionaryWithObjectsAndKeys: - resourcePathPattern, @"resourcePath", + resourcePathPattern, @"resourcePath", [NSNumber numberWithBool:addEscapes], @"addEscapes", nil]; [classRoutes setValue:routeEntry forKey:methodName]; } @@ -124,11 +124,11 @@ - (NSString *)resourcePathForObject:(NSObject *)object method:(RKRequestMethod)m RKPathMatcher *matcher = [RKPathMatcher matcherWithPattern:[routeEntry objectForKey:@"resourcePath"]]; NSString *interpolatedPath = [matcher pathFromObject:object addingEscapes:addEscapes]; - return interpolatedPath; + return interpolatedPath; } [NSException raise:@"Unable to find a routable path for object" format:@"Unable to find a routable path for object of type '%@' for HTTP Method '%@'", className, methodName]; - + return nil; } diff --git a/Code/ObjectMapping/RKObjectSerializer.h b/Code/ObjectMapping/RKObjectSerializer.h index 1217372fe7..87758e672c 100644 --- a/Code/ObjectMapping/RKObjectSerializer.h +++ b/Code/ObjectMapping/RKObjectSerializer.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/2/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/ObjectMapping/RKObjectSerializer.m b/Code/ObjectMapping/RKObjectSerializer.m index 4dbdd615c8..aecdcf890c 100644 --- a/Code/ObjectMapping/RKObjectSerializer.m +++ b/Code/ObjectMapping/RKObjectSerializer.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/2/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -51,7 +51,7 @@ - (id)initWithObject:(id)object mapping:(RKObjectMapping*)mapping { - (void)dealloc { [_object release]; [_mapping release]; - + [super dealloc]; } @@ -64,13 +64,13 @@ - (id)serializedObject:(NSError**)error { if (!success) { return nil; } - + // Optionally enclose the serialized object within a container... if (_mapping.rootKeyPath) { // TODO: Should log this... dictionary = [NSMutableDictionary dictionaryWithObject:dictionary forKey:_mapping.rootKeyPath]; } - + return dictionary; } @@ -83,10 +83,10 @@ - (id)serializedObjectForMIMEType:(NSString*)MIMEType error:(NSError**)error { if (string == nil) { return nil; } - + return string; } - + return nil; } @@ -101,7 +101,7 @@ - (id)serializedObjectForMIMEType:(NSString*)MIMEType error:(NSError**)error { return [RKRequestSerialization serializationWithData:data MIMEType:MIMEType]; } } - + return nil; } @@ -110,7 +110,7 @@ - (id)serializedObjectForMIMEType:(NSString*)MIMEType error:(NSError**)error { - (void)objectMappingOperation:(RKObjectMappingOperation *)operation didSetValue:(id)value forKeyPath:(NSString *)keyPath usingMapping:(RKObjectAttributeMapping *)mapping { id transformedValue = nil; Class orderedSetClass = NSClassFromString(@"NSOrderedSet"); - + if ([value isKindOfClass:[NSDate class]]) { // Date's are not natively serializable, must be encoded as a string @synchronized(self.mapping.preferredDateFormatter) { @@ -118,12 +118,12 @@ - (void)objectMappingOperation:(RKObjectMappingOperation *)operation didSetValue } } else if ([value isKindOfClass:[NSDecimalNumber class]]) { // Precision numbers are serialized as strings to work around Javascript notation limits - transformedValue = [(NSDecimalNumber*)value stringValue]; + transformedValue = [(NSDecimalNumber*)value stringValue]; } else if ([value isKindOfClass:orderedSetClass]) { // NSOrderedSets are not natively serializable, so let's just turn it into an NSArray transformedValue = [value array]; } - + if (transformedValue) { RKLogDebug(@"Serialized %@ value at keyPath to %@ (%@)", NSStringFromClass([value class]), NSStringFromClass([transformedValue class]), value); [operation.destinationObject setValue:transformedValue forKey:keyPath]; diff --git a/Code/ObjectMapping/RKParserRegistry.h b/Code/ObjectMapping/RKParserRegistry.h index 6a8e257cfc..0e0d4c3fdd 100644 --- a/Code/ObjectMapping/RKParserRegistry.h +++ b/Code/ObjectMapping/RKParserRegistry.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/18/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,42 +29,42 @@ */ @interface RKParserRegistry : NSObject { NSMutableDictionary *_MIMETypeToParserClasses; - NSMutableArray *_MIMETypeToParserClassesRegularExpressions; + NSMutableArray *_MIMETypeToParserClassesRegularExpressions; } /** Return the global shared singleton registry for MIME Type to Parsers - + @return The global shared RKParserRegistry instance. */ + (RKParserRegistry *)sharedRegistry; /** Sets the global shared registry singleton to a new instance of RKParserRegistry - + @param registry A new parser registry object to configure as the shared instance. */ + (void)setSharedRegistry:(RKParserRegistry *)registry; /** - Returns an instance of the RKParser conformant class registered to handle content + Returns an instance of the RKParser conformant class registered to handle content with the given MIME Type. - + MIME Types are searched in the order in which they are registered and exact string matches are favored over regular expressions. - + @param MIMEType The MIME Type of the content to be parsed/serialized. @return An instance of the RKParser conformant class registered to handle the given MIME Type. */ - (id)parserForMIMEType:(NSString *)MIMEType; /** - Returns an instance of the RKParser conformant class registered to handle content - with the given MIME Type. - + Returns an instance of the RKParser conformant class registered to handle content + with the given MIME Type. + MIME Types are searched in the order in which they are registered and exact string matches are favored over regular expressions. - + @param MIMEType The MIME Type of the content to be parsed/serialized. @return The RKParser conformant class registered to handle the given MIME Type. */ @@ -73,7 +73,7 @@ /** Registers an RKParser conformant class as the handler for MIME Types exactly matching the specified MIME Type string. - + @param parserClass The RKParser conformant class to instantiate when parsing/serializing MIME Types matching MIMETypeExpression. @param MIMEType A MIME Type string for which instances of parserClass should be used for parsing/serialization. */ @@ -84,7 +84,7 @@ /** Registers an RKParser conformant class as the handler for MIME Types matching the specified regular expression. - + @param parserClass The RKParser conformant class to instantiate when parsing/serializing MIME Types matching MIMETypeExpression. @param MIMETypeRegex A regular expression that matches MIME Types that should be handled by instances of parserClass. */ diff --git a/Code/ObjectMapping/RKParserRegistry.m b/Code/ObjectMapping/RKParserRegistry.m index fa976180fc..16ede88d42 100644 --- a/Code/ObjectMapping/RKParserRegistry.m +++ b/Code/ObjectMapping/RKParserRegistry.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/18/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,7 +29,7 @@ + (RKParserRegistry *)sharedRegistry { gSharedRegistry = [RKParserRegistry new]; [gSharedRegistry autoconfigure]; } - + return gSharedRegistry; } @@ -45,32 +45,32 @@ - (id)init { _MIMETypeToParserClasses = [[NSMutableDictionary alloc] init]; _MIMETypeToParserClassesRegularExpressions = [[NSMutableArray alloc] init]; } - + return self; } - (void)dealloc { [_MIMETypeToParserClasses release]; - [_MIMETypeToParserClassesRegularExpressions release]; + [_MIMETypeToParserClassesRegularExpressions release]; [super dealloc]; } - (Class)parserClassForMIMEType:(NSString *)MIMEType { - id parserClass = [_MIMETypeToParserClasses objectForKey:MIMEType]; + id parserClass = [_MIMETypeToParserClasses objectForKey:MIMEType]; #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1070 || __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000 - if (!parserClass) - { - for (NSArray *regexAndClass in _MIMETypeToParserClassesRegularExpressions) { - NSRegularExpression *regex = [regexAndClass objectAtIndex:0]; - NSUInteger numberOfMatches = [regex numberOfMatchesInString:MIMEType options:0 range:NSMakeRange(0, [MIMEType length])]; - if (numberOfMatches) { - parserClass = [regexAndClass objectAtIndex:1]; - break; - } - } - } + if (!parserClass) + { + for (NSArray *regexAndClass in _MIMETypeToParserClassesRegularExpressions) { + NSRegularExpression *regex = [regexAndClass objectAtIndex:0]; + NSUInteger numberOfMatches = [regex numberOfMatchesInString:MIMEType options:0 range:NSMakeRange(0, [MIMEType length])]; + if (numberOfMatches) { + parserClass = [regexAndClass objectAtIndex:1]; + break; + } + } + } #endif - return parserClass; + return parserClass; } - (void)setParserClass:(Class)parserClass forMIMEType:(NSString *)MIMEType { @@ -80,7 +80,7 @@ - (void)setParserClass:(Class)parserClass forMIMEType:(NSString *)MIME #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1070 || __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000 - (void)setParserClass:(Class)parserClass forMIMETypeRegularExpression:(NSRegularExpression *)MIMETypeRegex { - NSArray *expressionAndClass = [NSArray arrayWithObjects:MIMETypeRegex, parserClass, nil]; + NSArray *expressionAndClass = [NSArray arrayWithObjects:MIMETypeRegex, parserClass, nil]; [_MIMETypeToParserClassesRegularExpressions addObject:expressionAndClass]; } @@ -91,15 +91,15 @@ - (void)setParserClass:(Class)parserClass forMIMETypeRegularExpression if (parserClass) { return [[[parserClass alloc] init] autorelease]; } - + return nil; } - (void)autoconfigure { Class parserClass = nil; - + // JSON - NSSet *JSONParserClassNames = [NSSet setWithObjects:@"RKJSONParserJSONKit", @"RKJSONParserYAJL", @"RKJSONParserSBJSON", @"RKJSONParserNXJSON", nil]; + NSSet *JSONParserClassNames = [NSSet setWithObjects:@"RKJSONParserJSONKit", @"RKJSONParserYAJL", @"RKJSONParserSBJSON", @"RKJSONParserNXJSON", nil]; for (NSString *parserClassName in JSONParserClassNames) { parserClass = NSClassFromString(parserClassName); if (parserClass) { @@ -107,7 +107,7 @@ - (void)autoconfigure { break; } } - + // XML parserClass = NSClassFromString(@"RKXMLParserXMLReader"); if (parserClass) { diff --git a/Code/ObjectMapping/RKRouter.h b/Code/ObjectMapping/RKRouter.h index a058bd1b47..6d6c06383c 100644 --- a/Code/ObjectMapping/RKRouter.h +++ b/Code/ObjectMapping/RKRouter.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 7/20/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/RestKit.h b/Code/RestKit.h index e3541a5e9a..b779fa24b2 100644 --- a/Code/RestKit.h +++ b/Code/RestKit.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 2/19/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -26,7 +26,7 @@ /** Set the App logging component. This header file is generally only imported by apps that - are pulling in all of RestKit. By setting the + are pulling in all of RestKit. By setting the log component to App here, we allow the app developer to use RKLog() in their own app. */ diff --git a/Code/Support/NSArray+RKAdditions.h b/Code/Support/NSArray+RKAdditions.h new file mode 100644 index 0000000000..466429dfb9 --- /dev/null +++ b/Code/Support/NSArray+RKAdditions.h @@ -0,0 +1,27 @@ +// +// NSArray+RKAdditions.h +// RestKit +// +// Created by Blake Watters on 4/10/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import + +/** + Provides useful additions to the NSArray interface. + */ +@interface NSArray (RKAdditions) + +/** + Evaluates a given key path against the receiving array, divides the array entries into + sections grouped by the value for the key path, and returns an aggregate array of arrays + containing the sections. The receiving array is assumed to be sorted. + + @param keyPath The key path of the value to group the entries by. + @returns An array of section arrays, with each section containing a group of objects sharing + the same value for the given key path. + */ +- (NSArray *)sectionsGroupedByKeyPath:(NSString *)keyPath; + +@end diff --git a/Code/Support/NSArray+RKAdditions.m b/Code/Support/NSArray+RKAdditions.m new file mode 100644 index 0000000000..f632b1bbd6 --- /dev/null +++ b/Code/Support/NSArray+RKAdditions.m @@ -0,0 +1,57 @@ +// +// NSArray+RKAdditions.m +// RestKit +// +// Created by Blake Watters on 4/10/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "NSArray+RKAdditions.h" + +@implementation NSArray (RKAdditions) + +- (NSArray *)sectionsGroupedByKeyPath:(NSString *)keyPath +{ + // Code adapted from: https://gist.github.com/1243312 + NSMutableArray *sections = [NSMutableArray array]; + + // If we don't contain any items, return an empty collection of sections. + if([self count] == 0) { + return sections; + } + + // Create the first section and establish the first section's grouping value. + NSMutableArray *sectionItems = [NSMutableArray array]; + id currentGroup = [[self objectAtIndex:0] valueForKeyPath:keyPath]; + + // Iterate over our items, placing them in the appropriate section and + // creating new sections when necessary. + for (id item in self) { + // Retrieve the grouping value from the current item. + id itemGroup = [item valueForKeyPath:keyPath]; + + // Compare the current item's grouping value to the current section's + // grouping value. + if (![itemGroup isEqual:currentGroup] && (currentGroup != nil || itemGroup != nil)) { + // The current item doesn't belong in the current section, so + // store the section we've been building and create a new one, + // caching the new grouping value. + [sections addObject:sectionItems]; + sectionItems = [NSMutableArray array]; + currentGroup = itemGroup; + } + + // Add the item to the appropriate section. + [sectionItems addObject:item]; + } + + // If we were adding items to a section that has not yet been added + // to the aggregate section collection, add it now. + if ([sectionItems count] > 0) { + [sections addObject:sectionItems]; + } + + return sections; +} + +@end diff --git a/Code/Support/NSBundle+RKAdditions.m b/Code/Support/NSBundle+RKAdditions.m index 0579bde2cf..eed3412e57 100644 --- a/Code/Support/NSBundle+RKAdditions.m +++ b/Code/Support/NSBundle+RKAdditions.m @@ -68,12 +68,12 @@ - (NSString *)stringWithContentsOfResource:(NSString *)name withExtension:(NSStr return nil; } - NSString* fixtureData = [NSString stringWithContentsOfFile:resourcePath encoding:encoding error:&error]; + NSString* fixtureData = [NSString stringWithContentsOfFile:resourcePath encoding:encoding error:&error]; if (fixtureData == nil && error) { RKLogWarning(@"Failed to read "); } - return fixtureData; + return fixtureData; } #if TARGET_OS_IPHONE diff --git a/Code/Support/NSDictionary+RKAdditions.h b/Code/Support/NSDictionary+RKAdditions.h index b1131747aa..44fe6653c5 100644 --- a/Code/Support/NSDictionary+RKAdditions.h +++ b/Code/Support/NSDictionary+RKAdditions.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/5/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -32,9 +32,9 @@ + (id)dictionaryWithKeysAndObjects:(id)firstKey, ... NS_REQUIRES_NIL_TERMINATION; /** - Return a new dictionary by stripping out any percent escapes (such as %20) + Return a new dictionary by stripping out any percent escapes (such as %20) from the receiving dictionary's key and values. - + @return A new dictionary wherein any percent escape sequences in the key and values have been replaced with their literal values. */ @@ -42,14 +42,14 @@ /** Returns a dictionary by digesting a URL encoded set of key/value pairs into unencoded - values. Keys that appear multiple times with the string are decoded into an array of + values. Keys that appear multiple times with the string are decoded into an array of values. */ + (NSDictionary *)dictionaryWithURLEncodedString:(NSString *)URLEncodedString; /** Returns a representation of the dictionary as a URLEncoded string - + @returns A UTF-8 encoded string representation of the keys/values in the dictionary */ - (NSString *)stringWithURLEncodedEntries; diff --git a/Code/Support/NSDictionary+RKAdditions.m b/Code/Support/NSDictionary+RKAdditions.m index 53cba90bd1..dc5d79454a 100644 --- a/Code/Support/NSDictionary+RKAdditions.m +++ b/Code/Support/NSDictionary+RKAdditions.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/5/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -27,17 +27,17 @@ @implementation NSDictionary (RKAdditions) + (id)dictionaryWithKeysAndObjects:(id)firstKey, ... { - va_list args; + va_list args; va_start(args, firstKey); - NSMutableArray* keys = [NSMutableArray array]; - NSMutableArray* values = [NSMutableArray array]; + NSMutableArray* keys = [NSMutableArray array]; + NSMutableArray* values = [NSMutableArray array]; for (id key = firstKey; key != nil; key = va_arg(args, id)) { - id value = va_arg(args, id); + id value = va_arg(args, id); [keys addObject:key]; - [values addObject:value]; + [values addObject:value]; } va_end(args); - + return [self dictionaryWithObjects:values forKeys:keys]; } @@ -62,7 +62,7 @@ + (NSDictionary *)dictionaryWithURLEncodedString:(NSString *)URLEncodedString { if ([keyValuePairArray count] < 2) continue; // Verify that there is at least one key, and at least one value. Ignore extra = signs NSString *key = [[keyValuePairArray objectAtIndex:0] stringByReplacingURLEncoding]; NSString *value = [[keyValuePairArray objectAtIndex:1] stringByReplacingURLEncoding]; - + // URL spec says that multiple values are allowed per key id results = [queryComponents objectForKey:key]; if(results) { @@ -72,7 +72,7 @@ + (NSDictionary *)dictionaryWithURLEncodedString:(NSString *)URLEncodedString { // On second occurrence of the key, convert into an array NSMutableArray *values = [NSMutableArray arrayWithObjects:results, value, nil]; [queryComponents setObject:values forKey:key]; - } + } } else { [queryComponents setObject:value forKey:key]; } @@ -90,15 +90,15 @@ - (void)URLEncodeParts:(NSMutableArray*)parts path:(NSString*)inPath { [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { NSString *encodedKey = [[key description] stringByAddingURLEncoding]; NSString *path = inPath ? [inPath stringByAppendingFormat:@"[%@]", encodedKey] : encodedKey; - + if ([value isKindOfClass:[NSArray class]]) { - for (id item in value) { + for (id item in value) { if ([item isKindOfClass:[NSDictionary class]] || [item isKindOfClass:[NSMutableDictionary class]]) { [item URLEncodeParts:parts path:[path stringByAppendingString:@"[]"]]; } else { [self URLEncodePart:parts path:[path stringByAppendingString:@"[]"] value:item]; } - + } } else if([value isKindOfClass:[NSDictionary class]] || [value isKindOfClass:[NSMutableDictionary class]]) { [value URLEncodeParts:parts path:path]; diff --git a/Code/Support/NSString+RKAdditions.h b/Code/Support/NSString+RKAdditions.h index cb12255518..b5a21bbc7b 100644 --- a/Code/Support/NSString+RKAdditions.h +++ b/Code/Support/NSString+RKAdditions.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 6/15/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -31,7 +31,7 @@ This is a convenience method for constructing a new resource path that includes a query. For example, when given a resourcePath of /contacts and a dictionary of parameters containing foo=bar and color=red, will return /contacts?foo=bar&color=red - + *NOTE* - Assumes that the resource path does not already contain any query parameters. @param queryParameters A dictionary of query parameters to be URL encoded and appended to the resource path @return A new resource path with the query parameters appended @@ -43,11 +43,11 @@ Convenience method for generating a path against the properties of an object. Takes a string with property names prefixed with a colon and interpolates the values of the properties specified and returns the generated path. - + For example, given an 'article' object with an 'articleID' property of 12345 [@"articles/:articleID" interpolateWithObject:article] would generate @"articles/12345" This functionality is the basis for resource path generation in the Router. - + @param object The object to interpolate the properties against @see RKMakePathWithObject @see RKPathMatcher @@ -56,9 +56,9 @@ /** Returns a dictionary of parameter keys and values using UTF-8 encoding given a URL-style query string - on the receiving object. For example, when given the string /contacts?foo=bar&color=red, + on the receiving object. For example, when given the string /contacts?foo=bar&color=red, this will return a dictionary of parameters containing foo=bar and color=red, excluding the path "/contacts?" - + @param receiver A string in the form of @"/object/?sortBy=name", or @"/object/?sortBy=name&color=red" @return A new dictionary of query parameters, with keys like 'sortBy' and values like 'name'. */ @@ -66,12 +66,12 @@ /** Returns a dictionary of parameter keys and values given a URL-style query string - on the receiving object. For example, when given the string /contacts?foo=bar&color=red, + on the receiving object. For example, when given the string /contacts?foo=bar&color=red, this will return a dictionary of parameters containing foo=bar and color=red, excludes the path "/contacts?" - + This method originally appeared as queryContentsUsingEncoding: in the Three20 project: https://github.com/facebook/three20/blob/master/src/Three20Core/Sources/NSStringAdditions.m - + @param receiver A string in the form of @"/object/?sortBy=name", or @"/object/?sortBy=name&color=red" @param encoding The encoding for to use while parsing the query string. @return A new dictionary of query parameters, with keys like 'sortBy' and values like 'name'. @@ -80,12 +80,12 @@ /** Returns a dictionary of parameter keys and values arrays (if requested) given a URL-style query string - on the receiving object. For example, when given the string /contacts?foo=bar&color=red, + on the receiving object. For example, when given the string /contacts?foo=bar&color=red, this will return a dictionary of parameters containing foo=[bar] and color=[red], excludes the path "/contacts?" - + This method originally appeared as queryContentsUsingEncoding: in the Three20 project: https://github.com/facebook/three20/blob/master/src/Three20Core/Sources/NSStringAdditions.m - + @param receiver A string in the form of @"/object?sortBy=name", or @"/object?sortBy=name&color=red" @param shouldUseArrays If NO, it yields the same results as queryParametersUsingEncoding:, otherwise it creates value arrays instead of value strings. @param encoding The encoding for to use while parsing the query string. @@ -95,7 +95,7 @@ - (NSDictionary *)queryParametersUsingArrays:(BOOL)shouldUseArrays encoding:(NSStringEncoding)encoding; /** - Returns a URL encoded representation of self. + Returns a URL encoded representation of self. */ - (NSString *)stringByAddingURLEncoding; @@ -106,9 +106,9 @@ - (NSString *)stringByReplacingURLEncoding; /** - Returns a new string made by appending a path component to the original string, + Returns a new string made by appending a path component to the original string, along with a trailing slash if the component is designated a directory. - + @param pathComponent The path component to add to the URL. @param isDirectory: If TRUE, a trailing slash is appended after pathComponent. @return A new string with pathComponent appended. @@ -116,19 +116,19 @@ - (NSString *)stringByAppendingPathComponent:(NSString *)pathComponent isDirectory:(BOOL)isDirectory; /** - Interprets the receiver as a path and returns the MIME Type for the path extension + Interprets the receiver as a path and returns the MIME Type for the path extension using Core Services. - - For example, given a string with the path /Users/blake/Documents/monkey.json we would get + + For example, given a string with the path /Users/blake/Documents/monkey.json we would get @"application/json" as the MIME Type. - + @return The expected MIME Type of the resource identified by the path or nil if unknown */ - (NSString *)MIMETypeForPathExtension; /** Returns YES if the receiver contains a valid IP address - + For example, @"127.0.0.1" and @"10.0.1.35" would return YES while @"restkit.org" would return NO */ @@ -136,7 +136,7 @@ /** Returns a string of the MD5 sum of the receiver. - + @return A new string containing the MD5 sum of the receiver. */ - (NSString *)MD5; diff --git a/Code/Support/NSString+RKAdditions.m b/Code/Support/NSString+RKAdditions.m index 2da08ce33f..b53e85d89c 100644 --- a/Code/Support/NSString+RKAdditions.m +++ b/Code/Support/NSString+RKAdditions.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 6/15/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -85,7 +85,7 @@ - (NSDictionary*)queryParametersUsingArrays:(BOOL)shouldUseArrays encoding:(NSSt [scanner scanUpToCharactersFromSet:delimiterSet intoString:&pairString]; [scanner scanCharactersFromSet:delimiterSet intoString:NULL]; NSArray* kvPair = [pairString componentsSeparatedByString:@"="]; - + if (!shouldUseArrays) { if (kvPair.count == 2) { NSString* key = [[kvPair objectAtIndex:0] @@ -106,7 +106,7 @@ - (NSDictionary*)queryParametersUsingArrays:(BOOL)shouldUseArrays encoding:(NSSt } if (kvPair.count == 1) { [values addObject:[NSNull null]]; - + } else if (kvPair.count == 2) { NSString* value = [[kvPair objectAtIndex:1] stringByReplacingPercentEscapesUsingEncoding:encoding]; @@ -123,13 +123,13 @@ - (NSString *)stringByAddingURLEncoding { CFStringRef legalURLCharactersToBeEscaped = CFSTR(":/?#[]@!$ &'()*+,;=\"<>%{}|\\^~`\n\r"); CFStringRef encodedString = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)self, - NULL, + NULL, legalURLCharactersToBeEscaped, kCFStringEncodingUTF8); - if (encodedString) { + if (encodedString) { return [(NSString *)encodedString autorelease]; } - + // TODO: Log a warning? return @""; } @@ -162,7 +162,7 @@ - (NSString *)MIMETypeForPathExtension { - (BOOL)isIPAddress { struct sockaddr_in sa; char *hostNameOrIPAddressCString = (char *) [self UTF8String]; - int result = inet_pton(AF_INET, hostNameOrIPAddressCString, &(sa.sin_addr)); + int result = inet_pton(AF_INET, hostNameOrIPAddressCString, &(sa.sin_addr)); return (result != 0); } diff --git a/Code/Support/NSURL+RKAdditions.h b/Code/Support/NSURL+RKAdditions.h index f9e03680b0..dc2c9fbe20 100644 --- a/Code/Support/NSURL+RKAdditions.h +++ b/Code/Support/NSURL+RKAdditions.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 10/11/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -30,10 +30,10 @@ /** Returns the MIME Type for the resource identified by the URL by interpretting the path extension using Core Services. - - For example, given a URL to http://restkit.org/monkey.json we would get + + For example, given a URL to http://restkit.org/monkey.json we would get @"application/json" as the MIME Type. - + @return The expected MIME Type of the resource identified by the URL or nil if unknown */ - (NSString *)MIMETypeForPathExtension; diff --git a/Code/Support/NSURL+RKAdditions.m b/Code/Support/NSURL+RKAdditions.m index 0fb24f698b..ee937f8375 100644 --- a/Code/Support/NSURL+RKAdditions.m +++ b/Code/Support/NSURL+RKAdditions.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 10/11/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/Support/Parsers/JSON/RKJSONParserJSONKit.h b/Code/Support/Parsers/JSON/RKJSONParserJSONKit.h index 6e7c7a5e13..e283ec978f 100644 --- a/Code/Support/Parsers/JSON/RKJSONParserJSONKit.h +++ b/Code/Support/Parsers/JSON/RKJSONParserJSONKit.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/14/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/Support/Parsers/JSON/RKJSONParserJSONKit.m b/Code/Support/Parsers/JSON/RKJSONParserJSONKit.m index 384cc6c2f1..56f6ebf756 100644 --- a/Code/Support/Parsers/JSON/RKJSONParserJSONKit.m +++ b/Code/Support/Parsers/JSON/RKJSONParserJSONKit.m @@ -4,13 +4,13 @@ // // Created by Jeff Arena on 3/16/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -32,12 +32,12 @@ @implementation RKJSONParserJSONKit - (NSDictionary*)objectFromString:(NSString*)string error:(NSError**)error { - RKLogTrace(@"string='%@'", string); + RKLogTrace(@"string='%@'", string); return [string objectFromJSONStringWithParseOptions:JKParseOptionStrict error:error]; } - (NSString*)stringFromObject:(id)object error:(NSError**)error { - return [object JSONStringWithOptions:JKSerializeOptionNone error:error]; + return [object JSONStringWithOptions:JKSerializeOptionNone error:error]; } @end diff --git a/Code/Support/Parsers/XML/RKXMLParserXMLReader.h b/Code/Support/Parsers/XML/RKXMLParserXMLReader.h index 5ee142b9c7..f4aa125fd8 100644 --- a/Code/Support/Parsers/XML/RKXMLParserXMLReader.h +++ b/Code/Support/Parsers/XML/RKXMLParserXMLReader.h @@ -10,13 +10,13 @@ Provides a basic XML implementation using an adapted version of the XMLReader class by "Insert-Witty-Name" available at: https://github.com/RestKit/XML-to-NSDictionary - + RKXMLParserXMLReader will parse an XML document into an NSDictionary representation suitable for use with RestKit's key-value coding based object mapping implementation. - + XML attributes are represented as keys in a dictionary. - + **NOTE** When an XML tag is parsed containing both XML attributes and an enclosed text node, the value of the text node will be inserted in the parsed dictionary at the `@"text"` key. @@ -26,7 +26,7 @@ #import "RKParser.h" @interface RKXMLParserXMLReader : NSObject { - + } @end diff --git a/Code/Support/RKAlert.h b/Code/Support/RKAlert.h index 36db3af706..00e7ab49e8 100644 --- a/Code/Support/RKAlert.h +++ b/Code/Support/RKAlert.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 4/10/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/Support/RKAlert.m b/Code/Support/RKAlert.m index 1ec124b105..c6ed0f64eb 100644 --- a/Code/Support/RKAlert.m +++ b/Code/Support/RKAlert.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 4/10/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -46,7 +46,7 @@ void RKAlertWithTitle(NSString* message, NSString* title) { NSAlert *alert = [[alertClass alloc] init]; [alert setMessageText:message]; [alert setInformativeText:message]; - [alert addButtonWithTitle:NSLocalizedString(@"OK", nil)]; + [alert addButtonWithTitle:NSLocalizedString(@"OK", nil)]; [alert runModal]; [alert release]; } else { @@ -54,5 +54,5 @@ void RKAlertWithTitle(NSString* message, NSString* title) { } #elif TARGET_OS_UNIX RKLogCritical(@"%@: %@", title, message); -#endif +#endif } diff --git a/Code/Support/RKBenchmark.h b/Code/Support/RKBenchmark.h new file mode 100644 index 0000000000..73c7e991e4 --- /dev/null +++ b/Code/Support/RKBenchmark.h @@ -0,0 +1,121 @@ +// +// RKBenchmark.h +// RestKit +// +// Derived from Benchmark class: https://gist.github.com/1479490 +// Created by Sijawusz Pur Rahnama on 03/02/09. +// Copyleft 2009. Some rights reserved. +// + +#import + +/** + RKBenchmark objects provide a simple, lightweight interface for + quickly benchmarking the performance of units of code. Benchmark + objects can be used procedurally, by manually starting & stopping + the benchmark, or using a block interface to measure the execution + time of the block. + */ +@interface RKBenchmark : NSObject + +///----------------------------------------------------------------------------- +/// @name Accessing Benchmark Values +///----------------------------------------------------------------------------- + +/** + A name for the benchmark. Can be nil. + */ +@property (nonatomic, retain) NSString *name; + +/** + The start time of the benchmark as an absolute time value. + */ +@property (nonatomic, assign, readonly) CFAbsoluteTime startTime; + +/** + The end time of the benchmark as an absolute time value. + */ +@property (nonatomic, assign, readonly) CFAbsoluteTime endTime; + +/** + The elapsed time of the benchmark as determined by subtracting the + end time from the start time. Returns zero until the benchmark has + been stopped. + */ +@property (nonatomic, assign, readonly) CFTimeInterval elapsedTime; + +///----------------------------------------------------------------------------- +/// @name Quickly Performing Benchmarks +///----------------------------------------------------------------------------- + +/** + */ ++ (id)report:(NSString *)info executionBlock:(void (^)(void))block; + +/** + Performs a benchmark and returns a time interval measurement of the + total time elapsed during the execution of the blocl. + + @param block A block to execute and measure the elapsed time during execution. + @return A time interval equal to the total time elapsed during execution. + */ ++ (CFTimeInterval)measureWithExecutionBlock:(void (^)(void))block; + +///----------------------------------------------------------------------------- +/// @name Creating Benchmark Objects +///----------------------------------------------------------------------------- + +/** + Retrieves or creates a benchmark object instance with a given name. + + @param name A name for the benchmark. + @return A new or existing benchmark object with the given name. + */ ++ (RKBenchmark *)instanceWithName:(NSString *)name; + +/** + Creates and returns a benchmark object with a name. + + @param name A name for the benchmark. + @return A new benchmark object with the given name. + */ ++ (id)benchmarkWithName:(NSString *)name; + +/** + Initializes a new benchmark object with a name. + + @param name The name to initialize the receiver with. + @return The receiver, initialized with the given name. + */ +- (id)initWithName:(NSString *)name; + +///----------------------------------------------------------------------------- +/// @name Performing Benchmarks +///----------------------------------------------------------------------------- + +/** + Runs a benchmark by starting the receiver, executing the block, and then stopping + the benchmark object. + + @param executionBlock A block to execute as the body of the benchmark. + */ +- (void)run:(void (^)(void))executionBlock; + +/** + Starts the benchmark by recording the start time. + */ +- (void)start; + +/** + Stops the benchmark by recording the stop time. + */ +- (void)stop; + +/** + Logs the current benchmark status. If the receiver has been stopped, the + elapsed time of the benchmark is logged. If the benchmark is still running, + the total time since the benchmark was started is logged. + */ +- (void)log; + +@end diff --git a/Code/Support/RKBenchmark.m b/Code/Support/RKBenchmark.m new file mode 100644 index 0000000000..99124cb767 --- /dev/null +++ b/Code/Support/RKBenchmark.m @@ -0,0 +1,119 @@ +// +// RKBenchmark.h +// RestKit +// +// Derived from Benchmark class: https://gist.github.com/1479490 +// Created by Sijawusz Pur Rahnama on 03/02/09. +// Copyleft 2009. Some rights reserved. +// + +#import "RKBenchmark.h" + +@interface RKBenchmark () +@property (nonatomic, assign, readwrite) CFAbsoluteTime startTime; +@property (nonatomic, assign, readwrite) CFAbsoluteTime endTime; +@property (nonatomic, assign, readwrite) CFTimeInterval elapsedTime; +@property (nonatomic, assign, getter = isStopped) BOOL stopped; +@end + +@implementation RKBenchmark + +static NSMutableDictionary * __sharedBenchmarks = nil; + ++ (NSMutableDictionary *)sharedBenchmarks { + if (!__sharedBenchmarks) { + __sharedBenchmarks = [[NSMutableDictionary alloc] init]; + } + return __sharedBenchmarks; +} + ++ (id)instanceWithName:(NSString *)name { + @synchronized (self) { + // get the benchmark or create it on-the-fly + id benchmark = [[self sharedBenchmarks] objectForKey:name]; + if (!benchmark) { + benchmark = [self benchmarkWithName:name]; + [[self sharedBenchmarks] setObject:benchmark forKey:name]; + } + return benchmark; + } + return nil; +} + +@synthesize name = _name; +@synthesize startTime = _startTime; +@synthesize endTime = _endTime; +@synthesize elapsedTime = _elapsedTime; +@synthesize stopped = _stopped; + +# pragma mark - +# pragma mark Quick access class methods + ++ (id)report:(NSString *)info executionBlock:(void (^)(void))block { + RKBenchmark *benchmark = [self instanceWithName:info]; + [benchmark run:block]; + [benchmark log]; + return benchmark; +} + ++ (CFTimeInterval)measureWithExecutionBlock:(void (^)(void))block { + RKBenchmark *benchmark = [[self new] autorelease]; + [benchmark run:block]; + return benchmark.elapsedTime; +} + +# pragma mark - +# pragma mark Initializers + ++ (id)benchmarkWithName:(NSString *)name { + return [[[self alloc] initWithName:name] autorelease]; +} + +- (id)initWithName:(NSString *)name { + if (self = [self init]) { + self.name = name; + } + return self; +} + +# pragma mark - +# pragma mark Benchmark methods + +- (void)run:(void (^)(void))executionBlock { + [self start]; + executionBlock(); + [self stop]; +} + +- (void)start { + self.startTime = CFAbsoluteTimeGetCurrent(); +} + +- (void)stop { + self.endTime = CFAbsoluteTimeGetCurrent(); + self.stopped = YES; + + // Calculate elapsed time + CFDateRef startDate = CFDateCreate(NULL, self.startTime); + CFDateRef endDate = CFDateCreate(NULL, self.endTime); + self.elapsedTime = CFDateGetTimeIntervalSinceDate(endDate, startDate); + CFRelease(startDate); + CFRelease(endDate); +} + +- (void)log { + CFTimeInterval timeElapsed; + if (self.isStopped) { + timeElapsed = self.elapsedTime; + } else { + CFDateRef startDate = CFDateCreate(NULL, self.startTime); + timeElapsed = CFDateGetTimeIntervalSinceDate(startDate, (CFDateRef)[NSDate date]); + CFRelease(startDate); + } + + // log elapsed time + if (_name) NSLog(@"Benchmark '%@' took %f seconds.", _name, timeElapsed); + else NSLog(@"Benchmark took %f seconds.", timeElapsed); +} + +@end diff --git a/Code/Support/RKCache.h b/Code/Support/RKCache.h index 95e8d59a54..1b5d590fe2 100644 --- a/Code/Support/RKCache.h +++ b/Code/Support/RKCache.h @@ -19,8 +19,8 @@ // @interface RKCache : NSObject { - NSString* _cachePath; - NSRecursiveLock* _cacheLock; + NSString* _cachePath; + NSRecursiveLock* _cacheLock; } @property (nonatomic, readonly) NSString* cachePath; diff --git a/Code/Support/RKCache.m b/Code/Support/RKCache.m index 3a211da0a5..cc10686e17 100644 --- a/Code/Support/RKCache.m +++ b/Code/Support/RKCache.m @@ -29,46 +29,46 @@ @implementation RKCache - (id)initWithPath:(NSString*)cachePath subDirectories:(NSArray*)subDirectories { self = [super init]; - if (self) { - _cachePath = [cachePath copy]; - _cacheLock = [[NSRecursiveLock alloc] init]; + if (self) { + _cachePath = [cachePath copy]; + _cacheLock = [[NSRecursiveLock alloc] init]; - NSFileManager* fileManager = [NSFileManager defaultManager]; - NSMutableArray* pathArray = [NSMutableArray arrayWithObject:_cachePath]; + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSMutableArray* pathArray = [NSMutableArray arrayWithObject:_cachePath]; for (NSString* subDirectory in subDirectories) { [pathArray addObject:[_cachePath stringByAppendingPathComponent:subDirectory]]; } - for (NSString* path in pathArray) { - BOOL isDirectory = NO; - BOOL fileExists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory]; - if (!fileExists) { - NSError* error = nil; - BOOL created = [fileManager createDirectoryAtPath:path - withIntermediateDirectories:NO - attributes:nil - error:&error]; - if (!created || error != nil) { - RKLogError(@"Failed to create cache directory at %@: error %@", path, [error localizedDescription]); - } else { + for (NSString* path in pathArray) { + BOOL isDirectory = NO; + BOOL fileExists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory]; + if (!fileExists) { + NSError* error = nil; + BOOL created = [fileManager createDirectoryAtPath:path + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (!created || error != nil) { + RKLogError(@"Failed to create cache directory at %@: error %@", path, [error localizedDescription]); + } else { RKLogDebug(@"Created cache storage at path '%@'", path); } - } else { + } else { if (!isDirectory) { RKLogWarning(@"Skipped creation of cache directory as non-directory file exists at path: %@", path); } - } - } - } - return self; + } + } + } + return self; } - (void)dealloc { - [_cachePath release]; - _cachePath = nil; - [_cacheLock release]; - _cacheLock = nil; - [super dealloc]; + [_cachePath release]; + _cachePath = nil; + [_cacheLock release]; + _cacheLock = nil; + [super dealloc]; } - (NSString*)cachePath { @@ -76,22 +76,22 @@ - (NSString*)cachePath { } - (NSString*)pathForCacheKey:(NSString*)cacheKey { - [_cacheLock lock]; - NSString* pathForCacheKey = [_cachePath stringByAppendingPathComponent:cacheKey]; - [_cacheLock unlock]; + [_cacheLock lock]; + NSString* pathForCacheKey = [_cachePath stringByAppendingPathComponent:cacheKey]; + [_cacheLock unlock]; RKLogTrace(@"Found cachePath '%@' for %@", pathForCacheKey, cacheKey); - return pathForCacheKey; + return pathForCacheKey; } - (BOOL)hasEntry:(NSString*)cacheKey { [_cacheLock lock]; - BOOL hasEntry = NO; - NSFileManager* fileManager = [NSFileManager defaultManager]; - NSString* cachePath = [self pathForCacheKey:cacheKey]; - hasEntry = [fileManager fileExistsAtPath:cachePath]; - [_cacheLock unlock]; + BOOL hasEntry = NO; + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSString* cachePath = [self pathForCacheKey:cacheKey]; + hasEntry = [fileManager fileExistsAtPath:cachePath]; + [_cacheLock unlock]; RKLogTrace(@"Determined hasEntry: %@ => %@", cacheKey, hasEntry ? @"YES" : @"NO"); - return hasEntry; + return hasEntry; } - (void)writeDictionary:(NSDictionary*)dictionary withCacheKey:(NSString*)cacheKey { @@ -147,87 +147,87 @@ - (NSData*)dataForCacheKey:(NSString*)cacheKey { [_cacheLock lock]; NSData* data = nil; NSString* cachePath = [self pathForCacheKey:cacheKey]; - if (cachePath) { - data = [NSData dataWithContentsOfFile:cachePath]; + if (cachePath) { + data = [NSData dataWithContentsOfFile:cachePath]; if (data) { RKLogDebug(@"Read cached data '%@' from cachePath '%@' for '%@'", data, cachePath, cacheKey); } else { RKLogDebug(@"Read nil cached data from cachePath '%@' for '%@'", cachePath, cacheKey); } - } - [_cacheLock unlock]; - return data; + } + [_cacheLock unlock]; + return data; } - (void)invalidateEntry:(NSString*)cacheKey { [_cacheLock lock]; RKLogDebug(@"Invalidating cache entry for '%@'", cacheKey); NSString* cachePath = [self pathForCacheKey:cacheKey]; - if (cachePath) { - NSFileManager* fileManager = [NSFileManager defaultManager]; - [fileManager removeItemAtPath:cachePath error:NULL]; + if (cachePath) { + NSFileManager* fileManager = [NSFileManager defaultManager]; + [fileManager removeItemAtPath:cachePath error:NULL]; RKLogTrace(@"Removed cache entry at path '%@' for '%@'", cachePath, cacheKey); - } - [_cacheLock unlock]; + } + [_cacheLock unlock]; } - (void)invalidateSubDirectory:(NSString*)subDirectory { [_cacheLock lock]; - if (_cachePath && subDirectory) { + if (_cachePath && subDirectory) { NSString* subDirectoryPath = [_cachePath stringByAppendingPathComponent:subDirectory]; RKLogInfo(@"Invalidating cache at path: %@", subDirectoryPath); - NSFileManager* fileManager = [NSFileManager defaultManager]; - - BOOL isDirectory = NO; - BOOL fileExists = [fileManager fileExistsAtPath:subDirectoryPath isDirectory:&isDirectory]; - - if (fileExists && isDirectory) { - NSError* error = nil; - NSArray* cacheEntries = [fileManager contentsOfDirectoryAtPath:subDirectoryPath error:&error]; - - if (nil == error) { - for (NSString* cacheEntry in cacheEntries) { - NSString* cacheEntryPath = [subDirectoryPath stringByAppendingPathComponent:cacheEntry]; - [fileManager removeItemAtPath:cacheEntryPath error:&error]; - if (nil != error) { - RKLogError(@"Failed to delete cache entry for file: %@", cacheEntryPath); - } - } - } else { - RKLogWarning(@"Failed to fetch list of cache entries for cache path: %@", subDirectoryPath); - } - } - } - [_cacheLock unlock]; + NSFileManager* fileManager = [NSFileManager defaultManager]; + + BOOL isDirectory = NO; + BOOL fileExists = [fileManager fileExistsAtPath:subDirectoryPath isDirectory:&isDirectory]; + + if (fileExists && isDirectory) { + NSError* error = nil; + NSArray* cacheEntries = [fileManager contentsOfDirectoryAtPath:subDirectoryPath error:&error]; + + if (nil == error) { + for (NSString* cacheEntry in cacheEntries) { + NSString* cacheEntryPath = [subDirectoryPath stringByAppendingPathComponent:cacheEntry]; + [fileManager removeItemAtPath:cacheEntryPath error:&error]; + if (nil != error) { + RKLogError(@"Failed to delete cache entry for file: %@", cacheEntryPath); + } + } + } else { + RKLogWarning(@"Failed to fetch list of cache entries for cache path: %@", subDirectoryPath); + } + } + } + [_cacheLock unlock]; } - (void)invalidateAll { [_cacheLock lock]; - if (_cachePath) { + if (_cachePath) { RKLogInfo(@"Invalidating cache at path: %@", _cachePath); - NSFileManager* fileManager = [NSFileManager defaultManager]; - - BOOL isDirectory = NO; - BOOL fileExists = [fileManager fileExistsAtPath:_cachePath isDirectory:&isDirectory]; - - if (fileExists && isDirectory) { - NSError* error = nil; - NSArray* cacheEntries = [fileManager contentsOfDirectoryAtPath:_cachePath error:&error]; - - if (nil == error) { - for (NSString* cacheEntry in cacheEntries) { - NSString* cacheEntryPath = [_cachePath stringByAppendingPathComponent:cacheEntry]; - [fileManager removeItemAtPath:cacheEntryPath error:&error]; - if (nil != error) { - RKLogError(@"Failed to delete cache entry for file: %@", cacheEntryPath); - } - } - } else { - RKLogWarning(@"Failed to fetch list of cache entries for cache path: %@", _cachePath); - } - } - } - [_cacheLock unlock]; + NSFileManager* fileManager = [NSFileManager defaultManager]; + + BOOL isDirectory = NO; + BOOL fileExists = [fileManager fileExistsAtPath:_cachePath isDirectory:&isDirectory]; + + if (fileExists && isDirectory) { + NSError* error = nil; + NSArray* cacheEntries = [fileManager contentsOfDirectoryAtPath:_cachePath error:&error]; + + if (nil == error) { + for (NSString* cacheEntry in cacheEntries) { + NSString* cacheEntryPath = [_cachePath stringByAppendingPathComponent:cacheEntry]; + [fileManager removeItemAtPath:cacheEntryPath error:&error]; + if (nil != error) { + RKLogError(@"Failed to delete cache entry for file: %@", cacheEntryPath); + } + } + } else { + RKLogWarning(@"Failed to fetch list of cache entries for cache path: %@", _cachePath); + } + } + } + [_cacheLock unlock]; } @end diff --git a/Code/Support/RKDirectory.h b/Code/Support/RKDirectory.h index 8fb876af5b..dfd0b46ff6 100644 --- a/Code/Support/RKDirectory.h +++ b/Code/Support/RKDirectory.h @@ -9,7 +9,7 @@ #import /** - iOS and OS X agnostic accessors for safely returning directory paths for use + iOS and OS X agnostic accessors for safely returning directory paths for use by the framework and applications. */ @interface RKDirectory : NSObject @@ -27,7 +27,7 @@ Returns a path to the root caches directory used by RestKit for storage. On iOS, this is a sanboxed path specific for the executing application. On OS X, this is an application specific path under NSCachesDirectory (i.e. ~/Library/Caches). - + @return The full path to the Caches directory. */ + (NSString *)cachesDirectory; diff --git a/Code/Support/RKDirectory.m b/Code/Support/RKDirectory.m index 610f3e4e93..5da5deab47 100644 --- a/Code/Support/RKDirectory.m +++ b/Code/Support/RKDirectory.m @@ -19,43 +19,43 @@ + (NSString *)executableName RKLogWarning(@"Unable to determine CFBundleExecutable: storing data under RestKit directory name."); executableName = @"RestKit"; } - + return executableName; } + (NSString *)applicationDataDirectory { #if TARGET_OS_IPHONE - + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); return ([paths count] > 0) ? [paths objectAtIndex:0] : nil; - + #else - + NSFileManager *sharedFM = [NSFileManager defaultManager]; - + NSArray *possibleURLs = [sharedFM URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask]; NSURL *appSupportDir = nil; NSURL *appDirectory = nil; - + if ([possibleURLs count] >= 1) { appSupportDir = [possibleURLs objectAtIndex:0]; } - + if (appSupportDir) { NSString *executableName = [RKDirectory executableName]; appDirectory = [appSupportDir URLByAppendingPathComponent:executableName]; return [appDirectory path]; } - + return nil; #endif } + (NSString *)cachesDirectory { -#if TARGET_OS_IPHONE +#if TARGET_OS_IPHONE return [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]; #else NSString *path = nil; @@ -63,7 +63,7 @@ + (NSString *)cachesDirectory if ([paths count]) { path = [[paths objectAtIndex:0] stringByAppendingPathComponent:[RKDirectory executableName]]; } - + return path; #endif } diff --git a/Code/Support/RKDotNetDateFormatter.h b/Code/Support/RKDotNetDateFormatter.h index 8c73f01cb3..3e8466e7fb 100644 --- a/Code/Support/RKDotNetDateFormatter.h +++ b/Code/Support/RKDotNetDateFormatter.h @@ -33,7 +33,7 @@ } /** - Instantiates an autoreleased RKDotNetDateFormatter object with the timezone set to UTC + Instantiates an autoreleased RKDotNetDateFormatter object with the timezone set to UTC (Greenwich Mean Time). @return An autoreleased RKDotNetDateFormatter object @@ -42,7 +42,7 @@ + (RKDotNetDateFormatter *)dotNetDateFormatter; /** - Instantiates an autoreleased RKDotNetDateFormatter object. + Instantiates an autoreleased RKDotNetDateFormatter object. The supplied timeZone, such as one produced with [NSTimeZone timeZoneWithName:@"UTC"], is only used during calls to stringFromDate:, for a detailed explanation see dateFromString: @@ -60,14 +60,14 @@ /Date(-1112715000000)/ Where 1112715000000 is the number of milliseconds since January 1, 1970 00:00 GMT/UTC, and -0500 represents the timezone offset from GMT in 24-hour time. Negatives milliseconds are treated as dates before January 1, 1970. - + *NOTE* NSDate objects do not have timezones, and you should never change an actual date value based on a timezone offset. However, timezones are important when presenting dates to the user. Therefore, - If an offset is present in the ASP.NET string (it should be), we actually ignore the offset portion because - we want to store the actual date value in its raw form, without any pollution of timezone information. + If an offset is present in the ASP.NET string (it should be), we actually ignore the offset portion because + we want to store the actual date value in its raw form, without any pollution of timezone information. If, on the other hand, there is no offset in the ASP.NET string, we assume GMT (+0000) anyway. - In summation, for this class setTimeZone: is ignored except when using stringFromDate: - + In summation, for this class setTimeZone: is ignored except when using stringFromDate: + @param string The ASP.NET style string, /Date(1112715000000-0500)/ @return An NSDate object @see stringFromDate @@ -79,10 +79,10 @@ /** Returns an ASP.NET style date string from an NSDate, such as /Date(1112715000000+0000)/ Where 1112715000000 is the number of milliseconds since January 1, 1970 00:00 GMT/UTC, and +0000 is the - timezone offset from GMT in 24-hour time. - + timezone offset from GMT in 24-hour time. + *NOTE* GMT (+0000) is assumed otherwise specified via setTimeZone: - + @param date An NSDate @return The ASP.NET style string, /Date(1112715000000-0500)/ @see dateFromString diff --git a/Code/Support/RKDotNetDateFormatter.m b/Code/Support/RKDotNetDateFormatter.m index 57544d25e3..20685ef033 100644 --- a/Code/Support/RKDotNetDateFormatter.m +++ b/Code/Support/RKDotNetDateFormatter.m @@ -65,6 +65,12 @@ - (NSString *)stringFromDate:(NSDate *)date { return [NSString stringWithFormat:@"/Date(%1.0lf%@)/", milliseconds, timeZoneOffset]; } +- (BOOL)getObjectValue:(id *)outValue forString:(NSString *)string errorDescription:(NSString **)error { + NSDate *date = [self dateFromString:string]; + if (outValue) + *outValue = date; + return (date != nil); +} - (id)init { self = [super init]; diff --git a/Code/Support/RKFixCategoryBug.h b/Code/Support/RKFixCategoryBug.h index f2b023f674..7bb79094f5 100644 --- a/Code/Support/RKFixCategoryBug.h +++ b/Code/Support/RKFixCategoryBug.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/1/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -26,7 +26,7 @@ -all_load or -force_load to load object files from static libraries that only contain categories and no classes. See http://developer.apple.com/library/mac/#qa/qa2006/qa1490.html for more info. - + Shamelessly borrowed from Three20 */ #define RK_FIX_CATEGORY_BUG(name) @interface RK_FIX_CATEGORY_BUG##name @end \ diff --git a/Code/Support/RKLog.h b/Code/Support/RKLog.h index 945e4dd296..0bb6b456a1 100644 --- a/Code/Support/RKLog.h +++ b/Code/Support/RKLog.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/3/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,7 +20,7 @@ /** RestKit Logging is based on the LibComponentLogging framework - + @see lcl_config_components.h @see lcl_config_logger.h */ @@ -28,11 +28,11 @@ /** RKLogComponent defines the active component within any given portion of RestKit - + By default, messages will log to the base 'RestKit' log component. All other components used by RestKit are nested under this parent, so this effectively sets the default log level for the entire library. - + The component can be undef'd and redefined to change the active logging component. */ #define RKLogComponent lcl_cRestKit @@ -40,23 +40,23 @@ /** The logging macros. These macros will log to the currently active logging component at the log level identified in the name of the macro. - + For example, in the RKObjectMappingOperation class we would redefine the RKLogComponent: - + #undef RKLogComponent #define RKLogComponent lcl_cRestKitObjectMapping - + The lcl_c prefix is the LibComponentLogging data structure identifying the logging component we want to target within this portion of the codebase. See lcl_config_component.h for reference. - + Having defined the logging component, invoking the logger via: - + RKLogInfo(@"This is my log message!"); - + Would result in a log message similar to: - + I RestKit.ObjectMapping:RKLog.h:42 This is my log message! - + The message will only be logged if the log level for the active component is equal to or higher than the level the message was logged at (in this case, Info). */ @@ -80,7 +80,7 @@ lcl_log(RKLogComponent, lcl_vTrace, @"" __VA_ARGS__) /** Log Level Aliases - + These aliases simply map the log levels defined within LibComponentLogger to something more friendly */ #define RKLogLevelOff lcl_vOff @@ -94,14 +94,14 @@ lcl_log(RKLogComponent, lcl_vTrace, @"" __VA_ARGS__) /** Alias the LibComponentLogger logging configuration method. Also ensures logging is initialized for the framework. - + Expects the name of the component and a log level. - + Examples: - + // Log debugging messages from the Network component RKLogConfigureByName("RestKit/Network", RKLogLevelDebug); - + // Log only critical messages from the Object Mapping component RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelCritical); */ @@ -111,7 +111,7 @@ lcl_configure_by_name(name, level); /** Alias for configuring the LibComponentLogger logging component for the App. This - enables the end-user of RestKit to leverage RKLog() to log messages inside of + enables the end-user of RestKit to leverage RKLog() to log messages inside of their apps. */ #define RKLogSetAppLoggingLevel(level) \ @@ -138,6 +138,12 @@ lcl_configure_by_name("App", level); } \ } while(false); +/** + Temporarily turns off logging for the given logging component during execution of the block. + After the block has finished execution, the logging level is restored to its previous state. + */ +#define RKLogSilenceComponentWhileExecutingBlock(component, _block) \ + RKLogToComponentWithLevelWhileExecutingBlock(component, RKLogLevelOff, _block) /** Temporarily changes the logging level for the configured RKLogComponent and executes the block. Any logging @@ -149,7 +155,7 @@ lcl_configure_by_name("App", level); /** - Temporarily turns off logging for the execution of the block. + Temporarily turns off logging for current logging component during execution of the block. After the block has finished execution, the logging level is restored to its previous state. */ #define RKLogSilenceWhileExecutingBlock(_block) \ @@ -158,10 +164,10 @@ lcl_configure_by_name("App", level); /** Set the Default Log Level - + Based on the presence of the DEBUG flag, we default the logging for the RestKit parent component to Info or Warning. - + You can override this setting by defining RKLogLevelDefault as a pre-processor macro. */ #ifndef RKLogLevelDefault @@ -199,3 +205,15 @@ void RKLogInitialize(void); Trace or 6 */ void RKLogConfigureFromEnvironment(void); + +/** + Logs extensive information about an NSError generated as the results + of a failed key-value validation error. + */ +void RKLogValidationError(NSError *); + +/** + Logs the value of an NSUInteger as a binary string. Useful when + examining integers containing bitmasked values. + */ +void RKLogIntegerAsBinary(NSUInteger); diff --git a/Code/Support/RKLog.m b/Code/Support/RKLog.m index fc4502192d..ca5b06c9da 100644 --- a/Code/Support/RKLog.m +++ b/Code/Support/RKLog.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 6/10/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,7 +25,8 @@ static BOOL loggingInitialized = NO; -void RKLogInitialize(void) { +void RKLogInitialize(void) +{ if (loggingInitialized == NO) { lcl_configure_by_name("RestKit*", RKLogLevelDefault); lcl_configure_by_name("App", RKLogLevelDefault); @@ -35,9 +36,9 @@ void RKLogInitialize(void) { } -void RKLogConfigureFromEnvironment(void) { - - NSOrderedSet *validEnvVariables = [NSOrderedSet orderedSetWithObjects: +void RKLogConfigureFromEnvironment(void) +{ + NSArray *validEnvVariables = [NSArray arrayWithObjects: @"RKLogLevel.App", @"RKLogLevel.RestKit", @"RKLogLevel.RestKit.CoreData", @@ -75,8 +76,8 @@ void RKLogConfigureFromEnvironment(void) { } -int RKLogLevelForString(NSString *logLevel, NSString *envVarName) { - +int RKLogLevelForString(NSString *logLevel, NSString *envVarName) +{ // Forgive the user if they specify the full name for the value i.e. "RKLogLevelDebug" instead of "Debug" logLevel = [logLevel stringByReplacingOccurrencesOfString:@"RKLogLevel" withString:@""]; @@ -126,3 +127,45 @@ int RKLogLevelForString(NSString *logLevel, NSString *envVarName) { return -1; } } + +void RKLogValidationError(NSError *validationError) { + if ([[validationError domain] isEqualToString:@"NSCocoaErrorDomain"]) { + NSDictionary *userInfo = [validationError userInfo]; + NSArray *errors = [userInfo valueForKey:@"NSDetailedErrors"]; + if (errors) { + for (NSError *detailedError in errors) { + NSDictionary *subUserInfo = [detailedError userInfo]; + RKLogError(@"Core Data Save Error\n \ + NSLocalizedDescription:\t\t%@\n \ + NSValidationErrorKey:\t\t\t%@\n \ + NSValidationErrorPredicate:\t%@\n \ + NSValidationErrorObject:\n%@\n", + [subUserInfo valueForKey:@"NSLocalizedDescription"], + [subUserInfo valueForKey:@"NSValidationErrorKey"], + [subUserInfo valueForKey:@"NSValidationErrorPredicate"], + [subUserInfo valueForKey:@"NSValidationErrorObject"]); + } + } + else { + RKLogError(@"Core Data Save Error\n \ + NSLocalizedDescription:\t\t%@\n \ + NSValidationErrorKey:\t\t\t%@\n \ + NSValidationErrorPredicate:\t%@\n \ + NSValidationErrorObject:\n%@\n", + [userInfo valueForKey:@"NSLocalizedDescription"], + [userInfo valueForKey:@"NSValidationErrorKey"], + [userInfo valueForKey:@"NSValidationErrorPredicate"], + [userInfo valueForKey:@"NSValidationErrorObject"]); + } + } +} + +void RKLogIntegerAsBinary(NSUInteger bitMask) { + NSUInteger bit = ~(NSUIntegerMax >> 1); + NSMutableString *string = [NSMutableString string]; + do { + [string appendString:(((NSUInteger)bitMask & bit) ? @"1" : @"0")]; + } while ( bit >>= 1 ); + + NSLog(@"Value of %ld in binary: %@", (long) bitMask, string); +} diff --git a/Code/Support/RKMIMETypes.h b/Code/Support/RKMIMETypes.h index 3ba2640837..7cfdbaa300 100644 --- a/Code/Support/RKMIMETypes.h +++ b/Code/Support/RKMIMETypes.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/18/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/Support/RKMIMETypes.m b/Code/Support/RKMIMETypes.m index ef7c7d8ee1..818c21ec75 100644 --- a/Code/Support/RKMIMETypes.m +++ b/Code/Support/RKMIMETypes.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 5/18/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/Support/RKMutableBlockDictionary.m b/Code/Support/RKMutableBlockDictionary.m index 4d61f7d95e..a2dbbf58b1 100644 --- a/Code/Support/RKMutableBlockDictionary.m +++ b/Code/Support/RKMutableBlockDictionary.m @@ -64,7 +64,7 @@ - (void)dealloc { @implementation RKMutableBlockDictionary - (id)init { - return [self initWithCapacity:0]; + return [self initWithCapacity:0]; } - (id)initWithCapacity:(NSUInteger)capacity { @@ -82,7 +82,7 @@ - (void)dealloc { } - (id)copy { - return [self mutableCopy]; + return [self mutableCopy]; } - (void)setObject:(id)anObject forKey:(id)aKey { diff --git a/Code/Support/RKOrderedDictionary.h b/Code/Support/RKOrderedDictionary.h index fe03c2d538..bbbc68e16d 100644 --- a/Code/Support/RKOrderedDictionary.h +++ b/Code/Support/RKOrderedDictionary.h @@ -31,8 +31,8 @@ // Borrowed from Matt Gallagher - http://cocoawithlove.com/2008/12/ordereddictionary-subclassing-cocoa.html @interface RKOrderedDictionary : NSMutableDictionary { - NSMutableDictionary *dictionary; - NSMutableArray *array; + NSMutableDictionary *dictionary; + NSMutableArray *array; } /** diff --git a/Code/Support/RKOrderedDictionary.m b/Code/Support/RKOrderedDictionary.m index aedc85ff4e..2087c598e1 100644 --- a/Code/Support/RKOrderedDictionary.m +++ b/Code/Support/RKOrderedDictionary.m @@ -26,126 +26,126 @@ NSString *RKDescriptionForObject(NSObject *object, id locale, NSUInteger indent); NSString *RKDescriptionForObject(NSObject *object, id locale, NSUInteger indent) { - NSString *objectString; - if ([object isKindOfClass:[NSString class]]) - { - objectString = (NSString *)[[object retain] autorelease]; - } - else if ([object respondsToSelector:@selector(descriptionWithLocale:indent:)]) - { - objectString = [(NSDictionary *)object descriptionWithLocale:locale indent:indent]; - } - else if ([object respondsToSelector:@selector(descriptionWithLocale:)]) - { - objectString = [(NSSet *)object descriptionWithLocale:locale]; - } - else - { - objectString = [object description]; - } - return objectString; + NSString *objectString; + if ([object isKindOfClass:[NSString class]]) + { + objectString = (NSString *)[[object retain] autorelease]; + } + else if ([object respondsToSelector:@selector(descriptionWithLocale:indent:)]) + { + objectString = [(NSDictionary *)object descriptionWithLocale:locale indent:indent]; + } + else if ([object respondsToSelector:@selector(descriptionWithLocale:)]) + { + objectString = [(NSSet *)object descriptionWithLocale:locale]; + } + else + { + objectString = [object description]; + } + return objectString; } @implementation RKOrderedDictionary - (id)init { - return [self initWithCapacity:0]; + return [self initWithCapacity:0]; } - (id)initWithCapacity:(NSUInteger)capacity { - self = [super init]; - if (self != nil) - { - dictionary = [[NSMutableDictionary alloc] initWithCapacity:capacity]; - array = [[NSMutableArray alloc] initWithCapacity:capacity]; - } - return self; + self = [super init]; + if (self != nil) + { + dictionary = [[NSMutableDictionary alloc] initWithCapacity:capacity]; + array = [[NSMutableArray alloc] initWithCapacity:capacity]; + } + return self; } - (void)dealloc { - [dictionary release]; - [array release]; - [super dealloc]; + [dictionary release]; + [array release]; + [super dealloc]; } - (id)copy { - return [self mutableCopy]; + return [self mutableCopy]; } - (void)setObject:(id)anObject forKey:(id)aKey { - if (![dictionary objectForKey:aKey]) - { - [array addObject:aKey]; - } - [dictionary setObject:anObject forKey:aKey]; + if (![dictionary objectForKey:aKey]) + { + [array addObject:aKey]; + } + [dictionary setObject:anObject forKey:aKey]; } - (void)removeObjectForKey:(id)aKey { - [dictionary removeObjectForKey:aKey]; - [array removeObject:aKey]; + [dictionary removeObjectForKey:aKey]; + [array removeObject:aKey]; } - (NSUInteger)count { - return [dictionary count]; + return [dictionary count]; } - (id)objectForKey:(id)aKey { - return [dictionary objectForKey:aKey]; + return [dictionary objectForKey:aKey]; } - (NSEnumerator *)keyEnumerator { - return [array objectEnumerator]; + return [array objectEnumerator]; } - (NSEnumerator *)reverseKeyEnumerator { - return [array reverseObjectEnumerator]; + return [array reverseObjectEnumerator]; } - (void)insertObject:(id)anObject forKey:(id)aKey atIndex:(NSUInteger)anIndex { - if ([dictionary objectForKey:aKey]) - { - [self removeObjectForKey:aKey]; - } - [array insertObject:aKey atIndex:anIndex]; - [dictionary setObject:anObject forKey:aKey]; + if ([dictionary objectForKey:aKey]) + { + [self removeObjectForKey:aKey]; + } + [array insertObject:aKey atIndex:anIndex]; + [dictionary setObject:anObject forKey:aKey]; } - (id)keyAtIndex:(NSUInteger)anIndex { - return [array objectAtIndex:anIndex]; + return [array objectAtIndex:anIndex]; } - (NSString *)descriptionWithLocale:(id)locale indent:(NSUInteger)level { - NSMutableString *indentString = [NSMutableString string]; - NSUInteger i, count = level; - for (i = 0; i < count; i++) - { - [indentString appendFormat:@" "]; - } - - NSMutableString *description = [NSMutableString string]; - [description appendFormat:@"%@{\n", indentString]; - for (NSObject *key in self) - { - [description appendFormat:@"%@ %@ = %@;\n", + NSMutableString *indentString = [NSMutableString string]; + NSUInteger i, count = level; + for (i = 0; i < count; i++) + { + [indentString appendFormat:@" "]; + } + + NSMutableString *description = [NSMutableString string]; + [description appendFormat:@"%@{\n", indentString]; + for (NSObject *key in self) + { + [description appendFormat:@"%@ %@ = %@;\n", indentString, RKDescriptionForObject(key, locale, level), RKDescriptionForObject([self objectForKey:key], locale, level)]; - } - [description appendFormat:@"%@}\n", indentString]; - return description; + } + [description appendFormat:@"%@}\n", indentString]; + return description; } @end diff --git a/Code/Support/RKParser.h b/Code/Support/RKParser.h index dc137e1543..4302d767c6 100644 --- a/Code/Support/RKParser.h +++ b/Code/Support/RKParser.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 10/1/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/Support/RKPathMatcher.h b/Code/Support/RKPathMatcher.h index edc08f21a1..53c5ecfb96 100755 --- a/Code/Support/RKPathMatcher.h +++ b/Code/Support/RKPathMatcher.h @@ -4,13 +4,13 @@ // // Created by Greg Combs on 9/2/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,12 +23,12 @@ /** This class performs pattern matching and parameter parsing of strings, usually resource paths. - It provides much of the necessary tools to map a given resource path to local objects (the inverse + It provides much of the necessary tools to map a given resource path to local objects (the inverse of RKRouter's function). This makes it easier to implement RKManagedObjectCache, and generate fetched requests from a given resource path. There are two means of instantiating and using a matcher object in order to provide more flexibility in implementations, and to improve efficiency by eliminating repetitive and costly pattern initializations. - + @see RKManagedObjectCache @see RKMakePathWithObject @see RKRouter @@ -43,65 +43,65 @@ @property (copy, readonly) NSDictionary *queryParameters; /** - Creates an RKPathMatcher starting from a resource path string. This method should be followed by + Creates an RKPathMatcher starting from a resource path string. This method should be followed by matchesPattern:tokenizeQueryStrings:parsedArguments: - + @param pathString The string to evaluate and parse, such as /districts/tx/upper/?apikey=GC5512354 @return An instantiated RKPathMatcher without an established pattern. */ +(RKPathMatcher *)matcherWithPath:(NSString *)pathString; /** - Determines if the path string matches the provided pattern, and yields a dictionary with the resulting + 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 matcherWithPath: - Pattern strings should include encoded parameter keys, delimited by a single colon at the - beginning of the key name. - - *NOTE 1* - Numerous colon-encoded parameter keys can be joined in a long pattern, but each key must be + Pattern strings should include encoded parameter keys, delimited by a single colon at the + beginning of the key name. + + *NOTE 1* - Numerous colon-encoded parameter keys can be joined in a long pattern, but each key must be separated by at least one unmapped character. For instance, /:key1:key2:key3/ is invalid, whereas /:key1/:key2/:key3/ is acceptable. - + *NOTE 2* - The pattern matcher supports KVM, so :key1.otherKey normally resolves as it would in any other KVM situation, ... otherKey is a sub-key on a the object represented by key1. This presents problems in circumstances where you might want to build a pattern like /:filename.json, where the dot isn't intended as a sub-key on the filename, but rather - part of the json static string. In these instances, you need to escape the dot with two backslashes, like so: + part of the json static string. In these instances, you need to escape the dot with two backslashes, like so: /:filename\\.json - + @param patternString The pattern to use for evaluating, such as /:entityName/:stateID/:chamber/ @param shouldTokenize If YES, any query parameters will be tokenized and inserted into the parsed argument dictionary. @param arguments A pointer to a dictionary that contains the key/values from the pattern (and parameter) matching. - @return A boolean indicating if the path string successfully matched the pattern. + @return A boolean indicating if the path string successfully matched the pattern. */ - (BOOL)matchesPattern:(NSString *)patternString tokenizeQueryStrings:(BOOL)shouldTokenize parsedArguments:(NSDictionary **)arguments; /** - Creates an RKPathMatcher starting from a pattern string. This method should be followed by + Creates an RKPathMatcher starting from a pattern string. This method should be followed by matchesPath:tokenizeQueryStrings:parsedArguments: Patterns should include encoded parameter keys, - delimited by a single colon at the beginning of the key name. - - *NOTE 1* - Numerous colon-encoded parameter keys can be joined in a long pattern, but each key must be + delimited by a single colon at the beginning of the key name. + + *NOTE 1* - Numerous colon-encoded parameter keys can be joined in a long pattern, but each key must be separated by at least one unmapped character. For instance, /:key1:key2:key3/ is invalid, whereas /:key1/:key2/:key3/ is acceptable. - + *NOTE 2* - The pattern matcher supports KVM, so :key1.otherKey normally resolves as it would in any other KVM situation, ... otherKey is a sub-key on a the object represented by key1. This presents problems in circumstances where you might want to build a pattern like /:filename.json, where the dot isn't intended as a sub-key on the filename, but rather - part of the json static string. In these instances, you need to escape the dot with two backslashes, like so: + part of the json static string. In these instances, you need to escape the dot with two backslashes, like so: /:filename\\.json - + @param patternString The pattern to use for evaluating, such as /:entityName/:stateID/:chamber/ - @return An instantiated RKPathMatcher with an established pattern. + @return An instantiated RKPathMatcher with an established pattern. */ +(RKPathMatcher *)matcherWithPattern:(NSString *)patternString; /** - Determines if the provided resource path string matches a pattern, and yields a dictionary with the resulting + Determines if the provided resource path string matches a pattern, and yields a dictionary with the resulting matched key/value pairs. Use of this method should be preceded by matcherWithPattern: - + @param pathString The string to evaluate and parse, such as /districts/tx/upper/?apikey=GC5512354 @param shouldTokenize If YES, any query parameters will be tokenized and inserted into the parsed argument dictionary. @param arguments A pointer to a dictionary that contains the key/values from the pattern (and parameter) matching. - @return A boolean indicating if the path string successfully matched the pattern. + @return A boolean indicating if the path string successfully matched the pattern. */ - (BOOL)matchesPath:(NSString *)pathString tokenizeQueryStrings:(BOOL)shouldTokenize parsedArguments:(NSDictionary **)arguments; @@ -109,14 +109,14 @@ This generates a resource path by interpolating the properties of the 'object' argument, assuming the existence of a previously specified pattern established via matcherWithPattern:. Otherwise, this method is identical in function to RKMakePathWithObject (in fact it is a shortcut for this method). - + For example, given an 'article' object with an 'articleID' property value of 12345 ... - + RKPathMatcher *matcher = [RKPathMatcher matcherWithPattern:@"/articles/:articleID"]; NSString *resourcePath = [matcher pathFromObject:article]; - + ... will produce a 'resourcePath' containing the string "/articles/12345" - + @param object The object containing the properties to interpolate. @return A string with the object's interpolated property values inserted into the receiver's established pattern. @see RKMakePathWithObject @@ -128,14 +128,14 @@ This generates a resource path by interpolating the properties of the 'object' argument, assuming the existence of a previously specified pattern established via matcherWithPattern:. Otherwise, this method is identical in function to RKMakePathWithObject (in fact it is a shortcut for this method). - + For example, given an 'article' object with an 'articleID' property value of 12345 and a code of "This/That"... - + RKPathMatcher *matcher = [RKPathMatcher matcherWithPattern:@"/articles/:articleID/:code"]; NSString *resourcePath = [matcher pathFromObject:article addingEscapes:YES]; - + ... will produce a 'resourcePath' containing the string "/articles/12345/This%2FThat" - + @param object The object containing the properties to interpolate. @param addEscapes Conditionally add percent escapes to the interpolated property values @return A string with the object's interpolated property values inserted into the receiver's established pattern. diff --git a/Code/Support/RKPathMatcher.m b/Code/Support/RKPathMatcher.m index 577a798a3b..4726f499c4 100755 --- a/Code/Support/RKPathMatcher.m +++ b/Code/Support/RKPathMatcher.m @@ -4,13 +4,13 @@ // // Created by Greg Combs on 9/2/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -73,7 +73,7 @@ - (id)copyWithZone:(NSZone *)zone { copy.sourcePath = self.sourcePath; copy.rootPath = self.rootPath; copy.queryParameters = self.queryParameters; - + return copy; } @@ -109,7 +109,7 @@ - (BOOL)bifurcateSourcePathFromQueryParameters { NSArray *components = [self.sourcePath componentsSeparatedByString:@"?"]; if ([components count] > 1) { self.rootPath = [components objectAtIndex:0]; - self.queryParameters = [[components objectAtIndex:1] queryParametersUsingEncoding:NSUTF8StringEncoding]; + self.queryParameters = [[components objectAtIndex:1] queryParametersUsingEncoding:NSUTF8StringEncoding]; return YES; } return NO; diff --git a/Code/Support/RKPortCheck.h b/Code/Support/RKPortCheck.h new file mode 100644 index 0000000000..d943a75987 --- /dev/null +++ b/Code/Support/RKPortCheck.h @@ -0,0 +1,83 @@ +// +// RKPortCheck.h +// RestKit +// +// Created by Blake Watters on 5/10/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import + +/** + The RKPortCheck class provides a simple interface for quickly testing + the availability of a listening TCP port on a remote host by IP Address or + Hostname. + */ +@interface RKPortCheck : NSObject + +///----------------------------------------------------------------------------- +/// @name Creating a Port Check +///----------------------------------------------------------------------------- + +/** + Initializes the receiver with a given hostname or IP address as a string + and a numeric TCP port number. + + @param hostNameOrIPAddress A string containing the hostname or IP address to check. + @param port The TCP port on the remote host to check for a listening server on. + @return The receiver, initialized with host and port. + */ +- (id)initWithHost:(NSString *)hostNameOrIPAddress port:(NSUInteger)port; + +///----------------------------------------------------------------------------- +/// @name Accessing Host and Port +///----------------------------------------------------------------------------- + +/** + The hostname or IP address the receiver is checking. + */ +@property (nonatomic, retain, readonly) NSString *host; + +/** + The TCP port to check for a listening server on. + */ +@property (nonatomic, assign, readonly) NSUInteger port; + +///----------------------------------------------------------------------------- +/// @name Running the Check +///----------------------------------------------------------------------------- + +/** + Runs the check by creating a socket and attempting to connect to the + target host and port via TCP. The + */ +- (void)run; + +///----------------------------------------------------------------------------- +/// @name Inspecting Port Accessibility +///----------------------------------------------------------------------------- + +/** + Returns a Boolean value indicating if the check has been run. + + @return YES if the check has been run, otherwise NO. + */ +- (BOOL)hasRun; + +/** + Returns a Boolean value indicating if the host and port the receiver checked + is open and listening for incoming connections. + + @return YES if the port on the remote host is open, otherwise NO. + */ +- (BOOL)isOpen; + +/** + Returns a Boolean value indicating if the host and port the receiver checked + is NOT open and listening for incoming connections. + + @return YES if the port on the remote host is closed, otherwise NO. + */ +- (BOOL)isClosed; + +@end diff --git a/Code/Support/RKPortCheck.m b/Code/Support/RKPortCheck.m new file mode 100644 index 0000000000..0d95bc6421 --- /dev/null +++ b/Code/Support/RKPortCheck.m @@ -0,0 +1,99 @@ +// +// RKPortCheck.m +// RestKit +// +// Created by Blake Watters on 5/10/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKPortCheck.h" + +#include +#include + +@interface RKPortCheck () +@property (nonatomic, assign) struct sockaddr_in remote_saddr; +@property (nonatomic, assign, getter = isOpen) BOOL open; +@property (nonatomic, assign, getter = hasRun) BOOL run; +@end + +@implementation RKPortCheck + +@synthesize host = _host; +@synthesize port = _port; +@synthesize remote_saddr = _remote_saddr; +@synthesize open = _open; +@synthesize run = _run; + +- (id)initWithHost:(NSString *)hostNameOrIPAddress port:(NSUInteger)port +{ + self = [self init]; + if (self) { + _run = NO; + _host = [hostNameOrIPAddress retain]; + _port = port; + + struct sockaddr_in sa; + char *hostNameOrIPAddressCString = (char *) [hostNameOrIPAddress UTF8String]; + int result = inet_pton(AF_INET, hostNameOrIPAddressCString, &(sa.sin_addr)); + if (result != 0) { + // IP Address + bzero(&_remote_saddr, sizeof(struct sockaddr_in)); + _remote_saddr.sin_len = sizeof(struct sockaddr_in); + _remote_saddr.sin_family = AF_INET; + _remote_saddr.sin_port = htons(port); + inet_aton(hostNameOrIPAddressCString, &(_remote_saddr.sin_addr)); + } else { + // Hostname + struct hostent *hp; + if ((hp = gethostbyname(hostNameOrIPAddressCString)) == 0) { + return nil; + } + + bzero(&_remote_saddr, sizeof(struct sockaddr_in)); + _remote_saddr.sin_family = AF_INET; + _remote_saddr.sin_addr.s_addr = ((struct in_addr *)(hp->h_addr))->s_addr; + _remote_saddr.sin_port = htons(port); + } + } + + return self; +} + +- (void)dealloc +{ + [_host release]; + [super dealloc]; +} + +- (void)run +{ + int sd; + _run = YES; + + // Create Internet domain socket + if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { + _open = NO; + return; + } + + // Try to connect to the port + _open = (connect(sd,(struct sockaddr *) &_remote_saddr, sizeof(_remote_saddr)) == 0); + + if (_open) { + close(sd); + } +} + +- (BOOL)isOpen +{ + NSAssert(self.hasRun, @"Cannot determine port availability. Check has not been run."); + return _open; +} + +- (BOOL)isClosed +{ + return !self.isOpen; +} + +@end diff --git a/Code/Support/RKSearchEngine.h b/Code/Support/RKSearchEngine.h index bfc96eeaf4..918261b37b 100644 --- a/Code/Support/RKSearchEngine.h +++ b/Code/Support/RKSearchEngine.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 8/26/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -46,13 +46,13 @@ typedef enum { The search text should be matched inclusively using AND. Matches will include all search terms. */ - RKSearchModeAnd, + RKSearchModeAnd, /** The search text should be matched exclusively using OR. Matches will include any search terms. */ - RKSearchModeOr + RKSearchModeOr } RKSearchMode; /** diff --git a/Code/Support/RKSearchEngine.m b/Code/Support/RKSearchEngine.m index 17384576c6..a6bfa70808 100644 --- a/Code/Support/RKSearchEngine.m +++ b/Code/Support/RKSearchEngine.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 8/26/09. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,20 +29,20 @@ @implementation RKSearchEngine + (id)searchEngine { - return [[[RKSearchEngine alloc] init] autorelease]; + return [[[RKSearchEngine alloc] init] autorelease]; } - (id)init { self = [super init]; - if (self) { - mode = RKSearchModeOr; - tokenizeQuery = YES; - stripsWhitespace = YES; - caseSensitive = NO; - } - - return self; + if (self) { + mode = RKSearchModeOr; + tokenizeQuery = YES; + stripsWhitespace = YES; + caseSensitive = NO; + } + + return self; } #pragma mark Private @@ -50,47 +50,47 @@ - (id)init - (NSString *)stripWhitespaceIfNecessary:(NSString *)string { - if (stripsWhitespace) { - return [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - } + if (stripsWhitespace) { + return [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + } return string; } - (NSArray *)tokenizeOrCollect:(NSString *)string { - if (self.tokenizeQuery) { - return [string componentsSeparatedByString:@" "]; - } + if (self.tokenizeQuery) { + return [string componentsSeparatedByString:@" "]; + } return [NSArray arrayWithObject:string]; } - (NSArray *)searchWithTerms:(NSArray*)searchTerms onProperties:(NSArray *)properties inCollection:(NSArray *)collection compoundSelector:(SEL)selector { - NSPredicate *searchPredicate = nil; - - // do any of these properties contain all of these terms - NSMutableArray *propertyPredicates = [NSMutableArray array]; - for (NSString *property in properties) { - NSMutableArray *termPredicates = [NSMutableArray array]; - for (NSString *searchTerm in searchTerms) { - NSPredicate *predicate; - if (self.isCaseSensitive) { - predicate = [NSPredicate predicateWithFormat:@"(%K contains %@)", property, searchTerm]; - } else { - predicate = [NSPredicate predicateWithFormat:@"(%K contains[cd] %@)", property, searchTerm]; - } - [termPredicates addObject:predicate]; - } - - // build a predicate for all of the search terms - NSPredicate *termsPredicate = [NSCompoundPredicate performSelector:selector withObject:termPredicates]; - [propertyPredicates addObject:termsPredicate]; - } - - searchPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:propertyPredicates]; - return [collection filteredArrayUsingPredicate:searchPredicate]; + NSPredicate *searchPredicate = nil; + + // do any of these properties contain all of these terms + NSMutableArray *propertyPredicates = [NSMutableArray array]; + for (NSString *property in properties) { + NSMutableArray *termPredicates = [NSMutableArray array]; + for (NSString *searchTerm in searchTerms) { + NSPredicate *predicate; + if (self.isCaseSensitive) { + predicate = [NSPredicate predicateWithFormat:@"(%K contains %@)", property, searchTerm]; + } else { + predicate = [NSPredicate predicateWithFormat:@"(%K contains[cd] %@)", property, searchTerm]; + } + [termPredicates addObject:predicate]; + } + + // build a predicate for all of the search terms + NSPredicate *termsPredicate = [NSCompoundPredicate performSelector:selector withObject:termPredicates]; + [propertyPredicates addObject:termsPredicate]; + } + + searchPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:propertyPredicates]; + return [collection filteredArrayUsingPredicate:searchPredicate]; } #pragma mark Public @@ -98,23 +98,23 @@ - (NSArray *)searchWithTerms:(NSArray*)searchTerms onProperties:(NSArray *)prope - (NSArray *)searchFor:(NSString *)searchText inCollection:(NSArray *)collection { - NSArray *properties = [NSArray arrayWithObject:@"searchableText"]; - return [self searchFor:searchText onProperties:properties inCollection:collection]; + NSArray *properties = [NSArray arrayWithObject:@"searchableText"]; + return [self searchFor:searchText onProperties:properties inCollection:collection]; } - (NSArray *)searchFor:(NSString *)searchText onProperties:(NSArray *)properties inCollection:(NSArray *)collection { - NSString *searchQuery = [[searchText copy] autorelease]; - searchQuery = [self stripWhitespaceIfNecessary:searchQuery]; - NSArray *searchTerms = [self tokenizeOrCollect:searchQuery]; - - if (mode == RKSearchModeOr) { - return [self searchWithTerms:searchTerms onProperties:properties inCollection:collection compoundSelector:@selector(orPredicateWithSubpredicates:)]; - } else if (mode == RKSearchModeAnd) { - return [self searchWithTerms:searchTerms onProperties:properties inCollection:collection compoundSelector:@selector(andPredicateWithSubpredicates:)]; - } else { - return nil; - } + NSString *searchQuery = [[searchText copy] autorelease]; + searchQuery = [self stripWhitespaceIfNecessary:searchQuery]; + NSArray *searchTerms = [self tokenizeOrCollect:searchQuery]; + + if (mode == RKSearchModeOr) { + return [self searchWithTerms:searchTerms onProperties:properties inCollection:collection compoundSelector:@selector(orPredicateWithSubpredicates:)]; + } else if (mode == RKSearchModeAnd) { + return [self searchWithTerms:searchTerms onProperties:properties inCollection:collection compoundSelector:@selector(andPredicateWithSubpredicates:)]; + } else { + return nil; + } } @end diff --git a/Code/Support/Support.h b/Code/Support/Support.h index 096b54dca7..f3242978a2 100644 --- a/Code/Support/Support.h +++ b/Code/Support/Support.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 9/30/10. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,8 +25,12 @@ #import "RKPathMatcher.h" #import "RKDotNetDateFormatter.h" #import "RKDirectory.h" +#import "RKBenchmark.h" // Load our categories #import "NSDictionary+RKAdditions.h" #import "NSString+RKAdditions.h" #import "NSBundle+RKAdditions.h" +#import "NSArray+RKAdditions.h" +#import "NSData+RKAdditions.h" +#import "NSURL+RKAdditions.h" diff --git a/Code/Support/lcl_config_components.h b/Code/Support/lcl_config_components.h index cab6e4fe07..50ce8a2440 100644 --- a/Code/Support/lcl_config_components.h +++ b/Code/Support/lcl_config_components.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 6/8/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -42,7 +42,7 @@ // and its grouping information in a non-technical, human-readable way // which could be used by a user interface. Groups should be separated by the // path separator '/', e.g. 'Example/Main/Component 1'. -// +// // @@ -56,6 +56,7 @@ _lcl_component(RestKitNetworkQueue, "restkit.network.queue", _lcl_component(RestKitNetworkReachability, "restkit.network.reachability", "RestKit/Network/Reachability") _lcl_component(RestKitObjectMapping, "restkit.object_mapping", "RestKit/ObjectMapping") _lcl_component(RestKitCoreData, "restkit.core_data", "RestKit/CoreData") +_lcl_component(RestKitCoreDataCache, "restkit.core_data.cache", "RestKit/CoreData/Cache") _lcl_component(RestKitCoreDataSearchEngine, "restkit.core_data.search_engine", "RestKit/CoreData/SearchEngine") _lcl_component(RestKitSupport, "restkit.support", "RestKit/Support") _lcl_component(RestKitSupportParsers, "restkit.support.parsers", "RestKit/Support/Parsers") diff --git a/Code/Support/lcl_config_extensions.h b/Code/Support/lcl_config_extensions.h index ffbe792aed..f9b317652c 100644 --- a/Code/Support/lcl_config_extensions.h +++ b/Code/Support/lcl_config_extensions.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 6/8/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/Support/lcl_config_logger.h b/Code/Support/lcl_config_logger.h index 5fdbfe9e8e..44eabdcc6c 100644 --- a/Code/Support/lcl_config_logger.h +++ b/Code/Support/lcl_config_logger.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 6/8/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Code/Testing/RKMappingTest.h b/Code/Testing/RKMappingTest.h index 96b554c2ba..4d62375762 100644 --- a/Code/Testing/RKMappingTest.h +++ b/Code/Testing/RKMappingTest.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 2/17/12. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -35,7 +35,7 @@ /** Creates and returns a new test for a given object mapping and source object. - + @param mapping The object mapping being tested. @param sourceObject The source object being mapped. @return A new mapping test object for a mapping and sourceObject. @@ -45,7 +45,7 @@ /** Creates and returns a new test for a given object mapping, source object and destination object. - + @param mapping The object mapping being tested. @param sourceObject The source object being mapped from. @param destinationObject The destionation object being to. @@ -55,7 +55,7 @@ /** Initializes the receiver with a given object mapping, source object, and destination object. - + @param mapping The object mapping being tested. @param sourceObject The source object being mapped from. @param destinationObject The destionation object being to. @@ -70,7 +70,7 @@ /** Creates and adds an expectation that a key path on the source object will be mapped to a new key path on the destination object. - + @param sourceKeyPath A key path on the sourceObject that should be mapped from. @param destinationKeyPath A key path on the destinationObject that should be mapped to. @see RKObjectMappingTestExpectation @@ -80,7 +80,7 @@ /** Creates and adds an expectation that a key path on the source object will be mapped to a new key path on the destination object with a given value. - + @param sourceKeyPath A key path on the sourceObject that should be mapped from. @param destinationKeyPath A key path on the destinationObject that should be mapped from. @param value A value that is expected to be assigned to destinationKeyPath on the destinationObject. @@ -91,7 +91,7 @@ /** Creates and adds an expectation that a key path on the source object will be mapped to a new key path on the destination object with a value that passes a given test block. - + @param sourceKeyPath A key path on the sourceObject that should be mapped from. @param destinationKeyPath A key path on the destinationObject that should be mapped to. @param evaluationBlock A block with which to evaluate the success of the mapping. @@ -101,11 +101,11 @@ /** Adds an expectation to the receiver to be evaluated during verification. - + If the receiver has been configured with verifiesOnExpect = YES, the mapping operation is performed immediately and the expectation is evaluated. - - @param expectation An expectation object to evaluate during test verification. + + @param expectation An expectation object to evaluate during test verification. @see RKObjectMappingTestExpectation @see verifiesOnExpect */ @@ -127,7 +127,7 @@ /** Verifies that the mapping is configured correctly by performing an object mapping operation and ensuring that all expectations are met. - + @exception NSInternalInconsistencyException Raises an NSInternalInconsistencyException if mapping fails or any expectation is not satisfied. */ @@ -158,11 +158,11 @@ /** The destionation object being mapped to. - + If nil, the mapping test will instantiate a destination object to perform the mapping by invoking `[self.mapping mappableObjectForData:self.sourceObject]` and set the new object as the value for the destinationObject property. - + @see [RKObjectMapping mappableObjectForData:] */ @property(nonatomic, strong, readonly) id destinationObject; @@ -170,7 +170,7 @@ /** A Boolean value that determines if expectations should be verified immediately when added to the receiver. - + **Default**: NO */ @property(nonatomic, assign) BOOL verifiesOnExpect; diff --git a/Code/Testing/RKMappingTest.m b/Code/Testing/RKMappingTest.m index a346696798..4b015a7371 100644 --- a/Code/Testing/RKMappingTest.m +++ b/Code/Testing/RKMappingTest.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 2/17/12. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -51,7 +51,7 @@ + (RKMappingTestEvent *)eventWithMapping:(RKObjectAttributeMapping *)mapping val RKMappingTestEvent *event = [RKMappingTestEvent new]; event.value = value; event.mapping = mapping; - + return event; } @@ -108,9 +108,9 @@ + (RKMappingTest *)testForMapping:(RKObjectMapping *)mapping sourceObject:(id)so - (id)initWithMapping:(RKObjectMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject { NSAssert(sourceObject != nil, @"Cannot perform a mapping operation without a sourceObject object"); NSAssert(mapping != nil, @"Cannot perform a mapping operation without a mapping"); - + self = [super init]; - if (self) { + if (self) { _sourceObject = sourceObject; _destinationObject = destinationObject; _mapping = mapping; @@ -125,7 +125,7 @@ - (id)initWithMapping:(RKObjectMapping *)mapping sourceObject:(id)sourceObject d - (void)addExpectation:(RKMappingTestExpectation *)expectation { [self.expectations addObject:expectation]; - + if (self.verifiesOnExpect) { [self performMapping]; [self verifyExpectation:expectation]; @@ -150,7 +150,7 @@ - (RKMappingTestEvent *)eventMatchingKeyPathsForExpectation:(RKMappingTestExpect return event; } } - + return nil; } @@ -169,7 +169,7 @@ - (BOOL)event:(RKMappingTestEvent *)event satisfiesExpectation:(RKMappingTestExp - (void)performMapping { NSAssert(self.mapping.objectClass, @"Cannot test a mapping that does not have a destination objectClass"); - + // Ensure repeated invocations of verify only result in a single mapping operation if (! self.hasPerformedMapping) { id sourceObject = self.rootKeyPath ? [self.sourceObject valueForKeyPath:self.rootKeyPath] : self.sourceObject; @@ -184,7 +184,7 @@ - (void)performMapping { [NSException raise:NSInternalInconsistencyException format:@"%@: failure when mapping from %@ to %@ with mapping %@", [self description], self.sourceObject, self.destinationObject, self.mapping]; } - + self.performedMapping = YES; } } @@ -206,7 +206,7 @@ - (void)verifyExpectation:(RKMappingTestExpectation *)expectation { - (void)verify { [self performMapping]; - + for (RKMappingTestExpectation *expectation in self.expectations) { [self verifyExpectation:expectation]; } @@ -222,4 +222,8 @@ - (void)objectMappingOperation:(RKObjectMappingOperation *)operation didSetValue [self addEvent:[RKMappingTestEvent eventWithMapping:mapping value:value]]; } +- (void)objectMappingOperation:(RKObjectMappingOperation *)operation didNotSetUnchangedValue:(id)value forKeyPath:(NSString *)keyPath usingMapping:(RKObjectAttributeMapping *)mapping { + [self addEvent:[RKMappingTestEvent eventWithMapping:mapping value:value]]; +} + @end diff --git a/Code/Testing/RKMappingTestExpectation.h b/Code/Testing/RKMappingTestExpectation.h index eb293f7606..33a6f6e25b 100644 --- a/Code/Testing/RKMappingTestExpectation.h +++ b/Code/Testing/RKMappingTestExpectation.h @@ -11,7 +11,7 @@ /** An RKMappingTestExpectation defines an expected mapping event that should occur during the execution of a RKMappingTest. - + @see RKMappingTest */ @interface RKMappingTestExpectation : NSObject @@ -23,7 +23,7 @@ /** 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. The value mapped is not evaluated. - + @param sourceKeyPath A key path on the source object that should be mapped. @param destinationKeyPath A key path on the destination object that should be mapped onto. @return An expectation specifying that sourceKeyPath should be mapped to destionationKeyPath. @@ -33,7 +33,7 @@ /** 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. - + @param sourceKeyPath A key path on the source object that should be mapped. @param destinationKeyPath A key path on the destination object that should be mapped onto. @param value The value that is expected to be assigned to the destination object at destinationKeyPath. @@ -45,7 +45,7 @@ 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. - + @param sourceKeyPath A key path on the source object that should be mapped. @param destinationKeyPath A key path on the destination object that should be mapped onto. @param evaluationBlock A block with which to evaluate the success of the mapping. diff --git a/Code/Testing/RKMappingTestExpectation.m b/Code/Testing/RKMappingTestExpectation.m index 9f71d37dde..7613ce3959 100644 --- a/Code/Testing/RKMappingTestExpectation.m +++ b/Code/Testing/RKMappingTestExpectation.m @@ -27,7 +27,7 @@ + (RKMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKey RKMappingTestExpectation *expectation = [self new]; expectation.sourceKeyPath = sourceKeyPath; expectation.destinationKeyPath = destinationKeyPath; - + return expectation; } @@ -36,7 +36,7 @@ + (RKMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKey expectation.sourceKeyPath = sourceKeyPath; expectation.destinationKeyPath = destinationKeyPath; expectation.value = value; - + return expectation; } @@ -45,7 +45,7 @@ + (RKMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKey expectation.sourceKeyPath = sourceKeyPath; expectation.destinationKeyPath = destinationKeyPath; expectation.evaluationBlock = testBlock; - + return expectation; } @@ -62,7 +62,7 @@ - (NSString *)description { return [NSString stringWithFormat:@"expected sourceKeyPath '%@' to map to destinationKeyPath '%@' satisfying evaluation block", self.sourceKeyPath, self.destinationKeyPath]; } - + return [self mappingDescription]; } diff --git a/Code/Testing/RKTableControllerTestDelegate.h b/Code/Testing/RKTableControllerTestDelegate.h new file mode 100644 index 0000000000..ad70ef4d33 --- /dev/null +++ b/Code/Testing/RKTableControllerTestDelegate.h @@ -0,0 +1,31 @@ +// +// RKTableControllerTestDelegate.h +// RestKit +// +// Created by Blake Watters on 5/23/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#if TARGET_OS_IPHONE +#import "RKTableController.h" +#import "RKFetchedResultsTableController.h" + +@interface RKAbstractTableControllerTestDelegate : NSObject + +@property(nonatomic, readonly, getter = isCancelled) BOOL cancelled; +@property(nonatomic, assign) NSTimeInterval timeout; +@property(nonatomic, assign) BOOL awaitingResponse; + ++ (id)tableControllerDelegate; +- (void)waitForLoad; + +@end + +@interface RKTableControllerTestDelegate : RKAbstractTableControllerTestDelegate +@end + +@interface RKFetchedResultsTableControllerTestDelegate : RKAbstractTableControllerTestDelegate + +@end + +#endif diff --git a/Code/Testing/RKTableControllerTestDelegate.m b/Code/Testing/RKTableControllerTestDelegate.m new file mode 100644 index 0000000000..31332e7593 --- /dev/null +++ b/Code/Testing/RKTableControllerTestDelegate.m @@ -0,0 +1,139 @@ +// +// RKTableControllerTestDelegate.m +// RestKit +// +// Created by Blake Watters on 5/23/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKTableControllerTestDelegate.h" +#import "RKLog.h" + +#if TARGET_OS_IPHONE + +@implementation RKAbstractTableControllerTestDelegate + +@synthesize timeout = _timeout; +@synthesize awaitingResponse = _awaitingResponse; +@synthesize cancelled = _cancelled; + ++ (id)tableControllerDelegate { + return [[self new] autorelease]; +} + +- (id)init { + self = [super init]; + if (self) { + _timeout = 1.0; + _awaitingResponse = NO; + _cancelled = NO; + } + + return self; +} + +- (void)waitForLoad { + _awaitingResponse = YES; + NSDate *startDate = [NSDate date]; + + while (_awaitingResponse) { + RKLogTrace(@"Awaiting response = %d", _awaitingResponse); + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + if ([[NSDate date] timeIntervalSinceDate:startDate] > self.timeout) { + NSLog(@"%@: Timed out!!!", self); + _awaitingResponse = NO; + [NSException raise:nil format:@"*** Operation timed out after %f seconds...", self.timeout]; + } + } +} + +#pragma RKTableControllerDelegate methods + +- (void)tableControllerDidFinishLoad:(RKAbstractTableController *)tableController { + _awaitingResponse = NO; +} + +- (void)tableController:(RKAbstractTableController*)tableController didFailLoadWithError:(NSError *)error { + _awaitingResponse = NO; +} + +- (void)tableControllerDidCancelLoad:(RKAbstractTableController *)tableController { + _awaitingResponse = NO; + _cancelled = YES; +} + +- (void)tableControllerDidFinalizeLoad:(RKAbstractTableController *)tableController { + _awaitingResponse = NO; +} + +// NOTE - Delegate methods below are implemented to allow trampoline through +// OCMock expectations + +- (void)tableControllerDidStartLoad:(RKAbstractTableController *)tableController +{} + +- (void)tableControllerDidBecomeEmpty:(RKAbstractTableController *)tableController +{} + +- (void)tableController:(RKAbstractTableController *)tableController willLoadTableWithObjectLoader:(RKObjectLoader *)objectLoader +{} + +- (void)tableController:(RKAbstractTableController *)tableController didLoadTableWithObjectLoader:(RKObjectLoader *)objectLoader +{} + +- (void)tableController:(RKAbstractTableController*)tableController willBeginEditing:(id)object atIndexPath:(NSIndexPath*)indexPath +{} + +- (void)tableController:(RKAbstractTableController*)tableController didEndEditing:(id)object atIndexPath:(NSIndexPath*)indexPath +{} + +- (void)tableController:(RKAbstractTableController*)tableController didInsertSection:(RKTableSection*)section atIndex:(NSUInteger)sectionIndex +{} + +- (void)tableController:(RKAbstractTableController*)tableController didRemoveSection:(RKTableSection*)section atIndex:(NSUInteger)sectionIndex +{} + +- (void)tableController:(RKAbstractTableController*)tableController didInsertObject:(id)object atIndexPath:(NSIndexPath*)indexPath +{} + +- (void)tableController:(RKAbstractTableController*)tableController didUpdateObject:(id)object atIndexPath:(NSIndexPath*)indexPath +{} + +- (void)tableController:(RKAbstractTableController*)tableController didDeleteObject:(id)object atIndexPath:(NSIndexPath*)indexPath +{} + +- (void)tableController:(RKAbstractTableController*)tableController willAddSwipeView:(UIView*)swipeView toCell:(UITableViewCell*)cell forObject:(id)object +{} + +- (void)tableController:(RKAbstractTableController*)tableController willRemoveSwipeView:(UIView*)swipeView fromCell:(UITableViewCell*)cell forObject:(id)object +{} + +- (void)tableController:(RKTableController *)tableController didLoadObjects:(NSArray *)objects inSection:(NSUInteger)sectionIndex +{} + +- (void)tableController:(RKAbstractTableController *)tableController willDisplayCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath +{} + +- (void)tableController:(RKAbstractTableController *)tableController didSelectCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath +{} + +@end + +@implementation RKTableControllerTestDelegate + +- (void)tableController:(RKTableController *)tableController didLoadObjects:(NSArray *)objects inSection:(RKTableSection *)section +{} + +@end + +@implementation RKFetchedResultsTableControllerTestDelegate + +- (void)tableController:(RKFetchedResultsTableController *)tableController didInsertSectionAtIndex:(NSUInteger)sectionIndex +{} + +- (void)tableController:(RKFetchedResultsTableController *)tableController didDeleteSectionAtIndex:(NSUInteger)sectionIndex +{} + +@end + +#endif diff --git a/Code/Testing/RKTestConstants.m b/Code/Testing/RKTestConstants.m new file mode 100644 index 0000000000..d018355bce --- /dev/null +++ b/Code/Testing/RKTestConstants.m @@ -0,0 +1,19 @@ +// +// RKTestConstants.m +// RestKit +// +// Created by Blake Watters on 5/4/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +/* + This file defines constants used by the Testing module. It is necessary due to strange + linking errors when building for the Device. When these constants were defined within + RKTestFactory.m, they would resolve on the Simulator but produce linker when building + for Device. [sbw - 05/04/2012] + */ +NSString * const RKTestFactoryDefaultNamesClient = @"client"; +NSString * const RKTestFactoryDefaultNamesObjectManager = @"objectManager"; +NSString * const RKTestFactoryDefaultNamesMappingProvider = @"mappingProvider"; +NSString * const RKTestFactoryDefaultNamesManagedObjectStore = @"managedObjectStore"; +NSString * const RKTestFactoryDefaultStoreFilename = @"RKTests.sqlite"; diff --git a/Code/Testing/RKTestFactory.h b/Code/Testing/RKTestFactory.h index 748994030e..b107cd5c90 100644 --- a/Code/Testing/RKTestFactory.h +++ b/Code/Testing/RKTestFactory.h @@ -8,6 +8,13 @@ #import +/** + The default filename used for managed object stores created via the factory. + + @see [RKTestFactory setManagedObjectStoreFilename:] + */ +extern NSString * const RKTestFactoryDefaultStoreFilename; + /** Defines optional callback methods for extending the functionality of the factory. Implementation can be provided via a category. @@ -45,6 +52,14 @@ @end +/* + Default Factory Names + */ +extern NSString * const RKTestFactoryDefaultNamesClient; +extern NSString * const RKTestFactoryDefaultNamesObjectManager; +extern NSString * const RKTestFactoryDefaultNamesMappingProvider; +extern NSString * const RKTestFactoryDefaultNamesManagedObjectStore; + /** RKTestFactory provides an interface for initializing RestKit objects within a unit testing environment. The factory is used to ensure isolation @@ -52,6 +67,13 @@ down between tests and that each test is working within a clean Core Data environment. Callback hooks are provided so that application specific set up and tear down logic can be integrated as well. + + The factory also provides for the definition of named factories for instantiating objects + quickly. At initialization, there are factories defined for creating instances of RKClient, + RKObjectManager, RKObjectMappingProvider, and RKManagedObjectStore. These factories may be + redefined within your application should you choose to utilize a subclass or wish to centralize + configuration of objects across the test suite. You may also define additional factories for building + instances of objects specific to your application using the same infrastructure. */ @interface RKTestFactory : NSObject @@ -62,21 +84,21 @@ /** Returns the base URL with which to initialize RKClient and RKObjectManager instances created via the factory. - + @return The base URL for the factory. */ + (RKURL *)baseURL; /** Sets the base URL for the factory. - + @param URL The new base URL. */ + (void)setBaseURL:(RKURL *)URL; /** Returns the base URL as a string value. - + @return The base URL for the factory, as a string. */ + (NSString *)baseURLString; @@ -84,46 +106,108 @@ /** Sets the base URL for the factory to a new value by constructing an RKURL from the given string. - - @param A string containing the URL to set as the base URL for the factory. + + @param baseURLString A string containing the URL to set as the base URL for the factory. */ + (void)setBaseURLString:(NSString *)baseURLString; +/** + Returns the filename used when constructing instances of RKManagedObjectStore + via the factory. + + @return A string containing the filename to use when creating a managed object store. + */ ++ (NSString *)managedObjectStoreFilename; + +/** + Sets the filename to use when the factory constructs an instance of RKManagedObjectStore. + + @param managedObjectStoreFilename A string containing the filename to use when creating managed object + store instances. + */ ++ (void)setManagedObjectStoreFilename:(NSString *)managedObjectStoreFilename; + +///----------------------------------------------------------------------------- +/// @name Defining & Instantiating Objects from Factories +///----------------------------------------------------------------------------- + +/** + Defines a factory with a given name for building object instances using the + given block. When the factory singleton receives an objectFromFactory: message, + the block designated for the given factory name is invoked and the resulting object + reference is returned. + + Existing factories can be invoking defineFactory:withBlock: with an existing factory name. + + @param factoryName The name to assign the factory. + @param block A block to execute when building an object instance for the factory name. + @return A configured object instance. + */ ++ (void)defineFactory:(NSString *)factoryName withBlock:(id (^)())block; + +/** + Creates and returns a new instance of an object using the factory with the given + name. + + @param factoryName The name of the factory to use when building the requested object. + @raises NSInvalidArgumentException Raised if a factory with the given name is not defined. + @return An object built using the factory registered for the given name. + */ ++ (id)objectFromFactory:(NSString *)factoryName; + +/** + Returns a set of names for all defined factories. + + @return A set of the string names for all defined factories. + */ ++ (NSSet *)factoryNames; + ///----------------------------------------------------------------------------- /// @name Building Instances ///----------------------------------------------------------------------------- /** - Creates and returns an RKClient instance. - - @return A new client object. + Creates and returns an RKClient instance using the factory defined + for the name RKTestFactoryDefaultNamesClient. + + @return A new client instance. */ -+ (RKClient *)client; ++ (id)client; /** - Creates and returns an RKObjectManager instance. - - @return A new client object. + Creates and returns an RKObjectManager instance using the factory defined + for the name RKTestFactoryDefaultNamesObjectManager. + + @return A new object manager instance. */ -+ (RKObjectManager *)objectManager; ++ (id)objectManager; /** - Creates and returns a RKManagedObjectStore instance. - + Creates and returns an RKObjectMappingProvider instance using the factory defined + for the name RKTestFactoryDefaultNamesMappingProvider. + + @return A new object mapping provider instance. + */ ++ (id)mappingProvider; + +/** + Creates and returns a RKManagedObjectStore instance using the factory defined + for the name RKTestFactoryDefaultNamesManagedObjectStore. + A new managed object store will be configured and returned. If there is an existing persistent store (i.e. from a previous test invocation), then the persistent store is deleted. - - @return A new managed object store object. + + @return A new managed object store instance. */ -+ (RKManagedObjectStore *)managedObjectStore; ++ (id)managedObjectStore; ///----------------------------------------------------------------------------- /// @name Managing Test State ///----------------------------------------------------------------------------- /** - Sets up the RestKit testing environment. Invokes the didSetUp callback for application + Sets up the RestKit testing environment. Invokes the didSetUp callback for application specific setup. */ + (void)setUp; @@ -142,7 +226,7 @@ /** Clears the contents of the cache directory by removing the directory and recreating it. - + @see [RKDirectory cachesDirectory] */ + (void)clearCacheDirectory; diff --git a/Code/Testing/RKTestFactory.m b/Code/Testing/RKTestFactory.m index dea58028fd..426261aa6f 100644 --- a/Code/Testing/RKTestFactory.m +++ b/Code/Testing/RKTestFactory.m @@ -8,15 +8,16 @@ #import "RKTestFactory.h" -static NSString * const RKTestFactoryDefaultStoreFilename = @"RKTests.sqlite"; - @interface RKTestFactory () @property (nonatomic, strong) RKURL *baseURL; -@property (nonatomic, strong) Class clientClass; -@property (nonatomic, strong) Class objectManagerClass; +@property (nonatomic, strong) NSString *managedObjectStoreFilename; +@property (nonatomic, strong) NSMutableDictionary *factoryBlocks; + (RKTestFactory *)sharedFactory; +- (void)defineFactory:(NSString *)factoryName withBlock:(id (^)())block; +- (id)objectFromFactory:(NSString *)factoryName; +- (void)defineDefaultFactories; @end @@ -24,14 +25,18 @@ + (RKTestFactory *)sharedFactory; @implementation RKTestFactory -@synthesize baseURL; -@synthesize clientClass; -@synthesize objectManagerClass; +@synthesize baseURL = _baseURL; +@synthesize managedObjectStoreFilename = _managedObjectStoreFilename; +@synthesize factoryBlocks = _factoryBlocks; + (void)initialize { // Ensure the shared factory is initialized [self sharedFactory]; + + if ([RKTestFactory respondsToSelector:@selector(didInitialize)]) { + [RKTestFactory didInitialize]; + } } + (RKTestFactory *)sharedFactory @@ -39,7 +44,7 @@ + (RKTestFactory *)sharedFactory if (! sharedFactory) { sharedFactory = [RKTestFactory new]; } - + return sharedFactory; } @@ -48,38 +53,74 @@ - (id)init self = [super init]; if (self) { self.baseURL = [RKURL URLWithString:@"http://127.0.0.1:4567"]; - self.clientClass = [RKClient class]; - self.objectManagerClass = [RKObjectManager class]; - - if ([RKTestFactory respondsToSelector:@selector(didInitialize)]) { - [RKTestFactory didInitialize]; - } + self.managedObjectStoreFilename = RKTestFactoryDefaultStoreFilename; + self.factoryBlocks = [NSMutableDictionary new]; + [self defineDefaultFactories]; } - + return self; } -- (RKClient *)client +- (void)defineFactory:(NSString *)factoryName withBlock:(id (^)())block { - RKClient *client = [self.clientClass clientWithBaseURL:self.baseURL]; - [RKClient setSharedClient:client]; - client.requestQueue.suspended = NO; - - return client; + [self.factoryBlocks setObject:[block copy] forKey:factoryName]; } -- (RKObjectManager *)objectManager +- (id)objectFromFactory:(NSString *)factoryName { - [RKObjectManager setDefaultMappingQueue:dispatch_queue_create("org.restkit.ObjectMapping", DISPATCH_QUEUE_SERIAL)]; - [RKObjectMapping setDefaultDateFormatters:nil]; - RKObjectManager *objectManager = [self.objectManagerClass managerWithBaseURL:self.baseURL]; - [RKObjectManager setSharedManager:objectManager]; - [RKClient setSharedClient:objectManager.client]; - - // Force reachability determination - [objectManager.client.reachabilityObserver getFlags]; - - return objectManager; + id (^block)() = [self.factoryBlocks objectForKey:factoryName]; + NSAssert(block, @"No factory is defined with the name '%@'", factoryName); + + return block(); +} + +- (void)defineDefaultFactories +{ + [self defineFactory:RKTestFactoryDefaultNamesClient withBlock:^id { + __block RKClient *client; + + RKLogSilenceComponentWhileExecutingBlock(lcl_cRestKitNetworkReachability, ^{ + RKLogSilenceComponentWhileExecutingBlock(lcl_cRestKitSupport, ^{ + client = [RKClient clientWithBaseURL:self.baseURL]; + client.requestQueue.suspended = NO; + [client.reachabilityObserver getFlags]; + }); + }); + + return client; + }]; + + [self defineFactory:RKTestFactoryDefaultNamesObjectManager withBlock:^id { + __block RKObjectManager *objectManager; + + RKLogSilenceComponentWhileExecutingBlock(lcl_cRestKitNetworkReachability, ^{ + RKLogSilenceComponentWhileExecutingBlock(lcl_cRestKitSupport, ^{ + objectManager = [RKObjectManager managerWithBaseURL:self.baseURL]; + RKObjectMappingProvider *mappingProvider = [self objectFromFactory:RKTestFactoryDefaultNamesMappingProvider]; + objectManager.mappingProvider = mappingProvider; + + // Force reachability determination + [objectManager.client.reachabilityObserver getFlags]; + }); + }); + + return objectManager; + }]; + + [self defineFactory:RKTestFactoryDefaultNamesMappingProvider withBlock:^id { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + return mappingProvider; + }]; + + [self defineFactory:RKTestFactoryDefaultNamesManagedObjectStore withBlock:^id { + NSString *storePath = [[RKDirectory applicationDataDirectory] stringByAppendingPathComponent:RKTestFactoryDefaultStoreFilename]; + if ([[NSFileManager defaultManager] fileExistsAtPath:storePath]) { + [RKManagedObjectStore deleteStoreInApplicationDataDirectoryWithFilename:RKTestFactoryDefaultStoreFilename]; + } + RKManagedObjectStore *store = [RKManagedObjectStore objectStoreWithStoreFilename:RKTestFactoryDefaultStoreFilename]; + + return store; + }]; } #pragma mark - Public Static Interface @@ -104,28 +145,74 @@ + (void)setBaseURLString:(NSString *)baseURLString [[RKTestFactory sharedFactory] setBaseURL:[RKURL URLWithString:baseURLString]]; } ++ (NSString *)managedObjectStoreFilename +{ + return [RKTestFactory sharedFactory].managedObjectStoreFilename; +} + ++ (void)setManagedObjectStoreFilename:(NSString *)managedObjectStoreFilename +{ + [RKTestFactory sharedFactory].managedObjectStoreFilename = managedObjectStoreFilename; +} + ++ (void)defineFactory:(NSString *)factoryName withBlock:(id (^)())block +{ + [[RKTestFactory sharedFactory] defineFactory:factoryName withBlock:block]; +} + ++ (id)objectFromFactory:(NSString *)factoryName +{ + return [[RKTestFactory sharedFactory] objectFromFactory:factoryName]; +} + ++ (NSSet *)factoryNames +{ + return [NSSet setWithArray:[[RKTestFactory sharedFactory].factoryBlocks allKeys] ]; +} + + (id)client { - return [[RKTestFactory sharedFactory] client]; + RKClient *client = [self objectFromFactory:RKTestFactoryDefaultNamesClient]; + [RKClient setSharedClient:client]; + + return client; } + (id)objectManager { - return [[RKTestFactory sharedFactory] objectManager]; + RKObjectManager *objectManager = [self objectFromFactory:RKTestFactoryDefaultNamesObjectManager]; + [RKObjectManager setSharedManager:objectManager]; + [RKClient setSharedClient:objectManager.client]; + + return objectManager; +} + ++ (id)mappingProvider +{ + RKObjectMappingProvider *mappingProvider = [self objectFromFactory:RKTestFactoryDefaultNamesMappingProvider]; + + return mappingProvider; } + (id)managedObjectStore { - [RKManagedObjectStore deleteStoreInApplicationDataDirectoryWithFilename:RKTestFactoryDefaultStoreFilename]; - RKManagedObjectStore *store = [RKManagedObjectStore objectStoreWithStoreFilename:RKTestFactoryDefaultStoreFilename]; - [store deletePersistantStore]; - [RKManagedObjectStore setDefaultObjectStore:store]; - - return store; + RKManagedObjectStore *objectStore = [self objectFromFactory:RKTestFactoryDefaultNamesManagedObjectStore]; + [RKManagedObjectStore setDefaultObjectStore:objectStore]; + + return objectStore; } + (void)setUp { + [RKObjectManager setDefaultMappingQueue:dispatch_queue_create("org.restkit.ObjectMapping", DISPATCH_QUEUE_SERIAL)]; + [RKObjectMapping setDefaultDateFormatters:nil]; + + // Delete the store if it exists + NSString *path = [[RKDirectory applicationDataDirectory] stringByAppendingPathComponent:RKTestFactoryDefaultStoreFilename]; + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + [RKManagedObjectStore deleteStoreInApplicationDataDirectoryWithFilename:RKTestFactoryDefaultStoreFilename]; + } + if ([self respondsToSelector:@selector(didSetUp)]) { [self didSetUp]; } @@ -136,7 +223,7 @@ + (void)tearDown [RKObjectManager setSharedManager:nil]; [RKClient setSharedClient:nil]; [RKManagedObjectStore setDefaultObjectStore:nil]; - + if ([self respondsToSelector:@selector(didTearDown)]) { [self didTearDown]; } @@ -148,7 +235,7 @@ + (void)clearCacheDirectory NSString* cachePath = [RKDirectory cachesDirectory]; BOOL success = [[NSFileManager defaultManager] removeItemAtPath:cachePath error:&error]; if (success) { - RKLogInfo(@"Cleared cache directory..."); + RKLogDebug(@"Cleared cache directory..."); success = [[NSFileManager defaultManager] createDirectoryAtPath:cachePath withIntermediateDirectories:YES attributes:nil error:&error]; if (!success) { RKLogError(@"Failed creation of cache path '%@': %@", cachePath, [error localizedDescription]); diff --git a/Code/Testing/RKTestFixture.h b/Code/Testing/RKTestFixture.h index 4bc6bb2ce2..44fc99a5bd 100644 --- a/Code/Testing/RKTestFixture.h +++ b/Code/Testing/RKTestFixture.h @@ -45,6 +45,14 @@ */ + (void)setFixtureBundle:(NSBundle *)bundle; +/** + Returns the full path to the specified fixture file on within the fixture bundle. + + @param fixtureName The name of the fixture file. + @return The full path to the specified fixture file or nil if it cannot be located. + */ ++ (NSString *)pathForFixture:(NSString *)fixtureName; + #if TARGET_OS_IPHONE /** Creates and returns an image object by loading the image data from the fixture identified by the specified file name. diff --git a/Code/Testing/RKTestFixture.m b/Code/Testing/RKTestFixture.m index b86ace98b3..3f689bac45 100644 --- a/Code/Testing/RKTestFixture.m +++ b/Code/Testing/RKTestFixture.m @@ -37,6 +37,10 @@ + (void)setFixtureBundle:(NSBundle *)bundle { fixtureBundle = bundle; } ++ (NSString *)pathForFixture:(NSString *)fixtureName { + return [[self fixtureBundle] pathForResource:fixtureName ofType:nil]; +} + #if TARGET_OS_IPHONE + (UIImage *)imageWithContentsOfFixture:(NSString *)fixtureName { return [[self fixtureBundle] imageWithContentsOfResource:fixtureName withExtension:nil]; diff --git a/Code/Testing/RKTestNotificationObserver.m b/Code/Testing/RKTestNotificationObserver.m index da512de5f4..ba20a39ebb 100644 --- a/Code/Testing/RKTestNotificationObserver.m +++ b/Code/Testing/RKTestNotificationObserver.m @@ -46,16 +46,16 @@ + (RKTestNotificationObserver *)notificationObserverForName:(NSString *)notifica - (id)init { self = [super init]; - if (self) { + if (self) { timeout = 5; - awaitingNotification = NO; - } - return self; + awaitingNotification = NO; + } + return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; - [super dealloc]; + [super dealloc]; } - (void)waitForNotification { @@ -66,16 +66,16 @@ - (void)waitForNotification { name:self.name object:self.object]; - awaitingNotification = YES; - NSDate *startDate = [NSDate date]; + awaitingNotification = YES; + NSDate *startDate = [NSDate date]; - while (self.isAwaitingNotification) { - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - if ([[NSDate date] timeIntervalSinceDate:startDate] > self.timeout) { - [NSException raise:nil format:@"*** Operation timed out after %f seconds...", self.timeout]; - awaitingNotification = NO; - } - } + while (self.isAwaitingNotification) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + if ([[NSDate date] timeIntervalSinceDate:startDate] > self.timeout) { + [NSException raise:nil format:@"*** Operation timed out after %f seconds...", self.timeout]; + awaitingNotification = NO; + } + } } - (void)processNotification:(NSNotification*)notification { diff --git a/Code/Testing/RKTestResponseLoader.m b/Code/Testing/RKTestResponseLoader.m index b8ce64fade..a3e47cda41 100644 --- a/Code/Testing/RKTestResponseLoader.m +++ b/Code/Testing/RKTestResponseLoader.m @@ -53,42 +53,42 @@ + (RKTestResponseLoader *)responseLoader { - (id)init { self = [super init]; - if (self) { - timeout = 4; - awaitingResponse = NO; - } + if (self) { + timeout = 4; + awaitingResponse = NO; + } - return self; + return self; } - (void)dealloc { - [response release]; + [response release]; response = nil; - [error release]; + [error release]; error = nil; [objects release]; objects = nil; - [super dealloc]; + [super dealloc]; } - (void)waitForResponse { - awaitingResponse = YES; - NSDate *startDate = [NSDate date]; + awaitingResponse = YES; + NSDate *startDate = [NSDate date]; RKLogTrace(@"%@ Awaiting response loaded from for %f seconds...", self, self.timeout); - while (awaitingResponse) { - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - if ([[NSDate date] timeIntervalSinceDate:startDate] > self.timeout) { - [NSException raise:RKTestResponseLoaderTimeoutException format:@"*** Operation timed out after %f seconds...", self.timeout]; - awaitingResponse = NO; - } - } + while (awaitingResponse) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + if ([[NSDate date] timeIntervalSinceDate:startDate] > self.timeout) { + [NSException raise:RKTestResponseLoaderTimeoutException format:@"*** Operation timed out after %f seconds...", self.timeout]; + awaitingResponse = NO; + } + } } - (void)loadError:(NSError *)theError { awaitingResponse = NO; - successful = NO; + successful = NO; self.error = theError; } @@ -100,8 +100,12 @@ - (NSString *)errorMessage { return nil; } +- (void)request:(RKRequest *)request didReceiveResponse:(RKResponse *)response { + // Implemented for expectations +} + - (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)aResponse { - self.response = aResponse; + self.response = aResponse; // If request is an Object Loader, then objectLoader:didLoadObjects: // will be sent after didLoadResponse: @@ -129,15 +133,15 @@ - (void)requestDidCancelLoad:(RKRequest *)request { } - (void)objectLoader:(RKObjectLoader *)objectLoader didLoadObjects:(NSArray *)theObjects { - RKLogTrace(@"%@ Loaded response for %@ with body: %@", self, objectLoader, [objectLoader.response bodyAsString]); - RKLogDebug(@"%@ Loaded objects for %@: %@", self, objectLoader, objects); - self.objects = theObjects; - awaitingResponse = NO; - successful = YES; + RKLogTrace(@"%@ Loaded response for %@ with body: %@", self, objectLoader, [objectLoader.response bodyAsString]); + RKLogDebug(@"%@ Loaded objects for %@: %@", self, objectLoader, objects); + self.objects = theObjects; + awaitingResponse = NO; + successful = YES; } - (void)objectLoader:(RKObjectLoader *)objectLoader didFailWithError:(NSError *)theError { - [self loadError:theError]; + [self loadError:theError]; } - (void)objectLoaderDidLoadUnexpectedResponse:(RKObjectLoader *)objectLoader { @@ -147,6 +151,10 @@ - (void)objectLoaderDidLoadUnexpectedResponse:(RKObjectLoader *)objectLoader { unexpectedResponse = YES; } +- (void)objectLoaderDidFinishLoading:(RKObjectLoader *)objectLoader { + // Implemented for expectations +} + #pragma mark - OAuth delegates - (void)OAuthClient:(RKOAuthClient *)client didAcquireAccessToken:(NSString *)token { diff --git a/Code/UI/RKAbstractTableController.h b/Code/UI/RKAbstractTableController.h index 8e2a597212..d6faea9f57 100755 --- a/Code/UI/RKAbstractTableController.h +++ b/Code/UI/RKAbstractTableController.h @@ -21,60 +21,102 @@ #if TARGET_OS_IPHONE #import -#import "RKTableSection.h" #import "RKTableViewCellMappings.h" #import "RKTableItem.h" #import "RKObjectManager.h" #import "RKObjectMapping.h" #import "RKObjectLoader.h" -/** @name Constants */ +///----------------------------------------------------------------------------- +/// @name Constants +///----------------------------------------------------------------------------- -/** Posted when the table view model starts loading */ -extern NSString* const RKTableControllerDidStartLoadNotification; +/** + Posted when the table controller starts loading. + */ +extern NSString * const RKTableControllerDidStartLoadNotification; + +/** + Posted when the table controller finishes loading. + */ +extern NSString * const RKTableControllerDidFinishLoadNotification; -/** Posted when the table view model finishes loading */ -extern NSString* const RKTableControllerDidFinishLoadNotification; +/** + Posted when the table controller has loaded objects into the table view. + */ +extern NSString * const RKTableControllerDidLoadObjectsNotification; -/** Posted when the table view model has loaded objects into the table view */ -extern NSString* const RKTableControllerDidLoadObjectsNotification; +/** + Posted when the table controller has loaded an empty collection of objects into the table view. + */ +extern NSString * const RKTableControllerDidLoadEmptyNotification; -/** Posted when the table view model has loaded an empty collection of objects into the table view */ -extern NSString* const RKTableControllerDidLoadEmptyNotification; +/** + Posted when the table controller has loaded an error. + */ +extern NSString * const RKTableControllerDidLoadErrorNotification; -/** Posted when the table view model has loaded an error */ -extern NSString* const RKTableControllerDidLoadErrorNotification; +/** + Posted when the table controller has transitioned from an offline to online state. + */ +extern NSString * const RKTableControllerDidBecomeOnline; -/** Posted when the table view model has transitioned from offline to online */ -extern NSString* const RKTableControllerDidBecomeOnline; +/** + Posted when the table controller has transitioned from an online to an offline state. + */ +extern NSString * const RKTableControllerDidBecomeOffline; -/** Posted when the table view model has transitioned from online to offline */ -extern NSString* const RKTableControllerDidBecomeOffline; +@protocol RKAbstractTableControllerDelegate; -@protocol RKTableControllerDelegate; +/** + @enum RKTableControllerState + + @constant RKTableControllerStateNormal Indicates that the table has + loaded normally and is displaying cell content. It is not loading content, + is not empty, has not loaded an error, and is not offline. + + @constant RKTableControllerStateLoading Indicates that the table controller + is loading content from a remote source. + + @constant RKTableControllerStateEmpty Indicates that the table controller has + retrieved an empty collection of objects. + + @constant RKTableControllerStateError Indicates that the table controller has + encountered an error while attempting to load. + + @constant RKTableControllerStateOffline Indicates that the table controller is + offline and cannot perform network access. + + @constant RKTableControllerStateNotYetLoaded Indicates that the table controller is + has not yet attempted a load and state is unknown. + */ +enum RKTableControllerState { + RKTableControllerStateNormal = 0, + RKTableControllerStateLoading = 1 << 1, + RKTableControllerStateEmpty = 1 << 2, + RKTableControllerStateError = 1 << 3, + RKTableControllerStateOffline = 1 << 4, + RKTableControllerStateNotYetLoaded = 0xFF000000 +}; +typedef NSUInteger RKTableControllerState; /** - RestKit's table view abstraction leverages the object mapping engine to transform - local objects into UITableViewCell representations. The table view model encapsulates - the functionality of a UITableView dataSource and delegate into a single reusable - component. - */ -@interface RKAbstractTableController : NSObject { - @protected - UIView *_tableOverlayView; - UIImageView *_stateOverlayImageView; - UIView *_pullToRefreshHeaderView; - RKCache *_cache; -} - -///////////////////////////////////////////////////////////////////////// + RKAbstractTableController is an abstract base class for concrete table controller classes. + A table controller object acts as both the delegate and data source for a UITableView + object and leverages the RestKit object mapping engine to transform local domain models + into UITableViewCell representations. Concrete implementations are provided for the + display of static table views and Core Data backed fetched results controller basied + table views. + */ +@interface RKAbstractTableController : NSObject + +///----------------------------------------------------------------------------- /// @name Configuring the Table Controller -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- -@property (nonatomic, assign) id delegate; -@property (nonatomic, readonly) UIViewController* viewController; -@property (nonatomic, readonly) UITableView* tableView; -@property (nonatomic, readonly) NSMutableArray* sections; +@property (nonatomic, assign) id delegate; +@property (nonatomic, readonly) UIViewController *viewController; +@property (nonatomic, readonly) UITableView *tableView; @property (nonatomic, assign) UITableViewRowAnimation defaultRowAnimation; @property (nonatomic, assign) BOOL pullToRefreshEnabled; @@ -82,38 +124,39 @@ extern NSString* const RKTableControllerDidBecomeOffline; @property (nonatomic, assign) BOOL canMoveRows; @property (nonatomic, assign) BOOL autoResizesForKeyboard; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name Instantiation -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- -+ (id)tableControllerWithTableView:(UITableView*)tableView - forViewController:(UIViewController*)viewController; ++ (id)tableControllerWithTableView:(UITableView *)tableView + forViewController:(UIViewController *)viewController; -+ (id)tableControllerForTableViewController:(UITableViewController*)tableViewController; ++ (id)tableControllerForTableViewController:(UITableViewController *)tableViewController; -- (id)initWithTableView:(UITableView*)tableView - viewController:(UIViewController*)viewController; +- (id)initWithTableView:(UITableView *)tableView + viewController:(UIViewController *)viewController; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name Object to Table View Cell Mappings -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- -@property (nonatomic, retain) RKTableViewCellMappings* cellMappings; +@property (nonatomic, retain) RKTableViewCellMappings *cellMappings; -- (void)mapObjectsWithClass:(Class)objectClass toTableCellsWithMapping:(RKTableViewCellMapping*)cellMapping; -- (void)mapObjectsWithClassName:(NSString *)objectClassName toTableCellsWithMapping:(RKTableViewCellMapping*)cellMapping; +- (void)mapObjectsWithClass:(Class)objectClass toTableCellsWithMapping:(RKTableViewCellMapping *)cellMapping; +- (void)mapObjectsWithClassName:(NSString *)objectClassName toTableCellsWithMapping:(RKTableViewCellMapping *)cellMapping; - (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath; -- (RKTableViewCellMapping*)cellMappingForObjectAtIndexPath:(NSIndexPath *)indexPath; +- (RKTableViewCellMapping *)cellMappingForObjectAtIndexPath:(NSIndexPath *)indexPath; /** Return the index path of the object within the table */ - (NSIndexPath *)indexPathForObject:(id)object; - (UITableViewCell *)cellForObject:(id)object; +- (void)reloadRowForObject:(id)object withRowAnimation:(UITableViewRowAnimation)rowAnimation; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name Header and Footer Rows -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- - (void)addHeaderRowForItem:(RKTableItem *)tableItem; - (void)addFooterRowForItem:(RKTableItem *)tableItem; @@ -122,12 +165,12 @@ extern NSString* const RKTableControllerDidBecomeOffline; - (void)removeAllHeaderRows; - (void)removeAllFooterRows; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name RESTful Table Loading -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /** - The object manager instance this table view model is associated with. + The object manager instance this table controller is associated with. This instance is used for creating object loaders when loading Network tables and provides the managed object store used for Core Data tables. @@ -144,42 +187,102 @@ extern NSString* const RKTableControllerDidBecomeOffline; - (void)cancelLoad; - (BOOL)isAutoRefreshNeeded; -///////////////////////////////////////////////////////////////////////// -/// @name Model State Views -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- +/// @name Inspecting Table State +///----------------------------------------------------------------------------- +/** + The current state of the table controller. Note that the controller may be in more + than one state (e.g. loading | empty). + */ +@property (nonatomic, readonly, assign) RKTableControllerState state; + +/** + An error object that was encountered as the result of an attempt to load + the table. Will return a value when the table is in the error state, + otherwise nil. + */ +@property (nonatomic, readonly, retain) NSError *error; + +/** + Returns a Boolean value indicating if the table controller is currently + loading content. + */ - (BOOL)isLoading; + +/** + Returns a Boolean value indicating if the table controller has attempted + a load and transitioned into any state. + */ - (BOOL)isLoaded; + +/** + Returns a Boolean value indicating if the table controller has loaded an + empty set of content. + + When YES and there is not an empty item configured, the table controller + will optionally display an empty image overlayed on top of the table view. + + **NOTE**: It is possible for an empty table controller to display cells + witin the managed table view in the event an empty item or header/footer + rows are configured. + + @see imageForEmpty + */ - (BOOL)isEmpty; + +/** + Returns a Boolean value indicating if the table controller is online + and network operations may be performed. + */ - (BOOL)isOnline; -@property (nonatomic, readonly) BOOL isError; -@property (nonatomic, readonly, retain) NSError* error; +/** + Returns a Boolean value indicating if the table controller is offline. + + When YES, the table controller will optionally display an offline image + overlayed on top of the table view. + + @see imageForOffline + */ +- (BOOL)isOffline; + +/** + Returns a Boolean value indicating if the table controller encountered + an error while attempting to load. + + When YES, the table controller will optionally display an error image + overlayed on top of the table view. + + @see imageForError + */ +- (BOOL)isError; + +///----------------------------------------------------------------------------- +/// @name Model State Views +///----------------------------------------------------------------------------- /** An image to overlay onto the table when the table view does not have any row data to display. It will be centered - within the table view + within the table view. */ -// TODO: Should be emptyImage -@property (nonatomic, retain) UIImage* imageForEmpty; +@property (nonatomic, retain) UIImage *imageForEmpty; /** An image to overlay onto the table when a load operation has encountered an error. It will be centered within the table view. */ -// TODO: Should be errorImage -@property (nonatomic, retain) UIImage* imageForError; +@property (nonatomic, retain) UIImage *imageForError; /** An image to overlay onto the table with when the user does - not have connectivity to the Internet + not have connectivity to the Internet. @see RKReachabilityObserver */ -// TODO: Should be offlineImage -@property (nonatomic, retain) UIImage* imageForOffline; +@property (nonatomic, retain) UIImage *imageForOffline; /** A UIView to add to the table overlay during loading. It @@ -187,7 +290,19 @@ extern NSString* const RKTableControllerDidBecomeOffline; The loading view is always presented non-modally. */ -@property (nonatomic, retain) UIView* loadingView; +@property (nonatomic, retain) UIView *loadingView; + +/** + Returns the image, if any, configured for display when the table controller + is in the given state. + + **NOTE** This method accepts a single state value. + + @param state The table controller state + @return The image for the specified state, else nil. Always returns nil for + RKTableControllerStateNormal, RKTableControllerStateLoading and RKTableControllerStateLoading. + */ +- (UIImage *)imageForState:(RKTableControllerState)state; /** A rectangle configuring the dimensions for the overlay view that is @@ -200,6 +315,11 @@ extern NSString* const RKTableControllerDidBecomeOffline; */ @property (nonatomic, assign) CGRect overlayFrame; +/** + The image currently displayed within the overlay view. + */ +@property (nonatomic, readonly) UIImage *overlayImage; + /** When YES, the image view added to the table overlay for displaying table state (i.e. for offline, error and empty) will be displayed modally @@ -213,44 +333,47 @@ extern NSString* const RKTableControllerDidBecomeOffline; @property (nonatomic, assign) BOOL variableHeightRows; @property (nonatomic, assign) BOOL showsHeaderRowsWhenEmpty; @property (nonatomic, assign) BOOL showsFooterRowsWhenEmpty; -@property (nonatomic, retain) RKTableItem* emptyItem; +@property (nonatomic, retain) RKTableItem *emptyItem; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name Managing Sections -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- -/** The number of sections in the model. */ +/** + The number of sections in the table. + */ @property (nonatomic, readonly) NSUInteger sectionCount; -/** The number of rows across all sections in the model. */ +/** + The number of rows across all sections in the model. + */ @property (nonatomic, readonly) NSUInteger rowCount; -/** Returns the section at the specified index. - * @param index Must be less than the total number of sections. */ -- (RKTableSection *)sectionAtIndex:(NSUInteger)index; - -/** Returns the first section with the specified header title. - * @param title The header title. */ -- (RKTableSection *)sectionWithHeaderTitle:(NSString *)title; +/** + Returns the number of rows in the section at the given index. -/** Returns the index of the specified section. - * @param section Must be a valid non nil RKTableViewSection. - * @return If section is not found, method returns NSNotFound. */ -- (NSUInteger)indexForSection:(RKTableSection *)section; + @param index The index of the section to return the row count for. + @returns The number of rows contained within the section with the given index. + @raises NSInvalidArgumentException Raised if index is greater than or + equal to the total number of sections in the table. + */ +- (NSUInteger)numberOfRowsInSection:(NSUInteger)index; -/** Returns the UITableViewCell created by applying the specified - * mapping operation to the object identified by indexPath. - * @param indexPath The indexPath in the tableView for which a cell - * is needed. */ +/** + Returns the UITableViewCell created by applying the specified + mapping operation to the object identified by indexPath. + + @param indexPath The indexPath in the tableView for which a cell is needed. + */ - (UITableViewCell *)cellForObjectAtIndexPath:(NSIndexPath *)indexPath; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name Managing Swipe View -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- @property (nonatomic, assign) BOOL cellSwipeViewsEnabled; -@property (nonatomic, retain) UIView* cellSwipeView; -@property (nonatomic, readonly) UITableViewCell* swipeCell; +@property (nonatomic, retain) UIView *cellSwipeView; +@property (nonatomic, readonly) UITableViewCell *swipeCell; @property (nonatomic, readonly) id swipeObject; @property (nonatomic, readonly) BOOL animatingCellSwipe; @property (nonatomic, readonly) UISwipeGestureRecognizerDirection swipeDirection; @@ -260,46 +383,45 @@ extern NSString* const RKTableControllerDidBecomeOffline; @end -@protocol RKTableControllerDelegate +@protocol RKAbstractTableControllerDelegate @optional // Network -- (void)tableController:(RKAbstractTableController *)tableController willLoadTableWithObjectLoader:(RKObjectLoader*)objectLoader; -- (void)tableController:(RKAbstractTableController *)tableController didLoadTableWithObjectLoader:(RKObjectLoader*)objectLoader; +- (void)tableController:(RKAbstractTableController *)tableController willLoadTableWithObjectLoader:(RKObjectLoader *)objectLoader; +- (void)tableController:(RKAbstractTableController *)tableController didLoadTableWithObjectLoader:(RKObjectLoader *)objectLoader; // Basic States - (void)tableControllerDidStartLoad:(RKAbstractTableController *)tableController; -/** Sent when the table view has transitioned out of the loading state regardless of outcome **/ +/** + Sent when the table view has transitioned out of the loading state regardless of outcome + */ - (void)tableControllerDidFinishLoad:(RKAbstractTableController *)tableController; -- (void)tableController:(RKAbstractTableController *)tableController didFailLoadWithError:(NSError*)error; +- (void)tableController:(RKAbstractTableController *)tableController didFailLoadWithError:(NSError *)error; - (void)tableControllerDidCancelLoad:(RKAbstractTableController *)tableController; -- (void)tableController:(RKAbstractTableController *)tableController didLoadObjects:(NSArray*)objects inSection:(NSUInteger)sectionIndex; -/** Sent to the delegate when the controller is really and truly finished loading/updating, whether from the network or from Core Data, or from static data, ... this happens in didFinishLoading - **/ -- (void)tableControllerDidFinishFinalLoad:(RKAbstractTableController *)tableController; +/** + Sent to the delegate when the controller is really and truly finished loading/updating, whether from the network or from Core Data, + or from static data, ... this happens in didFinishLoading + */ +- (void)tableControllerDidFinalizeLoad:(RKAbstractTableController *)tableController; /** Sent to the delegate when the content of the table view has become empty */ -- (void)tableControllerDidBecomeEmpty:(RKAbstractTableController *)tableController; // didLoadEmpty??? +- (void)tableControllerDidBecomeEmpty:(RKAbstractTableController *)tableController; /** - Sent to the delegate when the table view model has transitioned from offline to online + Sent to the delegate when the table controller has transitioned from offline to online */ - (void)tableControllerDidBecomeOnline:(RKAbstractTableController *)tableController; /** - Sent to the delegate when the table view model has transitioned from online to offline + Sent to the delegate when the table controller has transitioned from online to offline */ - (void)tableControllerDidBecomeOffline:(RKAbstractTableController *)tableController; -// Sections -- (void)tableController:(RKAbstractTableController *)tableController didInsertSection:(RKTableSection *)section atIndex:(NSUInteger)sectionIndex; -- (void)tableController:(RKAbstractTableController *)tableController didRemoveSection:(RKTableSection *)section atIndex:(NSUInteger)sectionIndex; - // Objects - (void)tableController:(RKAbstractTableController *)tableController didInsertObject:(id)object atIndexPath:(NSIndexPath *)indexPath; - (void)tableController:(RKAbstractTableController *)tableController didUpdateObject:(id)object atIndexPath:(NSIndexPath *)indexPath; @@ -310,19 +432,13 @@ extern NSString* const RKTableControllerDidBecomeOffline; - (void)tableController:(RKAbstractTableController *)tableController didEndEditing:(id)object atIndexPath:(NSIndexPath *)indexPath; // Swipe Views -- (void)tableController:(RKAbstractTableController *)tableController willAddSwipeView:(UIView*)swipeView toCell:(UITableViewCell *)cell forObject:(id)object; -- (void)tableController:(RKAbstractTableController *)tableController willRemoveSwipeView:(UIView*)swipeView fromCell:(UITableViewCell *)cell forObject:(id)object; - -// BELOW NOT YET IMPLEMENTED +- (void)tableController:(RKAbstractTableController *)tableController willAddSwipeView:(UIView *)swipeView toCell:(UITableViewCell *)cell forObject:(id)object; +- (void)tableController:(RKAbstractTableController *)tableController willRemoveSwipeView:(UIView *)swipeView fromCell:(UITableViewCell *)cell forObject:(id)object; // Cells - (void)tableController:(RKAbstractTableController *)tableController willDisplayCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath; - (void)tableController:(RKAbstractTableController *)tableController didSelectCell:(UITableViewCell *)cell forObject:(id)object atIndexPath:(NSIndexPath *)indexPath; -// Objects -- (void)tableControllerDidBeginUpdates:(RKAbstractTableController *)tableController; -- (void)tableControllerDidEndUpdates:(RKAbstractTableController *)tableController; - @end #endif // TARGET_OS_IPHONE diff --git a/Code/UI/RKAbstractTableController.m b/Code/UI/RKAbstractTableController.m index 646440109a..9351bbfc64 100755 --- a/Code/UI/RKAbstractTableController.m +++ b/Code/UI/RKAbstractTableController.m @@ -26,6 +26,7 @@ #import "RKReachabilityObserver.h" #import "UIView+FindFirstResponder.h" #import "RKRefreshGestureRecognizer.h" +#import "RKTableSection.h" // Define logging component #undef RKLogComponent @@ -37,22 +38,21 @@ */ #define BOUNCE_PIXELS 5.0 -NSString* const RKTableControllerDidStartLoadNotification = @"RKTableControllerDidStartLoadNotification"; -NSString* const RKTableControllerDidFinishLoadNotification = @"RKTableControllerDidFinishLoadNotification"; -NSString* const RKTableControllerDidLoadObjectsNotification = @"RKTableControllerDidLoadObjectsNotification"; -NSString* const RKTableControllerDidLoadEmptyNotification = @"RKTableControllerDidLoadEmptyNotification"; -NSString* const RKTableControllerDidLoadErrorNotification = @"RKTableControllerDidLoadErrorNotification"; -NSString* const RKTableControllerDidBecomeOnline = @"RKTableControllerDidBecomeOnline"; -NSString* const RKTableControllerDidBecomeOffline = @"RKTableControllerDidBecomeOffline"; +NSString * const RKTableControllerDidStartLoadNotification = @"RKTableControllerDidStartLoadNotification"; +NSString * const RKTableControllerDidFinishLoadNotification = @"RKTableControllerDidFinishLoadNotification"; +NSString * const RKTableControllerDidLoadObjectsNotification = @"RKTableControllerDidLoadObjectsNotification"; +NSString * const RKTableControllerDidLoadEmptyNotification = @"RKTableControllerDidLoadEmptyNotification"; +NSString * const RKTableControllerDidLoadErrorNotification = @"RKTableControllerDidLoadErrorNotification"; +NSString * const RKTableControllerDidBecomeOnline = @"RKTableControllerDidBecomeOnline"; +NSString * const RKTableControllerDidBecomeOffline = @"RKTableControllerDidBecomeOffline"; -static NSString* lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; +static NSString * lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; @implementation RKAbstractTableController @synthesize delegate = _delegate; @synthesize viewController = _viewController; @synthesize tableView = _tableView; -@synthesize sections = _sections; @synthesize defaultRowAnimation = _defaultRowAnimation; @synthesize objectLoader = _objectLoader; @@ -61,10 +61,7 @@ @implementation RKAbstractTableController @synthesize autoRefreshFromNetwork = _autoRefreshFromNetwork; @synthesize autoRefreshRate = _autoRefreshRate; -@synthesize empty = _empty; -@synthesize loading = _loading; -@synthesize loaded = _loaded; -@synthesize online = _online; +@synthesize state = _state; @synthesize error = _error; @synthesize imageForEmpty = _imageForEmpty; @@ -95,26 +92,27 @@ @implementation RKAbstractTableController @synthesize tableOverlayView = _tableOverlayView; @synthesize stateOverlayImageView = _stateOverlayImageView; @synthesize cache = _cache; +@synthesize pullToRefreshHeaderView = _pullToRefreshHeaderView; #pragma mark - Instantiation -+ (id)tableControllerWithTableView:(UITableView*)tableView - forViewController:(UIViewController*)viewController { ++ (id)tableControllerWithTableView:(UITableView *)tableView + forViewController:(UIViewController *)viewController { return [[[self alloc] initWithTableView:tableView viewController:viewController] autorelease]; } -+ (id)tableControllerForTableViewController:(UITableViewController*)tableViewController { ++ (id)tableControllerForTableViewController:(UITableViewController *)tableViewController { return [self tableControllerWithTableView:tableViewController.tableView forViewController:tableViewController]; } -- (id)initWithTableView:(UITableView*)theTableView viewController:(UIViewController*)theViewController { +- (id)initWithTableView:(UITableView *)theTableView viewController:(UIViewController *)theViewController { NSAssert(theTableView, @"Cannot initialize a table view model with a nil tableView"); NSAssert(theViewController, @"Cannot initialize a table view model with a nil viewController"); self = [self init]; if (self) { self.tableView = theTableView; - self.viewController = theViewController; + _viewController = theViewController; // Assign directly to avoid side-effect of overloaded accessor method self.variableHeightRows = NO; self.defaultRowAnimation = UITableViewRowAnimationFade; self.overlayFrame = CGRectZero; @@ -134,7 +132,7 @@ - (id)init { userInfo:nil]; } - _sections = [NSMutableArray new]; + self.state = RKTableControllerStateNotYetLoaded; self.objectManager = [RKObjectManager sharedManager]; _cellMappings = [RKTableViewCellMappings new]; @@ -149,25 +147,13 @@ - (id)init { // Setup key-value observing [self addObserver:self - forKeyPath:@"loading" - options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld - context:nil]; - [self addObserver:self - forKeyPath:@"loaded" - options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld - context:nil]; - [self addObserver:self - forKeyPath:@"empty" + forKeyPath:@"state" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; [self addObserver:self forKeyPath:@"error" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; - [self addObserver:self - forKeyPath:@"online" - options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld - context:nil]; } return self; } @@ -187,19 +173,16 @@ - (void)dealloc { _tableOverlayView = nil; // Remove observers - [self removeObserver:self forKeyPath:@"loading"]; - [self removeObserver:self forKeyPath:@"loaded"]; - [self removeObserver:self forKeyPath:@"empty"]; + [self removeObserver:self forKeyPath:@"state"]; [self removeObserver:self forKeyPath:@"error"]; - [self removeObserver:self forKeyPath:@"online"]; [[NSNotificationCenter defaultCenter] removeObserver:self]; // TODO: WTF? Get UI crashes when enabled... // [_objectManager.requestQueue abortRequestsWithDelegate:self]; _objectLoader.delegate = nil; + [_objectLoader release]; _objectLoader = nil; - - [_sections release]; + [_cellMappings release]; [_headerItems release]; [_footerItems release]; @@ -218,8 +201,6 @@ - (void)setTableView:(UITableView *)tableView { } - (void)setViewController:(UIViewController *)viewController { - _viewController = viewController; - if ([viewController isKindOfClass:[UITableViewController class]]) { self.tableView = [(UITableViewController*)viewController tableView]; } @@ -240,19 +221,25 @@ - (void)setObjectManager:(RKObjectManager *)objectManager { _objectManager = objectManager; - // Set observers - [notificationCenter addObserver:self - selector:@selector(objectManagerConnectivityDidChange:) - name:RKObjectManagerDidBecomeOnlineNotification - object:objectManager]; - [notificationCenter addObserver:self - selector:@selector(objectManagerConnectivityDidChange:) - name:RKObjectManagerDidBecomeOfflineNotification - object:objectManager]; - - // Initialize online/offline state (if it is known) - if (objectManager.networkStatus != RKObjectManagerNetworkStatusUnknown) { - self.online = objectManager.isOnline; + if (objectManager) { + // Set observers + [notificationCenter addObserver:self + selector:@selector(objectManagerConnectivityDidChange:) + name:RKObjectManagerDidBecomeOnlineNotification + object:objectManager]; + [notificationCenter addObserver:self + selector:@selector(objectManagerConnectivityDidChange:) + name:RKObjectManagerDidBecomeOfflineNotification + object:objectManager]; + + // Initialize online/offline state (if it is known) + if (objectManager.networkStatus != RKObjectManagerNetworkStatusUnknown) { + if (objectManager.isOnline) { + self.state &= ~RKTableControllerStateOffline; + } else { + self.state |= RKTableControllerStateOffline; + } + } } } @@ -279,7 +266,7 @@ - (void)setAutoRefreshFromNetwork:(BOOL)autoRefreshFromNetwork { if (_autoRefreshFromNetwork != autoRefreshFromNetwork) { _autoRefreshFromNetwork = autoRefreshFromNetwork; if (_autoRefreshFromNetwork) { - NSString* cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] + NSString *cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"RKAbstractTableControllerCache"]; _cache = [[RKCache alloc] initWithPath:cachePath subDirectories:nil]; } else { @@ -292,126 +279,110 @@ - (void)setAutoRefreshFromNetwork:(BOOL)autoRefreshFromNetwork { } } -- (void)objectManagerConnectivityDidChange:(NSNotification *)notification { - RKLogTrace(@"%@ received network status change notification: %@", self, [notification name]); - self.online = self.objectManager.isOnline; -} - -#pragma mark - Managing Sections - -- (NSUInteger)sectionCount { - return [_sections count]; -} - -- (NSUInteger)rowCount { - return [[_sections valueForKeyPath:@"@sum.rowCount"] intValue]; +- (void)setLoading:(BOOL)loading { + if (loading) { + self.state |= RKTableControllerStateLoading; + } else { + self.state &= ~RKTableControllerStateLoading; + } } -- (RKTableSection *)sectionAtIndex:(NSUInteger)index { - return [_sections objectAtIndex:index]; +// NOTE: The loaded flag is handled specially. When loaded becomes NO, +// we clear all other flags. In practice this should not happen outside of init. +- (void)setLoaded:(BOOL)loaded { + if (loaded) { + self.state &= ~RKTableControllerStateNotYetLoaded; + } else { + self.state = RKTableControllerStateNotYetLoaded; + } } -- (NSUInteger)indexForSection:(RKTableSection *)section { - NSAssert(section, @"Cannot return index for a nil section"); - return [_sections indexOfObject:section]; +- (void)setEmpty:(BOOL)empty { + if (empty) { + self.state |= RKTableControllerStateEmpty; + } else { + self.state &= ~RKTableControllerStateEmpty; + } } -- (RKTableSection *)sectionWithHeaderTitle:(NSString *)title { - for (RKTableSection* section in _sections) { - if ([section.headerTitle isEqualToString:title]) { - return section; - } +- (void)setOffline:(BOOL)offline { + if (offline) { + self.state |= RKTableControllerStateOffline; + } else { + self.state &= ~RKTableControllerStateOffline; } - - return nil; } -- (UITableViewCell *)cellForObjectAtIndexPath:(NSIndexPath *)indexPath { - RKTableSection* section = [self sectionAtIndex:indexPath.section]; - id mappableObject = [section objectAtIndex:indexPath.row]; - RKTableViewCellMapping* cellMapping = [self.cellMappings cellMappingForObject:mappableObject]; - NSAssert(cellMapping, @"Cannot build a tableView cell for object %@: No cell mapping defined for objects of type '%@'", mappableObject, NSStringFromClass([mappableObject class])); - - UITableViewCell* cell = [cellMapping mappableObjectForData:self.tableView]; - NSAssert(cell, @"Cell mapping failed to dequeue or allocate a tableViewCell for object: %@", mappableObject); - - // Map the object state into the cell - RKObjectMappingOperation* mappingOperation = [[RKObjectMappingOperation alloc] initWithSourceObject:mappableObject destinationObject:cell mapping:cellMapping]; - NSError* error = nil; - BOOL success = [mappingOperation performMapping:&error]; - [mappingOperation release]; - // NOTE: If there is no mapping work performed, but no error is generated then - // we consider the operation a success. It is common for table cells to not contain - // any dynamically mappable content (i.e. header/footer rows, banners, etc.) - if (success == NO && error != nil) { - RKLogError(@"Failed to generate table cell for object: %@", error); - return nil; +- (void)setErrorState:(BOOL)error { + if (error) { + self.state |= RKTableControllerStateError; + } else { + self.state &= ~RKTableControllerStateError; } +} - return cell; +- (void)objectManagerConnectivityDidChange:(NSNotification *)notification { + RKLogTrace(@"%@ received network status change notification: %@", self, [notification name]); + [self setOffline:!self.objectManager.isOnline]; } -#pragma mark - UITableViewDataSource methods +#pragma mark - Abstract Methods -- (NSInteger)numberOfSectionsInTableView:(UITableView*)theTableView { - NSAssert(theTableView == self.tableView, @"numberOfSectionsInTableView: invoked with inappropriate tableView: %@", theTableView); - RKLogTrace(@"%@ numberOfSectionsInTableView = %d", self, self.sectionCount); - return self.sectionCount; +- (BOOL)isConsideredEmpty { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } -- (NSInteger)tableView:(UITableView*)theTableView numberOfRowsInSection:(NSInteger)section { - NSAssert(theTableView == self.tableView, @"tableView:numberOfRowsInSection: invoked with inappropriate tableView: %@", theTableView); - RKLogTrace(@"%@ numberOfRowsInSection:%d = %d", self, section, self.sectionCount); - return [[_sections objectAtIndex:section] rowCount]; +- (NSUInteger)sectionCount { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } -- (UITableViewCell *)tableView:(UITableView*)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - NSAssert(theTableView == self.tableView, @"tableView:cellForRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); - UITableViewCell* cell = [self cellForObjectAtIndexPath:indexPath]; - - RKLogTrace(@"%@ cellForRowAtIndexPath:%@ = %@", self, indexPath, cell); - return cell; +- (NSUInteger)rowCount { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } -- (NSString*)tableView:(UITableView*)theTableView titleForHeaderInSection:(NSInteger)section { - NSAssert(theTableView == self.tableView, @"tableView:titleForHeaderInSection: invoked with inappropriate tableView: %@", theTableView); - return [[_sections objectAtIndex:section] headerTitle]; +- (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } -- (NSString*)tableView:(UITableView*)theTableView titleForFooterInSection:(NSInteger)section { - NSAssert(theTableView == self.tableView, @"tableView:titleForFooterInSection: invoked with inappropriate tableView: %@", theTableView); - return [[_sections objectAtIndex:section] footerTitle]; +- (NSIndexPath *)indexPathForObject:(id)object { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } -- (BOOL)tableView:(UITableView*)theTableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { - NSAssert(theTableView == self.tableView, @"tableView:canEditRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); - return _canEditRows; +- (NSUInteger)numberOfRowsInSection:(NSUInteger)index { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } -- (BOOL)tableView:(UITableView*)theTableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { - NSAssert(theTableView == self.tableView, @"tableView:canMoveRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); - return _canMoveRows; +- (UITableViewCell *)cellForObjectAtIndexPath:(NSIndexPath *)indexPath { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } #pragma mark - Cell Mappings -- (void)mapObjectsWithClass:(Class)objectClass toTableCellsWithMapping:(RKTableViewCellMapping*)cellMapping { +- (void)mapObjectsWithClass:(Class)objectClass toTableCellsWithMapping:(RKTableViewCellMapping *)cellMapping { // TODO: Should we raise an exception/throw a warning if you are doing class mapping for a type // that implements a cellMapping instance method? Maybe a class declaration overrides [_cellMappings setCellMapping:cellMapping forClass:objectClass]; } -- (void)mapObjectsWithClassName:(NSString *)objectClassName toTableCellsWithMapping:(RKTableViewCellMapping*)cellMapping { +- (void)mapObjectsWithClassName:(NSString *)objectClassName toTableCellsWithMapping:(RKTableViewCellMapping *)cellMapping { [self mapObjectsWithClass:NSClassFromString(objectClassName) toTableCellsWithMapping:cellMapping]; } -- (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath { - NSAssert(indexPath, @"Cannot lookup object with a nil indexPath"); - RKTableSection* section = [self sectionAtIndex:indexPath.section]; - return [section objectAtIndex:indexPath.row]; -} - -- (RKTableViewCellMapping*)cellMappingForObjectAtIndexPath:(NSIndexPath *)indexPath { +- (RKTableViewCellMapping *)cellMappingForObjectAtIndexPath:(NSIndexPath *)indexPath { NSAssert(indexPath, @"Cannot lookup cell mapping for object with a nil indexPath"); id object = [self objectForRowAtIndexPath:indexPath]; return [self.cellMappings cellMappingForObject:object]; @@ -422,41 +393,24 @@ - (UITableViewCell *)cellForObject:(id)object { return indexPath ? [self cellForObjectAtIndexPath:indexPath] : nil; } -- (NSIndexPath *)indexPathForObject:(id)object { - NSUInteger sectionIndex = 0; - for (RKTableSection *section in self.sections) { - NSUInteger rowIndex = 0; - for (id rowObject in section.objects) { - if ([rowObject isEqual:object]) { - return [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]; - } - - rowIndex++; - } - sectionIndex++; - } - - return nil; -} - #pragma mark - Header and Footer Rows -- (void)addHeaderRowForItem:(RKTableItem*)tableItem { +- (void)addHeaderRowForItem:(RKTableItem *)tableItem { [_headerItems addObject:tableItem]; } -- (void)addFooterRowForItem:(RKTableItem*)tableItem { +- (void)addFooterRowForItem:(RKTableItem *)tableItem { [_footerItems addObject:tableItem]; } - (void)addHeaderRowWithMapping:(RKTableViewCellMapping *)cellMapping { - RKTableItem* tableItem = [RKTableItem tableItem]; + RKTableItem *tableItem = [RKTableItem tableItem]; tableItem.cellMapping = cellMapping; [self addHeaderRowForItem:tableItem]; } - (void)addFooterRowWithMapping:(RKTableViewCellMapping *)cellMapping { - RKTableItem* tableItem = [RKTableItem tableItem]; + RKTableItem *tableItem = [RKTableItem tableItem]; tableItem.cellMapping = cellMapping; [self addFooterRowForItem:tableItem]; } @@ -469,17 +423,32 @@ - (void)removeAllFooterRows { [_footerItems removeAllObjects]; } +#pragma mark - UITableViewDataSource methods + +- (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + NSAssert(theTableView == self.tableView, @"tableView:cellForRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); + UITableViewCell *cell = [self cellForObjectAtIndexPath:indexPath]; + + RKLogTrace(@"%@ cellForRowAtIndexPath:%@ = %@", self, indexPath, cell); + return cell; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + [NSException raise:@"Must be implemented in a subclass!" format:@"sectionCount must be implemented with a subclass"]; + return 0; +} + #pragma mark - UITableViewDelegate methods -- (void)tableView:(UITableView*)theTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { +- (void)tableView:(UITableView *)theTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSAssert(theTableView == self.tableView, @"tableView:didSelectRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); RKLogTrace(@"%@: Row at indexPath %@ selected for tableView %@", self, indexPath, theTableView); id object = [self objectForRowAtIndexPath:indexPath]; // NOTE: Do NOT use cellForObjectAtIndexPath here. See https://gist.github.com/eafbb641d37bb7137759 - UITableViewCell* cell = [theTableView cellForRowAtIndexPath:indexPath]; - RKTableViewCellMapping* cellMapping = [_cellMappings cellMappingForObject:object]; + UITableViewCell *cell = [theTableView cellForRowAtIndexPath:indexPath]; + RKTableViewCellMapping *cellMapping = [_cellMappings cellMappingForObject:object]; // NOTE: Handle deselection first as the onSelectCell processing may result in the tableView // being reloaded and our instances invalidated @@ -495,16 +464,24 @@ - (void)tableView:(UITableView*)theTableView didSelectRowAtIndexPath:(NSIndexPat RKLogTrace(@"%@: Invoking onSelectCellForObjectAtIndexPath block with cellMapping %@ for object %@ at indexPath = %@", self, cell, object, indexPath); cellMapping.onSelectCellForObjectAtIndexPath(cell, object, indexPath); } + + if ([self.delegate respondsToSelector:@selector(tableController:didSelectCell:forObject:atIndexPath:)]) { + [self.delegate tableController:self didSelectCell:cell forObject:object atIndexPath:indexPath]; + } } - (void)tableView:(UITableView *)theTableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { NSAssert(theTableView == self.tableView, @"tableView:didSelectRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); cell.hidden = NO; id mappableObject = [self objectForRowAtIndexPath:indexPath]; - RKTableViewCellMapping* cellMapping = [self.cellMappings cellMappingForObject:mappableObject]; + RKTableViewCellMapping *cellMapping = [self.cellMappings cellMappingForObject:mappableObject]; if (cellMapping.onCellWillAppearForObjectAtIndexPath) { cellMapping.onCellWillAppearForObjectAtIndexPath(cell, mappableObject, indexPath); } + + if ([self.delegate respondsToSelector:@selector(tableController:willDisplayCell:forObject:atIndexPath:)]) { + [self.delegate tableController:self willDisplayCell:cell forObject:mappableObject atIndexPath:indexPath]; + } // Informal protocol // TODO: Needs documentation!!! @@ -533,7 +510,7 @@ - (void)tableView:(UITableView *)theTableView willDisplayCell:(UITableViewCell * - (CGFloat)tableView:(UITableView *)theTableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if (self.variableHeightRows) { - RKTableViewCellMapping* cellMapping = [self cellMappingForObjectAtIndexPath:indexPath]; + RKTableViewCellMapping *cellMapping = [self cellMappingForObjectAtIndexPath:indexPath]; if (cellMapping.heightOfCellForObjectAtIndexPath) { id object = [self objectForRowAtIndexPath:indexPath]; @@ -550,55 +527,31 @@ - (CGFloat)tableView:(UITableView *)theTableView heightForRowAtIndexPath:(NSInde return self.tableView.rowHeight; } -- (CGFloat)tableView:(UITableView*)theTableView heightForHeaderInSection:(NSInteger)sectionIndex { - NSAssert(theTableView == self.tableView, @"heightForHeaderInSection: invoked with inappropriate tableView: %@", theTableView); - RKTableSection* section = [self sectionAtIndex:sectionIndex]; - return section.headerHeight; -} - -- (CGFloat)tableView:(UITableView*)theTableView heightForFooterInSection:(NSInteger)sectionIndex { - NSAssert(theTableView == self.tableView, @"heightForFooterInSection: invoked with inappropriate tableView: %@", theTableView); - RKTableSection* section = [self sectionAtIndex:sectionIndex]; - return section.footerHeight; -} - -- (UIView*)tableView:(UITableView*)theTableView viewForHeaderInSection:(NSInteger)sectionIndex { - NSAssert(theTableView == self.tableView, @"viewForHeaderInSection: invoked with inappropriate tableView: %@", theTableView); - RKTableSection* section = [self sectionAtIndex:sectionIndex]; - return section.headerView; -} - -- (UIView*)tableView:(UITableView*)theTableView viewForFooterInSection:(NSInteger)sectionIndex { - NSAssert(theTableView == self.tableView, @"viewForFooterInSection: invoked with inappropriate tableView: %@", theTableView); - RKTableSection* section = [self sectionAtIndex:sectionIndex]; - return section.footerView; -} - -- (void)tableView:(UITableView*)theTableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath { - RKTableViewCellMapping* cellMapping = [self cellMappingForObjectAtIndexPath:indexPath]; +- (void)tableView:(UITableView *)theTableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath { + RKTableViewCellMapping *cellMapping = [self cellMappingForObjectAtIndexPath:indexPath]; if (cellMapping.onTapAccessoryButtonForObjectAtIndexPath) { RKLogTrace(@"Found a block for tableView:accessoryButtonTappedForRowWithIndexPath: Executing..."); - UITableViewCell* cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath]; + UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath]; id object = [self objectForRowAtIndexPath:indexPath]; cellMapping.onTapAccessoryButtonForObjectAtIndexPath(cell, object, indexPath); } } -- (NSString*)tableView:(UITableView*)theTableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath { - RKTableViewCellMapping* cellMapping = [self cellMappingForObjectAtIndexPath:indexPath]; +- (NSString *)tableView:(UITableView *)theTableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath { + RKTableViewCellMapping *cellMapping = [self cellMappingForObjectAtIndexPath:indexPath]; if (cellMapping.titleForDeleteButtonForObjectAtIndexPath) { RKLogTrace(@"Found a block for tableView:titleForDeleteConfirmationButtonForRowAtIndexPath: Executing..."); - UITableViewCell* cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath]; + UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath]; id object = [self objectForRowAtIndexPath:indexPath]; return cellMapping.titleForDeleteButtonForObjectAtIndexPath(cell, object, indexPath); } return NSLocalizedString(@"Delete", nil); } -- (UITableViewCellEditingStyle)tableView:(UITableView*)theTableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { +- (UITableViewCellEditingStyle)tableView:(UITableView *)theTableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { if (_canEditRows) { - RKTableViewCellMapping* cellMapping = [self cellMappingForObjectAtIndexPath:indexPath]; - UITableViewCell* cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath]; + RKTableViewCellMapping *cellMapping = [self cellMappingForObjectAtIndexPath:indexPath]; + UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath]; if (cellMapping.editingStyleForObjectAtIndexPath) { RKLogTrace(@"Found a block for tableView:editingStyleForRowAtIndexPath: Executing..."); id object = [self objectForRowAtIndexPath:indexPath]; @@ -609,26 +562,26 @@ - (UITableViewCellEditingStyle)tableView:(UITableView*)theTableView editingStyle return UITableViewCellEditingStyleNone; } -- (void)tableView:(UITableView*)theTableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath { +- (void)tableView:(UITableView *)theTableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath { if ([self.delegate respondsToSelector:@selector(tableController:didEndEditing:atIndexPath:)]) { id object = [self objectForRowAtIndexPath:indexPath]; [self.delegate tableController:self didEndEditing:object atIndexPath:indexPath]; } } -- (void)tableView:(UITableView*)theTableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath { +- (void)tableView:(UITableView *)theTableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath { if ([self.delegate respondsToSelector:@selector(tableController:willBeginEditing:atIndexPath:)]) { id object = [self objectForRowAtIndexPath:indexPath]; [self.delegate tableController:self willBeginEditing:object atIndexPath:indexPath]; } } -- (NSIndexPath *)tableView:(UITableView*)theTableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath { +- (NSIndexPath *)tableView:(UITableView *)theTableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath { if (_canMoveRows) { - RKTableViewCellMapping* cellMapping = [self cellMappingForObjectAtIndexPath:sourceIndexPath]; + RKTableViewCellMapping *cellMapping = [self cellMappingForObjectAtIndexPath:sourceIndexPath]; if (cellMapping.targetIndexPathForMove) { RKLogTrace(@"Found a block for tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: Executing..."); - UITableViewCell* cell = [self tableView:self.tableView cellForRowAtIndexPath:sourceIndexPath]; + UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:sourceIndexPath]; id object = [self objectForRowAtIndexPath:sourceIndexPath]; return cellMapping.targetIndexPathForMove(cell, object, sourceIndexPath, proposedDestinationIndexPath); } @@ -636,7 +589,7 @@ - (NSIndexPath *)tableView:(UITableView*)theTableView targetIndexPathForMoveFrom return proposedDestinationIndexPath; } -- (NSIndexPath *)tableView:(UITableView*)theTableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath { +- (NSIndexPath *)tableView:(UITableView *)theTableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath { [self removeSwipeView:YES]; return indexPath; } @@ -647,18 +600,18 @@ - (void)cancelLoad { [self.objectLoader cancel]; } -- (NSDate*)lastUpdatedDate { +- (NSDate *)lastUpdatedDate { if (! self.objectLoader) { - return nil; + return nil; } - + if (_autoRefreshFromNetwork) { NSAssert(_cache, @"Found a nil cache when trying to read our last loaded time"); - NSDictionary* lastUpdatedDates = [_cache dictionaryForCacheKey:lastUpdatedDateDictionaryKey]; + NSDictionary *lastUpdatedDates = [_cache dictionaryForCacheKey:lastUpdatedDateDictionaryKey]; RKLogTrace(@"Last updated dates dictionary retrieved from tableController cache: %@", lastUpdatedDates); if (lastUpdatedDates) { - NSString* absoluteURLString = [self.objectLoader.URL absoluteString]; - NSNumber* lastUpdatedTimeIntervalSince1970 = (NSNumber*)[lastUpdatedDates objectForKey:absoluteURLString]; + NSString *absoluteURLString = [self.objectLoader.URL absoluteString]; + NSNumber *lastUpdatedTimeIntervalSince1970 = (NSNumber *)[lastUpdatedDates objectForKey:absoluteURLString]; if (absoluteURLString && lastUpdatedTimeIntervalSince1970) { return [NSDate dateWithTimeIntervalSince1970:[lastUpdatedTimeIntervalSince1970 doubleValue]]; } @@ -671,7 +624,7 @@ - (BOOL)isAutoRefreshNeeded { BOOL isAutoRefreshNeeded = NO; if (_autoRefreshFromNetwork) { isAutoRefreshNeeded = YES; - NSDate* lastUpdatedDate = [self lastUpdatedDate]; + NSDate *lastUpdatedDate = [self lastUpdatedDate]; RKLogTrace(@"Last updated: %@", lastUpdatedDate); if (lastUpdatedDate) { RKLogTrace(@"-timeIntervalSinceNow=%f, autoRefreshRate=%f", @@ -684,12 +637,12 @@ - (BOOL)isAutoRefreshNeeded { #pragma mark - RKRequestDelegate & RKObjectLoaderDelegate methods -- (void)requestDidStartLoad:(RKRequest*)request { +- (void)requestDidStartLoad:(RKRequest *)request { RKLogTrace(@"tableController %@ started loading.", self); - self.loading = YES; + [self didStartLoad]; } -- (void)requestDidCancelLoad:(RKRequest*)request { +- (void)requestDidCancelLoad:(RKRequest *)request { RKLogTrace(@"tableController %@ cancelled loading.", self); self.loading = NO; @@ -698,7 +651,7 @@ - (void)requestDidCancelLoad:(RKRequest*)request { } } -- (void)requestDidTimeout:(RKRequest*)request { +- (void)requestDidTimeout:(RKRequest *)request { RKLogTrace(@"tableController %@ timed out while loading.", self); self.loading = NO; } @@ -709,13 +662,13 @@ - (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response { // Updated the lastUpdatedDate dictionary using the URL of the request if (self.autoRefreshFromNetwork) { NSAssert(_cache, @"Found a nil cache when trying to save our last loaded time"); - NSMutableDictionary* lastUpdatedDates = [[_cache dictionaryForCacheKey:lastUpdatedDateDictionaryKey] mutableCopy]; + NSMutableDictionary *lastUpdatedDates = [[_cache dictionaryForCacheKey:lastUpdatedDateDictionaryKey] mutableCopy]; if (lastUpdatedDates) { [_cache invalidateEntry:lastUpdatedDateDictionaryKey]; } else { lastUpdatedDates = [[NSMutableDictionary alloc] init]; } - NSNumber* timeIntervalSince1970 = [NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970]]; + NSNumber *timeIntervalSince1970 = [NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970]]; RKLogTrace(@"Setting timeIntervalSince1970=%@ for URL %@", timeIntervalSince1970, [request.URL absoluteString]); [lastUpdatedDates setObject:timeIntervalSince1970 forKey:[request.URL absoluteString]]; @@ -726,8 +679,7 @@ - (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response { - (void)objectLoader:(RKObjectLoader *)objectLoader didFailWithError:(NSError *)error { RKLogError(@"tableController %@ failed network load with error: %@", self, error); - self.error = error; - [self didFinishLoad]; + [self didFailLoadWithError:error]; } - (void)objectLoaderDidFinishLoading:(RKObjectLoader *)objectLoader { @@ -735,25 +687,64 @@ - (void)objectLoaderDidFinishLoading:(RKObjectLoader *)objectLoader { [self.delegate tableController:self didLoadTableWithObjectLoader:objectLoader]; } - [self.objectLoader reset]; + [objectLoader reset]; + [self didFinishLoad]; +} + +- (void)didStartLoad { + self.loading = YES; +} + +- (void)didFailLoadWithError:(NSError *)error { + self.error = error; [self didFinishLoad]; } - (void)didFinishLoad { - self.empty = [self isEmpty]; + self.empty = [self isConsideredEmpty]; self.loading = [self.objectLoader isLoading]; // Mutate loading state after we have adjusted empty self.loaded = YES; + + if (![self isEmpty] && ![self isLoading]) { + [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadObjectsNotification object:self]; + } + + if (self.delegate && [_delegate respondsToSelector:@selector(tableControllerDidFinalizeLoad:)]) { + [self.delegate performSelector:@selector(tableControllerDidFinalizeLoad:) withObject:self]; + } +} - // Setup offline image state based on current online/offline state - [self updateOfflineImageForOnlineState:[self isOnline]]; - - [self resetOverlayView]; +#pragma mark - Table Overlay Views - if (self.delegate && [_delegate respondsToSelector:@selector(tableControllerDidFinishFinalLoad:)]) - [_delegate performSelector:@selector(tableControllerDidFinishFinalLoad:)]; +- (UIImage *)imageForState:(RKTableControllerState)state { + switch (state) { + case RKTableControllerStateNormal: + case RKTableControllerStateLoading: + case RKTableControllerStateNotYetLoaded: + break; + + case RKTableControllerStateEmpty: + return self.imageForEmpty; + break; + + case RKTableControllerStateError: + return self.imageForError; + break; + + case RKTableControllerStateOffline: + return self.imageForOffline; + break; + + default: + break; + } + + return nil; } -#pragma mark - Table Overlay Views +- (UIImage *)overlayImage { + return _stateOverlayImageView.image; +} // Adds an overlay view above the table - (void)addToOverlayView:(UIView *)view modally:(BOOL)modally { @@ -827,7 +818,7 @@ - (void)removeImageOverlay { [self resetOverlayView]; } -- (void)setImageForEmpty:(UIImage*)imageForEmpty { +- (void)setImageForEmpty:(UIImage *)imageForEmpty { [imageForEmpty retain]; BOOL imageRemoved = [self removeImageFromOverlay:_imageForEmpty]; [_imageForEmpty release]; @@ -835,7 +826,7 @@ - (void)setImageForEmpty:(UIImage*)imageForEmpty { if (imageRemoved) [self showImageInOverlay:_imageForEmpty]; } -- (void)setImageForError:(UIImage*)imageForError { +- (void)setImageForError:(UIImage *)imageForError { [imageForError retain]; BOOL imageRemoved = [self removeImageFromOverlay:_imageForError]; [_imageForError release]; @@ -843,7 +834,7 @@ - (void)setImageForError:(UIImage*)imageForError { if (imageRemoved) [self showImageInOverlay:_imageForError]; } -- (void)setImageForOffline:(UIImage*)imageForOffline { +- (void)setImageForOffline:(UIImage *)imageForOffline { [imageForOffline retain]; BOOL imageRemoved = [self removeImageFromOverlay:_imageForOffline]; [_imageForOffline release]; @@ -851,7 +842,7 @@ - (void)setImageForOffline:(UIImage*)imageForOffline { if (imageRemoved) [self showImageInOverlay:_imageForOffline]; } -- (void)setLoadingView:(UIView*)loadingView { +- (void)setLoadingView:(UIView *)loadingView { [loadingView retain]; BOOL viewRemoved = (_loadingView.superview != nil); [_loadingView removeFromSuperview]; @@ -861,41 +852,34 @@ - (void)setLoadingView:(UIView*)loadingView { if (viewRemoved) [self addToOverlayView:_loadingView modally:NO]; } -#pragma mark - KVO & Model States +#pragma mark - KVO & Table States - (BOOL)isLoading { - return self.loading; + return (self.state & RKTableControllerStateLoading) != 0; } - (BOOL)isLoaded { - return self.loaded; + return (self.state & RKTableControllerStateNotYetLoaded) == 0; +// return self.state != RKTableControllerStateNotYetLoaded; } +- (BOOL)isOffline { + return (self.state & RKTableControllerStateOffline) != 0; +} - (BOOL)isOnline { - return self.online; + return ![self isOffline]; } - (BOOL)isError { - return _error != nil; + return (self.state & RKTableControllerStateError) != 0; } - (BOOL)isEmpty { - NSUInteger nonRowItemsCount = [_headerItems count] + [_footerItems count]; - nonRowItemsCount += _emptyItem ? 1 : 0; - BOOL isEmpty = (self.rowCount - nonRowItemsCount) == 0; - RKLogTrace(@"Determined isEmpty = %@. self.rowCount = %d with %d nonRowItems in the table", isEmpty ? @"YES" : @"NO", self.rowCount, nonRowItemsCount); - return isEmpty; + return (self.state & RKTableControllerStateEmpty) != 0; } -- (void)isLoadingDidChangeTo:(BOOL)isLoading { - if (isLoading) { - // Remove any current state to allow drawing of the loading view - [self removeImageOverlay]; - - // Clear the error state - self.error = nil; - self.empty = NO; - +- (void)isLoadingDidChange { + if ([self isLoading]) { if ([self.delegate respondsToSelector:@selector(tableControllerDidStartLoad:)]) { [self.delegate tableControllerDidStartLoad:self]; } @@ -921,65 +905,40 @@ - (void)isLoadingDidChangeTo:(BOOL)isLoading { } // We don't want any image overlays applied until loading is finished - _stateOverlayImageView.hidden = isLoading; + _stateOverlayImageView.hidden = [self isLoading]; } -- (void)isLoadedDidChangeTo:(BOOL)isLoaded { - if (isLoaded) { +- (void)isLoadedDidChange { + if ([self isLoaded]) { RKLogDebug(@"%@: is now loaded.", self); } else { RKLogDebug(@"%@: is NOT loaded.", self); } } -- (void)errorDidChangeTo:(BOOL)isError { - if (isError) { +- (void)isErrorDidChange { + if ([self isError]) { if ([self.delegate respondsToSelector:@selector(tableController:didFailLoadWithError:)]) { [self.delegate tableController:self didFailLoadWithError:self.error]; } - NSDictionary* userInfo = [NSDictionary dictionaryWithObject:self.error forKey:RKErrorNotificationErrorKey]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:self.error forKey:RKErrorNotificationErrorKey]; [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadErrorNotification object:self userInfo:userInfo]; - - if (self.imageForError) { - [self showImageInOverlay:self.imageForError]; - } - } else { - [self removeImageFromOverlay:self.imageForError]; } } -- (void)isEmptyDidChangeTo:(BOOL)isEmpty { - if (isEmpty) { - // TODO: maybe this should be didLoadEmpty? +- (void)isEmptyDidChange { + if ([self isEmpty]) { if ([self.delegate respondsToSelector:@selector(tableControllerDidBecomeEmpty:)]) { [self.delegate tableControllerDidBecomeEmpty:self]; } [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadEmptyNotification object:self]; - - if (self.imageForEmpty) { - [self showImageInOverlay:self.imageForEmpty]; - } - } else { - if (self.imageForEmpty) { - [self removeImageFromOverlay:self.imageForEmpty]; - } - } -} - -- (void)updateOfflineImageForOnlineState:(BOOL)isOnline { - if (isOnline) { - [self removeImageFromOverlay:self.imageForOffline]; - } else { - if (self.imageForOffline) { - [self showImageInOverlay:self.imageForOffline]; - } } } -- (void)isOnlineDidChangeTo:(BOOL)isOnline { - if (isOnline) { +- (void)isOnlineDidChange { + if ([self isOnline]) { // We just transitioned to online if ([self.delegate respondsToSelector:@selector(tableControllerDidBecomeOnline:)]) { [self.delegate tableControllerDidBecomeOnline:self]; @@ -993,37 +952,59 @@ - (void)isOnlineDidChangeTo:(BOOL)isOnline { } [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidBecomeOffline object:self]; - } + } +} - [self updateOfflineImageForOnlineState:isOnline]; +- (void)updateTableViewForStateChange:(NSDictionary *)change { + RKTableControllerState oldState = [[change valueForKey:NSKeyValueChangeOldKey] integerValue]; + RKTableControllerState newState = [[change valueForKey:NSKeyValueChangeNewKey] integerValue]; + + // Determine state transitions + BOOL loadedChanged = ((oldState ^ newState) & RKTableControllerStateNotYetLoaded); + BOOL emptyChanged = ((oldState ^ newState) & RKTableControllerStateEmpty); + BOOL offlineChanged = ((oldState ^ newState) & RKTableControllerStateOffline); + BOOL loadingChanged = ((oldState ^ newState) & RKTableControllerStateLoading); + BOOL errorChanged = ((oldState ^ newState) & RKTableControllerStateError); + + if (loadedChanged) [self isLoadedDidChange]; + if (emptyChanged) [self isEmptyDidChange]; + if (offlineChanged) [self isOnlineDidChange]; + if (errorChanged) [self isErrorDidChange]; + if (loadingChanged) [self isLoadingDidChange]; + + // Clear the image from the overlay + _stateOverlayImageView.image = nil; + + // Determine the appropriate overlay image to display (if any) + if (self.state == RKTableControllerStateNormal) { + [self removeImageOverlay]; + } else { + if ([self isLoading]) { + // During a load we don't adjust the overlay + return; + } + + // Though the table can be in more than one state, we only + // want to display a single overlay image. + if ([self isOffline] && self.imageForOffline) { + [self showImageInOverlay:self.imageForOffline]; + } else if ([self isError] && self.imageForError) { + [self showImageInOverlay:self.imageForError]; + } else if ([self isEmpty] && self.imageForEmpty) { + [self showImageInOverlay:self.imageForEmpty]; + } + } + + // Remove the overlay if no longer in use + [self resetOverlayView]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - BOOL newValue = NO; - BOOL oldValue = NO; - if ([keyPath isEqualToString:@"loading"]) { - newValue = [[change valueForKey:NSKeyValueChangeNewKey] boolValue]; - oldValue = [[change valueForKey:NSKeyValueChangeOldKey] boolValue]; - if (newValue != oldValue) [self isLoadingDidChangeTo:newValue]; - } else if ([keyPath isEqualToString:@"loaded"]) { - newValue = [[change valueForKey:NSKeyValueChangeNewKey] boolValue]; - oldValue = [[change valueForKey:NSKeyValueChangeOldKey] boolValue]; - if (newValue != oldValue) [self isLoadedDidChangeTo:newValue]; + if ([keyPath isEqualToString:@"state"]) { + [self updateTableViewForStateChange:change]; } else if ([keyPath isEqualToString:@"error"]) { - newValue = (! [[change valueForKey:NSKeyValueChangeNewKey] isEqual:[NSNull null]]); - oldValue = (! [[change valueForKey:NSKeyValueChangeOldKey] isEqual:[NSNull null]]); - if (newValue != oldValue) [self errorDidChangeTo:newValue]; - } else if ([keyPath isEqualToString:@"empty"]) { - newValue = [[change valueForKey:NSKeyValueChangeNewKey] boolValue]; - oldValue = [[change valueForKey:NSKeyValueChangeOldKey] boolValue]; - if (newValue != oldValue) [self isEmptyDidChangeTo:newValue]; - } else if ([keyPath isEqualToString:@"online"]) { - newValue = [[change valueForKey:NSKeyValueChangeNewKey] boolValue]; - oldValue = [[change valueForKey:NSKeyValueChangeOldKey] boolValue]; - if (newValue != oldValue) [self isOnlineDidChangeTo:newValue]; + [self setErrorState:(self.error != nil)]; } - - RKLogTrace(@"Key-value observation triggered for keyPath '%@'. Old value = %d, new value = %d", keyPath, oldValue, newValue); } #pragma mark - Pull to Refresh @@ -1066,18 +1047,18 @@ - (void)pullToRefreshStateChanged:(UIGestureRecognizer *)gesture { } - (void)resetPullToRefreshRecognizer { - RKRefreshGestureRecognizer* recognizer = [self pullToRefreshGestureRecognizer]; + RKRefreshGestureRecognizer *recognizer = [self pullToRefreshGestureRecognizer]; if (recognizer) [recognizer setRefreshState:RKRefreshIdle]; } -- (BOOL)pullToRefreshDataSourceIsLoading:(UIGestureRecognizer*)gesture { - // If we have already been loaded and we are loading again, a refresh is taking place... - return [self isLoaded] && [self isLoading] && [self isOnline]; +- (BOOL)pullToRefreshDataSourceIsLoading:(UIGestureRecognizer *)gesture { + // If we have already been loaded and we are loading again, a refresh is taking place... + return [self isLoaded] && [self isLoading] && [self isOnline]; } -- (NSDate*)pullToRefreshDataSourceLastUpdated:(UIGestureRecognizer*)gesture { - NSDate* dataSourceLastUpdated = [self lastUpdatedDate]; +- (NSDate *)pullToRefreshDataSourceLastUpdated:(UIGestureRecognizer *)gesture { + NSDate *dataSourceLastUpdated = [self lastUpdatedDate]; return dataSourceLastUpdated ? dataSourceLastUpdated : [NSDate date]; } @@ -1085,20 +1066,20 @@ - (NSDate*)pullToRefreshDataSourceLastUpdated:(UIGestureRecognizer*)gesture { - (void)setupSwipeGestureRecognizers { // Setup a right swipe gesture recognizer - UISwipeGestureRecognizer* rightSwipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRight:)]; + UISwipeGestureRecognizer *rightSwipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRight:)]; rightSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionRight; [self.tableView addGestureRecognizer:rightSwipeGestureRecognizer]; [rightSwipeGestureRecognizer release]; // Setup a left swipe gesture recognizer - UISwipeGestureRecognizer* leftSwipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeft:)]; + UISwipeGestureRecognizer *leftSwipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeft:)]; leftSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft; [self.tableView addGestureRecognizer:leftSwipeGestureRecognizer]; [leftSwipeGestureRecognizer release]; } - (void)removeSwipeGestureRecognizers { - for (UIGestureRecognizer* recognizer in self.tableView.gestureRecognizers) { + for (UIGestureRecognizer *recognizer in self.tableView.gestureRecognizers) { if ([recognizer isKindOfClass:[UISwipeGestureRecognizer class]]) { [self.tableView removeGestureRecognizer:recognizer]; } @@ -1121,11 +1102,11 @@ - (void)setCellSwipeViewsEnabled:(BOOL)cellSwipeViewsEnabled { _cellSwipeViewsEnabled = cellSwipeViewsEnabled; } -- (void)swipe:(UISwipeGestureRecognizer*)recognizer direction:(UISwipeGestureRecognizerDirection)direction { +- (void)swipe:(UISwipeGestureRecognizer *)recognizer direction:(UISwipeGestureRecognizerDirection)direction { if (_cellSwipeViewsEnabled && recognizer && recognizer.state == UIGestureRecognizerStateEnded) { CGPoint location = [recognizer locationInView:self.tableView]; - NSIndexPath* indexPath = [self.tableView indexPathForRowAtPoint:location]; - UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath]; + NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location]; + UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; id object = [self objectForRowAtIndexPath:indexPath]; if (cell.frame.origin.x != 0) { @@ -1141,11 +1122,11 @@ - (void)swipe:(UISwipeGestureRecognizer*)recognizer direction:(UISwipeGestureRec } } -- (void)swipeLeft:(UISwipeGestureRecognizer*)recognizer { +- (void)swipeLeft:(UISwipeGestureRecognizer *)recognizer { [self swipe:recognizer direction:UISwipeGestureRecognizerDirectionLeft]; } -- (void)swipeRight:(UISwipeGestureRecognizer*)recognizer { +- (void)swipeRight:(UISwipeGestureRecognizer *)recognizer { [self swipe:recognizer direction:UISwipeGestureRecognizerDirectionRight]; } @@ -1184,7 +1165,7 @@ - (void)addSwipeViewTo:(UITableViewCell *)cell withObject:(id)object direction:( } } -- (void)animationDidStopAddingSwipeView:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context { +- (void)animationDidStopAddingSwipeView:(NSString *)animationID finished:(NSNumber *)finished context:(void*)context { _animatingCellSwipe = NO; } @@ -1222,7 +1203,7 @@ - (void)removeSwipeView:(BOOL)animated { } } -- (void)animationDidStopOne:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context { +- (void)animationDidStopOne:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.2]; if (_swipeDirection == UISwipeGestureRecognizerDirectionRight) { @@ -1236,7 +1217,7 @@ - (void)animationDidStopOne:(NSString*)animationID finished:(NSNumber*)finished [UIView commitAnimations]; } -- (void)animationDidStopTwo:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context { +- (void)animationDidStopTwo:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context { [UIView commitAnimations]; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.2]; @@ -1251,7 +1232,7 @@ - (void)animationDidStopTwo:(NSString*)animationID finished:(NSNumber*)finished [UIView commitAnimations]; } -- (void)animationDidStopThree:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context { +- (void)animationDidStopThree:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context { _animatingCellSwipe = NO; [_swipeCell release]; _swipeCell = nil; @@ -1260,7 +1241,7 @@ - (void)animationDidStopThree:(NSString*)animationID finished:(NSNumber*)finishe #pragma mark UIScrollViewDelegate methods -- (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView { +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { [self removeSwipeView:YES]; } @@ -1271,9 +1252,9 @@ - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView { #pragma mark - Keyboard Notification methods -- (void)resizeTableViewForKeyboard:(NSNotification*)notification { +- (void)resizeTableViewForKeyboard:(NSNotification *)notification { NSAssert(_autoResizesForKeyboard, @"Errantly receiving keyboard notifications while autoResizesForKeyboard=NO"); - NSDictionary* userInfo = [notification userInfo]; + NSDictionary *userInfo = [notification userInfo]; CGRect keyboardEndFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGFloat heightForViewShift = keyboardEndFrame.size.height; @@ -1281,13 +1262,13 @@ - (void)resizeTableViewForKeyboard:(NSNotification*)notification { keyboardEndFrame.size.height, heightForViewShift); CGFloat bottomBarOffset = 0.0; - UINavigationController* navigationController = self.viewController.navigationController; + UINavigationController *navigationController = self.viewController.navigationController; if (navigationController && navigationController.toolbar && !navigationController.toolbarHidden) { bottomBarOffset += navigationController.toolbar.frame.size.height; RKLogTrace(@"Found a visible toolbar. Reducing size of heightForViewShift by=%f", bottomBarOffset); } - UITabBarController* tabBarController = self.viewController.tabBarController; + UITabBarController *tabBarController = self.viewController.tabBarController; if (tabBarController && tabBarController.tabBar && !self.viewController.hidesBottomBarWhenPushed) { bottomBarOffset += tabBarController.tabBar.frame.size.height; RKLogTrace(@"Found a visible tabBar. Reducing size of heightForViewShift by=%f", bottomBarOffset); @@ -1306,7 +1287,7 @@ - (void)resizeTableViewForKeyboard:(NSNotification*)notification { nonKeyboardRect.origin.x, nonKeyboardRect.origin.y, nonKeyboardRect.size.width, nonKeyboardRect.size.height); - UIView* firstResponder = [self.tableView findFirstResponder]; + UIView *firstResponder = [self.tableView findFirstResponder]; if (firstResponder) { CGRect firstResponderFrame = firstResponder.frame; RKLogTrace(@"Found firstResponder=%@ at (%f, %f, %f, %f)", firstResponder, @@ -1337,9 +1318,14 @@ - (void)resizeTableViewForKeyboard:(NSNotification*)notification { } } -- (void)loadTableWithObjectLoader:(RKObjectLoader*)theObjectLoader { +- (void)loadTableWithObjectLoader:(RKObjectLoader *)theObjectLoader { NSAssert(theObjectLoader, @"Cannot perform a network load without an object loader"); if (! [self.objectLoader isEqual:theObjectLoader]) { + if (self.objectLoader) { + RKLogDebug(@"Cancelling in progress table load: asked to load with a new object loader."); + [self.objectLoader.queue cancelRequest:self.objectLoader]; + } + theObjectLoader.delegate = self; self.objectLoader = theObjectLoader; } @@ -1351,4 +1337,11 @@ - (void)loadTableWithObjectLoader:(RKObjectLoader*)theObjectLoader { } } +- (void)reloadRowForObject:(id)object withRowAnimation:(UITableViewRowAnimation)rowAnimation { + NSIndexPath *indexPath = [self indexPathForObject:object]; + if (indexPath) { + [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:rowAnimation]; + } +} + @end diff --git a/Code/UI/RKAbstractTableController_Internals.h b/Code/UI/RKAbstractTableController_Internals.h index 023952efe0..97c7b42be0 100644 --- a/Code/UI/RKAbstractTableController_Internals.h +++ b/Code/UI/RKAbstractTableController_Internals.h @@ -18,25 +18,29 @@ // limitations under the License. // -#import +#import #import "RKRefreshGestureRecognizer.h" +/* + A private continuation class for subclass implementations of RKAbstractTableController + */ @interface RKAbstractTableController () -@property (nonatomic, readwrite, assign) UITableView* tableView; -@property (nonatomic, readwrite, assign) UIViewController* viewController; -@property (nonatomic, readwrite, retain) RKObjectLoader* objectLoader; -@property (nonatomic, readwrite, assign) BOOL loading; -@property (nonatomic, readwrite, assign) BOOL loaded; -@property (nonatomic, readwrite, assign) BOOL empty; -@property (nonatomic, readwrite, assign) BOOL online; -@property (nonatomic, readwrite, retain) NSError* error; -@property (nonatomic, readwrite, retain) NSMutableArray* headerItems; -@property (nonatomic, readwrite, retain) NSMutableArray* footerItems; - +@property (nonatomic, readwrite, assign) UITableView *tableView; +@property (nonatomic, readwrite, assign) UIViewController *viewController; +@property (nonatomic, assign, readwrite) RKTableControllerState state; +@property (nonatomic, readwrite, retain) RKObjectLoader *objectLoader; +@property (nonatomic, readwrite, retain) NSError *error; +@property (nonatomic, readwrite, retain) NSMutableArray *headerItems; +@property (nonatomic, readwrite, retain) NSMutableArray *footerItems; @property (nonatomic, readonly) UIView *tableOverlayView; @property (nonatomic, readonly) UIImageView *stateOverlayImageView; @property (nonatomic, readonly) RKCache *cache; +@property (nonatomic, retain) UIView *pullToRefreshHeaderView; + +#pragma mark - Subclass Load Event Hooks + +- (void)didStartLoad; /** Must be invoked when the table controller has finished loading. @@ -45,7 +49,7 @@ and cleaning up the table overlay view. */ - (void)didFinishLoad; -- (void)updateOfflineImageForOnlineState:(BOOL)isOnline; +- (void)didFailLoadWithError:(NSError *)error; #pragma mark - Table View Overlay @@ -61,5 +65,14 @@ - (void)pullToRefreshStateChanged:(UIGestureRecognizer *)gesture; - (void)resetPullToRefreshRecognizer; +/** + Returns a Boolean value indicating if the table controller + should be considered empty and transitioned into the empty state. + Used by the abstract table controller to trigger state transitions. + + **NOTE**: This is an abstract method that MUST be implemented with + a subclass. + */ +- (BOOL)isConsideredEmpty; @end diff --git a/Code/UI/RKFetchedResultsTableController.h b/Code/UI/RKFetchedResultsTableController.h index 21e2d0c7cf..127a212f47 100755 --- a/Code/UI/RKFetchedResultsTableController.h +++ b/Code/UI/RKFetchedResultsTableController.h @@ -22,8 +22,19 @@ typedef UIView *(^RKFetchedResultsTableViewViewForHeaderInSectionBlock)(NSUInteger sectionIndex, NSString *sectionTitle); +@class RKFetchedResultsTableController; +@protocol RKFetchedResultsTableControllerDelegate + +@optional + +// Sections +- (void)tableController:(RKFetchedResultsTableController *)tableController didInsertSectionAtIndex:(NSUInteger)sectionIndex; +- (void)tableController:(RKFetchedResultsTableController *)tableController didDeleteSectionAtIndex:(NSUInteger)sectionIndex; + +@end + /** - Instances of RKFetchedResultsTableController provide an interface for driving a UITableView + Instances of RKFetchedResultsTableController provide an interface for driving a UITableView */ @interface RKFetchedResultsTableController : RKAbstractTableController { @private @@ -33,7 +44,8 @@ typedef UIView *(^RKFetchedResultsTableViewViewForHeaderInSectionBlock)(NSUInteg BOOL _isEmptyBeforeAnimation; } -@property (nonatomic, readonly) NSFetchedResultsController *fetchedResultsController; +@property (nonatomic, assign) id delegate; +@property (nonatomic, retain, readonly) NSFetchedResultsController *fetchedResultsController; @property (nonatomic, copy) NSString *resourcePath; @property (nonatomic, retain) NSFetchRequest *fetchRequest; @property (nonatomic, assign) CGFloat heightForHeaderInSection; diff --git a/Code/UI/RKFetchedResultsTableController.m b/Code/UI/RKFetchedResultsTableController.m index 38763bf829..c564f99d58 100755 --- a/Code/UI/RKFetchedResultsTableController.m +++ b/Code/UI/RKFetchedResultsTableController.m @@ -17,6 +17,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // + #import "RKFetchedResultsTableController.h" #import "RKAbstractTableController_Internals.h" #import "RKManagedObjectStore.h" @@ -31,12 +32,15 @@ #define RKLogComponent lcl_cRestKitUI @interface RKFetchedResultsTableController () -- (void)performFetch; +@property (nonatomic, retain, readwrite) NSFetchedResultsController *fetchedResultsController; + +- (BOOL)performFetch:(NSError **)error; - (void)updateSortedArray; @end @implementation RKFetchedResultsTableController +@dynamic delegate; @synthesize fetchedResultsController = _fetchedResultsController; @synthesize resourcePath = _resourcePath; @synthesize heightForHeaderInSection = _heightForHeaderInSection; @@ -51,9 +55,9 @@ @implementation RKFetchedResultsTableController @synthesize fetchRequest = _fetchRequest; - (void)dealloc { - _fetchedResultsController.delegate = nil; - [_fetchedResultsController release]; - _fetchedResultsController = nil; + _fetchedResultsController.delegate = nil; + [_fetchedResultsController release]; + _fetchedResultsController = nil; [_resourcePath release]; _resourcePath = nil; [_predicate release]; @@ -75,18 +79,31 @@ - (void)dealloc { #pragma mark - Helpers -- (void)performFetch { +- (BOOL)performFetch:(NSError **)error { // TODO: We could be doing a KVO on the predicate/sortDescriptors/sectionKeyPath and intelligently deleting the cache [NSFetchedResultsController deleteCacheWithName:_fetchedResultsController.cacheName]; - - NSError* error; - BOOL success = [_fetchedResultsController performFetch:&error]; + BOOL success = [_fetchedResultsController performFetch:error]; if (!success) { - self.error = error; - RKLogError(@"performFetch failed with error: %@", [error localizedDescription]); + RKLogError(@"performFetch failed with error: %@", [*error localizedDescription]); + return NO; } else { RKLogTrace(@"performFetch completed successfully"); + for (NSUInteger index = 0; index < [self sectionCount]; index++) { + if ([self.delegate respondsToSelector:@selector(tableController:didInsertSectionAtIndex:)]) { + [self.delegate tableController:self didInsertSectionAtIndex:index]; + } + + if ([self.delegate respondsToSelector:@selector(tableController:didInsertObject:atIndexPath:)]) { + for (NSUInteger row = 0; row < [self numberOfRowsInSection:index]; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:index]; + id object = [self objectForRowAtIndexPath:indexPath]; + [self.delegate tableController:self didInsertObject:object atIndexPath:indexPath]; + } + } + } } + + return YES; } - (void)updateSortedArray { @@ -207,7 +224,7 @@ - (NSIndexPath *)indexPathForFetchedResultsIndexPath:(NSIndexPath *)indexPath { #pragma mark - Public - (NSFetchRequest *)fetchRequest { - return _fetchRequest ? _fetchRequest : _fetchedResultsController.fetchRequest; + return _fetchRequest ? _fetchRequest : _fetchedResultsController.fetchRequest; } - (void)loadTable { @@ -217,8 +234,7 @@ - (void)loadTable { } else { fetchRequest = _fetchRequest; } - NSAssert(fetchRequest != nil, @"Attempted to load RKFetchedResultsTableController with nil fetchRequest for resourcePath %@, fetchRequest %@", - _resourcePath, _fetchRequest); + NSAssert(fetchRequest != nil, @"Attempted to load RKFetchedResultsTableController with nil fetchRequest for resourcePath %@, fetchRequest %@", _resourcePath, _fetchRequest); if (_predicate) { [fetchRequest setPredicate:_predicate]; @@ -226,21 +242,21 @@ - (void)loadTable { if (_sortDescriptors) { [fetchRequest setSortDescriptors:_sortDescriptors]; } - - [_fetchedResultsController setDelegate:nil]; - [_fetchedResultsController release]; - _fetchedResultsController = nil; - - _fetchedResultsController = - [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest - managedObjectContext:[NSManagedObjectContext contextForCurrentThread] - sectionNameKeyPath:_sectionNameKeyPath - cacheName:_cacheName]; + + _fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest + managedObjectContext:[NSManagedObjectContext contextForCurrentThread] + sectionNameKeyPath:_sectionNameKeyPath + cacheName:_cacheName]; _fetchedResultsController.delegate = self; - - [self performFetch]; + + // Perform the load + NSError *error; + [self didStartLoad]; + BOOL success = [self performFetch:&error]; + if (! success) { + [self didFailLoadWithError:error]; + } [self updateSortedArray]; - [self.tableView reloadData]; [self didFinishLoad]; @@ -286,13 +302,13 @@ - (void)setObjectMappingForClass:(Class)objectClass { NSParameterAssert(objectClass != NULL); NSAssert(self.objectLoader != NULL, @"Resource path (and thus object loader) must be set before setting object mapping."); NSAssert(self.objectManager != NULL, @"Object manager must exist before setting object mapping."); - self.objectLoader.objectMapping = [self.objectManager.mappingProvider objectMappingForClass:objectClass]; + self.objectLoader.objectMapping = [self.objectManager.mappingProvider objectMappingForClass:objectClass]; } #pragma mark - Managing Sections - (NSUInteger)sectionCount { - return [[_fetchedResultsController sections] count]; + return [[_fetchedResultsController sections] count]; } - (NSUInteger)rowCount { @@ -324,7 +340,7 @@ - (UITableViewCell *)cellForObjectAtIndexPath:(NSIndexPath *)indexPath { NSError* error = nil; BOOL success = [mappingOperation performMapping:&error]; [mappingOperation release]; - + // NOTE: If there is no mapping work performed, but no error is generated then // we consider the operation a success. It is common for table cells to not contain // any dynamically mappable content (i.e. header/footer rows, banners, etc.) @@ -352,7 +368,7 @@ - (UITableViewCell *)cellForObject:(id)object { - (NSInteger)numberOfSectionsInTableView:(UITableView*)theTableView { NSAssert(theTableView == self.tableView, @"numberOfSectionsInTableView: invoked with inappropriate tableView: %@", theTableView); RKLogTrace(@"numberOfSectionsInTableView: %d (%@)", [[_fetchedResultsController sections] count], [[_fetchedResultsController sections] valueForKey:@"name"]); - return [[_fetchedResultsController sections] count]; + return [[_fetchedResultsController sections] count]; } - (NSInteger)tableView:(UITableView*)theTableView numberOfRowsInSection:(NSInteger)section { @@ -382,43 +398,43 @@ - (NSString*)tableView:(UITableView*)theTableView titleForFooterInSection:(NSInt } - (NSArray*)sectionIndexTitlesForTableView:(UITableView*)theTableView { - if (theTableView.style == UITableViewStylePlain && self.showsSectionIndexTitles) { - return [_fetchedResultsController sectionIndexTitles]; - } - return nil; + if (theTableView.style == UITableViewStylePlain && self.showsSectionIndexTitles) { + return [_fetchedResultsController sectionIndexTitles]; + } + return nil; } - (NSInteger)tableView:(UITableView*)theTableView sectionForSectionIndexTitle:(NSString*)title atIndex:(NSInteger)index { - if (theTableView.style == UITableViewStylePlain && self.showsSectionIndexTitles) { - return [_fetchedResultsController sectionForSectionIndexTitle:title atIndex:index]; - } - return 0; + if (theTableView.style == UITableViewStylePlain && self.showsSectionIndexTitles) { + return [_fetchedResultsController sectionForSectionIndexTitle:title atIndex:index]; + } + return 0; } - (void)tableView:(UITableView*)theTableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { NSAssert(theTableView == self.tableView, @"tableView:commitEditingStyle:forRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); - if (self.canEditRows && editingStyle == UITableViewCellEditingStyleDelete) { + if (self.canEditRows && editingStyle == UITableViewCellEditingStyleDelete) { NSManagedObject* managedObject = [self objectForRowAtIndexPath:indexPath]; - RKObjectMapping* mapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[managedObject class]]; - if ([mapping isKindOfClass:[RKManagedObjectMapping class]]) { - RKManagedObjectMapping* managedObjectMapping = (RKManagedObjectMapping*)mapping; - NSString* primaryKeyAttribute = managedObjectMapping.primaryKeyAttribute; + RKObjectMapping* mapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[managedObject class]]; + if ([mapping isKindOfClass:[RKManagedObjectMapping class]]) { + RKManagedObjectMapping* managedObjectMapping = (RKManagedObjectMapping*)mapping; + NSString* primaryKeyAttribute = managedObjectMapping.primaryKeyAttribute; - if ([managedObject valueForKeyPath:primaryKeyAttribute]) { + if ([managedObject valueForKeyPath:primaryKeyAttribute]) { RKLogTrace(@"About to fire a delete request for managedObject: %@", managedObject); - [[RKObjectManager sharedManager] deleteObject:managedObject delegate:self]; - } else { + [[RKObjectManager sharedManager] deleteObject:managedObject delegate:self]; + } else { RKLogTrace(@"About to locally delete managedObject: %@", managedObject); - [managedObject.managedObjectContext deleteObject:managedObject]; + [managedObject.managedObjectContext deleteObject:managedObject]; NSError* error = nil; [managedObject.managedObjectContext save:&error]; if (error) { RKLogError(@"Failed to save managedObjectContext after a delete with error: %@", error); } - } - } - } + } + } + } } - (void)tableView:(UITableView*)theTableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destIndexPath { @@ -470,7 +486,7 @@ - (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath { return self.emptyItem; } else if ([self isHeaderIndexPath:indexPath]) { - NSUInteger row = (self.empty && self.emptyItem) ? (indexPath.row - 1) : indexPath.row; + NSUInteger row = ([self isEmpty] && self.emptyItem) ? (indexPath.row - 1) : indexPath.row; return [self.headerItems objectAtIndex:row]; } else if ([self isFooterIndexPath:indexPath]) { @@ -499,7 +515,7 @@ - (void)loadTableFromNetwork { #pragma mark - KVO & Model States -- (BOOL)isEmpty { +- (BOOL)isConsideredEmpty { NSUInteger fetchedObjectsCount = [[_fetchedResultsController fetchedObjects] count]; BOOL isEmpty = (fetchedObjectsCount == 0); RKLogTrace(@"Determined isEmpty = %@. fetchedObjects count = %d", isEmpty ? @"YES" : @"NO", fetchedObjectsCount); @@ -511,7 +527,7 @@ - (BOOL)isEmpty { - (void)controllerWillChangeContent:(NSFetchedResultsController*)controller { RKLogTrace(@"Beginning updates for fetchedResultsController (%@). Current section count = %d (resource path: %@)", controller, [[controller sections] count], _resourcePath); - if(_sortSelector) return; + if (_sortSelector) return; [self.tableView beginUpdates]; _isEmptyBeforeAnimation = [self isEmpty]; @@ -519,35 +535,43 @@ - (void)controllerWillChangeContent:(NSFetchedResultsController*)controller { - (void)controller:(NSFetchedResultsController*)controller didChangeSection:(id)sectionInfo - atIndex:(NSUInteger)sectionIndex + atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { - if(_sortSelector) return; + if (_sortSelector) return; switch (type) { - case NSFetchedResultsChangeInsert: + case NSFetchedResultsChangeInsert: [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; - break; + + if ([self.delegate respondsToSelector:@selector(tableController:didInsertSectionAtIndex:)]) { + [self.delegate tableController:self didInsertSectionAtIndex:sectionIndex]; + } + break; - case NSFetchedResultsChangeDelete: + case NSFetchedResultsChangeDelete: [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; - break; + + if ([self.delegate respondsToSelector:@selector(tableController:didDeleteSectionAtIndex:)]) { + [self.delegate tableController:self didDeleteSectionAtIndex:sectionIndex]; + } + break; - default: - RKLogTrace(@"Encountered unexpected section changeType: %d", type); - break; - } + default: + RKLogTrace(@"Encountered unexpected section changeType: %d", type); + break; + } } - (void)controller:(NSFetchedResultsController*)controller didChangeObject:(id)anObject - atIndexPath:(NSIndexPath *)indexPath + atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type - newIndexPath:(NSIndexPath *)newIndexPath { + newIndexPath:(NSIndexPath *)newIndexPath { - if(_sortSelector) return; + if (_sortSelector) return; NSIndexPath* adjIndexPath = [self indexPathForFetchedResultsIndexPath:indexPath]; NSIndexPath* adjNewIndexPath = [self indexPathForFetchedResultsIndexPath:newIndexPath]; @@ -578,10 +602,10 @@ - (void)controller:(NSFetchedResultsController*)controller withRowAnimation:UITableViewRowAnimationFade]; break; - default: - RKLogTrace(@"Encountered unexpected object changeType: %d", type); - break; - } + default: + RKLogTrace(@"Encountered unexpected object changeType: %d", type); + break; + } } - (void)controllerDidChangeContent:(NSFetchedResultsController*)controller { @@ -594,13 +618,27 @@ - (void)controllerDidChangeContent:(NSFetchedResultsController*)controller { [self updateSortedArray]; - if(_sortSelector) { + if (_sortSelector) { [self.tableView reloadData]; } else { [self.tableView endUpdates]; } - + [self didFinishLoad]; } +#pragma mark - UITableViewDataSource methods + +- (UITableViewCell *)tableView:(UITableView*)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + NSAssert(theTableView == self.tableView, @"tableView:cellForRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); + UITableViewCell* cell = [self cellForObjectAtIndexPath:indexPath]; + + RKLogTrace(@"%@ cellForRowAtIndexPath:%@ = %@", self, indexPath, cell); + return cell; +} + +- (NSUInteger)numberOfRowsInSection:(NSUInteger)index { + return [self tableView:self.tableView numberOfRowsInSection:index]; +} + @end diff --git a/Code/UI/RKObjectManager+RKTableController.h b/Code/UI/RKObjectManager+RKTableController.h index 621668cbff..c6d8ff43d9 100644 --- a/Code/UI/RKObjectManager+RKTableController.h +++ b/Code/UI/RKObjectManager+RKTableController.h @@ -20,7 +20,7 @@ /** Creates and returns a table controller object capable of loading remote object representations into a UITableView using the RestKit object mapping engine for a given table view controller. - + @param tableViewController A UITableViewController to instantiate a table controller for @return An RKTableController instance ready to drive the table view for the provided tableViewController. */ @@ -29,7 +29,7 @@ /** Creates and returns a table controller object capable of loading remote object representations into a UITableView using the RestKit object mapping engine for a given table view and view controller. - + @param tableView The UITableView object that table controller with acts as the delegate and data source for. @param viewController The UIViewController that owns the specified tableView. @return An RKTableController instance ready to drive the table view for the provided tableViewController. @@ -39,7 +39,7 @@ /** Creates and returns a fetched results table controller object capable of loading remote object representations stored in Core Data into a UITableView using the RestKit object mapping engine for a given table view controller. - + @param tableViewController A UITableViewController to instantiate a table controller for @return An RKFetchedResultsTableController instance ready to drive the table view for the provided tableViewController. */ @@ -48,7 +48,7 @@ /** Creates and returns a table controller object capable of loading remote object representations stored in Core Data into a UITableView using the RestKit object mapping engine for a given table view and view controller. - + @param tableView The UITableView object that table controller with acts as the delegate and data source for. @param viewController The UIViewController that owns the specified tableView. @return An RKFetchedResultsTableController instance ready to drive the table view for the provided tableViewController. diff --git a/Code/UI/RKTableController.h b/Code/UI/RKTableController.h index 8e4b9978f7..9cf8d8ad10 100644 --- a/Code/UI/RKTableController.h +++ b/Code/UI/RKTableController.h @@ -29,26 +29,32 @@ #import "RKObjectMapping.h" #import "RKObjectLoader.h" +@protocol RKTableControllerDelegate + +@optional + +- (void)tableController:(RKTableController *)tableController didLoadObjects:(NSArray *)objects inSection:(RKTableSection *)section; + +@end + @interface RKTableController : RKAbstractTableController -///////////////////////////////////////////////////////////////////////// +@property (nonatomic, assign) id delegate; + +///----------------------------------------------------------------------------- /// @name Static Tables -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- - (void)loadObjects:(NSArray *)objects; - (void)loadObjects:(NSArray *)objects inSection:(NSUInteger)sectionIndex; - (void)loadEmpty; -// Move to superclass??? -- (void)reloadRowForObject:(id)object withRowAnimation:(UITableViewRowAnimation)rowAnimation; - /** Load an array of RKTableItems into table cells of the specified class. A table cell mapping will be constructed on your behalf and yielded to the block for configuration. After the block is invoked, the objects will be loaded into the specified section. */ // TODO: Update comments... -- (void)loadTableItems:(NSArray *)tableItems withMappingBlock:(void (^)(RKTableViewCellMapping *))block; // TODO: Eliminate... - (void)loadTableItems:(NSArray *)tableItems withMapping:(RKTableViewCellMapping *)cellMapping; - (void)loadTableItems:(NSArray *)tableItems inSection:(NSUInteger)sectionIndex @@ -79,12 +85,16 @@ */ - (void)loadTableItems:(NSArray *)tableItems inSection:(NSUInteger)sectionIndex; +///----------------------------------------------------------------------------- /** @name Network Tables */ +///----------------------------------------------------------------------------- - (void)loadTableFromResourcePath:(NSString *)resourcePath; - (void)loadTableFromResourcePath:(NSString *)resourcePath usingBlock:(void (^)(RKObjectLoader *objectLoader))block; +///----------------------------------------------------------------------------- /** @name Forms */ +///----------------------------------------------------------------------------- /** The form that the table has been loaded with (if any) @@ -99,9 +109,36 @@ */ - (void)loadForm:(RKForm *)form; -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /// @name Managing Sections -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- + +@property (nonatomic, readonly) NSMutableArray *sections; + +/** + The key path on the loaded objects used to determine the section they belong to. + */ +@property(nonatomic, copy) NSString *sectionNameKeyPath; + +/** + Returns the section at the specified index. + @param index Must be less than the total number of sections. + */ +- (RKTableSection *)sectionAtIndex:(NSUInteger)index; + +/** + Returns the first section with the specified header title. + @param title The header title. + */ +- (RKTableSection *)sectionWithHeaderTitle:(NSString *)title; + +/** + Returns the index of the specified section. + + @param section Must be a valid non nil RKTableViewSection. + @return The index of the given section if contained within the receiver, otherwise NSNotFound. + */ +- (NSUInteger)indexForSection:(RKTableSection *)section; // Coalesces a series of table view updates performed within the block into // a single animation using beginUpdates: and endUpdates: on the table view @@ -109,27 +146,21 @@ - (void)updateTableViewUsingBlock:(void (^)())block; /** Adds a new section to the model. - * @param section Must be a valid non nil RKTableViewSection. */ + * @param section Must be a valid non nil RKTableViewSection. */ // NOTE: connects cellMappings if section.cellMappings is nil... - (void)addSection:(RKTableSection *)section; -/** - Creates an section and yields it to the block for configuration. After the block - is evaluated, the section is added to the table. - */ -- (void)addSectionUsingBlock:(void (^)(RKTableSection *section))block; - /** Inserts a new section at the specified index. - * @param section Must be a valid non nil RKTableViewSection. - * @param index Must be less than the total number of sections. */ + * @param section Must be a valid non nil RKTableViewSection. + * @param index Must be less than the total number of sections. */ - (void)insertSection:(RKTableSection *)section atIndex:(NSUInteger)index; /** Removes the specified section from the model. - * @param section The section to remove. */ + * @param section The section to remove. */ - (void)removeSection:(RKTableSection *)section; /** Removes the section at the specified index from the model. - * @param index Must be less than the total number of section. */ + * @param index Must be less than the total number of section. */ - (void)removeSectionAtIndex:(NSUInteger)index; /** Removes all sections from the model. */ diff --git a/Code/UI/RKTableController.m b/Code/UI/RKTableController.m index 3653ef11f4..eef6248f4f 100644 --- a/Code/UI/RKTableController.m +++ b/Code/UI/RKTableController.m @@ -22,26 +22,36 @@ #import "RKAbstractTableController_Internals.h" #import "RKLog.h" #import "RKFormSection.h" +#import "NSArray+RKAdditions.h" +#import "RKObjectMappingOperation.h" // Define logging component #undef RKLogComponent #define RKLogComponent lcl_cRestKitUI +@interface RKTableController () +@property (nonatomic, readwrite) NSMutableArray *sections; +@end + @implementation RKTableController +@dynamic delegate; @synthesize form = _form; +@synthesize sectionNameKeyPath = _sectionNameKeyPath; +@synthesize sections = _sections; #pragma mark - Instantiation - (id)init { self = [super init]; if (self) { + _sections = [NSMutableArray new]; [self addObserver:self forKeyPath:@"sections" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; - RKTableSection* section = [RKTableSection section]; + RKTableSection *section = [RKTableSection section]; [self addSection:section]; } @@ -51,7 +61,9 @@ - (id)init { - (void)dealloc { [self removeObserver:self forKeyPath:@"sections"]; [_form release]; - + [_sectionNameKeyPath release]; + [_sections release]; + [super dealloc]; } @@ -74,7 +86,7 @@ - (void)removeSectionsAtIndexes:(NSIndexSet *)indexes { [self.sections removeObjectsAtIndexes:indexes]; } -- (void)replaceObjectsAtIndexes:(NSIndexSet *)indexes withObjects:(NSArray *)objects { +- (void)replaceSectionsAtIndexes:(NSIndexSet *)indexes withObjects:(NSArray *)objects { [self.sections replaceObjectsAtIndexes:indexes withObjects:objects]; } @@ -86,15 +98,6 @@ - (void)addSection:(RKTableSection *)section { } [[self sectionsProxy] addObject:section]; - - // TODO: move into KVO? - if ([self.delegate respondsToSelector:@selector(tableController:didInsertSection:atIndex:)]) { - [self.delegate tableController:self didInsertSection:section atIndex:[self.sections indexOfObject:section]]; - } -} - -- (void)addSectionUsingBlock:(void (^)(RKTableSection *section))block { - [self addSection:[RKTableSection sectionUsingBlock:block]]; } - (void)removeSection:(RKTableSection *)section { @@ -104,10 +107,6 @@ - (void)removeSection:(RKTableSection *)section { reason:@"Tables must always have at least one section" userInfo:nil]; } - NSUInteger index = [self.sections indexOfObject:section]; - if ([self.delegate respondsToSelector:@selector(tableController:didRemoveSection:atIndex:)]) { - [self.delegate tableController:self didRemoveSection:section atIndex:index]; - } [[self sectionsProxy] removeObject:section]; } @@ -115,10 +114,6 @@ - (void)insertSection:(RKTableSection *)section atIndex:(NSUInteger)index { NSAssert(section, @"Cannot insert a nil section"); section.tableController = self; [[self sectionsProxy] insertObject:section atIndex:index]; - - if ([self.delegate respondsToSelector:@selector(tableController:didInsertSection:atIndex:)]) { - [self.delegate tableController:self didInsertSection:section atIndex:index]; - } } - (void)removeSectionAtIndex:(NSUInteger)index { @@ -127,21 +122,10 @@ - (void)removeSectionAtIndex:(NSUInteger)index { reason:@"Tables must always have at least one section" userInfo:nil]; } - RKTableSection* section = [self.sections objectAtIndex:index]; - if ([self.delegate respondsToSelector:@selector(tableController:didRemoveSection:atIndex:)]) { - [self.delegate tableController:self didRemoveSection:section atIndex:index]; - } [[self sectionsProxy] removeObjectAtIndex:index]; } - (void)removeAllSections:(BOOL)recreateFirstSection { - NSUInteger sectionCount = [self.sections count]; - for (NSUInteger index = 0; index < sectionCount; index++) { - RKTableSection* section = [self.sections objectAtIndex:index]; - if ([self.delegate respondsToSelector:@selector(tableController:didRemoveSection:atIndex:)]) { - [self.delegate tableController:self didRemoveSection:section atIndex:index]; - } - } [[self sectionsProxy] removeAllObjects]; if (recreateFirstSection) { @@ -179,7 +163,7 @@ - (NSArray*)objectsWithHeaderAndFooters:(NSArray *)objects forSection:(NSUIntege return [mutableObjects autorelease]; } -// TODO: NOTE - Everything currently needs to pass through this method to pick up header/footer rows... +// NOTE - Everything currently needs to pass through this method to pick up header/footer rows... - (void)loadObjects:(NSArray *)objects inSection:(NSUInteger)sectionIndex { // Clear any existing error state from the table self.error = nil; @@ -195,11 +179,16 @@ - (void)loadObjects:(NSArray *)objects inSection:(NSUInteger)sectionIndex { } [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:self.defaultRowAnimation]; - - // TODO: We should probably not be making this call in cases where we were - // loaded via a network API, as we are duplicating cleanup effort that - // already exists across our RKRequestDelegate & RKObjectLoaderDelegate methods - [self didFinishLoad]; + + if ([self.delegate respondsToSelector:@selector(tableController:didLoadObjects:inSection:)]) { + [self.delegate tableController:self didLoadObjects:objects inSection:section]; + } + + // The load is finalized via network callbacks for + // dynamic table controllers + if (nil == self.objectLoader) { + [self didFinishLoad]; + } } - (void)loadObjects:(NSArray *)objects { @@ -241,10 +230,6 @@ - (void)loadTableItems:(NSArray *)tableItems { [self loadTableItems:tableItems inSection:0]; } -- (void)loadTableItems:(NSArray *)tableItems withMappingBlock:(void (^)(RKTableViewCellMapping*))block { - [self loadTableItems:tableItems inSection:0 withMapping:[RKTableViewCellMapping cellMappingUsingBlock:block]]; -} - #pragma mark - Network Table Loading - (void)loadTableFromResourcePath:(NSString*)resourcePath { @@ -275,12 +260,6 @@ - (void)loadForm:(RKForm *)form { [self addSection:(RKTableSection *)section]; } - // TODO: How to handle animating loading a replacement form? -// if (self.loaded) { -// NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [form.sections count] - 1)]; -// [self.tableView reloadSections:indexSet withRowAnimation:self.defaultRowAnimation]; -// } - [self didFinishLoad]; [form didLoadInTableController:self]; } @@ -331,18 +310,22 @@ - (void)tableView:(UITableView*)theTableView moveRowAtIndexPath:(NSIndexPath *)s - (void)objectLoader:(RKObjectLoader *)loader didLoadObjects:(NSArray *)objects { // TODO: Could not get the KVO to work without a boolean property... - // TODO: Need to figure out how to group the objects into sections // TODO: Apply any sorting... - // Load them into the first section for now - [self loadObjects:objects inSection:0]; -} - -- (void)reloadRowForObject:(id)object withRowAnimation:(UITableViewRowAnimation)rowAnimation { - // TODO: Find the indexPath of the object... - NSIndexPath *indexPath = [self indexPathForObject:object]; - if (indexPath) { - [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:rowAnimation]; + if (self.sectionNameKeyPath) { + NSArray *sectionedObjects = [objects sectionsGroupedByKeyPath:self.sectionNameKeyPath]; + if ([sectionedObjects count] == 0) { + [self removeAllSections]; + } + for (NSArray *sectionOfObjects in sectionedObjects) { + NSUInteger sectionIndex = [sectionedObjects indexOfObject:sectionOfObjects]; + if (sectionIndex >= [self sectionCount]) { + [self addSection:[RKTableSection section]]; + } + [self loadObjects:sectionOfObjects inSection:sectionIndex]; + } + } else { + [self loadObjects:objects inSection:0]; } } @@ -378,4 +361,153 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N // TODO: KVO should be used for managing the row level manipulations on the table view as well... } +#pragma mark - Managing Sections + +- (NSUInteger)sectionCount { + return [_sections count]; +} + +- (NSUInteger)rowCount { + return [[_sections valueForKeyPath:@"@sum.rowCount"] intValue]; +} + +- (RKTableSection *)sectionAtIndex:(NSUInteger)index { + return [_sections objectAtIndex:index]; +} + +- (NSUInteger)indexForSection:(RKTableSection *)section { + NSAssert(section, @"Cannot return index for a nil section"); + return [_sections indexOfObject:section]; +} + +- (RKTableSection *)sectionWithHeaderTitle:(NSString *)title { + for (RKTableSection* section in _sections) { + if ([section.headerTitle isEqualToString:title]) { + return section; + } + } + + return nil; +} + +- (NSUInteger)numberOfRowsInSection:(NSUInteger)index { + return [self sectionAtIndex:index].rowCount; +} + +- (UITableViewCell *)cellForObjectAtIndexPath:(NSIndexPath *)indexPath { + RKTableSection* section = [self sectionAtIndex:indexPath.section]; + id mappableObject = [section objectAtIndex:indexPath.row]; + RKTableViewCellMapping* cellMapping = [self.cellMappings cellMappingForObject:mappableObject]; + NSAssert(cellMapping, @"Cannot build a tableView cell for object %@: No cell mapping defined for objects of type '%@'", mappableObject, NSStringFromClass([mappableObject class])); + + UITableViewCell* cell = [cellMapping mappableObjectForData:self.tableView]; + NSAssert(cell, @"Cell mapping failed to dequeue or allocate a tableViewCell for object: %@", mappableObject); + + // Map the object state into the cell + RKObjectMappingOperation* mappingOperation = [[RKObjectMappingOperation alloc] initWithSourceObject:mappableObject destinationObject:cell mapping:cellMapping]; + NSError* error = nil; + BOOL success = [mappingOperation performMapping:&error]; + [mappingOperation release]; + // NOTE: If there is no mapping work performed, but no error is generated then + // we consider the operation a success. It is common for table cells to not contain + // any dynamically mappable content (i.e. header/footer rows, banners, etc.) + if (success == NO && error != nil) { + RKLogError(@"Failed to generate table cell for object: %@", error); + return nil; + } + + return cell; +} + +#pragma mark - Cell Mappings + +- (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath { + NSAssert(indexPath, @"Cannot lookup object with a nil indexPath"); + RKTableSection* section = [self sectionAtIndex:indexPath.section]; + return [section objectAtIndex:indexPath.row]; +} + +#pragma mark - UITableViewDataSource methods + +- (NSString*)tableView:(UITableView*)theTableView titleForHeaderInSection:(NSInteger)section { + NSAssert(theTableView == self.tableView, @"tableView:titleForHeaderInSection: invoked with inappropriate tableView: %@", theTableView); + return [[_sections objectAtIndex:section] headerTitle]; +} + +- (NSString*)tableView:(UITableView*)theTableView titleForFooterInSection:(NSInteger)section { + NSAssert(theTableView == self.tableView, @"tableView:titleForFooterInSection: invoked with inappropriate tableView: %@", theTableView); + return [[_sections objectAtIndex:section] footerTitle]; +} + +- (BOOL)tableView:(UITableView*)theTableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { + NSAssert(theTableView == self.tableView, @"tableView:canEditRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); + return self.canEditRows; +} + +- (BOOL)tableView:(UITableView*)theTableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { + NSAssert(theTableView == self.tableView, @"tableView:canMoveRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); + return self.canMoveRows; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView*)theTableView { + NSAssert(theTableView == self.tableView, @"numberOfSectionsInTableView: invoked with inappropriate tableView: %@", theTableView); + RKLogTrace(@"%@ numberOfSectionsInTableView = %d", self, self.sectionCount); + return self.sectionCount; +} + +- (NSInteger)tableView:(UITableView*)theTableView numberOfRowsInSection:(NSInteger)section { + NSAssert(theTableView == self.tableView, @"tableView:numberOfRowsInSection: invoked with inappropriate tableView: %@", theTableView); + RKLogTrace(@"%@ numberOfRowsInSection:%d = %d", self, section, self.sectionCount); + return [[_sections objectAtIndex:section] rowCount]; +} + +- (NSIndexPath *)indexPathForObject:(id)object { + NSUInteger sectionIndex = 0; + for (RKTableSection *section in self.sections) { + NSUInteger rowIndex = 0; + for (id rowObject in section.objects) { + if ([rowObject isEqual:object]) { + return [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]; + } + + rowIndex++; + } + sectionIndex++; + } + + return nil; +} + +- (CGFloat)tableView:(UITableView *)theTableView heightForHeaderInSection:(NSInteger)sectionIndex { + NSAssert(theTableView == self.tableView, @"heightForHeaderInSection: invoked with inappropriate tableView: %@", theTableView); + RKTableSection *section = [self sectionAtIndex:sectionIndex]; + return section.headerHeight; +} + +- (CGFloat)tableView:(UITableView *)theTableView heightForFooterInSection:(NSInteger)sectionIndex { + NSAssert(theTableView == self.tableView, @"heightForFooterInSection: invoked with inappropriate tableView: %@", theTableView); + RKTableSection *section = [self sectionAtIndex:sectionIndex]; + return section.footerHeight; +} + +- (UIView *)tableView:(UITableView *)theTableView viewForHeaderInSection:(NSInteger)sectionIndex { + NSAssert(theTableView == self.tableView, @"viewForHeaderInSection: invoked with inappropriate tableView: %@", theTableView); + RKTableSection *section = [self sectionAtIndex:sectionIndex]; + return section.headerView; +} + +- (UIView *)tableView:(UITableView *)theTableView viewForFooterInSection:(NSInteger)sectionIndex { + NSAssert(theTableView == self.tableView, @"viewForFooterInSection: invoked with inappropriate tableView: %@", theTableView); + RKTableSection *section = [self sectionAtIndex:sectionIndex]; + return section.footerView; +} + +- (BOOL)isConsideredEmpty { + NSUInteger nonRowItemsCount = [self.headerItems count] + [self.footerItems count]; + nonRowItemsCount += self.emptyItem ? 1 : 0; + BOOL isEmpty = (self.rowCount - nonRowItemsCount) == 0; + RKLogTrace(@"Determined isConsideredEmpty = %@. self.rowCount = %d with %d nonRowItems in the table", isEmpty ? @"YES" : @"NO", self.rowCount, nonRowItemsCount); + return isEmpty; +} + @end diff --git a/Code/UI/RKTableItem.m b/Code/UI/RKTableItem.m index 56eacb0f54..63ed6f2591 100644 --- a/Code/UI/RKTableItem.m +++ b/Code/UI/RKTableItem.m @@ -33,7 +33,7 @@ @implementation RKTableItem + (NSArray*)tableItemsFromStrings:(NSString*)firstString, ... { va_list args; va_start(args, firstString); - NSMutableArray* tableItems = [NSMutableArray array]; + NSMutableArray* tableItems = [NSMutableArray array]; for (NSString* string = firstString; string != nil; string = va_arg(args, NSString*)) { RKTableItem* tableItem = [RKTableItem new]; tableItem.text = string; diff --git a/Code/UI/RKTableViewCellMapping.h b/Code/UI/RKTableViewCellMapping.h index 4ea24a2ccb..573d08239f 100644 --- a/Code/UI/RKTableViewCellMapping.h +++ b/Code/UI/RKTableViewCellMapping.h @@ -86,9 +86,9 @@ typedef void(^RKTableViewCellBlock)(UITableViewCell *cell); A Boolean value that determines whether the cell mapping manages basic cell attributes (accessoryType, selectionStyle, etc.) or defers to a Storyboard/XIB for defining basic cell attributes. - + Setting the accessoryType or selectionStyle will set the value to YES. - + **Default**: NO */ @property (nonatomic, assign) BOOL managesCellAttributes; @@ -203,7 +203,7 @@ typedef void(^RKTableViewCellBlock)(UITableViewCell *cell); /** Creates and returns an RKTableCellMapping instance configured with the default cell mappings. - + @return An RKTableCellMapping instance with default mappings applied. @see [RKTableCellMapping addDefaultMappings] */ diff --git a/Code/UI/RKTableViewCellMapping.m b/Code/UI/RKTableViewCellMapping.m index 9b1ea18873..dd579c4162 100644 --- a/Code/UI/RKTableViewCellMapping.m +++ b/Code/UI/RKTableViewCellMapping.m @@ -175,7 +175,9 @@ - (id)copyWithZone:(NSZone *)zone { @synchronized(_prepareCellBlocks) { for (void (^block)(UITableViewCell *) in _prepareCellBlocks) { - [copy addPrepareCellBlock:[block copy]]; + void (^blockCopy)(UITableViewCell *cell) = [block copy]; + [copy addPrepareCellBlock:blockCopy]; + [blockCopy release]; } } @@ -191,7 +193,7 @@ - (id)mappableObjectForData:(UITableView *)tableView { cell = [[[self.objectClass alloc] initWithStyle:self.style reuseIdentifier:self.reuseIdentifier] autorelease]; } - + if (self.managesCellAttributes) { cell.accessoryType = self.accessoryType; cell.selectionStyle = self.selectionStyle; @@ -243,7 +245,9 @@ - (NSString *)reuseIdentifier { #pragma mark - Control Action Helpers - (void)addPrepareCellBlock:(void (^)(UITableViewCell *cell))block { - [_prepareCellBlocks addObject:[block copy]]; + void (^blockCopy)(UITableViewCell *cell) = [block copy]; + [_prepareCellBlocks addObject:blockCopy]; + [blockCopy release]; } - (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents toControlAtKeyPath:(NSString *)keyPath { diff --git a/Docs/Images/Installation/03_Add_Header_Search_Path.png b/Docs/Images/Installation/03_Add_Header_Search_Path.png index bf9f3abfcb..720defb4e8 100644 Binary files a/Docs/Images/Installation/03_Add_Header_Search_Path.png and b/Docs/Images/Installation/03_Add_Header_Search_Path.png differ diff --git a/Docs/Images/Installation/09_Add_To_AppDelegate.png b/Docs/Images/Installation/09_Add_To_AppDelegate.png index 05d968b4bc..3aece1c8d7 100644 Binary files a/Docs/Images/Installation/09_Add_To_AppDelegate.png and b/Docs/Images/Installation/09_Add_To_AppDelegate.png differ diff --git a/Docs/Object Mapping.md b/Docs/Object Mapping.md index a5bd9f270f..333bd29382 100644 --- a/Docs/Object Mapping.md +++ b/Docs/Object Mapping.md @@ -737,7 +737,7 @@ Let's take a look at how you can leverage key-value validation to perform the ab @implementation Article - (BOOL)validateTitle:(id *)ioValue error:(NSError **)outError { // Force the title to uppercase - *iovalue = [(NSString*)iovalue uppercaseString]; + *ioValue = [(NSString*)ioValue uppercaseString]; return YES; } diff --git a/Docs/TESTING.md b/Docs/TESTING.md index fb191d5c30..20d8e378fc 100644 --- a/Docs/TESTING.md +++ b/Docs/TESTING.md @@ -3,24 +3,62 @@ RestKit Test Environment RestKit ships with a testing infrastructure built around OCUnit and a Ruby testing server environment built on Sinatra. To be able to run the -tests, you need to do a little bit of setup: +tests, you need to do a little bit of setup. These instructions are value for **Xcode version 4.3 and higher**. +1. Install the Xcode **Command Line Tools** by selecting the **Xcode** > **Preferences…** menu and then navigating to the **Downloads** tab, then clicking the **Install** button next to the appropriate entry in the table. +2. After installation completes, ensure your command line Xcode installation is configured by executing `xcode-select -print-path`. If no path is returned, configure xcode-select by executing `xcode-select -switch /Applications/Xcode.app/Contents/Developer`. +1. Ensure that you have **Ruby 1.9.2** available. We recommend installation via [RVM](http://beginrescueend.com/rvm/install/) or [Homebrew](http://mxcl.github.com/homebrew/). 1. Install the Ruby Bundler Gem (if necessary): `gem install bundler` 1. Install the other required Gems via Bundler: `bundle` -1. Start the spec server: `rake spec:server` -1. Build and execute the tests within Xcode via the **Product** > **Test** +1. Start the Test server: `rake server` +1. Build and execute the tests within Xcode via the **Product** > **Test** menu or on the command line via `rake`. -If the project builds the Specs target correctly and executes the suite, then -you are all set. If there are any issues, you may need to reach out to the mailing -list for help debugging. +If the project builds the RestKitTests target correctly and executes the suite, then +you are all set. If there are any issues, you may need to reach out to us via a Github issue for help debugging. Running Tests ------------- +### Within Xcode + By default, all tests will be executed when your run the **Test** build action. You can selectively enable/disable which tests are run by holding down the Option Key when selecting **Product** > **Test** (or type Apple+Option+U). +### On the Command Line + +RestKit includes full support for executing the test suite via the commandline via the excellent [Xcoder](https://github.com/rayh/xcoder) gem. The suite can be run in its entirety or as individual pieces targetting subsets of the suite. Test execution is performed via the `rake` tool. A list of the available test tasks as of this writing (obtained via `rake -T test`) follows: + + rake test # Run all the GateGuru tests + rake test:all # Run all tests for iOS and OS X + rake test:application # Run the application tests for iOS + rake test:application:ios # Run the application tests for iOS + rake test:logic # Run the unit tests for iOS and OS X + rake test:logic:ios # Run the logic tests for iOS + rake test:logic:osx # Run the logic tests for OS X + +Rake is also used for a number of other automation tasks in the project. Consult the full list of tasks via `rake -T` for more info. + +Test Server +------------- + +RestKit includes a [Sinatra](http://www.sinatrarb.com/) powered test server that is required to exercise the majority of the HTTP specific functionality within the library. Execution of the test server is handled via a rich library of Rake tasks provided by the [RestKit Gem](https://github.com/RestKit/RestKit-Gem). + +The server can be run interactively or daemonized into a background process. Tasks are provided for stopping, starting, restarting, tailing the logs of a backgrounded process, and for automatically starting and stopping the server via Rake task dependencies. A list of the available server tasks as of this writing (as obtained via `rake -T server`) follows: + + rake server # Run the Test server in the foreground + rake server:abort_unless_running # Abort the task chain unless the Test server is running + rake server:autostart # Starts the server if there is not already an instance running + rake server:autostop # Stops the server if executed via autostart + rake server:logs # Dumps the last 25 lines from the Test server logs + rake server:logs:tail # Tails the Test server logs + rake server:restart # Restart the Test server daemon + rake server:start # Start the Test server daemon + rake server:status # Check the status of the Test server daemon + rake server:stop # Stop the Test server daemon + +The tasks are reusable via the RestKit gem and can be used to provide a test server for applications using RestKit as well. Details about configuring the RestKit gem to quickly build an application specific test server are available on the [RestKit Gem Github Page](https://github.com/RestKit/RestKit-Gem). An example application leveraging the test server is provided in the [RKGithub](https://github.com/RestKit/RKGithub) application. + Writing Tests ------------- @@ -28,65 +66,49 @@ RestKit tests are divided into two portions. There are pure unit tests, which on configured and there are integration tests that test the full request/response life-cycle. In general, testing RestKit is very straight-forward. There are only a few items to keep in mind: 1. Tests are implemented in Objective-C and run inside the Simulator or on the Device. -1. Test files live in sub-directories under Specs/ appropriate to the layer the code under test belongs to +1. Test files live in sub-directories under Tests/ appropriate to the layer the code under test belongs to 1. Tests begin with "test" and should be camel-cased descriptive. i.e. testShouldConsiderA200ResponseSuccessful 1. Expectations are provided using OCHamcrest. Details of the matchers are available on the [OCHamcrest Github Page](http://jonreid.github.com/OCHamcrest/). Generally the matchers are of the form: assertThat([someObject someMethod], is(equalTo(@"some value"))); There is a corresponding `isNot` method available as well. -1. The RKSpecEnvironment.h header includes a number of helpers for initializing and configuring a clean testing environment. +1. The RKTestEnvironment.h header includes a number of helpers for initializing and configuring a clean testing environment. 1. OCMock is available for mock objects support. See [http://www.mulle-kybernetik.com/software/OCMock/](http://www.mulle-kybernetik.com/software/OCMock/) for details 1. RestKit is available for 32bit (iOS) and 64bit (OS X) platforms. This introduces some complexity when working with integer data types as NSInteger and NSUInteger are int's on 32bit and long's on 64bit. Cocoa and OC Hamcrest provide helper methods for dealing with these differences. Rather than using the **Int** flavor of methods (i.e. `[NSNumber numberWithInt:3]`) use the **Integer** flavor (i.e. `[NSNumber numberWithInteger:]`). This will account for the type differences without generating warnings or requiring casting. +### RestKit Testing Classes + +RestKit includes a number of testing specific classes as part of the library that are used within the test suite and are also available for testing applications leveraging RestKit. This functionality is covered in detail in the [Unit Testing with RestKit](https://github.com/RestKit/RestKit/wiki/Unit-Testing-with-RestKit) article on the Github site. + ### Writing Integration Tests RestKit ships with a Sinatra powered specs server for testing portions of the codebase that require interaction with a web service. Sinatra is a simple Ruby DSL for defining web server. See the [Sinatra homepage](http://www.sinatrarb.com/) for more details. -The specs server is built as a set of modular Sinatra application in the Specs/Server subdirectory of the RestKit -distribution. When you are adding new integration test coverage to the library, you will need to create a new Sinatra application -to serve your needs. By convention, these are namespaced by functional unit for simplicity. For example, if we are adding a new -cacheing component to the application and want to test the functionality, we would: - -1. Create a new file at `Server/lib/restkit/network/cacheing.rb` -1. Create a namespaced Ruby class inheriting from Sinatra::Base to suit our purposes: - - module RestKit - module Network - class Cacheing < Sinatra::Base - get '/cacheing/index' do - "OK" - end - end - end - end -1. Open restkit.rb and add a require for the cacheing server: - - require 'restkit/network/cacheing' -1. Open server.rb and add a use directive to the main spec server to import our module: +The Test server is built as a modular Sinatra application in the Tests/Server subdirectory of the RestKit distribution. When you are adding new integration test coverage to the library, you may need to add new routes to Sinatra to serve your needs. By convention, these are namespaced by functional unit for simplicity. For example, if we are adding a new +cacheing component to the application and want to test the functionality, we might add a new route to the Test server at Tests/Server/server.rb like so: - class RestKit::SpecServer < Sinatra::Base - self.app_file = __FILE__ - use RestKit::Network::Authentication - use RestKit::Network::Cacheing + get '/cacheing/index' do + "OK" + end You now have a functional server-side component to work with. Consult the Sinatra documentation for example on setting response headers, MIME types, etc. It's all very simple and low ceremony. -You can now switch to the RestKit sources and look the in Specs directory. Keeping with the cacheing example, we would create a new RKCacheingSpec.m file and pull in RKSpecEnvironment.h. From there we can utilize `RKSpecResponseLoader` to asynchronously test +You can now switch to the RestKit sources and look in the Tests directory. Keeping with the cacheing example, we would create a new RKCacheingTest.m file and pull in RKSTestEnvironment.h. From there we can utilize `RKTestResponseLoader` to asynchronously test the entire request/response cycle. The response loader essentially spins the run-loop to allow background processing to execute and simulate a blocking API. The response, objects, or errors generated by processing the response are made available via properties -on the RKSpecResponseLoader object. +on the RKTestResponseLoader object. Let's take a look at an example of how to use the response loader to test some functionality: - (void)testShouldFailAuthenticationWithInvalidCredentialsForHTTPAuthBasic { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - RKClient* client = [RKClient clientWithBaseURL:RKSpecGetBaseURL()]; - client.username = RKAuthenticationSpecUsername; + RKSpecResponseLoader *loader = [RKTestResponseLoader responseLoader]; + RKClient *client = [RKTestFactory client]; + client.username = RKAuthenticationTestUsername; client.password = @"INVALID"; [client get:@"/authentication/basic" delegate:loader]; [loader waitForResponse]; @@ -95,6 +117,29 @@ Let's take a look at an example of how to use the response loader to test some f assertThatInt([loader.failureError code], is(equalToInt(NSURLErrorUserCancelledAuthentication))); } -That's really all there is to it. Consult the existing test code in Specs/ for reference. +That's really all there is to it. Consult the existing test code in Tests/ for reference. + + +Continuous Integration +------------- -Happy Testing! +The RestKit team keeps the master, development, and active branches of RestKit under the watchful eye of the [Jenkins Continuous Integration Server](http://jenkins-ci.org/). There is a fair amount of complexity involved in getting iOS projects running under Jenkins, so to make things as easy as possible all Jenkins configuration has been collected into a single script within the source code. Currently use of the Jenkins build **requires** the use of RVM for managing the Ruby environment. + +To configure Jenkins to build RestKit, do the following: + +1. Ensure the RestKit test suite executes cleanly on the CI host using the above reference. +1. Install Jenkins (again, we recommend [Homebrew](http://mxcl.github.com/)): `brew install jenkins` +2. Install Jenkins as a system service. Instructions are printed post installation via Homebrew +3. Configure your CI user's OS X account to automatically manage the RVM environment. Create an `~/.rvmrc` file and populate it with the following: +```bash +export rvm_install_on_use_flag=1 +export rvm_gemset_create_on_use_flag=1 +export rvm_trust_rvmrcs_flag=1 +export rvm_always_trust_rvmrc_flag=1 +``` +4. Install the Git plugin for Jenkins and configure it for the fork you are tracking. +5. Create a Jenkins project for building RestKit within the Jenkins UI. +6. Add an **Execute shell** build step to the Jenkins project with a Command value of: `bash -x ./Tests/cibuild` +7. Save the project and tap the **Build Now** button to force Jenkins to build the project. + +When the RestKit build is invoked, the cibuild script will leverage Bundler to bundle all the required Ruby gems and then start up an instance of the test server on port 4567, execute all the tests, then shut down the server. diff --git a/Examples/RKCatalog/App/RKCatalogAppDelegate.m b/Examples/RKCatalog/App/RKCatalogAppDelegate.m index 00e7116d4f..2a327342e0 100644 --- a/Examples/RKCatalog/App/RKCatalogAppDelegate.m +++ b/Examples/RKCatalog/App/RKCatalogAppDelegate.m @@ -21,10 +21,10 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( // Add the navigation controller's view to the window and display. self.window.rootViewController = self.navigationController; [self.window makeKeyAndVisible]; - - + + gRKCatalogBaseURL = [[NSURL alloc] initWithString:@"http://rkcatalog.heroku.com"]; - + return YES; } diff --git a/Examples/RKCatalog/App/RootViewController.m b/Examples/RKCatalog/App/RootViewController.m index acfa75aa79..33ee604f67 100644 --- a/Examples/RKCatalog/App/RootViewController.m +++ b/Examples/RKCatalog/App/RootViewController.m @@ -13,7 +13,7 @@ @implementation RootViewController - (void)viewDidLoad { [super viewDidLoad]; - + _exampleTableItems = [[NSArray alloc] initWithObjects: @"RKAuthenticationExample", @"RKParamsExample", @@ -28,7 +28,7 @@ - (void)viewDidLoad { - (void)dealloc { [_exampleTableItems release]; - + [super dealloc]; } @@ -42,7 +42,7 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *cellIdentifier = @"RKCatalogCellIdentifier"; - + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease]; @@ -51,8 +51,8 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N } NSString* exampleName = [_exampleTableItems objectAtIndex:indexPath.row]; - cell.textLabel.text = exampleName; - + cell.textLabel.text = exampleName; + return cell; } @@ -60,7 +60,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath // Clear the singleton instances to isolate the examples [RKClient setSharedClient:nil]; [RKObjectManager setSharedManager:nil]; - + NSString* exampleName = [_exampleTableItems objectAtIndex:indexPath.row]; Class exampleClass = NSClassFromString(exampleName); UIViewController* exampleController = [[exampleClass alloc] initWithNibName:exampleName bundle:nil]; @@ -71,7 +71,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } [exampleController release]; } - + [tableView deselectRowAtIndexPath:indexPath animated:YES]; } diff --git a/Examples/RKCatalog/Examples/RKAuthenticationExample/RKAuthenticationExample.m b/Examples/RKCatalog/Examples/RKAuthenticationExample/RKAuthenticationExample.m index caf4e8f410..cde8d1a74d 100644 --- a/Examples/RKCatalog/Examples/RKAuthenticationExample/RKAuthenticationExample.m +++ b/Examples/RKCatalog/Examples/RKAuthenticationExample/RKAuthenticationExample.m @@ -30,7 +30,7 @@ - (void)dealloc { [authenticatedRequest cancel]; [authenticatedRequest release]; authenticatedRequest = nil; - + [super dealloc]; } @@ -46,7 +46,7 @@ - (void)sendRequest { newRequest.authenticationType = RKRequestAuthenticationTypeHTTP; newRequest.username = [usernameTextField text]; newRequest.password = [passwordTextField text]; - + self.authenticatedRequest = newRequest; } diff --git a/Examples/RKCatalog/Examples/RKBackgroundRequestExample/RKBackgroundRequestExample.m b/Examples/RKCatalog/Examples/RKBackgroundRequestExample/RKBackgroundRequestExample.m index 970fe0505f..2c89b017e3 100644 --- a/Examples/RKCatalog/Examples/RKBackgroundRequestExample/RKBackgroundRequestExample.m +++ b/Examples/RKCatalog/Examples/RKBackgroundRequestExample/RKBackgroundRequestExample.m @@ -21,13 +21,13 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil RKClient* client = [RKClient clientWithBaseURL:gRKCatalogBaseURL]; [RKClient setSharedClient:client]; } - + return self; } - (void)dealloc { [[RKClient sharedClient].requestQueue cancelRequestsWithDelegate:self]; - + [super dealloc]; } @@ -40,7 +40,7 @@ - (IBAction)sendRequest { } - (void)requestDidStartLoad:(RKRequest *)request { - _statusLabel.text = [NSString stringWithFormat:@"Sent request with background policy %d at %@", request.backgroundPolicy, [NSDate date]]; + _statusLabel.text = [NSString stringWithFormat:@"Sent request with background policy %d at %@", request.backgroundPolicy, [NSDate date]]; } - (void)requestDidTimeout:(RKRequest *)request { diff --git a/Examples/RKCatalog/Examples/RKCoreDataExample/RKCoreDataExample.m b/Examples/RKCatalog/Examples/RKCoreDataExample/RKCoreDataExample.m index b4e5cb142b..5bdb5e1173 100644 --- a/Examples/RKCatalog/Examples/RKCoreDataExample/RKCoreDataExample.m +++ b/Examples/RKCatalog/Examples/RKCoreDataExample/RKCoreDataExample.m @@ -37,7 +37,7 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil RKObjectManager* manager = [RKObjectManager managerWithBaseURLString:@"http://restkit.org"]; manager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"RKCoreDataExample.sqlite"]; [RKObjectManager setSharedManager:manager]; - + // Create some starter objects if the database is empty if ([Article count:nil] == 0) { for (int i = 1; i <= 5; i++) { @@ -45,12 +45,12 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil article.articleID = [NSNumber numberWithInt:i]; article.title = [NSString stringWithFormat:@"Article %d", i]; article.body = @"This is the body"; - + // Persist the object store [manager.objectStore save:nil]; } } - + NSArray* items = [NSArray arrayWithObjects:@"All", @"Sorted", @"By Predicate", @"By ID", nil]; _segmentedControl = [[UISegmentedControl alloc] initWithItems:items]; _segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar; @@ -58,7 +58,7 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil [_segmentedControl addTarget:self action:@selector(updateTableView) forControlEvents:UIControlEventValueChanged]; _segmentedControl.selectedSegmentIndex = 0; } - + return self; } @@ -72,45 +72,45 @@ - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSIntege return 35; } -- (UIView*)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { +- (UIView*)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { return _segmentedControl; } - (NSFetchRequest*)fetchRequestForSelectedSegment { NSFetchRequest* fetchRequest = [Article fetchRequest]; NSPredicate* predicate = nil; - + switch (_segmentedControl.selectedSegmentIndex) { // All objects case 0: // An empty fetch request will return all objects // Duplicates the functionality of [Article allObjects] break; - + // Sorted case 1:; NSSortDescriptor* sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"title" ascending:NO]; [fetchRequest setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]]; break; - + // By Predicate case 2: // Duplicates functionality of calling [Article objectsWithPredicate:predicate]; predicate = [NSPredicate predicateWithFormat:@"title CONTAINS[c] %@", @"2"]; [fetchRequest setPredicate:predicate]; break; - + // By ID case 3: // Duplicates functionality of [Article findByAttribute:@"articleID" withValue:[NSNumber numberWithInt:3]]; predicate = [NSPredicate predicateWithFormat:@"%K = %d", @"articleID", 3]; [fetchRequest setPredicate:predicate]; break; - - default: + + default: break; } - + return fetchRequest; } @@ -134,11 +134,11 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N if (nil == cell) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"ArticleCell"] autorelease]; } - + Article* article = [_articles objectAtIndex:indexPath.row]; cell.textLabel.text = article.title; cell.detailTextLabel.text = article.body; - + return cell; } diff --git a/Examples/RKCatalog/Examples/RKKeyValueMappingExample/RKKeyValueMappingExample.m b/Examples/RKCatalog/Examples/RKKeyValueMappingExample/RKKeyValueMappingExample.m index ee7be0ad92..c5e69b9a76 100644 --- a/Examples/RKCatalog/Examples/RKKeyValueMappingExample/RKKeyValueMappingExample.m +++ b/Examples/RKCatalog/Examples/RKKeyValueMappingExample/RKKeyValueMappingExample.m @@ -53,13 +53,13 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil if (self) { [RKObjectManager managerWithBaseURL:gRKCatalogBaseURL]; } - + return self; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[SimpleAccount class]]; [mapping mapKeyPathsToAttributes: @"id", @"accountID", @@ -69,14 +69,14 @@ - (void)viewDidAppear:(BOOL)animated { @"transactions.@avg.amount", @"averageTransactionAmount", @"transactions.@distinctUnionOfObjects.payee", @"distinctPayees", nil]; - + [[RKObjectManager sharedManager].mappingProvider setObjectMapping:mapping forResourcePathPattern:@"/RKKeyValueMappingExample"]; [[RKObjectManager sharedManager] loadObjectsAtResourcePath:@"/RKKeyValueMappingExample" delegate:self]; } - (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects { SimpleAccount* account = [objects objectAtIndex:0]; - + NSString* info = [NSString stringWithFormat: @"The count is %@\n" @"The average transaction amount is %@\n" diff --git a/Examples/RKCatalog/Examples/RKParamsExample/RKParamsExample.m b/Examples/RKCatalog/Examples/RKParamsExample/RKParamsExample.m index 3b2dd1f8f7..f7e96d3b47 100644 --- a/Examples/RKCatalog/Examples/RKParamsExample/RKParamsExample.m +++ b/Examples/RKCatalog/Examples/RKParamsExample/RKParamsExample.m @@ -16,7 +16,7 @@ @implementation RKParamsExample @synthesize uploadButton = _uploadButton; @synthesize statusLabel = _statusLabel; -- (void)dealloc { +- (void)dealloc { [RKClient setSharedClient:nil]; [_client release]; [super dealloc]; @@ -26,23 +26,23 @@ - (void)viewDidLoad { _client = [[RKClient alloc] initWithBaseURL:gRKCatalogBaseURL]; } -- (IBAction)uploadButtonWasTouched:(id)sender { +- (IBAction)uploadButtonWasTouched:(id)sender { RKParams* params = [RKParams params]; - + // Attach the Image from Image View NSLog(@"Got image: %@", [_imageView image]); NSData* imageData = UIImagePNGRepresentation([_imageView image]); [params setData:imageData MIMEType:@"image/png" forParam:@"image1"]; - + // Attach an Image from the App Bundle UIImage* image = [UIImage imageNamed:@"RestKit.png"]; imageData = UIImagePNGRepresentation(image); [params setData:imageData MIMEType:@"image/png" forParam:@"image2"]; - + // Log info about the serialization NSLog(@"RKParams HTTPHeaderValueForContentType = %@", [params HTTPHeaderValueForContentType]); NSLog(@"RKParams HTTPHeaderValueForContentLength = %d", [params HTTPHeaderValueForContentLength]); - + // Send it for processing! [_client post:@"/RKParamsExample" params:params delegate:self]; } @@ -59,7 +59,7 @@ - (void)request:(RKRequest *)request didSendBodyData:(NSInteger)bytesWritten tot - (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response { _uploadButton.enabled = YES; [_activityIndicatorView stopAnimating]; - + if ([response isOK]) { _statusLabel.text = @"Upload Successful!"; _statusLabel.textColor = [UIColor greenColor]; @@ -73,7 +73,7 @@ - (void)request:(RKRequest *)request didFailLoadWithError:(NSError *)error { _uploadButton.enabled = YES; [_activityIndicatorView stopAnimating]; _progressView.progress = 0.0; - + _statusLabel.text = [NSString stringWithFormat:@"Upload failed with error: %@", [error localizedDescription]]; _statusLabel.textColor = [UIColor redColor]; } diff --git a/Examples/RKCatalog/Examples/RKReachabilityExample/RKReachabilityExample.m b/Examples/RKCatalog/Examples/RKReachabilityExample/RKReachabilityExample.m index 7e05ba8c9b..c6fdceebf6 100644 --- a/Examples/RKCatalog/Examples/RKReachabilityExample/RKReachabilityExample.m +++ b/Examples/RKCatalog/Examples/RKReachabilityExample/RKReachabilityExample.m @@ -28,13 +28,13 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil name:RKReachabilityDidChangeNotification object:_observer]; } - + return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; - [_observer release]; + [_observer release]; [super dealloc]; } @@ -47,19 +47,19 @@ - (void)viewDidLoad { - (void)reachabilityChanged:(NSNotification *)notification { RKReachabilityObserver* observer = (RKReachabilityObserver *) [notification object]; - + RKLogCritical(@"Received reachability update: %@", observer); _flagsLabel.text = [NSString stringWithFormat:@"Host: %@ -> %@", observer.host, [observer reachabilityFlagsDescription]]; - + if ([observer isNetworkReachable]) { if ([observer isConnectionRequired]) { _statusLabel.text = @"Connection is available..."; _statusLabel.textColor = [UIColor yellowColor]; return; } - + _statusLabel.textColor = [UIColor greenColor]; - + if (RKReachabilityReachableViaWiFi == [observer networkStatus]) { _statusLabel.text = @"Online via WiFi"; } else if (RKReachabilityReachableViaWWAN == [observer networkStatus]) { diff --git a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/RKRelationshipMappingExample.m b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/RKRelationshipMappingExample.m index d737a37833..d3a006237e 100644 --- a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/RKRelationshipMappingExample.m +++ b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/RKRelationshipMappingExample.m @@ -21,21 +21,21 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:gRKCatalogBaseURL]; RKManagedObjectStore *objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"RKRelationshipMappingExample.sqlite"]; objectManager.objectStore = objectStore; - + RKManagedObjectMapping* taskMapping = [RKManagedObjectMapping mappingForClass:[Task class] inManagedObjectStore:objectStore]; taskMapping.primaryKeyAttribute = @"taskID"; [taskMapping mapKeyPath:@"id" toAttribute:@"taskID"]; [taskMapping mapKeyPath:@"name" toAttribute:@"name"]; [taskMapping mapKeyPath:@"assigned_user_id" toAttribute:@"assignedUserID"]; [objectManager.mappingProvider setMapping:taskMapping forKeyPath:@"task"]; - + RKManagedObjectMapping* userMapping = [RKManagedObjectMapping mappingForClass:[User class] inManagedObjectStore:objectStore]; userMapping.primaryKeyAttribute = @"userID"; [userMapping mapAttributes:@"name", @"email", nil]; [userMapping mapKeyPath:@"id" toAttribute:@"userID"]; [userMapping mapRelationship:@"tasks" withMapping:taskMapping]; [objectManager.mappingProvider setMapping:userMapping forKeyPath:@"user"]; - + // Hydrate the assignedUser association via primary key [taskMapping hasOne:@"assignedUser" withMapping:userMapping]; [taskMapping connectRelationship:@"assignedUser" withObjectForPrimaryKeyAttribute:@"assignedUserID"]; @@ -48,7 +48,7 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil [projectMapping mapRelationship:@"tasks" withMapping:taskMapping]; [objectManager.mappingProvider setMapping:projectMapping forKeyPath:@"project"]; } - + return self; } @@ -59,9 +59,9 @@ - (void)dealloc { - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - + self.title = @"Task List"; - + [[RKObjectManager sharedManager] loadObjectsAtResourcePath:@"/RKRelationshipMappingExample" delegate:self]; } @@ -93,7 +93,7 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger return [[[_objects objectAtIndex:indexPath.row] tasks] count]; } } - + return 0; } @@ -101,7 +101,7 @@ - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(10, 5, 150, 100)]; label.backgroundColor = [UIColor clearColor]; label.font = [UIFont boldSystemFontOfSize:18]; - + if (section == 0) { label.text = @"Projects"; } else if (section == 1) { @@ -118,27 +118,27 @@ - (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NS if (indexPath.section == 1) { return nil; } - + return indexPath; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [_selectedProject release]; _selectedProject = [[_objects objectAtIndex:indexPath.row] retain]; - - [self.tableView reloadData]; + + [self.tableView reloadData]; UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath]; cell.accessoryType = UITableViewCellAccessoryCheckmark; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; - + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease]; } - + if (indexPath.section == 0) { Project* project = (Project*) [_objects objectAtIndex:indexPath.row]; cell.accessoryType = UITableViewCellAccessoryNone; @@ -150,7 +150,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell.textLabel.text = [NSString stringWithFormat:@"%@", task.name]; cell.detailTextLabel.text = [NSString stringWithFormat:@"Assigned to: %@", task.assignedUser.name]; } - + return cell; } diff --git a/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.h b/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.h index fefc9c4533..5f61107de7 100644 --- a/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.h +++ b/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.h @@ -15,5 +15,5 @@ - (IBAction)sendRequest; - (IBAction)queueRequests; - + @end diff --git a/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.m b/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.m index 5a9db7ed17..941193a832 100644 --- a/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.m +++ b/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.m @@ -19,12 +19,12 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil if (self) { RKClient* client = [RKClient clientWithBaseURL:gRKCatalogBaseURL]; [RKClient setSharedClient:client]; - + // Ask RestKit to spin the network activity indicator for us client.requestQueue.delegate = self; client.requestQueue.showsNetworkActivityIndicatorWhenBusy = YES; } - + return self; } @@ -34,7 +34,7 @@ - (void)dealloc { [requestQueue cancelAllRequests]; [requestQueue release]; requestQueue = nil; - + [super dealloc]; } @@ -56,31 +56,31 @@ - (IBAction)queueRequests { queue.delegate = self; queue.concurrentRequestsLimit = 1; queue.showsNetworkActivityIndicatorWhenBusy = YES; - + // Queue up 4 requests RKRequest *request = [[RKClient sharedClient] requestWithResourcePath:@"/RKRequestQueueExample"]; request.delegate = self; [queue addRequest:request]; - + request = [[RKClient sharedClient] requestWithResourcePath:@"/RKRequestQueueExample"]; request.delegate = self; [queue addRequest:request]; - + request = [[RKClient sharedClient] requestWithResourcePath:@"/RKRequestQueueExample"]; request.delegate = self; [queue addRequest:request]; - + request = [[RKClient sharedClient] requestWithResourcePath:@"/RKRequestQueueExample"]; request.delegate = self; [queue addRequest:request]; - + // Start processing! [queue start]; self.requestQueue = queue; } - (void)requestQueue:(RKRequestQueue *)queue didSendRequest:(RKRequest *)request { - statusLabel.text = [NSString stringWithFormat:@"RKRequestQueue %@ is current loading %d of %d requests", + statusLabel.text = [NSString stringWithFormat:@"RKRequestQueue %@ is current loading %d of %d requests", queue, [queue loadingCount], [queue count]]; } diff --git a/Examples/RKMacOSX/RKMacOSX.xcodeproj/project.pbxproj b/Examples/RKMacOSX/RKMacOSX.xcodeproj/project.pbxproj index bc4febf28e..73e964d662 100644 --- a/Examples/RKMacOSX/RKMacOSX.xcodeproj/project.pbxproj +++ b/Examples/RKMacOSX/RKMacOSX.xcodeproj/project.pbxproj @@ -79,12 +79,6 @@ 25D6392B135184CE000879B1 /* RKMacOSXAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RKMacOSXAppDelegate.m; sourceTree = ""; }; 25D6392E135184CF000879B1 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; 25D63938135184F0000879B1 /* RestKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RestKit.xcodeproj; path = ../../RestKit.xcodeproj; sourceTree = ""; }; - 25D6397213518514000879B1 /* libRestKitCoreData.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libRestKitCoreData.a; sourceTree = SOURCE_ROOT; }; - 25D6397313518514000879B1 /* libRestKitJSONParserJSONKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libRestKitJSONParserJSONKit.a; sourceTree = SOURCE_ROOT; }; - 25D6397413518514000879B1 /* libRestKitNetwork.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libRestKitNetwork.a; sourceTree = SOURCE_ROOT; }; - 25D6397513518514000879B1 /* libRestKitObjectMapping.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libRestKitObjectMapping.a; sourceTree = SOURCE_ROOT; }; - 25D6397613518514000879B1 /* libRestKitSupport.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libRestKitSupport.a; sourceTree = SOURCE_ROOT; }; - 25D6397713518514000879B1 /* libRestKitThree20.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libRestKitThree20.a; sourceTree = SOURCE_ROOT; }; 25D6397E13518574000879B1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 25D639801351858A000879B1 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 25D63982135185B6000879B1 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; @@ -185,12 +179,6 @@ isa = PBXGroup; children = ( 25D63938135184F0000879B1 /* RestKit.xcodeproj */, - 25D6397213518514000879B1 /* libRestKitCoreData.a */, - 25D6397313518514000879B1 /* libRestKitJSONParserJSONKit.a */, - 25D6397413518514000879B1 /* libRestKitNetwork.a */, - 25D6397513518514000879B1 /* libRestKitObjectMapping.a */, - 25D6397613518514000879B1 /* libRestKitSupport.a */, - 25D6397713518514000879B1 /* libRestKitThree20.a */, ); name = RestKit; sourceTree = ""; @@ -222,7 +210,7 @@ 25D6390B135184CE000879B1 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0420; + LastUpgradeCheck = 0430; }; buildConfigurationList = 25D6390E135184CE000879B1 /* Build configuration list for PBXProject "RKMacOSX" */; compatibilityVersion = "Xcode 3.2"; @@ -349,7 +337,7 @@ 25D63933135184CF000879B1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = "$(ARCHS_STANDARD_32_64_BIT)"; + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = DEBUG; @@ -368,7 +356,7 @@ 25D63934135184CF000879B1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = "$(ARCHS_STANDARD_32_64_BIT)"; + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_VERSION = com.apple.compilers.llvm.clang.1_0; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/Examples/RKMacOSX/RKMacOSX/RKMacOSXAppDelegate.m b/Examples/RKMacOSX/RKMacOSX/RKMacOSXAppDelegate.m index 7036c2dc37..1f44acafab 100644 --- a/Examples/RKMacOSX/RKMacOSX/RKMacOSXAppDelegate.m +++ b/Examples/RKMacOSX/RKMacOSX/RKMacOSXAppDelegate.m @@ -10,8 +10,8 @@ @implementation RKMacOSXAppDelegate -@synthesize client; -@synthesize window; +@synthesize client = _client; +@synthesize window = _window; - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { diff --git a/Examples/RKTwitter/Classes/RKTStatus.h b/Examples/RKTwitter/Classes/RKTStatus.h index fff2c03da2..a3c23afef8 100644 --- a/Examples/RKTwitter/Classes/RKTStatus.h +++ b/Examples/RKTwitter/Classes/RKTStatus.h @@ -9,13 +9,13 @@ #import "RKTUser.h" @interface RKTStatus : NSObject { - NSNumber* _statusID; - NSDate* _createdAt; - NSString* _text; - NSString* _urlString; - NSString* _inReplyToScreenName; - NSNumber* _isFavorited; - RKTUser* _user; + NSNumber* _statusID; + NSDate* _createdAt; + NSString* _text; + NSString* _urlString; + NSString* _inReplyToScreenName; + NSNumber* _isFavorited; + RKTUser* _user; } /** diff --git a/Examples/RKTwitter/Classes/RKTStatus.m b/Examples/RKTwitter/Classes/RKTStatus.m index d1fff9518d..0bbf464549 100644 --- a/Examples/RKTwitter/Classes/RKTStatus.m +++ b/Examples/RKTwitter/Classes/RKTStatus.m @@ -15,21 +15,21 @@ @implementation RKTStatus @synthesize text = _text; @synthesize urlString = _urlString; @synthesize inReplyToScreenName = _inReplyToScreenName; -@synthesize isFavorited = _isFavorited; +@synthesize isFavorited = _isFavorited; @synthesize user = _user; - (NSString*)description { - return [NSString stringWithFormat:@"%@ (ID: %@)", self.text, self.statusID]; + return [NSString stringWithFormat:@"%@ (ID: %@)", self.text, self.statusID]; } - (void)dealloc { [_statusID release]; - [_createdAt release]; - [_text release]; + [_createdAt release]; + [_text release]; [_urlString release]; [_inReplyToScreenName release]; [_user release]; - + [super dealloc]; } diff --git a/Examples/RKTwitter/Classes/RKTUser.h b/Examples/RKTwitter/Classes/RKTUser.h index b2a1d042f2..20a6508b61 100644 --- a/Examples/RKTwitter/Classes/RKTUser.h +++ b/Examples/RKTwitter/Classes/RKTUser.h @@ -7,9 +7,9 @@ // @interface RKTUser : NSObject { - NSNumber* _userID; - NSString* _name; - NSString* _screenName; + NSNumber* _userID; + NSString* _name; + NSString* _screenName; } @property (nonatomic, retain) NSNumber* userID; diff --git a/Examples/RKTwitter/Classes/RKTUser.m b/Examples/RKTwitter/Classes/RKTUser.m index cf74eca957..aabd1d5f3b 100644 --- a/Examples/RKTwitter/Classes/RKTUser.m +++ b/Examples/RKTwitter/Classes/RKTUser.m @@ -16,9 +16,9 @@ @implementation RKTUser - (void)dealloc { [_userID release]; - [_name release]; - [_screenName release]; - + [_name release]; + [_screenName release]; + [super dealloc]; } diff --git a/Examples/RKTwitter/Classes/RKTwitterAppDelegate.m b/Examples/RKTwitter/Classes/RKTwitterAppDelegate.m index 835966ddee..7fa71dbe53 100644 --- a/Examples/RKTwitter/Classes/RKTwitterAppDelegate.m +++ b/Examples/RKTwitter/Classes/RKTwitterAppDelegate.m @@ -20,19 +20,19 @@ @implementation RKTwitterAppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { RKLogConfigureByName("RestKit/Network*", RKLogLevelTrace); RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelTrace); - + // Initialize RestKit - RKObjectManager* objectManager = [RKObjectManager managerWithBaseURLString:@"http://twitter.com"]; - + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURLString:@"http://twitter.com"]; + // Enable automatic network activity indicator management objectManager.client.requestQueue.showsNetworkActivityIndicatorWhenBusy = YES; - + // Setup our object mappings RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTUser class]]; [userMapping mapKeyPath:@"id" toAttribute:@"userID"]; [userMapping mapKeyPath:@"screen_name" toAttribute:@"screenName"]; [userMapping mapAttributes:@"name", nil]; - + RKObjectMapping* statusMapping = [RKObjectMapping mappingForClass:[RKTStatus class]]; [statusMapping mapKeyPathsToAttributes:@"id", @"statusID", @"created_at", @"createdAt", @@ -42,25 +42,25 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( @"favorited", @"isFavorited", nil]; [statusMapping mapRelationship:@"user" withMapping:userMapping]; - + // Update date format so that we can parse Twitter dates properly - // Wed Sep 29 15:31:08 +0000 2010 + // Wed Sep 29 15:31:08 +0000 2010 [RKObjectMapping addDefaultDateFormatterForString:@"E MMM d HH:mm:ss Z y" inTimeZone:nil]; - + // Uncomment these lines to use XML, comment it to use JSON // objectManager.acceptMIMEType = RKMIMETypeXML; // statusMapping.rootKeyPath = @"statuses.status"; - + // Register our mappings with the provider using a resource path pattern [objectManager.mappingProvider setObjectMapping:statusMapping forResourcePathPattern:@"/status/user_timeline/:username"]; - + // Create Window and View Controllers - RKTwitterViewController* viewController = [[[RKTwitterViewController alloc] initWithNibName:nil bundle:nil] autorelease]; - UINavigationController* controller = [[UINavigationController alloc] initWithRootViewController:viewController]; - UIWindow* window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 480)]; + RKTwitterViewController* viewController = [[[RKTwitterViewController alloc] initWithNibName:nil bundle:nil] autorelease]; + UINavigationController* controller = [[UINavigationController alloc] initWithRootViewController:viewController]; + UIWindow* window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 480)]; [window addSubview:controller.view]; [window makeKeyAndVisible]; - + return YES; } diff --git a/Examples/RKTwitter/Classes/RKTwitterViewController.h b/Examples/RKTwitter/Classes/RKTwitterViewController.h index cc492bebaa..19abdbbf83 100644 --- a/Examples/RKTwitter/Classes/RKTwitterViewController.h +++ b/Examples/RKTwitter/Classes/RKTwitterViewController.h @@ -10,8 +10,8 @@ #import @interface RKTwitterViewController : UIViewController { - UITableView* _tableView; - NSArray* _statuses; + UITableView* _tableView; + NSArray* _statuses; } @end diff --git a/Examples/RKTwitter/Classes/RKTwitterViewController.m b/Examples/RKTwitter/Classes/RKTwitterViewController.m index 464a3bb0ed..6e985a9b92 100644 --- a/Examples/RKTwitter/Classes/RKTwitterViewController.m +++ b/Examples/RKTwitter/Classes/RKTwitterViewController.m @@ -16,7 +16,7 @@ - (void)loadData; @implementation RKTwitterViewController - (void)loadTimeline { - // Load the object model via RestKit + // Load the object model via RestKit RKObjectManager* objectManager = [RKObjectManager sharedManager]; objectManager.client.baseURL = [RKURL URLWithString:@"http://www.twitter.com"]; [objectManager loadObjectsAtResourcePath:@"/status/user_timeline/RestKit" delegate:self]; @@ -24,31 +24,31 @@ - (void)loadTimeline { - (void)loadView { [super loadView]; - - // Setup View and Table View - self.title = @"RestKit Tweets"; + + // Setup View and Table View + self.title = @"RestKit Tweets"; [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleBlackTranslucent; self.navigationController.navigationBar.tintColor = [UIColor blackColor]; - self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(loadTimeline)] autorelease]; - - UIImageView* imageView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"BG.png"]] autorelease]; - imageView.frame = CGRectOffset(imageView.frame, 0, -64); - - [self.view insertSubview:imageView atIndex:0]; - - _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 480-64) style:UITableViewStylePlain]; - _tableView.dataSource = self; - _tableView.delegate = self; - _tableView.backgroundColor = [UIColor clearColor]; - _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(loadTimeline)] autorelease]; + + UIImageView* imageView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"BG.png"]] autorelease]; + imageView.frame = CGRectOffset(imageView.frame, 0, -64); + + [self.view insertSubview:imageView atIndex:0]; + + _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 480-64) style:UITableViewStylePlain]; + _tableView.dataSource = self; + _tableView.delegate = self; + _tableView.backgroundColor = [UIColor clearColor]; + _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; [self.view addSubview:_tableView]; - - [self loadTimeline]; + + [self loadTimeline]; } - (void)dealloc { - [_tableView release]; - [_statuses release]; + [_tableView release]; + [_statuses release]; [super dealloc]; } @@ -59,43 +59,43 @@ - (void)request:(RKRequest*)request didLoadResponse:(RKResponse*)response { } - (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects { - NSLog(@"Loaded statuses: %@", objects); - [_statuses release]; - _statuses = [objects retain]; - [_tableView reloadData]; + NSLog(@"Loaded statuses: %@", objects); + [_statuses release]; + _statuses = [objects retain]; + [_tableView reloadData]; } - (void)objectLoader:(RKObjectLoader*)objectLoader didFailWithError:(NSError*)error { - UIAlertView* alert = [[[UIAlertView alloc] initWithTitle:@"Error" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] autorelease]; - [alert show]; - NSLog(@"Hit error: %@", error); + UIAlertView* alert = [[[UIAlertView alloc] initWithTitle:@"Error" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] autorelease]; + [alert show]; + NSLog(@"Hit error: %@", error); } #pragma mark UITableViewDelegate methods - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - CGSize size = [[[_statuses objectAtIndex:indexPath.row] text] sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(300, 9000)]; - return size.height + 10; + CGSize size = [[[_statuses objectAtIndex:indexPath.row] text] sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(300, 9000)]; + return size.height + 10; } #pragma mark UITableViewDataSource methods - (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section { - return [_statuses count]; + return [_statuses count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - NSString* reuseIdentifier = @"Tweet Cell"; - UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier]; - if (nil == cell) { - cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier] autorelease]; - cell.textLabel.font = [UIFont systemFontOfSize:14]; - cell.textLabel.numberOfLines = 0; - cell.textLabel.backgroundColor = [UIColor clearColor]; - cell.contentView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"listbg.png"]]; - } - cell.textLabel.text = [[_statuses objectAtIndex:indexPath.row] text]; - return cell; + NSString* reuseIdentifier = @"Tweet Cell"; + UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier]; + if (nil == cell) { + cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier] autorelease]; + cell.textLabel.font = [UIFont systemFontOfSize:14]; + cell.textLabel.numberOfLines = 0; + cell.textLabel.backgroundColor = [UIColor clearColor]; + cell.contentView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"listbg.png"]]; + } + cell.textLabel.text = [[_statuses objectAtIndex:indexPath.row] text]; + return cell; } @end diff --git a/Examples/RKTwitter/main.m b/Examples/RKTwitter/main.m index f2eff9c9af..68a6317613 100644 --- a/Examples/RKTwitter/main.m +++ b/Examples/RKTwitter/main.m @@ -9,7 +9,7 @@ #import int main(int argc, char *argv[]) { - + NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; int retVal = UIApplicationMain(argc, argv, nil, @"RKTwitterAppDelegate"); [pool release]; diff --git a/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m b/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m index 7221fdf5a7..bd2802a451 100644 --- a/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m +++ b/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m @@ -19,11 +19,11 @@ @implementation RKTwitterAppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Initialize RestKit - RKObjectManager* objectManager = [RKObjectManager managerWithBaseURLString:@"http://twitter.com"]; - + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURLString:@"http://twitter.com"]; + // Enable automatic network activity indicator management objectManager.client.requestQueue.showsNetworkActivityIndicatorWhenBusy = YES; - + // Initialize object store #ifdef RESTKIT_GENERATE_SEED_DB NSString *seedDatabaseName = nil; @@ -34,8 +34,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( #endif objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:databaseName usingSeedDatabaseName:seedDatabaseName managedObjectModel:nil delegate:self]; - - // Setup our object mappings + + // Setup our object mappings /*! Mapping by entity. Here we are configuring a mapping by targetting a Core Data entity with a specific name. This allows us to map back Twitter user objects directly onto NSManagedObject instances -- @@ -46,7 +46,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [userMapping mapKeyPath:@"id" toAttribute:@"userID"]; [userMapping mapKeyPath:@"screen_name" toAttribute:@"screenName"]; [userMapping mapAttributes:@"name", nil]; - + /*! Map to a target object class -- just as you would for a non-persistent class. The entity is resolved for you using the Active Record pattern where the class name corresponds to the entity name within Core Data. @@ -59,21 +59,21 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( @"text", @"text", @"url", @"urlString", @"in_reply_to_screen_name", @"inReplyToScreenName", - @"favorited", @"isFavorited", + @"favorited", @"isFavorited", nil]; [statusMapping mapRelationship:@"user" withMapping:userMapping]; - + // Update date format so that we can parse Twitter dates properly - // Wed Sep 29 15:31:08 +0000 2010 + // Wed Sep 29 15:31:08 +0000 2010 [RKObjectMapping addDefaultDateFormatterForString:@"E MMM d HH:mm:ss Z y" inTimeZone:nil]; - + // Register our mappings with the provider [objectManager.mappingProvider setObjectMapping:statusMapping forResourcePathPattern:@"/status/user_timeline/:username"]; - + // Uncomment this to use XML, comment it to use JSON // objectManager.acceptMIMEType = RKMIMETypeXML; // [objectManager.mappingProvider setMapping:statusMapping forKeyPath:@"statuses.status"]; - + // Database seeding is configured as a copied target of the main application. There are only two differences // between the main application target and the 'Generate Seed Database' target: // 1) RESTKIT_GENERATE_SEED_DB is defined in the 'Preprocessor Macros' section of the build setting for the target @@ -84,24 +84,24 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelInfo); RKLogConfigureByName("RestKit/CoreData", RKLogLevelTrace); RKManagedObjectSeeder* seeder = [RKManagedObjectSeeder objectSeederWithObjectManager:objectManager]; - + // Seed the database with instances of RKTStatus from a snapshot of the RestKit Twitter timeline [seeder seedObjectsFromFile:@"restkit.json" withObjectMapping:statusMapping]; - + // Seed the database with RKTUser objects. The class will be inferred via element registration [seeder seedObjectsFromFiles:@"users.json", nil]; - + // Finalize the seeding operation and output a helpful informational message [seeder finalizeSeedingAndExit]; - + // NOTE: If all of your mapped objects use keyPath -> objectMapping registration, you can perform seeding in one line of code: // [RKManagedObjectSeeder generateSeedDatabaseWithObjectManager:objectManager fromFiles:@"users.json", nil]; #endif - + // Create Window and View Controllers - RKTwitterViewController* viewController = [[[RKTwitterViewController alloc] initWithNibName:nil bundle:nil] autorelease]; - UINavigationController* controller = [[UINavigationController alloc] initWithRootViewController:viewController]; - UIWindow* window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 480)]; + RKTwitterViewController* viewController = [[[RKTwitterViewController alloc] initWithNibName:nil bundle:nil] autorelease]; + UINavigationController* controller = [[UINavigationController alloc] initWithRootViewController:viewController]; + UIWindow* window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 480)]; [window addSubview:controller.view]; [window makeKeyAndVisible]; diff --git a/Examples/RKTwitterCoreData/Classes/RKTwitterViewController.h b/Examples/RKTwitterCoreData/Classes/RKTwitterViewController.h index 775e860fc6..909f46526c 100644 --- a/Examples/RKTwitterCoreData/Classes/RKTwitterViewController.h +++ b/Examples/RKTwitterCoreData/Classes/RKTwitterViewController.h @@ -10,8 +10,8 @@ #import @interface RKTwitterViewController : UIViewController { - UITableView* _tableView; - NSArray* _statuses; + UITableView* _tableView; + NSArray* _statuses; } - (void)loadObjectsFromDataStore; @end diff --git a/Examples/RKTwitterCoreData/Classes/RKTwitterViewController.m b/Examples/RKTwitterCoreData/Classes/RKTwitterViewController.m index f0524c4c39..e4eb73dd2f 100644 --- a/Examples/RKTwitterCoreData/Classes/RKTwitterViewController.m +++ b/Examples/RKTwitterCoreData/Classes/RKTwitterViewController.m @@ -13,108 +13,108 @@ @implementation RKTwitterViewController - (void)loadView { [super loadView]; - - // Setup View and Table View - self.title = @"RestKit Tweets"; + + // Setup View and Table View + self.title = @"RestKit Tweets"; [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleBlackTranslucent; self.navigationController.navigationBar.tintColor = [UIColor blackColor]; - self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(reloadButtonWasPressed:)] autorelease]; - - UIImageView* imageView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"BG.png"]] autorelease]; - imageView.frame = CGRectOffset(imageView.frame, 0, -64); - - [self.view insertSubview:imageView atIndex:0]; - - _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 480-64) style:UITableViewStylePlain]; - _tableView.dataSource = self; - _tableView.delegate = self; - _tableView.backgroundColor = [UIColor clearColor]; - _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(reloadButtonWasPressed:)] autorelease]; + + UIImageView* imageView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"BG.png"]] autorelease]; + imageView.frame = CGRectOffset(imageView.frame, 0, -64); + + [self.view insertSubview:imageView atIndex:0]; + + _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 480-64) style:UITableViewStylePlain]; + _tableView.dataSource = self; + _tableView.delegate = self; + _tableView.backgroundColor = [UIColor clearColor]; + _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; [self.view addSubview:_tableView]; - - // Load statuses from core data - [self loadObjectsFromDataStore]; + + // Load statuses from core data + [self loadObjectsFromDataStore]; } - (void)dealloc { - [_tableView release]; - [_statuses release]; + [_tableView release]; + [_statuses release]; [super dealloc]; } - (void)loadObjectsFromDataStore { - [_statuses release]; - NSFetchRequest* request = [RKTStatus fetchRequest]; - NSSortDescriptor* descriptor = [NSSortDescriptor sortDescriptorWithKey:@"createdAt" ascending:NO]; - [request setSortDescriptors:[NSArray arrayWithObject:descriptor]]; - _statuses = [[RKTStatus objectsWithFetchRequest:request] retain]; + [_statuses release]; + NSFetchRequest* request = [RKTStatus fetchRequest]; + NSSortDescriptor* descriptor = [NSSortDescriptor sortDescriptorWithKey:@"createdAt" ascending:NO]; + [request setSortDescriptors:[NSArray arrayWithObject:descriptor]]; + _statuses = [[RKTStatus objectsWithFetchRequest:request] retain]; } - (void)loadData { - // Load the object model via RestKit + // Load the object model via RestKit RKObjectManager* objectManager = [RKObjectManager sharedManager]; [objectManager loadObjectsAtResourcePath:@"/status/user_timeline/RestKit" delegate:self]; } - (void)reloadButtonWasPressed:(id)sender { - // Load the object model via RestKit - [self loadData]; + // Load the object model via RestKit + [self loadData]; } #pragma mark RKObjectLoaderDelegate methods - (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects { - [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:@"LastUpdatedAt"]; - [[NSUserDefaults standardUserDefaults] synchronize]; - NSLog(@"Loaded statuses: %@", objects); - [self loadObjectsFromDataStore]; - [_tableView reloadData]; + [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:@"LastUpdatedAt"]; + [[NSUserDefaults standardUserDefaults] synchronize]; + NSLog(@"Loaded statuses: %@", objects); + [self loadObjectsFromDataStore]; + [_tableView reloadData]; } - (void)objectLoader:(RKObjectLoader*)objectLoader didFailWithError:(NSError*)error { - UIAlertView* alert = [[[UIAlertView alloc] initWithTitle:@"Error" - message:[error localizedDescription] - delegate:nil + UIAlertView* alert = [[[UIAlertView alloc] initWithTitle:@"Error" + message:[error localizedDescription] + delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] autorelease]; - [alert show]; - NSLog(@"Hit error: %@", error); + [alert show]; + NSLog(@"Hit error: %@", error); } #pragma mark UITableViewDelegate methods - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - CGSize size = [[[_statuses objectAtIndex:indexPath.row] text] sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(300, 9000)]; - return size.height + 10; + CGSize size = [[[_statuses objectAtIndex:indexPath.row] text] sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(300, 9000)]; + return size.height + 10; } #pragma mark UITableViewDataSource methods - (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section { - return [_statuses count]; + return [_statuses count]; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { - NSDate* lastUpdatedAt = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastUpdatedAt"]; - NSString* dateString = [NSDateFormatter localizedStringFromDate:lastUpdatedAt dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterMediumStyle]; - if (nil == dateString) { - dateString = @"Never"; - } - return [NSString stringWithFormat:@"Last Load: %@", dateString]; + NSDate* lastUpdatedAt = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastUpdatedAt"]; + NSString* dateString = [NSDateFormatter localizedStringFromDate:lastUpdatedAt dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterMediumStyle]; + if (nil == dateString) { + dateString = @"Never"; + } + return [NSString stringWithFormat:@"Last Load: %@", dateString]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - NSString* reuseIdentifier = @"Tweet Cell"; - UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier]; - if (nil == cell) { - cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier] autorelease]; - cell.textLabel.font = [UIFont systemFontOfSize:14]; - cell.textLabel.numberOfLines = 0; - cell.textLabel.backgroundColor = [UIColor clearColor]; - cell.contentView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"listbg.png"]]; - } + NSString* reuseIdentifier = @"Tweet Cell"; + UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier]; + if (nil == cell) { + cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier] autorelease]; + cell.textLabel.font = [UIFont systemFontOfSize:14]; + cell.textLabel.numberOfLines = 0; + cell.textLabel.backgroundColor = [UIColor clearColor]; + cell.contentView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"listbg.png"]]; + } RKTStatus* status = [_statuses objectAtIndex:indexPath.row]; - cell.textLabel.text = status.text; - return cell; + cell.textLabel.text = status.text; + return cell; } @end diff --git a/Examples/RKTwitterCoreData/RKTwitterCoreData.xcodeproj/project.pbxproj b/Examples/RKTwitterCoreData/RKTwitterCoreData.xcodeproj/project.pbxproj index e075cb551b..1ccc048253 100755 --- a/Examples/RKTwitterCoreData/RKTwitterCoreData.xcodeproj/project.pbxproj +++ b/Examples/RKTwitterCoreData/RKTwitterCoreData.xcodeproj/project.pbxproj @@ -11,6 +11,9 @@ 1D60589B0D05DD56006BFB54 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; }; 1D60589F0D05DD5A006BFB54 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D30AB110D05D00D00671497 /* Foundation.framework */; }; 1DF5F4E00D08C38300B7A737 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1DF5F4DF0D08C38300B7A737 /* UIKit.framework */; }; + 25014071153706FD004E0466 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 250CA6CF147D90C50047D347 /* Security.framework */; }; + 25014085153707CB004E0466 /* libRestKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 250CA6C6147D90A50047D347 /* libRestKit.a */; }; + 25014086153707E2004E0466 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD39C14D14EEED8700E84874 /* QuartzCore.framework */; }; 250CA6CE147D90BA0047D347 /* libRestKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 250CA6C6147D90A50047D347 /* libRestKit.a */; }; 250CA6D0147D90C50047D347 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 250CA6CF147D90C50047D347 /* Security.framework */; }; 2538E814123419EC00ACB5D7 /* RKTStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 2538E813123419EC00ACB5D7 /* RKTStatus.m */; }; @@ -162,6 +165,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 25014086153707E2004E0466 /* QuartzCore.framework in Frameworks */, + 25014085153707CB004E0466 /* libRestKit.a in Frameworks */, + 25014071153706FD004E0466 /* Security.framework in Frameworks */, 25F2A1871322D59400A33DE4 /* Foundation.framework in Frameworks */, 25F2A1881322D59400A33DE4 /* UIKit.framework in Frameworks */, 25F2A1891322D59400A33DE4 /* CoreGraphics.framework in Frameworks */, @@ -212,7 +218,6 @@ 29B97314FDCFA39411CA2CEA /* CustomTemplate */ = { isa = PBXGroup; children = ( - CD39C14D14EEED8700E84874 /* QuartzCore.framework */, 2538E7FF123417E500ACB5D7 /* RestKit.xcodeproj */, 080E96DDFE201D6D7F000001 /* Classes */, 29B97315FDCFA39411CA2CEA /* Other Sources */, @@ -253,6 +258,7 @@ 29B97323FDCFA39411CA2CEA /* Frameworks */ = { isa = PBXGroup; children = ( + CD39C14D14EEED8700E84874 /* QuartzCore.framework */, 250CA6CF147D90C50047D347 /* Security.framework */, 1DF5F4DF0D08C38300B7A737 /* UIKit.framework */, 1D30AB110D05D00D00671497 /* Foundation.framework */, @@ -459,7 +465,7 @@ GCC_OPTIMIZATION_LEVEL = 0; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = RKTwitter_Prefix.pch; - HEADER_SEARCH_PATHS = "${TARGET_BUILD_DIR}/../../Headers"; + HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers\""; INFOPLIST_FILE = "Resources/RKTwitter-Info.plist"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RKTwitterCoreData; @@ -491,7 +497,7 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = RKTwitter_Prefix.pch; GCC_PREPROCESSOR_DEFINITIONS = RESTKIT_GENERATE_SEED_DB; - HEADER_SEARCH_PATHS = "\"$(SOURCE_ROOT)/../../Build\""; + HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers\""; INFOPLIST_FILE = "Resources/RKTwitter-Info.plist"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "Generate Seed Database"; @@ -506,7 +512,7 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = RKTwitter_Prefix.pch; GCC_PREPROCESSOR_DEFINITIONS = RESTKIT_GENERATE_SEED_DB; - HEADER_SEARCH_PATHS = "\"$(SOURCE_ROOT)/../../Build\""; + HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers\""; INFOPLIST_FILE = "Resources/RKTwitter-Info.plist"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "Generate Seed Database"; diff --git a/Examples/RKTwitterCoreData/main.m b/Examples/RKTwitterCoreData/main.m index f2eff9c9af..68a6317613 100644 --- a/Examples/RKTwitterCoreData/main.m +++ b/Examples/RKTwitterCoreData/main.m @@ -9,7 +9,7 @@ #import int main(int argc, char *argv[]) { - + NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; int retVal = UIApplicationMain(argc, argv, nil, @"RKTwitterAppDelegate"); [pool release]; diff --git a/Examples/RKTwitterCoreData/restkit.json b/Examples/RKTwitterCoreData/restkit.json index fc83ef6733..ae03977e6a 100644 --- a/Examples/RKTwitterCoreData/restkit.json +++ b/Examples/RKTwitterCoreData/restkit.json @@ -1 +1 @@ -[{"in_reply_to_status_id_str":"42716229147967491","text":"@JustJenFelice Be sure to join the Google Group and reach out if you need any support!","contributors":null,"retweeted":false,"in_reply_to_user_id_str":"7426812","retweet_count":0,"id_str":"44029179364253696","source":"\u003Ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003ETweetie for Mac\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Sat Mar 05 13:39:09 +0000 2011","place":null,"in_reply_to_status_id":42716229147967491,"coordinates":null,"favorited":false,"user":{"following":true,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","is_translator":false,"show_all_inline_media":false,"geo_enabled":false,"time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","follow_request_sent":false,"profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","profile_background_tile":true,"location":"","contributors_enabled":false,"statuses_count":136,"lang":"en","verified":false,"notifications":false,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","listed_count":5,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":44029179364253696,"in_reply_to_screen_name":"JustJenFelice","in_reply_to_user_id":7426812},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/h180ef Blake Watters - Added new initializer for starting from a seed database. Need to finish cleaning up API an...","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"43646264163844096","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Fri Mar 04 12:17:34 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":43646264163844096,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/fa2Law Blake Watters - Silenced a couple of Xcode 4 warning. Don't send a serialization with a GET request by def...","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"43074105142026241","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Mar 02 22:24:01 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":43074105142026241,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/fBWUre Jeremy Ellison - update projects to link libxml2 (verified working). Update readme to mention changes","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"42660583622975488","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Tue Mar 01 19:00:50 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":42660583622975488,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/ho92wG Jeremy Ellison - Working XML Support. Twitter example working (XML and JSON)","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"42660582243049472","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Tue Mar 01 19:00:50 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":42660582243049472,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/dPAmx6 Jeremy Ellison - make RKXMLParser support RKParser protocol","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"42660580796022784","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Tue Mar 01 19:00:49 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":42660580796022784,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/gfGynL Jeremy Ellison - add RKXMLParser","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"42660579399307264","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Tue Mar 01 19:00:49 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":42660579399307264,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/fVQI5Q Blake Watters - Fixed crash during dealloc of RKClient due to initialization of baseURL observer using a b...","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"41221933492076544","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Fri Feb 25 19:44:09 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":41221933492076544,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/hJtNCV Jeremy Ellison - Merge branch 'gtio-mapping-updates' of github.com:twotoasters\/RestKit into gtio-mapping-u...","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"41221089635864576","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Fri Feb 25 19:40:48 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":41221089635864576,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/fZYIyg Jeremy Ellison - Add did cancel delegate","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"41221088297877504","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Fri Feb 25 19:40:47 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":41221088297877504,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":"36907142883573761","text":"@chrisabruce New Xcode 4 specific install instructions are now included. Should be fully compatible with Xcode 3 and 4 now","contributors":null,"retweeted":false,"in_reply_to_user_id_str":"104933758","retweet_count":0,"id_str":"37909707221897216","source":"\u003Ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003ETweetie for Mac\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 16:22:33 +0000 2011","place":null,"in_reply_to_status_id":36907142883573761,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37909707221897216,"in_reply_to_screen_name":"chrisabruce","in_reply_to_user_id":104933758},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/gUIrfk Blake Watters - Updates to quick start section","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37906943557369856","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 16:11:34 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37906943557369856,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/eabC8R Blake Watters - Trying to get formatting right for Github flavored markdown","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37905652214923264","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 16:06:26 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37905652214923264,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/g8b9VX Blake Watters - Merge branch 'master' of github.com:twotoasters\/RestKit\n\nConflicts:\n\tRestKit.xcodeproj\/pro...","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37905228544217089","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 16:04:45 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37905228544217089,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/f1DpxP Blake Watters - Xcode 4 specific install instructions","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37905226803589120","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 16:04:44 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37905226803589120,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/dNaG1Z Blake Watters - Fix for whitespace. Really annoyed at Xcode 4","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37713900346548224","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 03:24:29 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37713900346548224,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/g72vPt Blake Watters - trying to get the project to work with various Xcode output path settings","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37697830005112832","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 02:20:37 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37697830005112832,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"Xcode 4 build environment changes are very frustrating.","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37686673852604416","source":"\u003Ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003ETweetie for Mac\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 01:36:17 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37686673852604416,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/dPjF4h Blake Watters - More Xcode 4 crap","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37684000310951936","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 01:25:40 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37684000310951936,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":"36907142883573761","text":"@chrisabruce Xcode 4 fixes were recently pushed","contributors":null,"retweeted":false,"in_reply_to_user_id_str":"104933758","retweet_count":0,"id_str":"37676602791821312","source":"\u003Ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003ETweetie for Mac\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 00:56:16 +0000 2011","place":null,"in_reply_to_status_id":36907142883573761,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37676602791821312,"in_reply_to_screen_name":"chrisabruce","in_reply_to_user_id":104933758}] \ No newline at end of file +[{"in_reply_to_status_id_str":"42716229147967491","text":"@JustJenFelice Be sure to join the Google Group and reach out if you need any support!","contributors":null,"retweeted":false,"in_reply_to_user_id_str":"7426812","retweet_count":0,"id_str":"44029179364253696","source":"\u003Ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003ETweetie for Mac\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Sat Mar 05 13:39:09 +0000 2011","place":null,"in_reply_to_status_id":42716229147967491,"coordinates":null,"favorited":false,"user":{"following":true,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","is_translator":false,"show_all_inline_media":false,"geo_enabled":false,"time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","follow_request_sent":false,"profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","profile_background_tile":true,"location":"","contributors_enabled":false,"statuses_count":136,"lang":"en","verified":false,"notifications":false,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","listed_count":5,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":44029179364253696,"in_reply_to_screen_name":"JustJenFelice","in_reply_to_user_id":7426812},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/h180ef Blake Watters - Added new initializer for starting from a seed database. Need to finish cleaning up API an...","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"43646264163844096","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Fri Mar 04 12:17:34 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":43646264163844096,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/fa2Law Blake Watters - Silenced a couple of Xcode 4 warning. Don't send a serialization with a GET request by def...","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"43074105142026241","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Mar 02 22:24:01 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":43074105142026241,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/fBWUre Jeremy Ellison - update projects to link libxml2 (verified working). Update readme to mention changes","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"42660583622975488","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Tue Mar 01 19:00:50 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":42660583622975488,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/ho92wG Jeremy Ellison - Working XML Support. Twitter example working (XML and JSON)","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"42660582243049472","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Tue Mar 01 19:00:50 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":42660582243049472,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/dPAmx6 Jeremy Ellison - make RKXMLParser support RKParser protocol","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"42660580796022784","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Tue Mar 01 19:00:49 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":42660580796022784,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/gfGynL Jeremy Ellison - add RKXMLParser","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"42660579399307264","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Tue Mar 01 19:00:49 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":42660579399307264,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/fVQI5Q Blake Watters - Fixed crash during dealloc of RKClient due to initialization of baseURL observer using a b...","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"41221933492076544","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Fri Feb 25 19:44:09 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":41221933492076544,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/hJtNCV Jeremy Ellison - Merge branch 'gtio-mapping-updates' of github.com:twotoasters\/RestKit into gtio-mapping-u...","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"41221089635864576","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Fri Feb 25 19:40:48 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":41221089635864576,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/fZYIyg Jeremy Ellison - Add did cancel delegate","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"41221088297877504","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Fri Feb 25 19:40:47 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":41221088297877504,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":"36907142883573761","text":"@chrisabruce New Xcode 4 specific install instructions are now included. Should be fully compatible with Xcode 3 and 4 now","contributors":null,"retweeted":false,"in_reply_to_user_id_str":"104933758","retweet_count":0,"id_str":"37909707221897216","source":"\u003Ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003ETweetie for Mac\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 16:22:33 +0000 2011","place":null,"in_reply_to_status_id":36907142883573761,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37909707221897216,"in_reply_to_screen_name":"chrisabruce","in_reply_to_user_id":104933758},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/gUIrfk Blake Watters - Updates to quick start section","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37906943557369856","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 16:11:34 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37906943557369856,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/eabC8R Blake Watters - Trying to get formatting right for Github flavored markdown","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37905652214923264","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 16:06:26 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37905652214923264,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/g8b9VX Blake Watters - Merge branch 'master' of github.com:twotoasters\/RestKit\n\nConflicts:\n\tRestKit.xcodeproj\/pro...","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37905228544217089","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 16:04:45 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37905228544217089,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/f1DpxP Blake Watters - Xcode 4 specific install instructions","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37905226803589120","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 16:04:44 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37905226803589120,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/dNaG1Z Blake Watters - Fix for whitespace. Really annoyed at Xcode 4","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37713900346548224","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 03:24:29 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37713900346548224,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/g72vPt Blake Watters - trying to get the project to work with various Xcode output path settings","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37697830005112832","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 02:20:37 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37697830005112832,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"Xcode 4 build environment changes are very frustrating.","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37686673852604416","source":"\u003Ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003ETweetie for Mac\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 01:36:17 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37686673852604416,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":null,"text":"[RestKit] http:\/\/bit.ly\/dPjF4h Blake Watters - More Xcode 4 crap","contributors":null,"retweeted":false,"in_reply_to_user_id_str":null,"retweet_count":0,"id_str":"37684000310951936","source":"\u003Ca href=\"http:\/\/github.com\" rel=\"nofollow\"\u003EGitHub Service Hooks\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 01:25:40 +0000 2011","place":null,"in_reply_to_status_id":null,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37684000310951936,"in_reply_to_screen_name":null,"in_reply_to_user_id":null},{"in_reply_to_status_id_str":"36907142883573761","text":"@chrisabruce Xcode 4 fixes were recently pushed","contributors":null,"retweeted":false,"in_reply_to_user_id_str":"104933758","retweet_count":0,"id_str":"37676602791821312","source":"\u003Ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003ETweetie for Mac\u003C\/a\u003E","geo":null,"truncated":false,"created_at":"Wed Feb 16 00:56:16 +0000 2011","place":null,"in_reply_to_status_id":36907142883573761,"coordinates":null,"favorited":false,"user":{"is_translator":false,"contributors_enabled":false,"following":null,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/181380497\/twitter-bg.png","favourites_count":0,"follow_request_sent":null,"statuses_count":136,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1190091434\/restkit-twitter-logo_normal.png","description":"RestKit is a framework for consuming and modeling restful web resources on OS X and iOS (iPhone, iPad & iPod Touch) - Built by @twotoasters","time_zone":null,"friends_count":87,"profile_text_color":"333333","url":"http:\/\/restkit.org","profile_sidebar_fill_color":"efefef","screen_name":"RestKit","id_str":"208727870","geo_enabled":false,"profile_background_tile":true,"location":"","listed_count":5,"lang":"en","verified":false,"notifications":null,"created_at":"Wed Oct 27 20:46:12 +0000 2010","profile_link_color":"009999","show_all_inline_media":false,"profile_sidebar_border_color":"eeeeee","protected":false,"followers_count":122,"name":"RestKit","profile_use_background_image":true,"id":208727870,"utc_offset":null,"profile_background_color":"131516"},"id":37676602791821312,"in_reply_to_screen_name":"chrisabruce","in_reply_to_user_id":104933758}] diff --git a/Examples/RKTwitterCoreData/users.json b/Examples/RKTwitterCoreData/users.json index fa42a9c3c1..550fd6f217 100644 --- a/Examples/RKTwitterCoreData/users.json +++ b/Examples/RKTwitterCoreData/users.json @@ -4,4 +4,4 @@ "name": "Blake Watters", "screen_name": "Blake Watters" } -}] \ No newline at end of file +}] diff --git a/Examples/RestKit CLI/RestKit CLI/main.m b/Examples/RestKit CLI/RestKit CLI/main.m index cc28242fba..b3ea0cf162 100644 --- a/Examples/RestKit CLI/RestKit CLI/main.m +++ b/Examples/RestKit CLI/RestKit CLI/main.m @@ -14,7 +14,7 @@ int main (int argc, const char * argv[]) @autoreleasepool { RKLogConfigureByName("App", RKLogLevelTrace); - + // Validate arguments if (argc < 2) { printf("usage: %s path/to/file [keyPath]\n", argv[0]); @@ -22,11 +22,11 @@ int main (int argc, const char * argv[]) "If keyPath is provided it will be evaluated against the payload and the result printed.\n"); return 0; } - - NSString *filePathOrURL = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding]; + + NSString *filePathOrURL = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding]; NSURL *URL = nil; - NSString *keyPath = nil; - + NSString *keyPath = nil; + if ([filePathOrURL rangeOfString:@"://"].length == 0) { // Local file URL = [NSURL fileURLWithPath:filePathOrURL]; @@ -35,14 +35,14 @@ int main (int argc, const char * argv[]) URL = [NSURL URLWithString:filePathOrURL]; } if (argc == 3) keyPath = [NSString stringWithCString:argv[2] encoding:NSUTF8StringEncoding]; - + NSError *error = nil; NSString *payload = [NSString stringWithContentsOfURL:URL encoding:NSUTF8StringEncoding error:&error]; if (!payload) { RKLogError(@"Failed to read file at path %@: %@", URL, error); return 0; } - + NSString *MIMEType = [[URL absoluteString] MIMETypeForPathExtension]; RKLogInfo(@"Parsing %@ using MIME Type: %@", URL, MIMEType); id parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:MIMEType]; @@ -53,8 +53,8 @@ int main (int argc, const char * argv[]) return 0; } RKLogInfo(@"Parsed data => %@", parsedData); - if (keyPath) RKLogInfo(@"valueForKeyPath:@\"%@\" => %@", keyPath, [parsedData valueForKeyPath:keyPath]); + if (keyPath) RKLogInfo(@"valueForKeyPath:@\"%@\" => %@", keyPath, [parsedData valueForKeyPath:keyPath]); } - + return 0; } diff --git a/Gemfile b/Gemfile index 5f77ceb267..c61e70c5a1 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,17 @@ gem "rake", "~> 0.9.0" gem "bundler", "~> 1.1.0" gem "sinatra", :git => "git://github.com/sinatra/sinatra.git" gem "thin", "~> 1.3.1" +gem 'xcoder', :git => "git://github.com/rayh/xcoder.git" +gem 'restkit', :git => 'git://github.com/RestKit/RestKit-Gem.git' +gem 'ruby-debug19' +gem 'faker', '1.0.1' + +## OAuth stuff + +# gem 'oauth' +gem 'rack-oauth2-server', :git => 'https://github.com/assaf/rack-oauth2-server.git' +gem 'rspec' +gem 'rack-test' +gem 'mongo' +gem 'simple_oauth', :git => 'https://github.com/laserlemon/simple_oauth.git' + diff --git a/Gemfile.lock b/Gemfile.lock index a1371c63eb..1a70d42182 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,21 +1,98 @@ +GIT + remote: git://github.com/RestKit/RestKit-Gem.git + revision: 96058139a46c46aa344c1481e8c75b390c82d5e9 + specs: + restkit (0.0.0) + +GIT + remote: git://github.com/rayh/xcoder.git + revision: e400000f29d959932cae2558fc77fd9ace13ead0 + specs: + xcoder (0.1.12) + builder + json + nokogiri + plist + rest-client + GIT remote: git://github.com/sinatra/sinatra.git - revision: 0bf6b92cedd86df89068b3c0b1b0f58ad28287eb + revision: cab56ed7d64b9ffb6d4df48cb8d7d3153890f3ee specs: sinatra (1.4.0) rack (~> 1.3, >= 1.3.6) rack-protection (~> 1.2) tilt (~> 1.3, >= 1.3.3) +GIT + remote: https://github.com/assaf/rack-oauth2-server.git + revision: 8c03cdb4cbaf49fdd3d227d8a84a2486261818f8 + specs: + rack-oauth2-server (2.6.0) + bson_ext + json + jwt (~> 0.1.4) + mongo (~> 1) + rack (~> 1.1) + sinatra (~> 1.1) + +GIT + remote: https://github.com/laserlemon/simple_oauth.git + revision: e1146b615df769bfe7824a337122069cc2d5476d + specs: + simple_oauth (0.1.8) + GEM remote: http://rubygems.org/ specs: - daemons (1.1.4) + archive-tar-minitar (0.5.2) + bson (1.6.2) + bson_ext (1.6.2) + bson (~> 1.6.2) + builder (3.0.0) + columnize (0.3.6) + daemons (1.1.8) + diff-lcs (1.1.3) eventmachine (0.12.10) + faker (1.0.1) + i18n (~> 0.4) + i18n (0.6.0) + json (1.7.3) + jwt (0.1.4) + json (>= 1.2.4) + linecache19 (0.5.12) + ruby_core_source (>= 0.1.4) + mime-types (1.18) + mongo (1.6.2) + bson (~> 1.6.2) + nokogiri (1.5.2) + plist (3.1.0) rack (1.4.1) rack-protection (1.2.0) rack + rack-test (0.6.1) + rack (>= 1.0) rake (0.9.2.2) + rest-client (1.6.7) + mime-types (>= 1.16) + rspec (2.10.0) + rspec-core (~> 2.10.0) + rspec-expectations (~> 2.10.0) + rspec-mocks (~> 2.10.0) + rspec-core (2.10.0) + rspec-expectations (2.10.0) + diff-lcs (~> 1.1.3) + rspec-mocks (2.10.1) + ruby-debug-base19 (0.11.25) + columnize (>= 0.3.1) + linecache19 (>= 0.5.11) + ruby_core_source (>= 0.1.4) + ruby-debug19 (0.11.6) + columnize (>= 0.3.1) + linecache19 (>= 0.5.11) + ruby-debug-base19 (>= 0.11.19) + ruby_core_source (0.1.5) + archive-tar-minitar (>= 0.5.2) thin (1.3.1) daemons (>= 1.0.9) eventmachine (>= 0.12.6) @@ -27,6 +104,15 @@ PLATFORMS DEPENDENCIES bundler (~> 1.1.0) + faker (= 1.0.1) + mongo + rack-oauth2-server! + rack-test rake (~> 0.9.0) + restkit! + rspec + ruby-debug19 + simple_oauth! sinatra! thin (~> 1.3.1) + xcoder! diff --git a/Rakefile b/Rakefile index 47c25f448d..233efb0c88 100644 --- a/Rakefile +++ b/Rakefile @@ -1,13 +1,99 @@ require 'rubygems' +require 'bundler/setup' +require 'xcoder' +require 'restkit/rake' +require 'ruby-debug' + +RestKit::Rake::ServerTask.new do |t| + t.port = 4567 + t.pid_file = 'Tests/Server/server.pid' + t.rackup_file = 'Tests/Server/server.ru' + t.log_file = 'Tests/Server/server.log' + + t.adapter(:thin) do |thin| + thin.config_file = 'Tests/Server/thin.yml' + end +end namespace :test do - desc "Run the RestKit test server" - task :server do - server_path = File.dirname(__FILE__) + '/Tests/Server/server.rb' - system("ruby \"#{server_path}\"") + task :kill_simulator do + system(%q{killall -m -KILL "iPhone Simulator"}) + end + + namespace :logic do + desc "Run the logic tests for iOS" + task :ios => :kill_simulator do + config = Xcode.project(:RestKit).target(:RestKitTests).config(:Debug) + builder = config.builder + build_dir = File.dirname(config.target.project.path) + '/Build' + builder.symroot = build_dir + '/Products' + builder.objroot = build_dir + builder.test(:sdk => 'iphonesimulator') + end + + desc "Run the logic tests for OS X" + task :osx do + config = Xcode.project(:RestKit).target(:RestKitFrameworkTests).config(:Debug) + builder = config.builder + build_dir = File.dirname(config.target.project.path) + '/Build' + builder.symroot = build_dir + '/Products' + builder.objroot = build_dir + builder.test(:sdk => 'macosx') + end + end + + desc "Run the unit tests for iOS and OS X" + task :logic => ['logic:ios', 'logic:osx'] + + namespace :application do + desc "Run the application tests for iOS" + task :ios => :kill_simulator do + config = Xcode.project(:RKApplicationTests).target('Application Tests').config(:Debug) + builder = config.builder + build_dir = File.dirname(config.target.project.path) + '/Build' + builder.symroot = build_dir + '/Products' + builder.objroot = build_dir + builder.test(:sdk => 'iphonesimulator') + end + end + + desc "Run the application tests for iOS" + task :application => 'application:ios' + + desc "Run all tests for iOS and OS X" + task :all do + Rake.application.invoke_task("test:logic") + unit_status = $?.exitstatus + Rake.application.invoke_task("test:application") + integration_status = $?.exitstatus + puts "\033[0;31m!! Unit Tests failed with exit status of #{unit_status}" if unit_status != 0 + puts "\033[0;31m!! Integration Tests failed with exit status of #{integration_status}" if integration_status != 0 + puts "\033[0;32m** All Tests executed successfully" if unit_status == 0 && integration_status == 0 + end + + task :check_mongodb do + port_check = RestKit::Server::PortCheck.new('127.0.0.1', 27017) + port_check.run + if port_check.closed? + puts "\033[0;33m!! Warning: MongoDB was not found running on port 27017" + puts "MongoDB is required for the execution of the OAuth tests. Tests in RKOAuthClientTest will NOT be executed" + `which mongo` + if $?.exitstatus == 0 + puts "Execute MongoDB via `mongod run --config /usr/local/etc/mongod.conf`" + else + puts "Install mongodb with Homebrew via `brew install mongodb`" + end + puts "\033[0m" + sleep(5) + end end end +desc 'Run all the RestKit tests' +task :test => "test:all" + +task :default => ["test:check_mongodb", "server:autostart", "test:all", "server:autostop"] + def restkit_version @restkit_version ||= ENV['VERSION'] || File.read("VERSION").chomp end @@ -28,8 +114,6 @@ def run(command, min_exit_status = 0) return $?.exitstatus end -task :default => 'test:server' - desc "Build RestKit for iOS and Mac OS X" task :build do run("xcodebuild -workspace RestKit.xcodeproj/project.xcworkspace -scheme RestKit -sdk iphonesimulator5.0 clean build") @@ -71,9 +155,11 @@ namespace :docs do run(command, 1) end - desc "Build and upload the documentation set to the remote server" - task :upload do - version = ENV['VERSION'] || File.read("VERSION").chomp + desc "Build and publish the documentation set to the remote server (using rsync over SSH)" + task :publish, :version, :destination do |t, args| + args.with_defaults(:version => File.read("VERSION").chomp, :destination => "restkit.org:/var/www/public/restkit.org/public/api/") + version = args[:version] + destination = args[:destination] puts "Generating RestKit docset for version #{version}..." command = apple_doc_command << " --keep-intermediate-files" << @@ -81,44 +167,18 @@ namespace :docs do " --docset-feed-url http://restkit.org/api/%DOCSETATOMFILENAME" << " --docset-package-url http://restkit.org/api/%DOCSETPACKAGEFILENAME --publish-docset --verbose 3 Code/" run(command, 1) - puts "Uploading docset to restkit.org..." - command = "rsync -rvpPe ssh --delete Docs/API/html/ restkit.org:/var/www/public/restkit.org/public/api/#{version}" + puts "Uploading docset to #{destination}..." + versioned_destination = File.join(destination, version) + command = "rsync -rvpPe ssh --delete Docs/API/html/ #{versioned_destination}" run(command) if $?.exitstatus == 0 - command = "rsync -rvpPe ssh Docs/API/publish/ restkit.org:/var/www/public/restkit.org/public/api/" + command = "rsync -rvpPe ssh Docs/API/publish/ #{destination}" run(command) end end end -def is_port_open?(ip, port) - require 'socket' - require 'timeout' - - begin - Timeout::timeout(1) do - begin - s = TCPSocket.new(ip, port) - s.close - return true - rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH - return false - end - end - rescue Timeout::Error - end - - return false -end - -task :ensure_server_is_running do - unless is_port_open?('127.0.0.1', 4567) - puts "Unable to find RestKit Test server listening on port 4567. Run `rake test:server` and try again." - exit(-1) - end -end - namespace :build do desc "Build all Example projects to ensure they are building properly" task :examples do @@ -143,6 +203,28 @@ namespace :build do end desc "Validate a branch is ready for merging by checking for common issues" -task :validate => [:ensure_server_is_running, :build, 'docs:check', 'uispec:all'] do +task :validate => [:build, 'docs:check', 'uispec:all'] do puts "Project state validated successfully. Proceed with merge." end + +namespace :payload do + task :generate do + require 'json' + require 'faker' + + ids = (1..25).to_a + child_ids = (50..100).to_a + child_counts = (10..25).to_a + hash = ids.inject({'parents' => []}) do |hash, parent_id| + child_count = child_counts.sample + children = (0..child_count).collect do + {'name' => Faker::Name.name, 'childID' => child_ids.sample} + end + parent = {'parentID' => parent_id, 'name' => Faker::Name.name, 'children' => children} + hash['parents'] << parent + hash + end + File.open('payload.json', 'w+') { |f| f << hash.to_json } + puts "Generated payload at: payload.json" + end +end diff --git a/Resources/PLISTs/RestKitFrameworkTests-Info.plist b/Resources/PLISTs/RestKitFrameworkTests-Info.plist index ddf878a8c8..883b278e89 100644 --- a/Resources/PLISTs/RestKitFrameworkTests-Info.plist +++ b/Resources/PLISTs/RestKitFrameworkTests-Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - org.restkit.unit-tests + org.restkit.tests CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType diff --git a/Resources/PLISTs/RestKitTests-Info.plist b/Resources/PLISTs/RestKitTests-Info.plist index ddf878a8c8..883b278e89 100644 --- a/Resources/PLISTs/RestKitTests-Info.plist +++ b/Resources/PLISTs/RestKitTests-Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - org.restkit.unit-tests + org.restkit.tests CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType diff --git a/RestKit.xcodeproj/project.pbxproj b/RestKit.xcodeproj/project.pbxproj index a824b39c92..9f55bfc2bc 100644 --- a/RestKit.xcodeproj/project.pbxproj +++ b/RestKit.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 2501405315366000004E0466 /* RKObjectiveCppTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2501405215366000004E0466 /* RKObjectiveCppTest.mm */; }; + 2501405415366000004E0466 /* RKObjectiveCppTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2501405215366000004E0466 /* RKObjectiveCppTest.mm */; }; 25055B8414EEF32A00B9C4DD /* RKMappingTest.h in Headers */ = {isa = PBXBuildFile; fileRef = 25055B8014EEF32A00B9C4DD /* RKMappingTest.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25055B8514EEF32A00B9C4DD /* RKMappingTest.h in Headers */ = {isa = PBXBuildFile; fileRef = 25055B8014EEF32A00B9C4DD /* RKMappingTest.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25055B8614EEF32A00B9C4DD /* RKMappingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25055B8114EEF32A00B9C4DD /* RKMappingTest.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; @@ -36,6 +38,8 @@ 250DF22D14C5190E0001DEFA /* RKOrderedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = 250DF22914C5190E0001DEFA /* RKOrderedDictionary.m */; }; 250DF25F14C680F90001DEFA /* RKObjectMappingProvider+Contexts.h in Headers */ = {isa = PBXBuildFile; fileRef = 250DF25E14C680F90001DEFA /* RKObjectMappingProvider+Contexts.h */; settings = {ATTRIBUTES = (Public, ); }; }; 250DF26014C680F90001DEFA /* RKObjectMappingProvider+Contexts.h in Headers */ = {isa = PBXBuildFile; fileRef = 250DF25E14C680F90001DEFA /* RKObjectMappingProvider+Contexts.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25119FB6154A34B400C6BC58 /* parents_and_children.json in Resources */ = {isa = PBXBuildFile; fileRef = 25119FB5154A34B400C6BC58 /* parents_and_children.json */; }; + 25119FB7154A34B400C6BC58 /* parents_and_children.json in Resources */ = {isa = PBXBuildFile; fileRef = 25119FB5154A34B400C6BC58 /* parents_and_children.json */; }; 2513504E14B8FE6B00A7E893 /* RKConfigurationDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 2513504D14B8FE6B00A7E893 /* RKConfigurationDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2513504F14B8FE6B00A7E893 /* RKConfigurationDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 2513504D14B8FE6B00A7E893 /* RKConfigurationDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160D1A14564E810060A5C5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25160D1914564E810060A5C5 /* Foundation.framework */; }; @@ -472,6 +476,12 @@ 2516112E1456F5520060A5C5 /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2516112D1456F5520060A5C5 /* CoreData.framework */; }; 251611301456F5590060A5C5 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2516112F1456F5590060A5C5 /* Security.framework */; }; 251611321456F56C0060A5C5 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 251611311456F56C0060A5C5 /* MobileCoreServices.framework */; }; + 252A202D153471380078F8AD /* NSArray+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 252A202B153471380078F8AD /* NSArray+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 252A202E153471380078F8AD /* NSArray+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 252A202C153471380078F8AD /* NSArray+RKAdditions.m */; }; + 252A2030153471470078F8AD /* NSArray+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 252A202C153471380078F8AD /* NSArray+RKAdditions.m */; }; + 252A20311534714D0078F8AD /* NSArray+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 252A202B153471380078F8AD /* NSArray+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 252A2034153477870078F8AD /* NSArray+RKAdditionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 252A2033153477870078F8AD /* NSArray+RKAdditionsTest.m */; }; + 252A2035153477870078F8AD /* NSArray+RKAdditionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 252A2033153477870078F8AD /* NSArray+RKAdditionsTest.m */; }; 252EFAFA14D8EAEC004863C8 /* RKEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFAF914D8EAEC004863C8 /* RKEvent.m */; }; 252EFAFB14D8EAEC004863C8 /* RKEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFAF914D8EAEC004863C8 /* RKEvent.m */; }; 252EFB0814D98F4D004863C8 /* RKSearchableManagedObjectTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFB0614D98F4D004863C8 /* RKSearchableManagedObjectTest.m */; }; @@ -496,6 +506,14 @@ 254A62BC14AD544200939BEE /* RKObjectPaginator.m in Sources */ = {isa = PBXBuildFile; fileRef = 254A62B814AD544200939BEE /* RKObjectPaginator.m */; }; 254A62C014AD591C00939BEE /* RKObjectPaginatorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 254A62BF14AD591C00939BEE /* RKObjectPaginatorTest.m */; }; 254A62C114AD591C00939BEE /* RKObjectPaginatorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 254A62BF14AD591C00939BEE /* RKObjectPaginatorTest.m */; }; + 25545959155F0527007D7625 /* RKBenchmark.h in Headers */ = {isa = PBXBuildFile; fileRef = 25545957155F0527007D7625 /* RKBenchmark.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2554595A155F0527007D7625 /* RKBenchmark.h in Headers */ = {isa = PBXBuildFile; fileRef = 25545957155F0527007D7625 /* RKBenchmark.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2554595B155F0527007D7625 /* RKBenchmark.m in Sources */ = {isa = PBXBuildFile; fileRef = 25545958155F0527007D7625 /* RKBenchmark.m */; }; + 2554595C155F0527007D7625 /* RKBenchmark.m in Sources */ = {isa = PBXBuildFile; fileRef = 25545958155F0527007D7625 /* RKBenchmark.m */; }; + 2572538D155C543000CB05ED /* RKPortCheck.h in Headers */ = {isa = PBXBuildFile; fileRef = 2572538B155C543000CB05ED /* RKPortCheck.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2572538E155C543000CB05ED /* RKPortCheck.h in Headers */ = {isa = PBXBuildFile; fileRef = 2572538B155C543000CB05ED /* RKPortCheck.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2572538F155C543000CB05ED /* RKPortCheck.m in Sources */ = {isa = PBXBuildFile; fileRef = 2572538C155C543000CB05ED /* RKPortCheck.m */; }; + 25725390155C543000CB05ED /* RKPortCheck.m in Sources */ = {isa = PBXBuildFile; fileRef = 2572538C155C543000CB05ED /* RKPortCheck.m */; }; 257ABAB015112DD500CCAA76 /* NSManagedObjectContext+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 257ABAAE15112DD400CCAA76 /* NSManagedObjectContext+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 257ABAB115112DD500CCAA76 /* NSManagedObjectContext+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 257ABAAE15112DD400CCAA76 /* NSManagedObjectContext+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 257ABAB215112DD500CCAA76 /* NSManagedObjectContext+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 257ABAAF15112DD400CCAA76 /* NSManagedObjectContext+RKAdditions.m */; }; @@ -513,6 +531,22 @@ 259C3027151280A1003066A2 /* grayArrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 25EC1AE314F8022600C3CF3F /* grayArrow@2x.png */; }; 259C3028151280A1003066A2 /* whiteArrow.png in Resources */ = {isa = PBXBuildFile; fileRef = 25EC1AE414F8022600C3CF3F /* whiteArrow.png */; }; 259C3029151280A1003066A2 /* whiteArrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 25EC1AE514F8022600C3CF3F /* whiteArrow@2x.png */; }; + 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 */; }; + 259D98551550C69A008C90F5 /* RKEntityByAttributeCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 259D98521550C69A008C90F5 /* RKEntityByAttributeCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 259D98561550C69A008C90F5 /* RKEntityByAttributeCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D98531550C69A008C90F5 /* RKEntityByAttributeCache.m */; }; + 259D98571550C69A008C90F5 /* RKEntityByAttributeCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D98531550C69A008C90F5 /* RKEntityByAttributeCache.m */; }; + 259D985A1550C6BE008C90F5 /* RKEntityByAttributeCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D98591550C6BE008C90F5 /* RKEntityByAttributeCacheTest.m */; }; + 259D985B1550C6BE008C90F5 /* RKEntityByAttributeCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D98591550C6BE008C90F5 /* RKEntityByAttributeCacheTest.m */; }; + 259D985E155218E5008C90F5 /* RKEntityCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 259D985C155218E4008C90F5 /* RKEntityCache.h */; }; + 259D985F155218E5008C90F5 /* RKEntityCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 259D985C155218E4008C90F5 /* RKEntityCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 259D9860155218E5008C90F5 /* RKEntityCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D985D155218E4008C90F5 /* RKEntityCache.m */; }; + 259D9861155218E5008C90F5 /* RKEntityCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D985D155218E4008C90F5 /* RKEntityCache.m */; }; + 259D986415521B20008C90F5 /* RKEntityCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D986315521B1F008C90F5 /* RKEntityCacheTest.m */; }; + 259D986515521B20008C90F5 /* RKEntityCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D986315521B1F008C90F5 /* RKEntityCacheTest.m */; }; + 25A2476E153E667E003240B6 /* RKCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A2476D153E667E003240B6 /* RKCacheTest.m */; }; + 25A2476F153E667E003240B6 /* RKCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A2476D153E667E003240B6 /* RKCacheTest.m */; }; 25A34245147D8AAA0009758D /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25A34244147D8AAA0009758D /* Security.framework */; }; 25B408261491CDDC00F21111 /* RKDirectory.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B408241491CDDB00F21111 /* RKDirectory.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25B408271491CDDC00F21111 /* RKDirectory.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B408241491CDDB00F21111 /* RKDirectory.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -630,6 +664,8 @@ 25B6EA0414CF943E00B1E881 /* RKMutableBlockDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9FC14CF943E00B1E881 /* RKMutableBlockDictionary.m */; }; 25B6EA0614CF946400B1E881 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25B6EA0514CF946300B1E881 /* QuartzCore.framework */; }; 25B6EA0814CF947E00B1E881 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25B6EA0714CF947D00B1E881 /* CoreGraphics.framework */; }; + 25C954A715542A47005C9E08 /* RKTestConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 25C954A415542A47005C9E08 /* RKTestConstants.m */; }; + 25C954A815542A47005C9E08 /* RKTestConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 25C954A415542A47005C9E08 /* RKTestConstants.m */; }; 25CA7A8F14EC570200888FF8 /* RKObjectMappingDefinition.m in Sources */ = {isa = PBXBuildFile; fileRef = 25CA7A8E14EC570100888FF8 /* RKObjectMappingDefinition.m */; }; 25CA7A9014EC570200888FF8 /* RKObjectMappingDefinition.m in Sources */ = {isa = PBXBuildFile; fileRef = 25CA7A8E14EC570100888FF8 /* RKObjectMappingDefinition.m */; }; 25CA7A9114EC5C2D00888FF8 /* RKTestFixture.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFB2114D9B35D004863C8 /* RKTestFixture.m */; }; @@ -639,14 +675,14 @@ 25DB7509151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25DB7507151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m */; }; 25E36E0215195CED00F9E448 /* RKFetchRequestMappingCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25E36E0115195CED00F9E448 /* RKFetchRequestMappingCacheTest.m */; }; 25E36E0315195CED00F9E448 /* RKFetchRequestMappingCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25E36E0115195CED00F9E448 /* RKFetchRequestMappingCacheTest.m */; }; + 25E4DAB4156DA97F00A5C84B /* RKTableControllerTestDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 25E4DAB2156DA97F00A5C84B /* RKTableControllerTestDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25E4DAB5156DA97F00A5C84B /* RKTableControllerTestDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 25E4DAB2156DA97F00A5C84B /* RKTableControllerTestDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25E4DAB6156DA97F00A5C84B /* RKTableControllerTestDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 25E4DAB3156DA97F00A5C84B /* RKTableControllerTestDelegate.m */; }; + 25E4DAB7156DA97F00A5C84B /* RKTableControllerTestDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 25E4DAB3156DA97F00A5C84B /* RKTableControllerTestDelegate.m */; }; 25EC1A2C14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.h in Headers */ = {isa = PBXBuildFile; fileRef = 25EC1A2A14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25EC1A2D14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.h in Headers */ = {isa = PBXBuildFile; fileRef = 25EC1A2A14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25EC1A2E14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.m in Sources */ = {isa = PBXBuildFile; fileRef = 25EC1A2B14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.m */; }; 25EC1A2F14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.m in Sources */ = {isa = PBXBuildFile; fileRef = 25EC1A2B14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.m */; }; - 25EC1A3414F72AF000C3CF3F /* RKInMemoryEntityCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 7394DF4114CF1BB200CE7BCE /* RKInMemoryEntityCache.m */; }; - 25EC1A3614F72AF100C3CF3F /* RKInMemoryEntityCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 7394DF4114CF1BB200CE7BCE /* RKInMemoryEntityCache.m */; }; - 25EC1A3714F72B0100C3CF3F /* RKInMemoryEntityCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 7394DF4014CF1BB200CE7BCE /* RKInMemoryEntityCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25EC1A3814F72B0200C3CF3F /* RKInMemoryEntityCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 7394DF4014CF1BB200CE7BCE /* RKInMemoryEntityCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25EC1A3914F72B0900C3CF3F /* RKFetchRequestManagedObjectCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 7394DF3814CF168C00CE7BCE /* RKFetchRequestManagedObjectCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25EC1A3A14F72B0A00C3CF3F /* RKFetchRequestManagedObjectCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 7394DF3814CF168C00CE7BCE /* RKFetchRequestManagedObjectCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25EC1A3B14F72B1300C3CF3F /* RKFetchRequestManagedObjectCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 7394DF3914CF168C00CE7BCE /* RKFetchRequestManagedObjectCache.m */; }; @@ -662,10 +698,8 @@ 25EC1A4614F7393E00C3CF3F /* RKObjectMappingProviderContextEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = 73DA8E2114D1C3870054DD73 /* RKObjectMappingProviderContextEntry.m */; }; 25EC1A4714F7394100C3CF3F /* RKObjectMappingProviderContextEntry.h in Headers */ = {isa = PBXBuildFile; fileRef = 73DA8E2014D1C3870054DD73 /* RKObjectMappingProviderContextEntry.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25EC1A4814F7394200C3CF3F /* RKObjectMappingProviderContextEntry.h in Headers */ = {isa = PBXBuildFile; fileRef = 73DA8E2014D1C3870054DD73 /* RKObjectMappingProviderContextEntry.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25EC1A4A14F73CA200C3CF3F /* RKInMemoryEntityCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25EC1A4914F73CA200C3CF3F /* RKInMemoryEntityCacheTest.m */; }; - 25EC1A4B14F73CA200C3CF3F /* RKInMemoryEntityCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25EC1A4914F73CA200C3CF3F /* RKInMemoryEntityCacheTest.m */; }; - 25EC1A6314F7402A00C3CF3F /* RKManagedObjectMappingCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 7394DF3514CF157A00CE7BCE /* RKManagedObjectMappingCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25EC1A6514F7402A00C3CF3F /* RKManagedObjectMappingCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 7394DF3514CF157A00CE7BCE /* RKManagedObjectMappingCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25EC1A6314F7402A00C3CF3F /* RKManagedObjectCaching.h in Headers */ = {isa = PBXBuildFile; fileRef = 7394DF3514CF157A00CE7BCE /* RKManagedObjectCaching.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25EC1A6514F7402A00C3CF3F /* RKManagedObjectCaching.h in Headers */ = {isa = PBXBuildFile; fileRef = 7394DF3514CF157A00CE7BCE /* RKManagedObjectCaching.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25EC1ABC14F8019F00C3CF3F /* RKRefreshGestureRecognizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 25EC1AB814F8019F00C3CF3F /* RKRefreshGestureRecognizer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25EC1ABD14F8019F00C3CF3F /* RKRefreshGestureRecognizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 25EC1AB814F8019F00C3CF3F /* RKRefreshGestureRecognizer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25EC1ABE14F8019F00C3CF3F /* RKRefreshGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 25EC1AB914F8019F00C3CF3F /* RKRefreshGestureRecognizer.m */; }; @@ -741,6 +775,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2501405215366000004E0466 /* RKObjectiveCppTest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RKObjectiveCppTest.mm; sourceTree = ""; }; 25055B8014EEF32A00B9C4DD /* RKMappingTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKMappingTest.h; path = Testing/RKMappingTest.h; sourceTree = ""; }; 25055B8114EEF32A00B9C4DD /* RKMappingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKMappingTest.m; path = Testing/RKMappingTest.m; sourceTree = ""; }; 25055B8214EEF32A00B9C4DD /* RKTestFactory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKTestFactory.h; path = Testing/RKTestFactory.h; sourceTree = ""; }; @@ -755,6 +790,7 @@ 250DF22814C5190E0001DEFA /* RKOrderedDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKOrderedDictionary.h; sourceTree = ""; }; 250DF22914C5190E0001DEFA /* RKOrderedDictionary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKOrderedDictionary.m; sourceTree = ""; }; 250DF25E14C680F90001DEFA /* RKObjectMappingProvider+Contexts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RKObjectMappingProvider+Contexts.h"; sourceTree = ""; }; + 25119FB5154A34B400C6BC58 /* parents_and_children.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = parents_and_children.json; sourceTree = ""; }; 2513504D14B8FE6B00A7E893 /* RKConfigurationDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKConfigurationDelegate.h; sourceTree = ""; }; 25160D1614564E810060A5C5 /* libRestKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRestKit.a; sourceTree = BUILT_PRODUCTS_DIR; }; 25160D1914564E810060A5C5 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; @@ -1007,6 +1043,9 @@ 2516112D1456F5520060A5C5 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 2516112F1456F5590060A5C5 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 251611311456F56C0060A5C5 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; + 252A202B153471380078F8AD /* NSArray+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+RKAdditions.h"; sourceTree = ""; }; + 252A202C153471380078F8AD /* NSArray+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+RKAdditions.m"; sourceTree = ""; }; + 252A2033153477870078F8AD /* NSArray+RKAdditionsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+RKAdditionsTest.m"; sourceTree = ""; }; 252EFAF814D8EAEC004863C8 /* RKEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKEvent.h; sourceTree = ""; }; 252EFAF914D8EAEC004863C8 /* RKEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKEvent.m; sourceTree = ""; }; 252EFAFC14D8EB30004863C8 /* RKTestNotificationObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKTestNotificationObserver.h; path = Testing/RKTestNotificationObserver.h; sourceTree = ""; }; @@ -1023,11 +1062,23 @@ 254A62B714AD544200939BEE /* RKObjectPaginator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectPaginator.h; sourceTree = ""; }; 254A62B814AD544200939BEE /* RKObjectPaginator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectPaginator.m; sourceTree = ""; }; 254A62BF14AD591C00939BEE /* RKObjectPaginatorTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectPaginatorTest.m; sourceTree = ""; }; + 25545957155F0527007D7625 /* RKBenchmark.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKBenchmark.h; sourceTree = ""; }; + 25545958155F0527007D7625 /* RKBenchmark.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKBenchmark.m; sourceTree = ""; }; + 2572538B155C543000CB05ED /* RKPortCheck.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKPortCheck.h; sourceTree = ""; }; + 2572538C155C543000CB05ED /* RKPortCheck.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKPortCheck.m; sourceTree = ""; }; 257ABAAE15112DD400CCAA76 /* NSManagedObjectContext+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObjectContext+RKAdditions.h"; sourceTree = ""; }; 257ABAAF15112DD400CCAA76 /* NSManagedObjectContext+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObjectContext+RKAdditions.m"; sourceTree = ""; }; 257ABAB41511371C00CCAA76 /* NSManagedObject+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObject+RKAdditions.h"; sourceTree = ""; }; 257ABAB51511371D00CCAA76 /* NSManagedObject+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+RKAdditions.m"; sourceTree = ""; }; 259C301615128079003066A2 /* RestKitResources.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RestKitResources.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; + 259D983B154F6C90008C90F5 /* benchmark_parents_and_children.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = benchmark_parents_and_children.json; sourceTree = ""; }; + 259D98521550C69A008C90F5 /* RKEntityByAttributeCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKEntityByAttributeCache.h; sourceTree = ""; }; + 259D98531550C69A008C90F5 /* RKEntityByAttributeCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKEntityByAttributeCache.m; sourceTree = ""; }; + 259D98591550C6BE008C90F5 /* RKEntityByAttributeCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKEntityByAttributeCacheTest.m; sourceTree = ""; }; + 259D985C155218E4008C90F5 /* RKEntityCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKEntityCache.h; sourceTree = ""; }; + 259D985D155218E4008C90F5 /* RKEntityCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKEntityCache.m; sourceTree = ""; }; + 259D986315521B1F008C90F5 /* RKEntityCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKEntityCacheTest.m; sourceTree = ""; }; + 25A2476D153E667E003240B6 /* RKCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKCacheTest.m; sourceTree = ""; }; 25A34244147D8AAA0009758D /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = SDKs/MacOSX10.7.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; }; 25B408241491CDDB00F21111 /* RKDirectory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKDirectory.h; sourceTree = ""; }; 25B408251491CDDB00F21111 /* RKDirectory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKDirectory.m; sourceTree = ""; }; @@ -1121,13 +1172,15 @@ 25B6E9FC14CF943E00B1E881 /* RKMutableBlockDictionary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKMutableBlockDictionary.m; sourceTree = ""; }; 25B6EA0514CF946300B1E881 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 25B6EA0714CF947D00B1E881 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 25C954A415542A47005C9E08 /* RKTestConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKTestConstants.m; path = Testing/RKTestConstants.m; sourceTree = ""; }; 25CA7A8E14EC570100888FF8 /* RKObjectMappingDefinition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappingDefinition.m; sourceTree = ""; }; 25CAAA9315254E7800CAE5D7 /* ArrayOfHumans.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ArrayOfHumans.json; sourceTree = ""; }; 25DB7507151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+ActiveRecordTest.m"; sourceTree = ""; }; 25E36E0115195CED00F9E448 /* RKFetchRequestMappingCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKFetchRequestMappingCacheTest.m; sourceTree = ""; }; + 25E4DAB2156DA97F00A5C84B /* RKTableControllerTestDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKTableControllerTestDelegate.h; path = Testing/RKTableControllerTestDelegate.h; sourceTree = ""; }; + 25E4DAB3156DA97F00A5C84B /* RKTableControllerTestDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKTableControllerTestDelegate.m; path = Testing/RKTableControllerTestDelegate.m; sourceTree = ""; }; 25EC1A2A14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RKObjectManager+RKTableController.h"; sourceTree = ""; }; 25EC1A2B14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RKObjectManager+RKTableController.m"; sourceTree = ""; }; - 25EC1A4914F73CA200C3CF3F /* RKInMemoryEntityCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKInMemoryEntityCacheTest.m; sourceTree = ""; }; 25EC1AB814F8019F00C3CF3F /* RKRefreshGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKRefreshGestureRecognizer.h; sourceTree = ""; }; 25EC1AB914F8019F00C3CF3F /* RKRefreshGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKRefreshGestureRecognizer.m; sourceTree = ""; }; 25EC1ABA14F8019F00C3CF3F /* RKRefreshTriggerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKRefreshTriggerView.h; sourceTree = ""; }; @@ -1151,6 +1204,7 @@ 25EC1B3814F84B5C00C3CF3F /* UIImage+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+RKAdditions.m"; sourceTree = ""; }; 25FABED414E37A2B00E609E7 /* RKTestResponseLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKTestResponseLoader.h; path = Testing/RKTestResponseLoader.h; sourceTree = ""; }; 25FABED514E37A2B00E609E7 /* RKTestResponseLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKTestResponseLoader.m; path = Testing/RKTestResponseLoader.m; sourceTree = ""; }; + 41A4EBF715374D1800740BC8 /* redirection.rb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.ruby; path = redirection.rb; sourceTree = ""; }; 49A66B0814CEFB0400A6F062 /* LICENCE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = LICENCE; path = XMLReader/LICENCE; sourceTree = ""; }; 49A66B0914CEFB0400A6F062 /* XMLReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = XMLReader.h; path = XMLReader/XMLReader.h; sourceTree = ""; }; 49A66B0A14CEFB0400A6F062 /* XMLReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMLReader.m; path = XMLReader/XMLReader.m; sourceTree = ""; }; @@ -1162,13 +1216,11 @@ 49D2759C14C9EF1E0090845D /* README.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = README.txt; path = iso8601parser/README.txt; sourceTree = ""; }; 49D275AB14C9F3020090845D /* RKISO8601DateFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKISO8601DateFormatter.h; sourceTree = ""; }; 49D275AC14C9F3020090845D /* RKISO8601DateFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKISO8601DateFormatter.m; sourceTree = ""; }; - 7394DF3514CF157A00CE7BCE /* RKManagedObjectMappingCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKManagedObjectMappingCache.h; sourceTree = ""; }; + 7394DF3514CF157A00CE7BCE /* RKManagedObjectCaching.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKManagedObjectCaching.h; sourceTree = ""; }; 7394DF3814CF168C00CE7BCE /* RKFetchRequestManagedObjectCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKFetchRequestManagedObjectCache.h; sourceTree = ""; }; 7394DF3914CF168C00CE7BCE /* RKFetchRequestManagedObjectCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKFetchRequestManagedObjectCache.m; sourceTree = ""; }; 7394DF3C14CF19F200CE7BCE /* RKInMemoryManagedObjectCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKInMemoryManagedObjectCache.h; sourceTree = ""; }; 7394DF3D14CF19F200CE7BCE /* RKInMemoryManagedObjectCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKInMemoryManagedObjectCache.m; sourceTree = ""; }; - 7394DF4014CF1BB200CE7BCE /* RKInMemoryEntityCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKInMemoryEntityCache.h; sourceTree = ""; }; - 7394DF4114CF1BB200CE7BCE /* RKInMemoryEntityCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKInMemoryEntityCache.m; sourceTree = ""; }; 73D3907114CA19F90093E3D6 /* parent.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = parent.json; sourceTree = ""; }; 73D3907314CA1A4A0093E3D6 /* child.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = child.json; sourceTree = ""; }; 73D3907814CA1D710093E3D6 /* channels.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = channels.xml; sourceTree = ""; }; @@ -1329,15 +1381,13 @@ 25160D48145650490060A5C5 /* NSManagedObject+ActiveRecord.m */, 7394DF3814CF168C00CE7BCE /* RKFetchRequestManagedObjectCache.h */, 7394DF3914CF168C00CE7BCE /* RKFetchRequestManagedObjectCache.m */, - 7394DF4014CF1BB200CE7BCE /* RKInMemoryEntityCache.h */, - 7394DF4114CF1BB200CE7BCE /* RKInMemoryEntityCache.m */, 7394DF3C14CF19F200CE7BCE /* RKInMemoryManagedObjectCache.h */, 7394DF3D14CF19F200CE7BCE /* RKInMemoryManagedObjectCache.m */, 25160D4A145650490060A5C5 /* RKManagedObjectLoader.h */, 25160D4B145650490060A5C5 /* RKManagedObjectLoader.m */, 25160D4C145650490060A5C5 /* RKManagedObjectMapping.h */, 25160D4D145650490060A5C5 /* RKManagedObjectMapping.m */, - 7394DF3514CF157A00CE7BCE /* RKManagedObjectMappingCache.h */, + 7394DF3514CF157A00CE7BCE /* RKManagedObjectCaching.h */, 25160D4E145650490060A5C5 /* RKManagedObjectMappingOperation.h */, 25160D4F145650490060A5C5 /* RKManagedObjectMappingOperation.m */, 25160D50145650490060A5C5 /* RKManagedObjectSeeder.h */, @@ -1356,6 +1406,10 @@ 257ABAB51511371D00CCAA76 /* NSManagedObject+RKAdditions.m */, 25079C6D151B93DB00266AE7 /* NSEntityDescription+RKAdditions.h */, 25079C6E151B93DB00266AE7 /* NSEntityDescription+RKAdditions.m */, + 259D98521550C69A008C90F5 /* RKEntityByAttributeCache.h */, + 259D98531550C69A008C90F5 /* RKEntityByAttributeCache.m */, + 259D985C155218E4008C90F5 /* RKEntityCache.h */, + 259D985D155218E4008C90F5 /* RKEntityCache.m */, ); path = CoreData; sourceTree = ""; @@ -1453,6 +1507,8 @@ 25160DA2145650490060A5C5 /* Support */ = { isa = PBXGroup; children = ( + 25545957155F0527007D7625 /* RKBenchmark.h */, + 25545958155F0527007D7625 /* RKBenchmark.m */, 25B6E9F914CF943D00B1E881 /* RKCache.h */, 25B6E9FA14CF943E00B1E881 /* RKCache.m */, 25B6E9FB14CF943E00B1E881 /* RKMutableBlockDictionary.h */, @@ -1495,6 +1551,10 @@ 25B6E95714CF7A1C00B1E881 /* RKErrors.m */, 252EFB1914D9A7CB004863C8 /* NSBundle+RKAdditions.h */, 252EFB1A14D9A7CB004863C8 /* NSBundle+RKAdditions.m */, + 252A202B153471380078F8AD /* NSArray+RKAdditions.h */, + 252A202C153471380078F8AD /* NSArray+RKAdditions.m */, + 2572538B155C543000CB05ED /* RKPortCheck.h */, + 2572538C155C543000CB05ED /* RKPortCheck.m */, ); path = Support; sourceTree = ""; @@ -1647,7 +1707,6 @@ 25160FC61456F2330060A5C5 /* CoreData */ = { isa = PBXGroup; children = ( - 25EC1A4914F73CA200C3CF3F /* RKInMemoryEntityCacheTest.m */, 252EFB0614D98F4D004863C8 /* RKSearchableManagedObjectTest.m */, 252EFB0714D98F4D004863C8 /* RKSearchWordObserverTest.m */, 25160FC71456F2330060A5C5 /* RKManagedObjectLoaderTest.m */, @@ -1658,6 +1717,8 @@ 25E36E0115195CED00F9E448 /* RKFetchRequestMappingCacheTest.m */, 25079C75151B952200266AE7 /* NSEntityDescription+RKAdditionsTest.m */, 25DB7507151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m */, + 259D98591550C6BE008C90F5 /* RKEntityByAttributeCacheTest.m */, + 259D986315521B1F008C90F5 /* RKEntityCacheTest.m */, ); name = CoreData; path = Logic/CoreData; @@ -1685,6 +1746,7 @@ 25160FD01456F2330060A5C5 /* JSON */ = { isa = PBXGroup; children = ( + 259D983B154F6C90008C90F5 /* benchmark_parents_and_children.json */, 252EFB2714DA0689004863C8 /* NakedEvents.json */, 25160FD11456F2330060A5C5 /* ArrayOfNestedDictionaries.json */, 25160FD21456F2330060A5C5 /* ArrayOfResults.json */, @@ -1704,6 +1766,7 @@ 25160FE71456F2330060A5C5 /* user.json */, 25160FE81456F2330060A5C5 /* users.json */, 25CAAA9315254E7800CAE5D7 /* ArrayOfHumans.json */, + 25119FB5154A34B400C6BC58 /* parents_and_children.json */, ); path = JSON; sourceTree = ""; @@ -1873,6 +1936,7 @@ 2516104A1456F2330060A5C5 /* network */ = { isa = PBXGroup; children = ( + 41A4EBF715374D1800740BC8 /* redirection.rb */, 2516104B1456F2330060A5C5 /* authentication.rb */, 2516104C1456F2330060A5C5 /* etags.rb */, 2516104D1456F2330060A5C5 /* oauth2.rb */, @@ -1891,6 +1955,9 @@ 251610551456F2330060A5C5 /* RKJSONParserJSONKitTest.m */, 251610561456F2330060A5C5 /* RKPathMatcherTest.m */, 251610571456F2330060A5C5 /* RKXMLParserTest.m */, + 252A2033153477870078F8AD /* NSArray+RKAdditionsTest.m */, + 2501405215366000004E0466 /* RKObjectiveCppTest.mm */, + 25A2476D153E667E003240B6 /* RKCacheTest.m */, ); name = Support; path = Logic/Support; @@ -1912,6 +1979,9 @@ 25055B8D14EEF40000B9C4DD /* RKMappingTestExpectation.h */, 25055B8E14EEF40000B9C4DD /* RKMappingTestExpectation.m */, 252EFB2414D9B6F2004863C8 /* Testing.h */, + 25C954A415542A47005C9E08 /* RKTestConstants.m */, + 25E4DAB2156DA97F00A5C84B /* RKTableControllerTestDelegate.h */, + 25E4DAB3156DA97F00A5C84B /* RKTableControllerTestDelegate.m */, ); name = Testing; sourceTree = ""; @@ -2186,18 +2256,23 @@ 25055B8814EEF32A00B9C4DD /* RKTestFactory.h in Headers */, 25055B8F14EEF40000B9C4DD /* RKMappingTestExpectation.h in Headers */, 25EC1A2C14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.h in Headers */, - 25EC1A3714F72B0100C3CF3F /* RKInMemoryEntityCache.h in Headers */, 25EC1A3914F72B0900C3CF3F /* RKFetchRequestManagedObjectCache.h in Headers */, 25EC1A3D14F72B2800C3CF3F /* RKInMemoryManagedObjectCache.h in Headers */, 25EC1A4114F72C7200C3CF3F /* RKObjectMappingProvider+CoreData.h in Headers */, 25EC1A4714F7394100C3CF3F /* RKObjectMappingProviderContextEntry.h in Headers */, - 25EC1A6314F7402A00C3CF3F /* RKManagedObjectMappingCache.h in Headers */, + 25EC1A6314F7402A00C3CF3F /* RKManagedObjectCaching.h in Headers */, 25EC1ABC14F8019F00C3CF3F /* RKRefreshGestureRecognizer.h in Headers */, 25EC1AC014F8019F00C3CF3F /* RKRefreshTriggerView.h in Headers */, 25EC1B3914F84B5D00C3CF3F /* UIImage+RKAdditions.h in Headers */, 257ABAB015112DD500CCAA76 /* NSManagedObjectContext+RKAdditions.h in Headers */, 257ABAB61511371E00CCAA76 /* NSManagedObject+RKAdditions.h in Headers */, 25079C6F151B93DB00266AE7 /* NSEntityDescription+RKAdditions.h in Headers */, + 252A202D153471380078F8AD /* NSArray+RKAdditions.h in Headers */, + 2572538D155C543000CB05ED /* RKPortCheck.h in Headers */, + 259D98541550C69A008C90F5 /* RKEntityByAttributeCache.h in Headers */, + 259D985E155218E5008C90F5 /* RKEntityCache.h in Headers */, + 25545959155F0527007D7625 /* RKBenchmark.h in Headers */, + 25E4DAB4156DA97F00A5C84B /* RKTableControllerTestDelegate.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2306,18 +2381,23 @@ 25055B8914EEF32A00B9C4DD /* RKTestFactory.h in Headers */, 25055B9014EEF40000B9C4DD /* RKMappingTestExpectation.h in Headers */, 25EC1A2D14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.h in Headers */, - 25EC1A3814F72B0200C3CF3F /* RKInMemoryEntityCache.h in Headers */, 25EC1A3A14F72B0A00C3CF3F /* RKFetchRequestManagedObjectCache.h in Headers */, 25EC1A3E14F72B2900C3CF3F /* RKInMemoryManagedObjectCache.h in Headers */, 25EC1A4214F72C7300C3CF3F /* RKObjectMappingProvider+CoreData.h in Headers */, 25EC1A4814F7394200C3CF3F /* RKObjectMappingProviderContextEntry.h in Headers */, - 25EC1A6514F7402A00C3CF3F /* RKManagedObjectMappingCache.h in Headers */, + 25EC1A6514F7402A00C3CF3F /* RKManagedObjectCaching.h in Headers */, 25EC1ABD14F8019F00C3CF3F /* RKRefreshGestureRecognizer.h in Headers */, 25EC1AC114F8019F00C3CF3F /* RKRefreshTriggerView.h in Headers */, 25EC1B3A14F84B5D00C3CF3F /* UIImage+RKAdditions.h in Headers */, 257ABAB115112DD500CCAA76 /* NSManagedObjectContext+RKAdditions.h in Headers */, 257ABAB71511371E00CCAA76 /* NSManagedObject+RKAdditions.h in Headers */, 25079C70151B93DB00266AE7 /* NSEntityDescription+RKAdditions.h in Headers */, + 252A20311534714D0078F8AD /* NSArray+RKAdditions.h in Headers */, + 2572538E155C543000CB05ED /* RKPortCheck.h in Headers */, + 259D98551550C69A008C90F5 /* RKEntityByAttributeCache.h in Headers */, + 259D985F155218E5008C90F5 /* RKEntityCache.h in Headers */, + 2554595A155F0527007D7625 /* RKBenchmark.h in Headers */, + 25E4DAB5156DA97F00A5C84B /* RKTableControllerTestDelegate.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2487,6 +2567,8 @@ 25B6E9BE14CF829400B1E881 /* OCMock-Info.plist in Resources */, 252EFB2814DA0689004863C8 /* NakedEvents.json in Resources */, 25CAAA9415254E7800CAE5D7 /* ArrayOfHumans.json in Resources */, + 25119FB6154A34B400C6BC58 /* parents_and_children.json in Resources */, + 259D983C154F6C90008C90F5 /* benchmark_parents_and_children.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2547,6 +2629,8 @@ 25B6E9BF14CF829400B1E881 /* OCMock-Info.plist in Resources */, 252EFB2914DA0689004863C8 /* NakedEvents.json in Resources */, 25CAAA9515254E7800CAE5D7 /* ArrayOfHumans.json in Resources */, + 25119FB7154A34B400C6BC58 /* parents_and_children.json in Resources */, + 259D983D154F6C90008C90F5 /* benchmark_parents_and_children.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2706,7 +2790,6 @@ 25055B8A14EEF32A00B9C4DD /* RKTestFactory.m in Sources */, 25055B9114EEF40000B9C4DD /* RKMappingTestExpectation.m in Sources */, 25EC1A2E14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.m in Sources */, - 25EC1A3414F72AF000C3CF3F /* RKInMemoryEntityCache.m in Sources */, 25EC1A3B14F72B1300C3CF3F /* RKFetchRequestManagedObjectCache.m in Sources */, 25EC1A3F14F72B3100C3CF3F /* RKInMemoryManagedObjectCache.m in Sources */, 25EC1A4314F72D0D00C3CF3F /* RKObjectMappingProvider+CoreData.m in Sources */, @@ -2717,6 +2800,13 @@ 257ABAB215112DD500CCAA76 /* NSManagedObjectContext+RKAdditions.m in Sources */, 257ABAB81511371E00CCAA76 /* NSManagedObject+RKAdditions.m in Sources */, 25079C71151B93DB00266AE7 /* NSEntityDescription+RKAdditions.m in Sources */, + 252A202E153471380078F8AD /* NSArray+RKAdditions.m in Sources */, + 25C954A715542A47005C9E08 /* RKTestConstants.m in Sources */, + 2572538F155C543000CB05ED /* RKPortCheck.m in Sources */, + 259D98561550C69A008C90F5 /* RKEntityByAttributeCache.m in Sources */, + 259D9860155218E5008C90F5 /* RKEntityCache.m in Sources */, + 2554595B155F0527007D7625 /* RKBenchmark.m in Sources */, + 25E4DAB6156DA97F00A5C84B /* RKTableControllerTestDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2796,10 +2886,14 @@ 252EFB0814D98F4D004863C8 /* RKSearchableManagedObjectTest.m in Sources */, 252EFB0A14D98F4D004863C8 /* RKSearchWordObserverTest.m in Sources */, 252EFB0D14D98F76004863C8 /* RKMutableBlockDictionaryTest.m in Sources */, - 25EC1A4A14F73CA200C3CF3F /* RKInMemoryEntityCacheTest.m in Sources */, 25E36E0215195CED00F9E448 /* RKFetchRequestMappingCacheTest.m in Sources */, 25079C76151B952200266AE7 /* NSEntityDescription+RKAdditionsTest.m in Sources */, 25DB7508151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m in Sources */, + 252A2034153477870078F8AD /* NSArray+RKAdditionsTest.m in Sources */, + 2501405315366000004E0466 /* RKObjectiveCppTest.mm in Sources */, + 25A2476E153E667E003240B6 /* RKCacheTest.m in Sources */, + 259D985A1550C6BE008C90F5 /* RKEntityByAttributeCacheTest.m in Sources */, + 259D986415521B20008C90F5 /* RKEntityCacheTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2886,7 +2980,6 @@ 25055B8B14EEF32A00B9C4DD /* RKTestFactory.m in Sources */, 25055B9214EEF40000B9C4DD /* RKMappingTestExpectation.m in Sources */, 25EC1A2F14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.m in Sources */, - 25EC1A3614F72AF100C3CF3F /* RKInMemoryEntityCache.m in Sources */, 25EC1A3C14F72B1400C3CF3F /* RKFetchRequestManagedObjectCache.m in Sources */, 25EC1A4014F72B3300C3CF3F /* RKInMemoryManagedObjectCache.m in Sources */, 25EC1A4614F7393E00C3CF3F /* RKObjectMappingProviderContextEntry.m in Sources */, @@ -2897,6 +2990,13 @@ 257ABAB91511371E00CCAA76 /* NSManagedObject+RKAdditions.m in Sources */, 25079C72151B93DB00266AE7 /* NSEntityDescription+RKAdditions.m in Sources */, 250B849E152B6F63002581F9 /* RKObjectMappingProvider+CoreData.m in Sources */, + 252A2030153471470078F8AD /* NSArray+RKAdditions.m in Sources */, + 25C954A815542A47005C9E08 /* RKTestConstants.m in Sources */, + 25725390155C543000CB05ED /* RKPortCheck.m in Sources */, + 259D98571550C69A008C90F5 /* RKEntityByAttributeCache.m in Sources */, + 259D9861155218E5008C90F5 /* RKEntityCache.m in Sources */, + 2554595C155F0527007D7625 /* RKBenchmark.m in Sources */, + 25E4DAB7156DA97F00A5C84B /* RKTableControllerTestDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2976,10 +3076,14 @@ 252EFB0914D98F4D004863C8 /* RKSearchableManagedObjectTest.m in Sources */, 252EFB0B14D98F4D004863C8 /* RKSearchWordObserverTest.m in Sources */, 252EFB0E14D98F76004863C8 /* RKMutableBlockDictionaryTest.m in Sources */, - 25EC1A4B14F73CA200C3CF3F /* RKInMemoryEntityCacheTest.m in Sources */, 25E36E0315195CED00F9E448 /* RKFetchRequestMappingCacheTest.m in Sources */, 25079C77151B952200266AE7 /* NSEntityDescription+RKAdditionsTest.m in Sources */, 25DB7509151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m in Sources */, + 252A2035153477870078F8AD /* NSArray+RKAdditionsTest.m in Sources */, + 2501405415366000004E0466 /* RKObjectiveCppTest.mm in Sources */, + 25A2476F153E667E003240B6 /* RKCacheTest.m in Sources */, + 259D985B1550C6BE008C90F5 /* RKEntityByAttributeCacheTest.m in Sources */, + 259D986515521B20008C90F5 /* RKEntityCacheTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3165,6 +3269,7 @@ ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = RestKit; SDKROOT = macosx; + SKIP_INSTALL = YES; WRAPPER_EXTENSION = framework; }; name = Debug; @@ -3191,6 +3296,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.7; PRODUCT_NAME = RestKit; SDKROOT = macosx; + SKIP_INSTALL = YES; WRAPPER_EXTENSION = framework; }; name = Release; diff --git a/Tests/Application/RKApplicationTests.xcodeproj/project.pbxproj b/Tests/Application/RKApplicationTests.xcodeproj/project.pbxproj index 777670bc85..afd7c2cb78 100644 --- a/Tests/Application/RKApplicationTests.xcodeproj/project.pbxproj +++ b/Tests/Application/RKApplicationTests.xcodeproj/project.pbxproj @@ -111,6 +111,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 252A204215347F0B0078F8AD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 253B489814E341C500B0483F /* RestKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 259C301615128079003066A2; + remoteInfo = RestKitResources; + }; 253B488514E3415800B0483F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 253B485614E3415800B0483F /* Project object */; @@ -427,6 +434,7 @@ 253B48A514E341C500B0483F /* RestKitTests.octest */, 253B48A714E341C500B0483F /* RestKit.framework */, 253B48A914E341C500B0483F /* RestKitFrameworkTests.octest */, + 252A204315347F0B0078F8AD /* RestKitResources.bundle */, ); name = Products; sourceTree = ""; @@ -698,7 +706,7 @@ 253B485614E3415800B0483F /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0420; + LastUpgradeCheck = 0430; ORGANIZATIONNAME = RestKit; }; buildConfigurationList = 253B485914E3415800B0483F /* Build configuration list for PBXProject "RKApplicationTests" */; @@ -726,6 +734,13 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ + 252A204315347F0B0078F8AD /* RestKitResources.bundle */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = RestKitResources.bundle; + remoteRef = 252A204215347F0B0078F8AD /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 253B48A314E341C500B0483F /* libRestKit.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -824,7 +839,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n"; + shellScript = "# Run the unit tests in this test bundle.\n\"${SRCROOT}/../RunPlatformUnitTests\"\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -981,6 +996,7 @@ buildSettings = { GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "RKApplicationTests/App/RKApplicationTests-Prefix.pch"; + HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers\""; INFOPLIST_FILE = "RKApplicationTests/App/RKApplicationTests-Info.plist"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -993,6 +1009,7 @@ buildSettings = { GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "RKApplicationTests/App/RKApplicationTests-Prefix.pch"; + HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers\""; INFOPLIST_FILE = "RKApplicationTests/App/RKApplicationTests-Info.plist"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1015,13 +1032,13 @@ HEADER_SEARCH_PATHS = ( "$(SRCROOT)/../Models", "$(SRCROOT)/..", - "$(SRCROOT)/../../Build", + "\"$(BUILT_PRODUCTS_DIR)/../../Headers\"", "$(SRCROOT)/../Vendor/OCMock", ); INFOPLIST_FILE = "RKApplicationTests/Tests/RKApplicationTestsTests-Info.plist"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUNDLE_LOADER)"; - USER_HEADER_SEARCH_PATHS = "$(SRCROOT)/../../Build/RestKit"; + USER_HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers/RestKit\""; WRAPPER_EXTENSION = octest; }; name = Debug; @@ -1041,13 +1058,13 @@ HEADER_SEARCH_PATHS = ( "$(SRCROOT)/../Models", "$(SRCROOT)/..", - "$(SRCROOT)/../../Build", + "\"$(BUILT_PRODUCTS_DIR)/../../Headers\"", "$(SRCROOT)/../Vendor/OCMock", ); INFOPLIST_FILE = "RKApplicationTests/Tests/RKApplicationTestsTests-Info.plist"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUNDLE_LOADER)"; - USER_HEADER_SEARCH_PATHS = "$(SRCROOT)/../../Build/RestKit"; + USER_HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers/RestKit\""; WRAPPER_EXTENSION = octest; }; name = Release; diff --git a/Tests/Application/RKApplicationTests/Tests/RKApplicationTestsTests-Info.plist b/Tests/Application/RKApplicationTests/Tests/RKApplicationTestsTests-Info.plist index 95311b9689..883b278e89 100644 --- a/Tests/Application/RKApplicationTests/Tests/RKApplicationTestsTests-Info.plist +++ b/Tests/Application/RKApplicationTests/Tests/RKApplicationTestsTests-Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - org.restkit.${PRODUCT_NAME:rfc1034identifier} + org.restkit.tests CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType diff --git a/Tests/Application/UI/RKFetchedResultsTableControllerTest.m b/Tests/Application/UI/RKFetchedResultsTableControllerTest.m index 2671ee4ccc..70430320e0 100644 --- a/Tests/Application/UI/RKFetchedResultsTableControllerTest.m +++ b/Tests/Application/UI/RKFetchedResultsTableControllerTest.m @@ -12,8 +12,9 @@ #import "RKManagedObjectMapping.h" #import "RKHuman.h" #import "RKEvent.h" -#import "RKManagedObjectCache.h" #import "RKAbstractTableController_Internals.h" +#import "RKManagedObjectCaching.h" +#import "RKTableControllerTestDelegate.h" // Expose the object loader delegate for testing purposes... @interface RKFetchedResultsTableController () @@ -31,37 +32,34 @@ - (NSIndexPath*)fetchedResultsIndexPathForIndexPath:(NSIndexPath*)indexPath; @end -@interface RKFetchedResultsTableControllerSpecViewController : UIViewController +@interface RKFetchedResultsTableControllerSpecViewController : UITableViewController @end @implementation RKFetchedResultsTableControllerSpecViewController @end -@interface RKFetchedResultsTableControllerTest : RKTestCase { - NSAutoreleasePool *_autoreleasePool; -} - +@interface RKFetchedResultsTableControllerTest : RKTestCase @end @implementation RKFetchedResultsTableControllerTest - (void)setUp { - _autoreleasePool = [NSAutoreleasePool new]; + [RKTestFactory setUp]; + + [[[[UIApplication sharedApplication] windows] objectAtIndex:0] setRootViewController:nil]; } - (void)tearDown { - [_autoreleasePool drain]; - _autoreleasePool = nil; + [RKTestFactory tearDown]; } - (void)bootstrapStoreAndCache { -// RKLogConfigureByName("RestKit/UI", RKLogLevelTrace); - RKManagedObjectStore* store = RKTestNewManagedObjectStore(); + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKHuman" inManagedObjectStore:store]; [humanMapping mapKeyPath:@"id" toAttribute:@"railsID"]; [humanMapping mapAttributes:@"name", nil]; humanMapping.primaryKeyAttribute = @"railsID"; - + [RKHuman truncateAll]; assertThatInt([RKHuman count:nil], is(equalToInt(0))); RKHuman* blake = [RKHuman createEntity]; @@ -70,67 +68,65 @@ - (void)bootstrapStoreAndCache { RKHuman* other = [RKHuman createEntity]; other.railsID = [NSNumber numberWithInt:5678]; other.name = @"other"; - NSError* error = [store save]; + NSError* error = nil; + [store save:&error]; assertThat(error, is(nilValue())); assertThatInt([RKHuman count:nil], is(equalToInt(2))); - - RKObjectManager* objectManager = RKTestNewObjectManager(); + + RKObjectManager* objectManager = [RKTestFactory objectManager]; [objectManager.mappingProvider setMapping:humanMapping forKeyPath:@"human"]; objectManager.objectStore = store; - - id mockObjectCache = [OCMockObject mockForProtocol:@protocol(RKManagedObjectCache)]; - [[[mockObjectCache stub] andReturn:[RKHuman requestAllSortedBy:@"name" ascending:YES]] fetchRequestForResourcePath:@"/JSON/humans/all.json"]; - objectManager.objectStore.managedObjectCache = mockObjectCache; + + [objectManager.mappingProvider setObjectMapping:humanMapping forResourcePathPattern:@"/JSON/humans/all\\.json" withFetchRequestBlock:^NSFetchRequest *(NSString *resourcePath) { + return [RKHuman requestAllSortedBy:@"name" ascending:YES]; + }]; } - (void)bootstrapNakedObjectStoreAndCache { -// RKLogConfigureByName("RestKit/UI", RKLogLevelTrace); - RKManagedObjectStore* store = RKTestNewManagedObjectStore(); + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; RKManagedObjectMapping *eventMapping = [RKManagedObjectMapping mappingForClass:[RKEvent class] inManagedObjectStore:store]; [eventMapping mapKeyPath:@"event_id" toAttribute:@"eventID"]; [eventMapping mapKeyPath:@"type" toAttribute:@"eventType"]; [eventMapping mapAttributes:@"location", @"summary", nil]; eventMapping.primaryKeyAttribute = @"eventID"; [RKEvent truncateAll]; - + assertThatInt([RKEvent count:nil], is(equalToInt(0))); RKEvent *nakedEvent = [RKEvent createEntity]; nakedEvent.eventID = @"RK4424"; nakedEvent.eventType = @"Concert"; nakedEvent.location = @"Performance Hall"; nakedEvent.summary = @"Shindig"; - NSError* error = [store save]; + NSError* error = nil; + [store save:&error]; assertThat(error, is(nilValue())); assertThatInt([RKEvent count:nil], is(equalToInt(1))); - - RKObjectManager* objectManager = RKTestNewObjectManager(); + + RKObjectManager* objectManager = [RKTestFactory objectManager]; [objectManager.mappingProvider addObjectMapping:eventMapping]; objectManager.objectStore = store; - - id mockObjectCache = [OCMockObject mockForProtocol:@protocol(RKManagedObjectCache)]; - [[[mockObjectCache stub] andReturn:[RKEvent requestAllSortedBy:@"eventType" ascending:YES]] fetchRequestForResourcePath:@"/JSON/NakedEvents.json"]; - objectManager.objectStore.managedObjectCache = mockObjectCache; + + id mockMappingProvider = [OCMockObject partialMockForObject:objectManager.mappingProvider]; + [[[mockMappingProvider stub] andReturn:[RKEvent requestAllSortedBy:@"eventType" ascending:YES]] fetchRequestForResourcePath:@"/JSON/NakedEvents.json"]; } - (void)bootstrapEmptyStoreAndCache { -// RKLogConfigureByName("RestKit/UI", RKLogLevelTrace); - RKManagedObjectStore* store = RKTestNewManagedObjectStore(); + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKHuman" inManagedObjectStore:store]; [humanMapping mapKeyPath:@"id" toAttribute:@"railsID"]; [humanMapping mapAttributes:@"name", nil]; humanMapping.primaryKeyAttribute = @"railsID"; - + [RKHuman truncateAll]; assertThatInt([RKHuman count:nil], is(equalToInt(0))); - - RKObjectManager* objectManager = RKTestNewObjectManager(); + + RKObjectManager* objectManager = [RKTestFactory objectManager]; [objectManager.mappingProvider setMapping:humanMapping forKeyPath:@"human"]; objectManager.objectStore = store; - - id mockObjectCache = [OCMockObject niceMockForProtocol:@protocol(RKManagedObjectCache)]; - [[[mockObjectCache stub] andReturn:[RKHuman requestAllSortedBy:@"name" ascending:YES]] fetchRequestForResourcePath:@"/JSON/humans/all.json"]; - [[[mockObjectCache stub] andReturn:[RKHuman requestAllSortedBy:@"name" ascending:YES]] fetchRequestForResourcePath:@"/empty/array"]; - objectManager.objectStore.managedObjectCache = mockObjectCache; + + id mockMappingProvider = [OCMockObject partialMockForObject:objectManager.mappingProvider]; + [[[mockMappingProvider stub] andReturn:[RKHuman requestAllSortedBy:@"name" ascending:YES]] fetchRequestForResourcePath:@"/JSON/humans/all.json"]; + [[[mockMappingProvider stub] andReturn:[RKHuman requestAllSortedBy:@"name" ascending:YES]] fetchRequestForResourcePath:@"/empty/array"]; } - (void)stubObjectManagerToOnline { @@ -145,40 +141,34 @@ - (void)stubObjectManagerToOnline { - (void)testLoadWithATableViewControllerAndResourcePath { [self bootstrapStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; - + assertThat(tableController.viewController, is(equalTo(viewController))); - assertThat(tableController.tableView, is(equalTo(tableView))); + assertThat(tableController.tableView, is(equalTo(viewController.tableView))); assertThat(tableController.resourcePath, is(equalTo(@"/JSON/humans/all.json"))); } - (void)testLoadWithATableViewControllerAndResourcePathFromNakedObjects { [self bootstrapNakedObjectStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/NakedEvents.json"; [tableController setObjectMappingForClass:[RKEvent class]]; [tableController loadTable]; - + assertThat(tableController.viewController, is(equalTo(viewController))); - assertThat(tableController.tableView, is(equalTo(tableView))); + assertThat(tableController.tableView, is(equalTo(viewController.tableView))); assertThat(tableController.resourcePath, is(equalTo(@"/JSON/NakedEvents.json"))); - + RKTableViewCellMapping* cellMapping = [RKTableViewCellMapping mappingForClass:[UITableViewCell class]]; [cellMapping mapKeyPath:@"summary" toAttribute:@"textLabel.text"]; RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; [mappings setCellMapping:cellMapping forClass:[RKEvent class]]; tableController.cellMappings = mappings; - + UITableViewCell* cell = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; assertThat(cell.textLabel.text, is(equalTo(@"Shindig"))); } @@ -186,21 +176,17 @@ - (void)testLoadWithATableViewControllerAndResourcePathFromNakedObjects { - (void)testLoadWithATableViewControllerAndResourcePathAndPredicateAndSortDescriptors { [self bootstrapStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; NSPredicate* predicate = [NSPredicate predicateWithValue:TRUE]; NSArray* sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.predicate = predicate; tableController.sortDescriptors = sortDescriptors; [tableController loadTable]; - + assertThat(tableController.viewController, is(equalTo(viewController))); - assertThat(tableController.tableView, is(equalTo(tableView))); assertThat(tableController.resourcePath, is(equalTo(@"/JSON/humans/all.json"))); assertThat(tableController.fetchRequest, is(notNilValue())); assertThat([tableController.fetchRequest predicate], is(equalTo(predicate))); @@ -209,18 +195,14 @@ - (void)testLoadWithATableViewControllerAndResourcePathAndPredicateAndSortDescri - (void)testLoadWithATableViewControllerAndResourcePathAndSectionNameAndCacheName { [self bootstrapStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; tableController.cacheName = @"allHumansCache"; [tableController loadTable]; - + assertThat(tableController.viewController, is(equalTo(viewController))); - assertThat(tableController.tableView, is(equalTo(tableView))); assertThat(tableController.resourcePath, is(equalTo(@"/JSON/humans/all.json"))); assertThat(tableController.fetchRequest, is(notNilValue())); assertThat(tableController.fetchedResultsController.sectionNameKeyPath, is(equalTo(@"name"))); @@ -229,23 +211,19 @@ - (void)testLoadWithATableViewControllerAndResourcePathAndSectionNameAndCacheNam - (void)testLoadWithAllParams { [self bootstrapStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; NSPredicate* predicate = [NSPredicate predicateWithValue:TRUE]; NSArray* sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.predicate = predicate; tableController.sortDescriptors = sortDescriptors; tableController.sectionNameKeyPath = @"name"; tableController.cacheName = @"allHumansCache"; [tableController loadTable]; - + assertThat(tableController.viewController, is(equalTo(viewController))); - assertThat(tableController.tableView, is(equalTo(tableView))); assertThat(tableController.resourcePath, is(equalTo(@"/JSON/humans/all.json"))); assertThat(tableController.fetchRequest, is(notNilValue())); assertThat([tableController.fetchRequest predicate], is(equalTo(predicate))); @@ -260,10 +238,10 @@ - (void)testAlwaysHaveAtLeastOneSection { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; - + assertThatInt(tableController.sectionCount, is(equalToInt(1))); } @@ -275,7 +253,7 @@ - (void)testProperlyCountSections { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController loadTable]; @@ -288,7 +266,7 @@ - (void)testProperlyCountRows { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; assertThatInt([tableController rowCount], is(equalToInt(2))); @@ -300,7 +278,7 @@ - (void)testProperlyCountRowsWithHeaderItems { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -318,7 +296,7 @@ - (void)testProperlyCountRowsWithEmptyItemWhenEmpty { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Empty"; @@ -336,7 +314,7 @@ - (void)testProperlyCountRowsWithEmptyItemWhenFull { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Empty"; @@ -354,7 +332,7 @@ - (void)testProperlyCountRowsWithHeaderAndEmptyItemsWhenEmptyDontShowHeaders { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -375,11 +353,8 @@ - (void)testProperlyCountRowsWithHeaderAndEmptyItemsWhenEmptyDontShowHeaders { - (void)testProperlyCountRowsWithHeaderAndEmptyItemsWhenEmptyShowHeaders { [self bootstrapEmptyStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -400,11 +375,8 @@ - (void)testProperlyCountRowsWithHeaderAndEmptyItemsWhenEmptyShowHeaders { - (void)testProperlyCountRowsWithHeaderAndEmptyItemsWhenFull { [self bootstrapStoreAndCache]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -446,11 +418,11 @@ - (void)testReturnTheNumberOfSectionsInTableView { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController loadTable]; - + assertThatInt([tableController numberOfSectionsInTableView:tableView], is(equalToInt(2))); } @@ -460,10 +432,10 @@ - (void)testReturnTheNumberOfRowsInSectionInTableView { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; - + assertThatInt([tableController tableView:tableView numberOfRowsInSection:0], is(equalToInt(2))); } @@ -473,11 +445,11 @@ - (void)testReturnTheHeaderTitleForSection { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController loadTable]; - + assertThat([tableController tableView:tableView titleForHeaderInSection:1], is(equalTo(@"other"))); } @@ -487,16 +459,16 @@ - (void)testReturnTheTableViewCellForRowAtIndexPath { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; - + RKTableViewCellMapping* cellMapping = [RKTableViewCellMapping mappingForClass:[UITableViewCell class]]; [cellMapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; [mappings setCellMapping:cellMapping forClass:[RKHuman class]]; tableController.cellMappings = mappings; - + UITableViewCell* cell = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; assertThat(cell.textLabel.text, is(equalTo(@"blake"))); } @@ -509,13 +481,14 @@ - (void)testReturnTheObjectForARowAtIndexPath { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; - + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; assertThatBool(blake == [tableController objectForRowAtIndexPath:indexPath], is(equalToBool(YES))); + [tableController release]; } #pragma mark - Editing @@ -524,37 +497,35 @@ - (void)testFireADeleteRequestWhenTheCanEditRowsPropertyIsSet { [self bootstrapStoreAndCache]; [self stubObjectManagerToOnline]; [[RKObjectManager sharedManager].router routeClass:[RKHuman class] - toResourcePath:@"/humans/(railsID)" + toResourcePath:@"/humans/:railsID" forMethod:RKRequestMethodDELETE]; - UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; - RKFetchedResultsTableController* tableController = - [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.canEditRows = YES; + RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMapping]; + [cellMapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [tableController mapObjectsWithClass:[RKHuman class] toTableCellsWithMapping:cellMapping]; [tableController loadTable]; - + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:1 inSection:0]; NSIndexPath* deleteIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]; RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(2))); assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(other))); assertThat([tableController objectForRowAtIndexPath:deleteIndexPath], is(equalTo(blake))); BOOL delegateCanEdit = [tableController tableView:tableController.tableView - canEditRowAtIndexPath:deleteIndexPath]; + canEditRowAtIndexPath:deleteIndexPath]; assertThatBool(delegateCanEdit, is(equalToBool(YES))); - -// RKTestNotificationObserver* observer = [RKTestNotificationObserver notificationObserverForNotificationName:RKRequestDidLoadResponseNotification]; + [RKTestNotificationObserver waitForNotificationWithName:RKRequestDidLoadResponseNotification usingBlock:^{ [tableController tableView:tableController.tableView - commitEditingStyle:UITableViewCellEditingStyleDelete - forRowAtIndexPath:deleteIndexPath]; + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:deleteIndexPath]; }]; -// observer.timeout = 30; - + assertThatInt([tableController rowCount], is(equalToInt(1))); assertThat([tableController objectForRowAtIndexPath:deleteIndexPath], is(equalTo(other))); assertThatBool([blake isDeleted], is(equalToBool(YES))); @@ -563,36 +534,36 @@ - (void)testFireADeleteRequestWhenTheCanEditRowsPropertyIsSet { - (void)testLocallyCommitADeleteWhenTheCanEditRowsPropertyIsSet { [self bootstrapStoreAndCache]; [self stubObjectManagerToOnline]; - + UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.canEditRows = YES; [tableController loadTable]; - + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; NSIndexPath* deleteIndexPath = [NSIndexPath indexPathForRow:1 inSection:0]; RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; blake.railsID = nil; other.railsID = nil; - + NSError* error = nil; [blake.managedObjectContext save:&error]; assertThat(error, is(nilValue())); - + assertThatInt([tableController rowCount], is(equalToInt(2))); assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(blake))); assertThat([tableController objectForRowAtIndexPath:deleteIndexPath], is(equalTo(other))); BOOL delegateCanEdit = [tableController tableView:tableController.tableView - canEditRowAtIndexPath:deleteIndexPath]; + canEditRowAtIndexPath:deleteIndexPath]; assertThatBool(delegateCanEdit, is(equalToBool(YES))); [tableController tableView:tableController.tableView - commitEditingStyle:UITableViewCellEditingStyleDelete - forRowAtIndexPath:deleteIndexPath]; + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:deleteIndexPath]; assertThatInt([tableController rowCount], is(equalToInt(1))); assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(blake))); } @@ -600,26 +571,26 @@ - (void)testLocallyCommitADeleteWhenTheCanEditRowsPropertyIsSet { - (void)testNotCommitADeletionWhenTheCanEditRowsPropertyIsNotSet { [self bootstrapStoreAndCache]; [self stubObjectManagerToOnline]; - + UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; - + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(2))); BOOL delegateCanEdit = [tableController tableView:tableController.tableView - canEditRowAtIndexPath:indexPath]; + canEditRowAtIndexPath:indexPath]; assertThatBool(delegateCanEdit, is(equalToBool(NO))); [tableController tableView:tableController.tableView - commitEditingStyle:UITableViewCellEditingStyleDelete - forRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; assertThatInt([tableController rowCount], is(equalToInt(2))); assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(blake))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], @@ -629,27 +600,27 @@ - (void)testNotCommitADeletionWhenTheCanEditRowsPropertyIsNotSet { - (void)testDoNothingToCommitAnInsertionWhenTheCanEditRowsPropertyIsSet { [self bootstrapStoreAndCache]; [self stubObjectManagerToOnline]; - + UITableView* tableView = [UITableView new]; RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.canEditRows = YES; [tableController loadTable]; - + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(2))); BOOL delegateCanEdit = [tableController tableView:tableController.tableView - canEditRowAtIndexPath:indexPath]; + canEditRowAtIndexPath:indexPath]; assertThatBool(delegateCanEdit, is(equalToBool(YES))); [tableController tableView:tableController.tableView - commitEditingStyle:UITableViewCellEditingStyleInsert - forRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + commitEditingStyle:UITableViewCellEditingStyleInsert + forRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; assertThatInt([tableController rowCount], is(equalToInt(2))); assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(blake))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], @@ -662,22 +633,22 @@ - (void)testNotMoveARowWhenTheCanMoveRowsPropertyIsSet { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.canMoveRows = YES; [tableController loadTable]; - + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(2))); BOOL delegateCanMove = [tableController tableView:tableController.tableView - canMoveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + canMoveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; assertThatBool(delegateCanMove, is(equalToBool(YES))); [tableController tableView:tableController.tableView - moveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] - toIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + moveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] + toIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; assertThatInt([tableController rowCount], is(equalToInt(2))); assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(blake))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], @@ -692,7 +663,7 @@ - (void)testDetermineIfASectionIndexIsAHeaderSection { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; assertThatBool([tableController isHeaderSection:0], is(equalToBool(YES))); @@ -706,7 +677,7 @@ - (void)testDetermineIfARowIndexIsAHeaderRow { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -726,7 +697,7 @@ - (void)testDetermineIfASectionIndexIsAFooterSectionSingleSection { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -746,7 +717,7 @@ - (void)testDetermineIfASectionIndexIsAFooterSectionMultipleSections { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { @@ -767,7 +738,7 @@ - (void)testDetermineIfARowIndexIsAFooterRow { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -787,7 +758,7 @@ - (void)testDetermineIfASectionIndexIsAnEmptySection { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; assertThatBool([tableController isEmptySection:0], is(equalToBool(YES))); @@ -801,7 +772,7 @@ - (void)testDetermineIfARowIndexIsAnEmptyRow { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController loadTable]; assertThatBool([tableController isEmptyRow:0], is(equalToBool(YES))); @@ -815,7 +786,7 @@ - (void)testDetermineIfAnIndexPathIsAHeaderIndexPath { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -838,7 +809,7 @@ - (void)testDetermineIfAnIndexPathIsAFooterIndexPath { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -861,7 +832,7 @@ - (void)testDetermineIfAnIndexPathIsAnEmptyIndexPathSingleSectionEmptyItemOnly { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Empty"; @@ -884,7 +855,7 @@ - (void)testConvertAnIndexPathForHeaderRows { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -905,7 +876,7 @@ - (void)testConvertAnIndexPathForFooterRowsSingleSection { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -925,7 +896,7 @@ - (void)testConvertAnIndexPathForFooterRowsMultipleSections { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { @@ -947,7 +918,7 @@ - (void)testConvertAnIndexPathForEmptyRow { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Empty"; @@ -968,7 +939,7 @@ - (void)testConvertAnIndexPathForHeaderFooterRowsSingleSection { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -995,7 +966,7 @@ - (void)testConvertAnIndexPathForHeaderFooterRowsMultipleSections { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { @@ -1024,7 +995,7 @@ - (void)testConvertAnIndexPathForHeaderFooterEmptyRowsSingleSection { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1058,7 +1029,7 @@ - (void)testConvertAnIndexPathForHeaderFooterEmptyRowsMultipleSections { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; tableController.sectionNameKeyPath = @"name"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { @@ -1092,7 +1063,7 @@ - (void)testConvertAnIndexPathForHeaderEmptyRows { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1118,7 +1089,7 @@ - (void)testShowHeaderRows { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; RKTableItem* headerRow = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1130,10 +1101,10 @@ - (void)testShowHeaderRows { tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(3))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalTo(headerRow))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo(blake))); @@ -1147,7 +1118,7 @@ - (void)testShowFooterRows { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; RKTableItem* footerRow = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -1159,10 +1130,10 @@ - (void)testShowFooterRows { tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(3))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalTo(blake))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo(other))); @@ -1176,7 +1147,7 @@ - (void)testHideHeaderRowsWhenEmptyWhenPropertyIsNotSet { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1187,7 +1158,7 @@ - (void)testHideHeaderRowsWhenEmptyWhenPropertyIsNotSet { tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + assertThatBool(tableController.isLoaded, is(equalToBool(YES))); assertThatInt([tableController rowCount], is(equalToInt(0))); assertThatBool(tableController.isEmpty, is(equalToBool(YES))); @@ -1199,7 +1170,7 @@ - (void)testHideFooterRowsWhenEmptyWhenPropertyIsNotSet { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -1210,7 +1181,7 @@ - (void)testHideFooterRowsWhenEmptyWhenPropertyIsNotSet { tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + assertThatBool(tableController.isLoaded, is(equalToBool(YES))); assertThatInt([tableController rowCount], is(equalToInt(0))); assertThatBool(tableController.isEmpty, is(equalToBool(YES))); @@ -1222,7 +1193,7 @@ - (void)testRemoveHeaderAndFooterCountsWhenDeterminingIsEmpty { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1245,7 +1216,7 @@ - (void)testRemoveHeaderAndFooterCountsWhenDeterminingIsEmpty { tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + assertThatBool(tableController.isLoaded, is(equalToBool(YES))); assertThatInt([tableController rowCount], is(equalToInt(1))); assertThatBool(tableController.isEmpty, is(equalToBool(YES))); @@ -1257,9 +1228,9 @@ - (void)testNotShowTheEmptyItemWhenTheTableIsNotEmpty { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; - + RKTableItem* headerRow = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { @@ -1267,7 +1238,7 @@ - (void)testNotShowTheEmptyItemWhenTheTableIsNotEmpty { }]; }]; [tableController addHeaderRowForItem:headerRow]; - + RKTableItem* footerRow = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { @@ -1275,7 +1246,7 @@ - (void)testNotShowTheEmptyItemWhenTheTableIsNotEmpty { }]; }]; [tableController addFooterRowForItem:footerRow]; - + RKTableItem* emptyItem = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Empty"; tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { @@ -1286,10 +1257,10 @@ - (void)testNotShowTheEmptyItemWhenTheTableIsNotEmpty { tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + RKHuman* blake = [RKHuman findFirstByAttribute:@"name" withValue:@"blake"]; RKHuman* other = [RKHuman findFirstByAttribute:@"name" withValue:@"other"]; - + assertThatInt([tableController rowCount], is(equalToInt(4))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalTo(headerRow))); assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo(blake))); @@ -1304,7 +1275,7 @@ - (void)testShowTheEmptyItemWhenTheTableIsEmpty { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1327,7 +1298,7 @@ - (void)testShowTheEmptyItemWhenTheTableIsEmpty { tableController.showsHeaderRowsWhenEmpty = NO; tableController.showsFooterRowsWhenEmpty = NO; [tableController loadTable]; - + assertThatBool(tableController.isLoaded, is(equalToBool(YES))); assertThatInt([tableController rowCount], is(equalToInt(1))); assertThatBool(tableController.isEmpty, is(equalToBool(YES))); @@ -1339,7 +1310,7 @@ - (void)testShowTheEmptyItemPlusHeadersAndFootersWhenTheTableIsEmpty { RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; + viewController:viewController]; tableController.resourcePath = @"/JSON/humans/all.json"; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1362,7 +1333,7 @@ - (void)testShowTheEmptyItemPlusHeadersAndFootersWhenTheTableIsEmpty { tableController.showsHeaderRowsWhenEmpty = YES; tableController.showsFooterRowsWhenEmpty = YES; [tableController loadTable]; - + assertThatBool(tableController.isLoaded, is(equalToBool(YES))); assertThatInt([tableController rowCount], is(equalToInt(3))); assertThatBool(tableController.isEmpty, is(equalToBool(YES))); @@ -1371,21 +1342,20 @@ - (void)testShowTheEmptyItemPlusHeadersAndFootersWhenTheTableIsEmpty { - (void)testShowTheEmptyImageAfterLoadingAnEmptyCollectionIntoAnEmptyFetch { [self bootstrapEmptyStoreAndCache]; [self stubObjectManagerToOnline]; - + UITableView* tableView = [UITableView new]; - + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView - viewController:viewController]; - + viewController:viewController]; + UIImage *image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; - + tableController.imageForEmpty = image; tableController.resourcePath = @"/empty/array"; tableController.autoRefreshFromNetwork = YES; [tableController.cache invalidateAll]; - -// RKLogConfigureByName("RestKit/Network", RKLogLevelTrace); + [RKTestNotificationObserver waitForNotificationWithName:RKTableControllerDidFinishLoadNotification usingBlock:^{ [tableController loadTable]; }]; @@ -1395,4 +1365,103 @@ - (void)testShowTheEmptyImageAfterLoadingAnEmptyCollectionIntoAnEmptyFetch { assertThat(tableController.stateOverlayImageView.image, is(notNilValue())); } +- (void)testPostANotificationWhenObjectsAreLoaded { + [self bootstrapNakedObjectStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/NakedEvents.json"; + [tableController setObjectMappingForClass:[RKEvent class]]; + + id observerMock = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidLoadObjectsNotification object:tableController]; + [[observerMock expect] notificationWithName:RKTableControllerDidLoadObjectsNotification object:tableController]; + [tableController loadTable]; + [observerMock verify]; +} + +#pragma mark - Delegate Methods + +- (void)testDelegateIsInformedOnInsertSection { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:viewController.tableView viewController:viewController]; + RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMapping]; + [cellMapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [tableController mapObjectsWithClass:[RKHuman class] toTableCellsWithMapping:cellMapping]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.cacheName = @"allHumansCache"; + + RKFetchedResultsTableControllerTestDelegate *delegate = [RKFetchedResultsTableControllerTestDelegate tableControllerDelegate]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[mockDelegate expect] tableController:tableController didInsertSectionAtIndex:0]; + tableController.delegate = mockDelegate; + [[[[UIApplication sharedApplication] windows] objectAtIndex:0] setRootViewController:viewController]; + [tableController loadTable]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + assertThatInt([tableController sectionCount], is(equalToInt(1))); + [mockDelegate verify]; +} + +- (void)testDelegateIsInformedOfDidStartLoad { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:viewController.tableView viewController:viewController]; + RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMapping]; + [cellMapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [tableController mapObjectsWithClass:[RKHuman class] toTableCellsWithMapping:cellMapping]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.cacheName = @"allHumansCache"; + + id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKFetchedResultsTableControllerDelegate)]; + [[mockDelegate expect] tableControllerDidStartLoad:tableController]; + tableController.delegate = mockDelegate; + [[[[UIApplication sharedApplication] windows] objectAtIndex:0] setRootViewController:viewController]; + [tableController loadTable]; + [mockDelegate verify]; +} + +- (void)testDelegateIsInformedOfDidFinishLoad { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:viewController.tableView viewController:viewController]; + RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMapping]; + [cellMapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [tableController mapObjectsWithClass:[RKHuman class] toTableCellsWithMapping:cellMapping]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.cacheName = @"allHumansCache"; + + id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKFetchedResultsTableControllerDelegate)]; + [[mockDelegate expect] tableControllerDidFinishLoad:tableController]; + tableController.delegate = mockDelegate; + [[[[UIApplication sharedApplication] windows] objectAtIndex:0] setRootViewController:viewController]; + [tableController loadTable]; + [mockDelegate verify]; +} + +- (void)testDelegateIsInformedOfDidInsertObjectAtIndexPath { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:viewController.tableView viewController:viewController]; + RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMapping]; + [cellMapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [tableController mapObjectsWithClass:[RKHuman class] toTableCellsWithMapping:cellMapping]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.cacheName = @"allHumansCache"; + + id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKFetchedResultsTableControllerDelegate)]; + [[mockDelegate expect] tableController:tableController didInsertObject:OCMOCK_ANY atIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [[mockDelegate expect] tableController:tableController didInsertObject:OCMOCK_ANY atIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + tableController.delegate = mockDelegate; + [[[[UIApplication sharedApplication] windows] objectAtIndex:0] setRootViewController:viewController]; + [tableController loadTable]; + [mockDelegate verify]; +} + @end diff --git a/Tests/Application/UI/RKTableControllerTest.m b/Tests/Application/UI/RKTableControllerTest.m index 31ec463679..0378a792bb 100644 --- a/Tests/Application/UI/RKTableControllerTest.m +++ b/Tests/Application/UI/RKTableControllerTest.m @@ -12,93 +12,23 @@ #import "RKTestUser.h" #import "RKMappableObject.h" #import "RKAbstractTableController_Internals.h" +#import "RKTableControllerTestDelegate.h" // Expose the object loader delegate for testing purposes... @interface RKTableController () - (void)animationDidStopAddingSwipeView:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context; @end -@interface RKTestTableControllerDelegate : NSObject - -@property (nonatomic, assign) NSTimeInterval timeout; -@property (nonatomic, assign) BOOL awaitingResponse; - -+ (id)tableControllerDelegate; -- (void)waitForLoad; -@end - -@implementation RKTestTableControllerDelegate - -@synthesize timeout = _timeout; -@synthesize awaitingResponse = _awaitingResponse; - -+ (id)tableControllerDelegate { - return [[self new] autorelease]; -} - -- (id)init { - self = [super init]; - if (self) { - _timeout = 3; - _awaitingResponse = NO; - } - - return self; -} - -- (void)waitForLoad { - _awaitingResponse = YES; - NSDate* startDate = [NSDate date]; - - while (_awaitingResponse) { - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - if ([[NSDate date] timeIntervalSinceDate:startDate] > self.timeout) { - [NSException raise:nil format:@"*** Operation timed out after %f seconds...", self.timeout]; - _awaitingResponse = NO; - } - } -} - -#pragma RKTableControllerDelegate methods - -- (void)tableControllerDidFinishLoad:(RKAbstractTableController*)tableController { - _awaitingResponse = NO; -} - -- (void)tableController:(RKAbstractTableController*)tableController didFailLoadWithError:(NSError *)error { - _awaitingResponse = NO; -} - -// NOTE - Delegate methods below are implemented to allow trampoline through -// OCMock expectations - -- (void)tableControllerDidStartLoad:(RKAbstractTableController*)tableController {} -- (void)tableControllerDidBecomeEmpty:(RKAbstractTableController*)tableController {} -- (void)tableController:(RKAbstractTableController*)tableController willLoadTableWithObjectLoader:(RKObjectLoader*)objectLoader {} -- (void)tableController:(RKAbstractTableController*)tableController didLoadTableWithObjectLoader:(RKObjectLoader*)objectLoader {} -- (void)tableControllerDidCancelLoad:(RKAbstractTableController*)tableController {} -- (void)tableController:(RKAbstractTableController*)tableController willBeginEditing:(id)object atIndexPath:(NSIndexPath*)indexPath {} -- (void)tableController:(RKAbstractTableController*)tableController didEndEditing:(id)object atIndexPath:(NSIndexPath*)indexPath {} -- (void)tableController:(RKAbstractTableController*)tableController didInsertSection:(RKTableSection*)section atIndex:(NSUInteger)sectionIndex {} -- (void)tableController:(RKAbstractTableController*)tableController didRemoveSection:(RKTableSection*)section atIndex:(NSUInteger)sectionIndex {} -- (void)tableController:(RKAbstractTableController*)tableController didInsertObject:(id)object atIndexPath:(NSIndexPath*)indexPath {} -- (void)tableController:(RKAbstractTableController*)tableController didUpdateObject:(id)object atIndexPath:(NSIndexPath*)indexPath {} -- (void)tableController:(RKAbstractTableController*)tableController didDeleteObject:(id)object atIndexPath:(NSIndexPath*)indexPath {} -- (void)tableController:(RKAbstractTableController*)tableController willAddSwipeView:(UIView*)swipeView toCell:(UITableViewCell*)cell forObject:(id)object {} -- (void)tableController:(RKAbstractTableController*)tableController willRemoveSwipeView:(UIView*)swipeView fromCell:(UITableViewCell*)cell forObject:(id)object {} - +@interface RKTableControllerTestTableViewController : UITableViewController @end -@interface RKTableControllerSpecTableViewController : UITableViewController +@implementation RKTableControllerTestTableViewController @end -@implementation RKTableControllerSpecTableViewController +@interface RKTableControllerTestViewController : UIViewController @end -@interface RKTableControllerSpecViewController : UIViewController -@end - -@implementation RKTableControllerSpecViewController +@implementation RKTableControllerTestViewController @end @interface RKTestUserTableViewCell : UITableViewCell @@ -109,30 +39,24 @@ @implementation RKTestUserTableViewCell //////////////////////////////////////////////////////////////////////////////////////////////////////////////// -@interface RKTableControllerTest : SenTestCase { - NSAutoreleasePool *_autoreleasePool; -} +@interface RKTableControllerTest : RKTestCase @end @implementation RKTableControllerTest -- (void)setUp { - // Ensure the fixture bundle is configured - NSBundle *fixtureBundle = [NSBundle bundleWithIdentifier:@"org.restkit.Application-Tests"]; - [RKTestFixture setFixtureBundle:fixtureBundle]; - -// [RKObjectManager setSharedManager:nil]; -// _autoreleasePool = [[NSAutoreleasePool alloc] init]; +- (void)setUp +{ + [RKTestFactory setUp]; } -- (void)tearDown { -// [_autoreleasePool drain]; -// _autoreleasePool = nil; +- (void)tearDown +{ + [RKTestFactory tearDown]; } - (void)testInitializeWithATableViewController { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; assertThat(viewController.tableView, is(notNilValue())); RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThat(tableController.viewController, is(equalTo(viewController))); @@ -141,14 +65,20 @@ - (void)testInitializeWithATableViewController { - (void)testInitializeWithATableViewAndViewController { UITableView* tableView = [UITableView new]; - RKTableControllerSpecViewController* viewController = [RKTableControllerSpecViewController new]; + RKTableControllerTestViewController* viewController = [RKTableControllerTestViewController new]; RKTableController* tableController = [RKTableController tableControllerWithTableView:tableView forViewController:viewController]; assertThat(tableController.viewController, is(equalTo(viewController))); assertThat(tableController.tableView, is(equalTo(tableView))); } +- (void)testInitializesToUnloadedState { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); +} + - (void)testAlwaysHaveAtLeastOneSection { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; assertThat(viewController.tableView, is(notNilValue())); RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatInt(tableController.sectionCount, is(equalToInt(1))); @@ -157,7 +87,7 @@ - (void)testAlwaysHaveAtLeastOneSection { - (void)testDisconnectFromTheTableViewOnDealloc { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatInt(tableController.sectionCount, is(equalToInt(1))); [pool drain]; @@ -166,7 +96,7 @@ - (void)testDisconnectFromTheTableViewOnDealloc { } - (void)testNotDisconnectFromTheTableViewIfDelegateOrDataSourceAreNotSelf { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [[RKTableController alloc] initWithTableView:viewController.tableView viewController:viewController]; viewController.tableView.delegate = viewController; viewController.tableView.dataSource = viewController; @@ -179,160 +109,91 @@ - (void)testNotDisconnectFromTheTableViewIfDelegateOrDataSourceAreNotSelf { #pragma mark - Section Management - (void)testAddASection { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - tableController.delegate = mockDelegate; [tableController addSection:section]; assertThatInt([tableController.sections count], is(equalToInt(2))); - [mockDelegate verify]; } - (void)testConnectTheSectionToTheTableModelOnAdd { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - tableController.delegate = mockDelegate; [tableController addSection:section]; assertThat(section.tableController, is(equalTo(tableController))); - [mockDelegate verify]; } - (void)testConnectTheSectionToTheCellMappingsOfTheTableModelWhenNil { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - tableController.delegate = mockDelegate; assertThat(section.cellMappings, is(nilValue())); [tableController addSection:section]; assertThat(section.cellMappings, is(equalTo(tableController.cellMappings))); - [mockDelegate verify]; } - (void)testNotConnectTheSectionToTheCellMappingsOfTheTableModelWhenNonNil { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - tableController.delegate = mockDelegate; section.cellMappings = [NSMutableDictionary dictionary]; [tableController addSection:section]; assertThatBool(section.cellMappings == tableController.cellMappings, is(equalToBool(NO))); - [mockDelegate verify]; } - (void)testCountTheSections { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - tableController.delegate = mockDelegate; [tableController addSection:section]; assertThatInt(tableController.sectionCount, is(equalToInt(2))); - [mockDelegate verify]; } - (void)testRemoveASection { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:section - atIndex:1]; - tableController.delegate = mockDelegate; [tableController addSection:section]; assertThatInt(tableController.sectionCount, is(equalToInt(2))); [tableController removeSection:section]; assertThatInt(tableController.sectionCount, is(equalToInt(1))); - [mockDelegate verify]; } - (void)testNotLetRemoveTheLastSection { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - tableController.delegate = mockDelegate; [tableController addSection:section]; assertThatInt(tableController.sectionCount, is(equalToInt(2))); [tableController removeSection:section]; - [mockDelegate verify]; } -- (void)testInsertASectionAtASpecificIndex { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; +- (void)testInsertASectionAtATestificIndex { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* referenceSection = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:referenceSection - atIndex:2]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; - tableController.delegate = mockDelegate; [tableController insertSection:referenceSection atIndex:2]; assertThatInt(tableController.sectionCount, is(equalToInt(6))); assertThat([tableController.sections objectAtIndex:2], is(equalTo(referenceSection))); - [mockDelegate verify]; } - (void)testRemoveASectionByIndex { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:section - atIndex:1]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:section - atIndex:1]; - tableController.delegate = mockDelegate; [tableController addSection:section]; assertThatInt(tableController.sectionCount, is(equalToInt(2))); [tableController removeSectionAtIndex:1]; assertThatInt(tableController.sectionCount, is(equalToInt(1))); - [mockDelegate verify]; } - (void)testRaiseAnExceptionWhenAttemptingToRemoveTheLastSection { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; NSException* exception = nil; @try { @@ -347,61 +208,21 @@ - (void)testRaiseAnExceptionWhenAttemptingToRemoveTheLastSection { } - (void)testReturnTheSectionAtAGivenIndex { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* referenceSection = [RKTableSection section]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:referenceSection - atIndex:2]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; - tableController.delegate = mockDelegate; [tableController insertSection:referenceSection atIndex:2]; assertThatInt(tableController.sectionCount, is(equalToInt(6))); assertThat([tableController sectionAtIndex:2], is(equalTo(referenceSection))); - [mockDelegate verify]; } - (void)testRemoveAllSections { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; - id mockDelegate = [OCMockObject partialMockForObject:delegate]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:OCMOCK_ANY - atIndex:0]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:OCMOCK_ANY - atIndex:1]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:OCMOCK_ANY - atIndex:2]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:OCMOCK_ANY - atIndex:3]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didInsertSection:OCMOCK_ANY - atIndex:4]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:OCMOCK_ANY - atIndex:0]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:OCMOCK_ANY - atIndex:1]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:OCMOCK_ANY - atIndex:2]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:OCMOCK_ANY - atIndex:3]; - [[[mockDelegate expect] andForwardToRealObject] tableController:tableController - didRemoveSection:OCMOCK_ANY - atIndex:4]; - tableController.delegate = mockDelegate; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; @@ -409,11 +230,10 @@ - (void)testRemoveAllSections { assertThatInt(tableController.sectionCount, is(equalToInt(5))); [tableController removeAllSections]; assertThatInt(tableController.sectionCount, is(equalToInt(1))); - [mockDelegate verify]; } - (void)testReturnASectionByHeaderTitle { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController addSection:[RKTableSection section]]; [tableController addSection:[RKTableSection section]]; @@ -425,7 +245,7 @@ - (void)testReturnASectionByHeaderTitle { } - (void)testNotifyTheTableViewOnSectionInsertion { - RKTableControllerSpecViewController *viewController = [RKTableControllerSpecViewController new]; + RKTableControllerTestViewController *viewController = [RKTableControllerTestViewController new]; id mockTableView = [OCMockObject niceMockForClass:[UITableView class]]; RKTableController *tableController = [RKTableController tableControllerWithTableView:mockTableView forViewController:viewController]; [[mockTableView expect] insertSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:tableController.defaultRowAnimation]; @@ -434,7 +254,7 @@ - (void)testNotifyTheTableViewOnSectionInsertion { } - (void)testNotifyTheTableViewOnSectionRemoval { - RKTableControllerSpecViewController *viewController = [RKTableControllerSpecViewController new]; + RKTableControllerTestViewController *viewController = [RKTableControllerTestViewController new]; id mockTableView = [OCMockObject niceMockForClass:[UITableView class]]; RKTableController *tableController = [RKTableController tableControllerWithTableView:mockTableView forViewController:viewController]; [[mockTableView expect] insertSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:tableController.defaultRowAnimation]; @@ -446,7 +266,7 @@ - (void)testNotifyTheTableViewOnSectionRemoval { } - (void)testNotifyTheTableOfSectionRemovalAndReaddWhenRemovingAllSections { - RKTableControllerSpecViewController *viewController = [RKTableControllerSpecViewController new]; + RKTableControllerTestViewController *viewController = [RKTableControllerTestViewController new]; id mockTableView = [OCMockObject niceMockForClass:[UITableView class]]; RKTableController *tableController = [RKTableController tableControllerWithTableView:mockTableView forViewController:viewController]; [[mockTableView expect] deleteSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:tableController.defaultRowAnimation]; @@ -458,10 +278,10 @@ - (void)testNotifyTheTableOfSectionRemovalAndReaddWhenRemovingAllSections { [mockTableView verify]; } -#pragma mark - UITableViewDataSource specs +#pragma mark - UITableViewDataSource Tests - (void)testRaiseAnExceptionIfSentAMessageWithATableViewItIsNotBoundTo { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; NSException* exception = nil; @try { @@ -476,7 +296,7 @@ - (void)testRaiseAnExceptionIfSentAMessageWithATableViewItIsNotBoundTo { } - (void)testReturnTheNumberOfSectionsInTableView { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatInt([tableController numberOfSectionsInTableView:viewController.tableView], is(equalToInt(1))); [tableController addSection:[RKTableSection section]]; @@ -484,7 +304,7 @@ - (void)testReturnTheNumberOfSectionsInTableView { } - (void)testReturnTheNumberOfRowsInSectionInTableView { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatInt([tableController tableView:viewController.tableView numberOfRowsInSection:0], is(equalToInt(0))); NSArray* objects = [NSArray arrayWithObject:@"one"]; @@ -493,7 +313,7 @@ - (void)testReturnTheNumberOfRowsInSectionInTableView { } - (void)testReturnTheHeaderTitleForSection { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; [tableController addSection:section]; @@ -503,7 +323,7 @@ - (void)testReturnTheHeaderTitleForSection { } - (void)testReturnTheTitleForFooterInSection { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; [tableController addSection:section]; @@ -513,7 +333,7 @@ - (void)testReturnTheTitleForFooterInSection { } - (void)testReturnTheNumberOfRowsAcrossAllSections { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableSection* section = [RKTableSection section]; id sectionMock = [OCMockObject partialMockForObject:section]; @@ -524,7 +344,7 @@ - (void)testReturnTheNumberOfRowsAcrossAllSections { } - (void)testReturnTheTableViewCellForRowAtIndexPath { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableItem* item = [RKTableItem tableItemWithText:@"Test!" detailText:@"Details!" image:nil]; [tableController loadTableItems:[NSArray arrayWithObject:item] inSection:0 withMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { @@ -541,13 +361,13 @@ - (void)testReturnTheTableViewCellForRowAtIndexPath { #pragma mark - Table Cell Mapping - (void)testInitializeCellMappings { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThat(tableController.cellMappings, is(notNilValue())); } - (void)testRegisterMappingsForObjectsToTableViewCell { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThat([tableController.cellMappings cellMappingForClass:[RKTestUser class]], is(nilValue())); [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMapping]]; @@ -557,7 +377,7 @@ - (void)testRegisterMappingsForObjectsToTableViewCell { } - (void)testDefaultTheReuseIdentifierToTheNameOfTheObjectClass { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThat([tableController.cellMappings cellMappingForClass:[RKTestUser class]], is(nilValue())); RKTableViewCellMapping* cellMapping = [RKTableViewCellMapping cellMapping]; @@ -566,7 +386,7 @@ - (void)testDefaultTheReuseIdentifierToTheNameOfTheObjectClass { } - (void)testDefaultTheReuseIdentifierToTheNameOfTheObjectClassWhenCreatingMappingWithBlockSyntax { - RKTableControllerSpecTableViewController *viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController *viewController = [RKTableControllerTestTableViewController new]; RKTableController *tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThat([tableController.cellMappings cellMappingForClass:[RKTestUser class]], is(nilValue())); RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping *cellMapping) { @@ -577,7 +397,7 @@ - (void)testDefaultTheReuseIdentifierToTheNameOfTheObjectClassWhenCreatingMappin } - (void)testReturnTheObjectForARowAtIndexPath { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTestUser* user = [RKTestUser user]; [tableController loadObjects:[NSArray arrayWithObject:user]]; @@ -586,7 +406,7 @@ - (void)testReturnTheObjectForARowAtIndexPath { } - (void)testReturnTheCellMappingForTheRowAtIndexPath { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableViewCellMapping* cellMapping = [RKTableViewCellMapping cellMapping]; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:cellMapping]; @@ -616,7 +436,7 @@ - (void)testReturnATableViewCellForTheObjectAtAGivenIndexPath { } - (void)testChangeTheReuseIdentifierWhenMutatedWithinTheBlockInitializer { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThat([tableController.cellMappings cellMappingForClass:[RKTestUser class]], is(nilValue())); [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping *cellMapping) { @@ -631,7 +451,7 @@ - (void)testChangeTheReuseIdentifierWhenMutatedWithinTheBlockInitializer { #pragma mark - Static Object Loading - (void)testLoadAnArrayOfObjects { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; NSArray* objects = [NSArray arrayWithObject:@"one"]; assertThat([tableController sectionAtIndex:0].objects, is(empty())); @@ -639,8 +459,8 @@ - (void)testLoadAnArrayOfObjects { assertThat([tableController sectionAtIndex:0].objects, is(equalTo(objects))); } -- (void)testLoadAnArrayOfObjectsToTheSpecifiedSection { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; +- (void)testLoadAnArrayOfObjectsToTheTestifiedSection { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController addSection:[RKTableSection section]]; NSArray* objects = [NSArray arrayWithObject:@"one"]; @@ -650,7 +470,7 @@ - (void)testLoadAnArrayOfObjectsToTheSpecifiedSection { } - (void)testLoadAnArrayOfTableItems { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; NSArray* tableItems = [RKTableItem tableItemsFromStrings:@"One", @"Two", @"Three", nil]; [tableController loadTableItems:tableItems]; @@ -665,7 +485,7 @@ - (void)testLoadAnArrayOfTableItems { } - (void)testAllowYouToTriggerAnEmptyLoad { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatBool([tableController isLoaded], is(equalToBool(NO))); [tableController loadEmpty]; @@ -676,9 +496,9 @@ - (void)testAllowYouToTriggerAnEmptyLoad { #pragma mark - Network Load - (void)testLoadCollectionOfObjectsAndMapThemIntoTableViewCells { - RKObjectManager* objectManager = RKTestNewObjectManager(); + RKObjectManager* objectManager = [RKTestFactory objectManager]; objectManager.client.cachePolicy = RKRequestCachePolicyNone; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { @@ -686,7 +506,7 @@ - (void)testLoadCollectionOfObjectsAndMapThemIntoTableViewCells { [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate tableControllerDelegate]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate tableControllerDelegate]; delegate.timeout = 10; tableController.delegate = delegate; [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { @@ -700,7 +520,7 @@ - (void)testLoadCollectionOfObjectsAndMapThemIntoTableViewCells { } - (void)testSetTheModelToTheLoadedStateIfObjectsAreLoadedSuccessfully { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatBool([tableController isLoaded], is(equalToBool(NO))); NSArray* objects = [NSArray arrayWithObject:[RKTestUser new]]; @@ -711,7 +531,7 @@ - (void)testSetTheModelToTheLoadedStateIfObjectsAreLoadedSuccessfully { } - (void)testSetTheModelToErrorStateIfTheObjectLoaderFailsWithAnError { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; id mockObjectLoader = [OCMockObject niceMockForClass:[RKObjectLoader class]]; NSError* error = [NSError errorWithDomain:@"Test" code:0 userInfo:nil]; @@ -722,8 +542,73 @@ - (void)testSetTheModelToErrorStateIfTheObjectLoaderFailsWithAnError { assertThatBool([tableController isEmpty], is(equalToBool(YES))); } +- (void)testErrorIsClearedAfterSubsequentLoad { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + id mockObjectLoader = [OCMockObject niceMockForClass:[RKObjectLoader class]]; + NSError* error = [NSError errorWithDomain:@"Test" code:0 userInfo:nil]; + [tableController objectLoader:mockObjectLoader didFailWithError:error]; + assertThatBool([tableController isLoading], is(equalToBool(NO))); + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatBool([tableController isError], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + + [tableController objectLoader:mockObjectLoader didLoadObjects:[NSArray array]]; + assertThatBool([tableController isError], is(equalToBool(NO))); + assertThat(tableController.error, is(nilValue())); +} + +- (void)testDisplayOfErrorImageTakesPresendenceOverEmpty { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + UIImage *imageForEmpty = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; + UIImage *imageForError = [imageForEmpty copy]; + tableController.imageForEmpty = imageForEmpty; + tableController.imageForError = imageForError; + + id mockObjectLoader = [OCMockObject niceMockForClass:[RKObjectLoader class]]; + NSError* error = [NSError errorWithDomain:@"Test" code:0 userInfo:nil]; + [tableController objectLoader:mockObjectLoader didFailWithError:error]; + assertThatBool([tableController isLoading], is(equalToBool(NO))); + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatBool([tableController isError], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + + UIImage *overlayImage = [tableController overlayImage]; + assertThat(overlayImage, isNot(nilValue())); + assertThat(overlayImage, is(equalTo(imageForError))); +} + +- (void)testBitwiseLoadingTransition { + RKTableControllerState oldState = RKTableControllerStateNotYetLoaded; + RKTableControllerState newState = RKTableControllerStateLoading; + + BOOL loadingTransitioned = ((oldState ^ newState) & RKTableControllerStateLoading); + assertThatBool(loadingTransitioned, is(equalToBool(YES))); + + oldState = RKTableControllerStateOffline | RKTableControllerStateEmpty; + newState = RKTableControllerStateOffline | RKTableControllerStateEmpty | RKTableControllerStateLoading; + loadingTransitioned = ((oldState ^ newState) & RKTableControllerStateLoading); + assertThatBool(loadingTransitioned, is(equalToBool(YES))); + + oldState = RKTableControllerStateNormal; + newState = RKTableControllerStateLoading; + loadingTransitioned = ((oldState ^ newState) & RKTableControllerStateLoading); + assertThatBool(loadingTransitioned, is(equalToBool(YES))); + + oldState = RKTableControllerStateOffline | RKTableControllerStateEmpty | RKTableControllerStateLoading; + newState = RKTableControllerStateOffline | RKTableControllerStateLoading; + loadingTransitioned = ((oldState ^ newState) & RKTableControllerStateLoading); + assertThatBool(loadingTransitioned, is(equalToBool(NO))); + + oldState = RKTableControllerStateNotYetLoaded; + newState = RKTableControllerStateOffline; + loadingTransitioned = ((oldState ^ newState) & RKTableControllerStateLoading); + assertThatBool(loadingTransitioned, is(equalToBool(NO))); +} + - (void)testSetTheModelToAnEmptyStateIfTheObjectLoaderReturnsAnEmptyCollection { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatBool([tableController isLoaded], is(equalToBool(NO))); NSArray* objects = [NSArray array]; @@ -734,7 +619,7 @@ - (void)testSetTheModelToAnEmptyStateIfTheObjectLoaderReturnsAnEmptyCollection { } - (void)testSetTheModelToALoadedStateEvenIfTheObjectLoaderReturnsAnEmptyCollection { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatBool([tableController isLoaded], is(equalToBool(NO))); NSArray* objects = [NSArray array]; @@ -746,7 +631,7 @@ - (void)testSetTheModelToALoadedStateEvenIfTheObjectLoaderReturnsAnEmptyCollecti } - (void)testEnterTheLoadingStateWhenTheRequestStartsLoading { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatBool([tableController isLoaded], is(equalToBool(NO))); assertThatBool([tableController isLoading], is(equalToBool(NO))); @@ -756,11 +641,11 @@ - (void)testEnterTheLoadingStateWhenTheRequestStartsLoading { } - (void)testExitTheLoadingStateWhenTheRequestFinishesLoading { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatBool([tableController isLoaded], is(equalToBool(NO))); assertThatBool([tableController isLoading], is(equalToBool(NO))); - id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + id mockLoader = [OCMockObject niceMockForClass:[RKObjectLoader class]]; [tableController requestDidStartLoad:mockLoader]; assertThatBool([tableController isLoading], is(equalToBool(YES))); [tableController objectLoaderDidFinishLoading:mockLoader]; @@ -768,7 +653,7 @@ - (void)testExitTheLoadingStateWhenTheRequestFinishesLoading { } - (void)testClearTheLoadingStateWhenARequestIsCancelled { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatBool([tableController isLoaded], is(equalToBool(NO))); assertThatBool([tableController isLoading], is(equalToBool(NO))); @@ -780,7 +665,7 @@ - (void)testClearTheLoadingStateWhenARequestIsCancelled { } - (void)testClearTheLoadingStateWhenARequestTimesOut { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; assertThatBool([tableController isLoaded], is(equalToBool(NO))); assertThatBool([tableController isLoading], is(equalToBool(NO))); @@ -795,11 +680,74 @@ - (void)testDoSomethingWhenTheRequestLoadsAnUnexpectedResponse { RKLogCritical(@"PENDING - Undefined Behavior!!!"); } -#pragma mark - RKTableViewDelegate specs +- (void)testLoadCollectionOfObjectsAndMapThemIntoSections { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + objectManager.client.cachePolicy = RKRequestCachePolicyNone; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { + mapping.cellClass = [RKTestUserTableViewCell class]; + [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; + }]]; + tableController.sectionNameKeyPath = @"name"; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate tableControllerDelegate]; + delegate.timeout = 10; + tableController.delegate = delegate; + [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [delegate waitForLoad]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInt(tableController.sectionCount, is(equalToInt(3))); + assertThatInt(tableController.rowCount, is(equalToInt(3))); +} + +- (void)testLoadingACollectionOfObjectsIntoSectionsAndThenLoadingAnEmptyCollectionChangesTableToEmpty { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + objectManager.client.cachePolicy = RKRequestCachePolicyNone; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { + mapping.cellClass = [RKTestUserTableViewCell class]; + [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; + }]]; + tableController.sectionNameKeyPath = @"name"; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate tableControllerDelegate]; + delegate.timeout = 10; + tableController.delegate = delegate; + [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [delegate waitForLoad]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInt(tableController.sectionCount, is(equalToInt(3))); + assertThatInt(tableController.rowCount, is(equalToInt(3))); + delegate = [RKTableControllerTestDelegate tableControllerDelegate]; + delegate.timeout = 10; + tableController.delegate = delegate; + [tableController loadTableFromResourcePath:@"/204" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [delegate waitForLoad]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); +} + +#pragma mark - RKTableViewDelegate Tests - (void)testNotifyTheDelegateWhenLoadingStarts { - RKObjectManager* objectManager = RKTestNewObjectManager(); - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { @@ -807,7 +755,7 @@ - (void)testNotifyTheDelegateWhenLoadingStarts { [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - id mockDelegate = [OCMockObject partialMockForObject:[RKTestTableControllerDelegate new]]; + id mockDelegate = [OCMockObject partialMockForObject:[RKTableControllerTestDelegate new]]; [[[mockDelegate expect] andForwardToRealObject] tableControllerDidStartLoad:tableController]; tableController.delegate = mockDelegate; [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { @@ -816,12 +764,12 @@ - (void)testNotifyTheDelegateWhenLoadingStarts { }]; }]; [mockDelegate waitForLoad]; - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotifyTheDelegateWhenLoadingFinishes { - RKObjectManager* objectManager = RKTestNewObjectManager(); - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { @@ -829,7 +777,7 @@ - (void)testNotifyTheDelegateWhenLoadingFinishes { [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableControllerDidFinishLoad:tableController]; tableController.delegate = mockDelegate; @@ -839,12 +787,35 @@ - (void)testNotifyTheDelegateWhenLoadingFinishes { }]; }]; [mockDelegate waitForLoad]; - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testNotifyTheDelegateOnDidFinalizeLoad { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { + mapping.cellClass = [RKTestUserTableViewCell class]; + [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; + }]]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[mockDelegate expect] tableControllerDidFinalizeLoad:tableController]; + tableController.delegate = mockDelegate; + [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [mockDelegate waitForLoad]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotifyTheDelegateWhenAnErrorOccurs { - RKObjectManager* objectManager = RKTestNewObjectManager(); - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { @@ -852,7 +823,7 @@ - (void)testNotifyTheDelegateWhenAnErrorOccurs { [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController didFailLoadWithError:OCMOCK_ANY]; tableController.delegate = mockDelegate; @@ -862,13 +833,13 @@ - (void)testNotifyTheDelegateWhenAnErrorOccurs { }]; }]; [mockDelegate waitForLoad]; - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotifyTheDelegateWhenAnEmptyCollectionIsLoaded { - RKObjectManager* objectManager = RKTestNewObjectManager(); + RKObjectManager* objectManager = [RKTestFactory objectManager]; objectManager.client.cachePolicy = RKRequestCachePolicyNone; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { @@ -876,7 +847,7 @@ - (void)testNotifyTheDelegateWhenAnEmptyCollectionIsLoaded { [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; delegate.timeout = 5; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableControllerDidBecomeEmpty:tableController]; @@ -887,12 +858,12 @@ - (void)testNotifyTheDelegateWhenAnEmptyCollectionIsLoaded { }]; }]; [mockDelegate waitForLoad]; - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotifyTheDelegateWhenModelWillLoadWithObjectLoader { - RKObjectManager* objectManager = RKTestNewObjectManager(); - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { @@ -900,7 +871,7 @@ - (void)testNotifyTheDelegateWhenModelWillLoadWithObjectLoader { [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController willLoadTableWithObjectLoader:OCMOCK_ANY]; tableController.delegate = mockDelegate; @@ -910,12 +881,12 @@ - (void)testNotifyTheDelegateWhenModelWillLoadWithObjectLoader { }]; }]; [mockDelegate waitForLoad]; - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotifyTheDelegateWhenModelDidLoadWithObjectLoader { - RKObjectManager* objectManager = RKTestNewObjectManager(); - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { @@ -923,7 +894,7 @@ - (void)testNotifyTheDelegateWhenModelDidLoadWithObjectLoader { [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController didLoadTableWithObjectLoader:OCMOCK_ANY]; tableController.delegate = mockDelegate; @@ -933,12 +904,12 @@ - (void)testNotifyTheDelegateWhenModelDidLoadWithObjectLoader { }]; }]; [mockDelegate waitForLoad]; - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotifyTheDelegateWhenModelDidCancelLoad { - RKObjectManager* objectManager = RKTestNewObjectManager(); - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { @@ -946,7 +917,7 @@ - (void)testNotifyTheDelegateWhenModelDidCancelLoad { [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; }]]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableControllerDidCancelLoad:tableController]; tableController.delegate = mockDelegate; @@ -956,15 +927,14 @@ - (void)testNotifyTheDelegateWhenModelDidCancelLoad { }]; }]; [tableController cancelLoad]; - [mockDelegate waitForLoad]; - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotifyTheDelegateWhenDidEndEditingARow { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableItem* tableItem = [RKTableItem tableItem]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController didEndEditing:OCMOCK_ANY @@ -972,14 +942,14 @@ - (void)testNotifyTheDelegateWhenDidEndEditingARow { tableController.delegate = mockDelegate; [tableController loadTableItems:[NSArray arrayWithObject:tableItem]]; [tableController tableView:tableController.tableView didEndEditingRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotifyTheDelegateWhenWillBeginEditingARow { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableItem* tableItem = [RKTableItem tableItem]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController willBeginEditing:OCMOCK_ANY @@ -987,14 +957,14 @@ - (void)testNotifyTheDelegateWhenWillBeginEditingARow { tableController.delegate = mockDelegate; [tableController loadTableItems:[NSArray arrayWithObject:tableItem]]; [tableController tableView:tableController.tableView willBeginEditingRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotifyTheDelegateWhenAnObjectIsInserted { NSArray* objects = [NSArray arrayWithObject:@"first object"]; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController didInsertObject:@"first object" @@ -1009,14 +979,14 @@ - (void)testNotifyTheDelegateWhenAnObjectIsInserted { [[tableController.sections objectAtIndex:0] insertObject:@"new object" atIndex:1]; assertThat([[tableController.sections objectAtIndex:0] objectAtIndex:0], is(equalTo(@"first object"))); assertThat([[tableController.sections objectAtIndex:0] objectAtIndex:1], is(equalTo(@"new object"))); - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotifyTheDelegateWhenAnObjectIsUpdated { NSArray* objects = [NSArray arrayWithObjects:@"first object", @"second object", nil]; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController didInsertObject:@"first object" @@ -1033,14 +1003,14 @@ - (void)testNotifyTheDelegateWhenAnObjectIsUpdated { assertThat([[tableController.sections objectAtIndex:0] objectAtIndex:1], is(equalTo(@"second object"))); [[tableController.sections objectAtIndex:0] replaceObjectAtIndex:1 withObject:@"new second object"]; assertThat([[tableController.sections objectAtIndex:0] objectAtIndex:1], is(equalTo(@"new second object"))); - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testNotifyTheDelegateWhenAnObjectIsDeleted { NSArray* objects = [NSArray arrayWithObjects:@"first object", @"second object", nil]; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController didInsertObject:@"first object" @@ -1057,14 +1027,90 @@ - (void)testNotifyTheDelegateWhenAnObjectIsDeleted { assertThat([[tableController.sections objectAtIndex:0] objectAtIndex:1], is(equalTo(@"second object"))); [[tableController.sections objectAtIndex:0] removeObjectAtIndex:1]; assertThat([[tableController.sections objectAtIndex:0] objectAtIndex:0], is(equalTo(@"first object"))); - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testNotifyTheDelegateWhenObjectsAreLoadedInASection { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { + mapping.cellClass = [RKTestUserTableViewCell class]; + [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; + }]]; + + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[mockDelegate expect] tableController:tableController didLoadObjects:OCMOCK_ANY inSection:OCMOCK_ANY]; + tableController.delegate = mockDelegate; + + [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [mockDelegate waitForLoad]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testDelegateIsNotifiedOfWillDisplayCellForObjectAtIndexPath { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { + mapping.cellClass = [RKTestUserTableViewCell class]; + [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + }]]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController willLoadTableWithObjectLoader:OCMOCK_ANY]; + tableController.delegate = mockDelegate; + [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [[mockDelegate expect] tableController:tableController willDisplayCell:OCMOCK_ANY forObject:OCMOCK_ANY atIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [[mockDelegate expect] tableController:tableController willDisplayCell:OCMOCK_ANY forObject:OCMOCK_ANY atIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + [[mockDelegate expect] tableController:tableController willDisplayCell:OCMOCK_ANY forObject:OCMOCK_ANY atIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + [[[UIApplication sharedApplication].windows objectAtIndex:0] setRootViewController:viewController]; + [mockDelegate waitForLoad]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testDelegateIsNotifiedOfDidSelectRowForObjectAtIndexPath { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { + mapping.cellClass = [RKTestUserTableViewCell class]; + [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + }]]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController willLoadTableWithObjectLoader:OCMOCK_ANY]; + tableController.delegate = mockDelegate; + [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [[mockDelegate expect] tableController:tableController didSelectCell:OCMOCK_ANY forObject:OCMOCK_ANY atIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [[[UIApplication sharedApplication].windows objectAtIndex:0] setRootViewController:viewController]; + [mockDelegate waitForLoad]; + [tableController tableView:tableController.tableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + STAssertNoThrow([mockDelegate verify], nil); } #pragma mark - Notifications - (void)testPostANotificationWhenLoadingStarts { - RKObjectManager* objectManager = RKTestNewObjectManager(); - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { @@ -1075,7 +1121,7 @@ - (void)testPostANotificationWhenLoadingStarts { id observerMock = [OCMockObject observerMock]; [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidStartLoadNotification object:tableController]; [[observerMock expect] notificationWithName:RKTableControllerDidStartLoadNotification object:tableController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; tableController.delegate = delegate; [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { @@ -1087,8 +1133,8 @@ - (void)testPostANotificationWhenLoadingStarts { } - (void)testPostANotificationWhenLoadingFinishes { - RKObjectManager* objectManager = RKTestNewObjectManager(); - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { @@ -1100,7 +1146,7 @@ - (void)testPostANotificationWhenLoadingFinishes { id observerMock = [OCMockObject observerMock]; [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidFinishLoadNotification object:tableController]; [[observerMock expect] notificationWithName:RKTableControllerDidFinishLoadNotification object:tableController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; tableController.delegate = delegate; [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { @@ -1112,9 +1158,35 @@ - (void)testPostANotificationWhenLoadingFinishes { [observerMock verify]; } +- (void)testPostANotificationWhenObjectsAreLoaded { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { + mapping.cellClass = [RKTestUserTableViewCell class]; + [mapping mapKeyPath:@"name" toAttribute:@"textLabel.text"]; + [mapping mapKeyPath:@"nickName" toAttribute:@"detailTextLabel.text"]; + }]]; + + id observerMock = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidLoadObjectsNotification object:tableController]; + [[observerMock expect] notificationWithName:RKTableControllerDidLoadObjectsNotification object:tableController]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + tableController.delegate = delegate; + + [tableController loadTableFromResourcePath:@"/JSON/users.json" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [delegate waitForLoad]; + [observerMock verify]; +} + - (void)testPostANotificationWhenAnErrorOccurs { - RKObjectManager* objectManager = RKTestNewObjectManager(); - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { @@ -1126,7 +1198,7 @@ - (void)testPostANotificationWhenAnErrorOccurs { id observerMock = [OCMockObject observerMock]; [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidLoadErrorNotification object:tableController]; [[observerMock expect] notificationWithName:RKTableControllerDidLoadErrorNotification object:tableController userInfo:OCMOCK_ANY]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; tableController.delegate = delegate; [tableController loadTableFromResourcePath:@"/fail" usingBlock:^(RKObjectLoader* objectLoader) { @@ -1139,9 +1211,9 @@ - (void)testPostANotificationWhenAnErrorOccurs { } - (void)testPostANotificationWhenAnEmptyCollectionIsLoaded { - RKObjectManager* objectManager = RKTestNewObjectManager(); + RKObjectManager* objectManager = [RKTestFactory objectManager]; objectManager.client.cachePolicy = RKRequestCachePolicyNone; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* mapping) { @@ -1153,7 +1225,7 @@ - (void)testPostANotificationWhenAnEmptyCollectionIsLoaded { id observerMock = [OCMockObject observerMock]; [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidLoadEmptyNotification object:tableController]; [[observerMock expect] notificationWithName:RKTableControllerDidLoadEmptyNotification object:tableController]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; tableController.delegate = delegate; [tableController loadTableFromResourcePath:@"/empty/array" usingBlock:^(RKObjectLoader* objectLoader) { objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { @@ -1164,10 +1236,113 @@ - (void)testPostANotificationWhenAnEmptyCollectionIsLoaded { [observerMock verify]; } +#pragma mark - State Transitions + +- (void)testInitializesToNotYetLoadedState { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool(tableController.state == RKTableControllerStateNotYetLoaded, is(equalToBool(YES))); +} + +- (void)testInitialLoadSetsStateToLoading { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + [tableController requestDidStartLoad:mockLoader]; + assertThatBool([tableController isLoading], is(equalToBool(YES))); + assertThatBool([tableController isLoaded], is(equalToBool(NO))); +} + +- (void)testSuccessfulLoadSetsStateToNormal { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + [tableController objectLoader:mockLoader didLoadObjects:[NSArray arrayWithObject:@"test"]]; + assertThatBool([tableController isLoading], is(equalToBool(NO))); + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInteger(tableController.state, is(equalToInteger(RKTableControllerStateNormal))); +} + +- (void)testErrorLoadsSetsStateToError { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + NSError *error = [NSError errorWithDomain:@"Test" code:1234 userInfo:nil]; + [tableController objectLoader:mockLoader didFailWithError:error]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatBool([tableController isError], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); +} + +- (void)testSecondaryLoadAfterErrorSetsStateToErrorAndLoading { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + NSError *error = [NSError errorWithDomain:@"Test" code:1234 userInfo:nil]; + [tableController objectLoader:mockLoader didFailWithError:error]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatBool([tableController isError], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + [tableController requestDidStartLoad:mockLoader]; + assertThatBool([tableController isLoading], is(equalToBool(YES))); + assertThatBool([tableController isError], is(equalToBool(YES))); +} + +- (void)testEmptyLoadSetsStateToEmpty { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController loadEmpty]; + assertThatBool([tableController isEmpty], is(equalToBool(YES))); +} + +- (void)testSecondaryLoadAfterEmptySetsStateToEmptyAndLoading { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController loadEmpty]; + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + [tableController requestDidStartLoad:mockLoader]; + assertThatBool([tableController isLoading], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); +} + +- (void)testTransitionToOfflineAfterLoadSetsStateToOfflineAndLoaded { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + id mockManager = [OCMockObject partialMockForObject:objectManager]; + BOOL isOnline = YES; + [[[mockManager stub] andReturnValue:OCMOCK_VALUE(isOnline)] isOnline]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = mockManager; + [tableController loadEmpty]; + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + assertThatBool([tableController isOffline], is(equalToBool(NO))); + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + [tableController requestDidStartLoad:mockLoader]; + assertThatBool([tableController isLoading], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + isOnline = NO; + id mockManager2 = [OCMockObject partialMockForObject:objectManager]; + [[[mockManager2 stub] andReturnValue:OCMOCK_VALUE(isOnline)] isOnline]; + tableController.objectManager = mockManager2; + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOfflineNotification object:tableController.objectManager]; + assertThatBool(tableController.isOffline, is(equalToBool(YES))); +} + #pragma mark - State Views - (void)testPermitYouToOverlayAnImageOnTheTable { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; [tableController showImageInOverlay:image]; @@ -1177,7 +1352,7 @@ - (void)testPermitYouToOverlayAnImageOnTheTable { } - (void)testPermitYouToRemoveAnImageOverlayFromTheTable { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; [tableController showImageInOverlay:image]; @@ -1187,7 +1362,7 @@ - (void)testPermitYouToRemoveAnImageOverlayFromTheTable { } - (void)testTriggerDisplayOfTheErrorViewOnTransitionToErrorState { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; tableController.imageForError = image; @@ -1200,7 +1375,7 @@ - (void)testTriggerDisplayOfTheErrorViewOnTransitionToErrorState { } - (void)testTriggerHidingOfTheErrorViewOnTransitionOutOfTheErrorState { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; tableController.imageForError = image; @@ -1216,7 +1391,7 @@ - (void)testTriggerHidingOfTheErrorViewOnTransitionOutOfTheErrorState { } - (void)testTriggerDisplayOfTheEmptyViewOnTransitionToEmptyState { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; tableController.imageForEmpty = image; @@ -1228,7 +1403,7 @@ - (void)testTriggerDisplayOfTheEmptyViewOnTransitionToEmptyState { } - (void)testTriggerHidingOfTheEmptyViewOnTransitionOutOfTheEmptyState { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; tableController.imageForEmpty = image; @@ -1242,7 +1417,7 @@ - (void)testTriggerHidingOfTheEmptyViewOnTransitionOutOfTheEmptyState { } - (void)testTriggerDisplayOfTheLoadingViewOnTransitionToTheLoadingState { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; tableController.loadingView = spinner; @@ -1252,7 +1427,7 @@ - (void)testTriggerDisplayOfTheLoadingViewOnTransitionToTheLoadingState { } - (void)testTriggerHidingOfTheLoadingViewOnTransitionOutOfTheLoadingState { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; tableController.loadingView = spinner; @@ -1267,7 +1442,7 @@ - (void)testTriggerHidingOfTheLoadingViewOnTransitionOutOfTheLoadingState { #pragma mark - Header, Footer, and Empty Rows - (void)testShowHeaderRows { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1298,7 +1473,7 @@ - (void)testShowHeaderRows { } - (void)testShowFooterRows { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -1329,7 +1504,7 @@ - (void)testShowFooterRows { } - (void)testHideHeaderRowsWhenEmptyWhenPropertyIsNotSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1349,7 +1524,7 @@ - (void)testHideHeaderRowsWhenEmptyWhenPropertyIsNotSet { } - (void)testHideFooterRowsWhenEmptyWhenPropertyIsNotSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Footer"; @@ -1369,7 +1544,7 @@ - (void)testHideFooterRowsWhenEmptyWhenPropertyIsNotSet { } - (void)testRemoveHeaderAndFooterCountsWhenDeterminingIsEmpty { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1398,7 +1573,7 @@ - (void)testRemoveHeaderAndFooterCountsWhenDeterminingIsEmpty { } - (void)testNotShowTheEmptyItemWhenTheTableIsNotEmpty { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1449,7 +1624,7 @@ - (void)testNotShowTheEmptyItemWhenTheTableIsNotEmpty { } - (void)testShowTheEmptyItemWhenTheTableIsEmpty { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1490,7 +1665,7 @@ - (void)testShowTheEmptyItemWhenTheTableIsEmpty { } - (void)testShowTheEmptyItemPlusHeadersAndFootersWhenTheTableIsEmpty { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { tableItem.text = @"Header"; @@ -1530,10 +1705,10 @@ - (void)testShowTheEmptyItemPlusHeadersAndFootersWhenTheTableIsEmpty { assertThatBool(cellThree.hidden, is(equalToBool(NO))); } -#pragma mark - UITableViewDelegate specs +#pragma mark - UITableViewDelegate Tests - (void)testInvokeTheOnSelectCellForObjectAtIndexPathBlockHandler { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableItem* tableItem = [RKTableItem tableItem]; __block BOOL dispatched = NO; @@ -1547,7 +1722,7 @@ - (void)testInvokeTheOnSelectCellForObjectAtIndexPathBlockHandler { } - (void)testInvokeTheOnCellWillAppearForObjectAtIndexPathBlockHandler { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableItem* tableItem = [RKTableItem tableItem]; __block BOOL dispatched = NO; @@ -1562,7 +1737,7 @@ - (void)testInvokeTheOnCellWillAppearForObjectAtIndexPathBlockHandler { } - (void)testOptionallyHideHeaderRowsWhenTheyAppearAndTheTableIsEmpty { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.showsHeaderRowsWhenEmpty = NO; RKTableItem* tableItem = [RKTableItem tableItem]; @@ -1575,7 +1750,7 @@ - (void)testOptionallyHideHeaderRowsWhenTheyAppearAndTheTableIsEmpty { } - (void)testOptionallyHideFooterRowsWhenTheyAppearAndTheTableIsEmpty { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.showsFooterRowsWhenEmpty = NO; RKTableItem* tableItem = [RKTableItem tableItem]; @@ -1588,60 +1763,63 @@ - (void)testOptionallyHideFooterRowsWhenTheyAppearAndTheTableIsEmpty { } - (void)testInvokeABlockCallbackWhenTheCellAccessoryButtonIsTapped { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableItem* tableItem = [RKTableItem tableItem]; __block BOOL dispatched = NO; - [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMappingBlock:^(RKTableViewCellMapping* cellMapping) { + RKTableViewCellMapping *mapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping *cellMapping) { cellMapping.onTapAccessoryButtonForObjectAtIndexPath = ^(UITableViewCell* cell, id object, NSIndexPath* indexPath) { dispatched = YES; }; }]; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMapping:mapping]; [tableController tableView:tableController.tableView accessoryButtonTappedForRowWithIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; assertThatBool(dispatched, is(equalToBool(YES))); } - (void)testInvokeABlockCallbackWhenTheDeleteConfirmationButtonTitleIsDetermined { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; RKTableItem* tableItem = [RKTableItem tableItem]; NSString* deleteTitle = @"Delete Me"; - [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMappingBlock:^(RKTableViewCellMapping* cellMapping) { + RKTableViewCellMapping *mapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping *cellMapping) { cellMapping.titleForDeleteButtonForObjectAtIndexPath = ^ NSString*(UITableViewCell* cell, id object, NSIndexPath* indexPath) { return deleteTitle; }; }]; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMapping:mapping]; NSString* delegateTitle = [tableController tableView:tableController.tableView titleForDeleteConfirmationButtonForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; assertThat(delegateTitle, is(equalTo(deleteTitle))); } - (void)testInvokeABlockCallbackWhenCellEditingStyleIsDetermined { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.canEditRows = YES; RKTableItem* tableItem = [RKTableItem tableItem]; - [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMappingBlock:^(RKTableViewCellMapping* cellMapping) { + RKTableViewCellMapping *mapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping *cellMapping) { cellMapping.editingStyleForObjectAtIndexPath = ^ UITableViewCellEditingStyle(UITableViewCell* cell, id object, NSIndexPath* indexPath) { return UITableViewCellEditingStyleInsert; }; }]; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMapping:mapping]; UITableViewCellEditingStyle delegateStyle = [tableController tableView:tableController.tableView editingStyleForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; assertThatInt(delegateStyle, is(equalToInt(UITableViewCellEditingStyleInsert))); } - (void)testInvokeABlockCallbackWhenACellIsMoved { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.canMoveRows = YES; RKTableItem* tableItem = [RKTableItem tableItem]; NSIndexPath* moveToIndexPath = [NSIndexPath indexPathForRow:2 inSection:0]; - [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMappingBlock:^(RKTableViewCellMapping* cellMapping) { + [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { cellMapping.targetIndexPathForMove = ^ NSIndexPath*(UITableViewCell* cell, id object, NSIndexPath* sourceIndexPath, NSIndexPath* destinationIndexPath) { return moveToIndexPath; }; - }]; + }]]; NSIndexPath* delegateIndexPath = [tableController tableView:tableController.tableView targetIndexPathForMoveFromRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]toProposedIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; assertThat(delegateIndexPath, is(equalTo(moveToIndexPath))); @@ -1650,41 +1828,41 @@ - (void)testInvokeABlockCallbackWhenACellIsMoved { #pragma mark Variable Height Rows - (void)testReturnTheRowHeightConfiguredOnTheTableViewWhenVariableHeightRowsIsDisabled { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.variableHeightRows = NO; tableController.tableView.rowHeight = 55; RKTableItem* tableItem = [RKTableItem tableItem]; - [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMappingBlock:^(RKTableViewCellMapping* cellMapping) { + [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { cellMapping.rowHeight = 200; - }]; + }]]; CGFloat height = [tableController tableView:tableController.tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; assertThatFloat(height, is(equalToFloat(55))); } - (void)testReturnTheHeightFromTheTableCellMappingWhenVariableHeightRowsAreEnabled { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.variableHeightRows = YES; tableController.tableView.rowHeight = 55; RKTableItem* tableItem = [RKTableItem tableItem]; - [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMappingBlock:^(RKTableViewCellMapping* cellMapping) { + [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { cellMapping.rowHeight = 200; - }]; + }]]; CGFloat height = [tableController tableView:tableController.tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; assertThatFloat(height, is(equalToFloat(200))); } - (void)testInvokeAnBlockCallbackToDetermineTheCellHeightWhenVariableHeightRowsAreEnabled { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.variableHeightRows = YES; tableController.tableView.rowHeight = 55; RKTableItem* tableItem = [RKTableItem tableItem]; - [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMappingBlock:^(RKTableViewCellMapping* cellMapping) { + [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { cellMapping.rowHeight = 200; cellMapping.heightOfCellForObjectAtIndexPath = ^ CGFloat(id object, NSIndexPath* indexPath) { return 150; }; - }]; + }]]; CGFloat height = [tableController tableView:tableController.tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; assertThatFloat(height, is(equalToFloat(150))); } @@ -1692,7 +1870,7 @@ - (void)testInvokeAnBlockCallbackToDetermineTheCellHeightWhenVariableHeightRowsA #pragma mark - Editing - (void)testAllowEditingWhenTheCanEditRowsPropertyIsSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.canEditRows = YES; RKTableItem* tableItem = [RKTableItem tableItem]; @@ -1703,7 +1881,7 @@ - (void)testAllowEditingWhenTheCanEditRowsPropertyIsSet { } - (void)testCommitADeletionWhenTheCanEditRowsPropertyIsSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.canEditRows = YES; [tableController loadObjects:[NSArray arrayWithObjects:@"First Object", @"Second Object", nil]]; @@ -1720,7 +1898,7 @@ - (void)testCommitADeletionWhenTheCanEditRowsPropertyIsSet { } - (void)testNotCommitADeletionWhenTheCanEditRowsPropertyIsNotSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController loadObjects:[NSArray arrayWithObjects:@"First Object", @"Second Object", nil]]; assertThatInt([tableController rowCount], is(equalToInt(2))); @@ -1738,7 +1916,7 @@ - (void)testNotCommitADeletionWhenTheCanEditRowsPropertyIsNotSet { } - (void)testDoNothingToCommitAnInsertionWhenTheCanEditRowsPropertyIsSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.canEditRows = YES; [tableController loadObjects:[NSArray arrayWithObjects:@"First Object", @"Second Object", nil]]; @@ -1757,7 +1935,7 @@ - (void)testDoNothingToCommitAnInsertionWhenTheCanEditRowsPropertyIsSet { } - (void)testAllowMovingWhenTheCanMoveRowsPropertyIsSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.canMoveRows = YES; RKTableItem* tableItem = [RKTableItem tableItem]; @@ -1768,7 +1946,7 @@ - (void)testAllowMovingWhenTheCanMoveRowsPropertyIsSet { } - (void)testMoveARowWithinASectionWhenTheCanMoveRowsPropertyIsSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.canMoveRows = YES; [tableController loadObjects:[NSArray arrayWithObjects:@"First Object", @"Second Object", nil]]; @@ -1787,7 +1965,7 @@ - (void)testMoveARowWithinASectionWhenTheCanMoveRowsPropertyIsSet { } - (void)testMoveARowAcrossSectionsWhenTheCanMoveRowsPropertyIsSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.canMoveRows = YES; [tableController loadObjects:[NSArray arrayWithObjects:@"First Object", @"Second Object", nil]]; @@ -1808,7 +1986,7 @@ - (void)testMoveARowAcrossSectionsWhenTheCanMoveRowsPropertyIsSet { } - (void)testNotMoveARowWhenTheCanMoveRowsPropertyIsNotSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; [tableController loadObjects:[NSArray arrayWithObjects:@"First Object", @"Second Object", nil]]; assertThatInt([tableController rowCount], is(equalToInt(2))); @@ -1828,11 +2006,11 @@ - (void)testNotMoveARowWhenTheCanMoveRowsPropertyIsNotSet { #pragma mark - Reachability Integration - (void)testTransitionToTheOnlineStateWhenAReachabilityNoticeIsReceived { - RKObjectManager* objectManager = RKTestNewObjectManager(); + RKObjectManager* objectManager = [RKTestFactory objectManager]; id mockManager = [OCMockObject partialMockForObject:objectManager]; BOOL online = YES; [[[mockManager stub] andReturnValue:OCMOCK_VALUE(online)] isOnline]; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOnlineNotification object:objectManager]; @@ -1840,11 +2018,11 @@ - (void)testTransitionToTheOnlineStateWhenAReachabilityNoticeIsReceived { } - (void)testTransitionToTheOfflineStateWhenAReachabilityNoticeIsReceived { - RKObjectManager* objectManager = RKTestNewObjectManager(); + RKObjectManager* objectManager = [RKTestFactory objectManager]; id mockManager = [OCMockObject partialMockForObject:objectManager]; BOOL online = NO; [[[mockManager stub] andReturnValue:OCMOCK_VALUE(online)] isOnline]; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOfflineNotification object:objectManager]; @@ -1852,7 +2030,7 @@ - (void)testTransitionToTheOfflineStateWhenAReachabilityNoticeIsReceived { } - (void)testNotifyTheDelegateOnTransitionToOffline { - RKObjectManager* objectManager = RKTestNewObjectManager(); + RKObjectManager* objectManager = [RKTestFactory objectManager]; id mockManager = [OCMockObject partialMockForObject:objectManager]; [mockManager setExpectationOrderMatters:YES]; RKObjectManagerNetworkStatus networkStatus = RKObjectManagerNetworkStatusOnline; @@ -1861,17 +2039,17 @@ - (void)testNotifyTheDelegateOnTransitionToOffline { [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; online = NO; // After the notification is posted [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(RKTableControllerDelegate)]; [[mockDelegate expect] tableControllerDidBecomeOffline:tableController]; tableController.delegate = mockDelegate; [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOfflineNotification object:objectManager]; - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testPostANotificationOnTransitionToOffline { - RKObjectManager* objectManager = RKTestNewObjectManager(); + RKObjectManager* objectManager = [RKTestFactory objectManager]; id mockManager = [OCMockObject partialMockForObject:objectManager]; [mockManager setExpectationOrderMatters:YES]; RKObjectManagerNetworkStatus networkStatus = RKObjectManagerNetworkStatusOnline; @@ -1880,7 +2058,7 @@ - (void)testPostANotificationOnTransitionToOffline { [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; online = NO; // After the notification is posted [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; id observerMock = [OCMockObject observerMock]; @@ -1892,28 +2070,33 @@ - (void)testPostANotificationOnTransitionToOffline { } - (void)testNotifyTheDelegateOnTransitionToOnline { - RKObjectManager* objectManager = RKTestNewObjectManager(); + RKObjectManager* objectManager = [RKTestFactory objectManager]; id mockManager = [OCMockObject partialMockForObject:objectManager]; - BOOL online = YES; + BOOL online = NO; + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + online = YES; [[[mockManager stub] andReturnValue:OCMOCK_VALUE(online)] isOnline]; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + [RKObjectManager setSharedManager:nil]; // Don't want the controller to initialize with the sharedManager... RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; id mockDelegate = [OCMockObject mockForProtocol:@protocol(RKTableControllerDelegate)]; [[mockDelegate expect] tableControllerDidBecomeOnline:tableController]; tableController.delegate = mockDelegate; [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOnlineNotification object:objectManager]; - [mockDelegate verify]; - [mockManager release]; - [mockDelegate release]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testPostANotificationOnTransitionToOnline { - RKObjectManager* objectManager = RKTestNewObjectManager(); + RKObjectManager* objectManager = [RKTestFactory objectManager]; id mockManager = [OCMockObject partialMockForObject:objectManager]; - BOOL online = YES; + BOOL online = NO; + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + online = YES; [[[mockManager stub] andReturnValue:OCMOCK_VALUE(online)] isOnline]; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + [RKObjectManager setSharedManager:nil]; // Don't want the controller to initialize with the sharedManager... RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.objectManager = objectManager; @@ -1926,7 +2109,7 @@ - (void)testPostANotificationOnTransitionToOnline { } - (void)testShowTheOfflineImageOnTransitionToOffline { - RKObjectManager* objectManager = RKTestNewObjectManager(); + RKObjectManager* objectManager = [RKTestFactory objectManager]; id mockManager = [OCMockObject partialMockForObject:objectManager]; [mockManager setExpectationOrderMatters:YES]; RKObjectManagerNetworkStatus networkStatus = RKObjectManagerNetworkStatusOnline; @@ -1935,7 +2118,7 @@ - (void)testShowTheOfflineImageOnTransitionToOffline { [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; online = NO; // After the notification is posted [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; tableController.imageForOffline = image; @@ -1948,7 +2131,7 @@ - (void)testShowTheOfflineImageOnTransitionToOffline { } - (void)testRemoveTheOfflineImageOnTransitionToOnlineFromOffline { - RKObjectManager* objectManager = RKTestNewObjectManager(); + RKObjectManager* objectManager = [RKTestFactory objectManager]; id mockManager = [OCMockObject partialMockForObject:objectManager]; [mockManager setExpectationOrderMatters:YES]; RKObjectManagerNetworkStatus networkStatus = RKObjectManagerNetworkStatusOnline; @@ -1957,8 +2140,9 @@ - (void)testRemoveTheOfflineImageOnTransitionToOnlineFromOffline { [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; online = NO; // After the notification is posted [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController loadEmpty]; // Load to change the isLoaded state UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; tableController.imageForOffline = image; @@ -1979,7 +2163,7 @@ - (void)testRemoveTheOfflineImageOnTransitionToOnlineFromOffline { #pragma mark - Swipe Menus - (void)testAllowSwipeMenusWhenTheSwipeViewsEnabledPropertyIsSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.cellSwipeViewsEnabled = YES; RKTableItem* tableItem = [RKTableItem tableItem]; @@ -1989,7 +2173,7 @@ - (void)testAllowSwipeMenusWhenTheSwipeViewsEnabledPropertyIsSet { } - (void)testNotAllowEditingWhenTheSwipeViewsEnabledPropertyIsSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.cellSwipeViewsEnabled = YES; RKTableItem* tableItem = [RKTableItem tableItem]; @@ -2000,7 +2184,7 @@ - (void)testNotAllowEditingWhenTheSwipeViewsEnabledPropertyIsSet { } - (void)testRaiseAnExceptionWhenEnablingSwipeViewsWhenTheCanEditRowsPropertyIsSet { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.canEditRows = YES; @@ -2017,11 +2201,11 @@ - (void)testRaiseAnExceptionWhenEnablingSwipeViewsWhenTheCanEditRowsPropertyIsSe } - (void)testCallTheDelegateBeforeShowingTheSwipeView { - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.cellSwipeViewsEnabled = YES; RKTableItem* tableItem = [RKTableItem tableItem]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController willAddSwipeView:OCMOCK_ANY @@ -2032,16 +2216,15 @@ - (void)testCallTheDelegateBeforeShowingTheSwipeView { [tableController addSwipeViewTo:[RKTestUserTableViewCell new] withObject:@"object" direction:UISwipeGestureRecognizerDirectionRight]; - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } - (void)testCallTheDelegateBeforeHidingTheSwipeView { -// RKLogConfigureByName("RestKit/UI", RKLogLevelTrace); - RKTableControllerSpecTableViewController* viewController = [RKTableControllerSpecTableViewController new]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; tableController.cellSwipeViewsEnabled = YES; RKTableItem* tableItem = [RKTableItem tableItem]; - RKTestTableControllerDelegate* delegate = [RKTestTableControllerDelegate new]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; id mockDelegate = [OCMockObject partialMockForObject:delegate]; [[[mockDelegate expect] andForwardToRealObject] tableController:tableController willAddSwipeView:OCMOCK_ANY @@ -2060,7 +2243,7 @@ - (void)testCallTheDelegateBeforeHidingTheSwipeView { finished:nil context:nil]; [tableController removeSwipeView:YES]; - [mockDelegate verify]; + STAssertNoThrow([mockDelegate verify], nil); } @end diff --git a/Tests/Fixtures/JSON/ArrayOfNestedDictionaries.json b/Tests/Fixtures/JSON/ArrayOfNestedDictionaries.json index 1d85aad6f8..ded2caf2fc 100644 --- a/Tests/Fixtures/JSON/ArrayOfNestedDictionaries.json +++ b/Tests/Fixtures/JSON/ArrayOfNestedDictionaries.json @@ -26,4 +26,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/Tests/Fixtures/JSON/ArrayOfResults.json b/Tests/Fixtures/JSON/ArrayOfResults.json index b7f67129f7..2af487cbd1 100644 --- a/Tests/Fixtures/JSON/ArrayOfResults.json +++ b/Tests/Fixtures/JSON/ArrayOfResults.json @@ -12,4 +12,4 @@ "photo_url": "1308634984.jpg" } ] -} \ No newline at end of file +} diff --git a/Tests/Fixtures/JSON/ConnectingParents.json b/Tests/Fixtures/JSON/ConnectingParents.json index 3945fe047a..4b78cfe2f4 100644 --- a/Tests/Fixtures/JSON/ConnectingParents.json +++ b/Tests/Fixtures/JSON/ConnectingParents.json @@ -9,4 +9,4 @@ "parentID": 1 } ] -} \ No newline at end of file +} diff --git a/Tests/Fixtures/JSON/Dynamic/boy.json b/Tests/Fixtures/JSON/Dynamic/boy.json index 9cac23c961..df95b9e139 100644 --- a/Tests/Fixtures/JSON/Dynamic/boy.json +++ b/Tests/Fixtures/JSON/Dynamic/boy.json @@ -1 +1 @@ -{ "name": "Blake Watters", "type": "Boy", "numeric_type": 1 } \ No newline at end of file +{ "name": "Blake Watters", "type": "Boy", "numeric_type": 1 } diff --git a/Tests/Fixtures/JSON/Dynamic/child.json b/Tests/Fixtures/JSON/Dynamic/child.json index bb11b91068..1a85ebccaf 100644 --- a/Tests/Fixtures/JSON/Dynamic/child.json +++ b/Tests/Fixtures/JSON/Dynamic/child.json @@ -1 +1 @@ -{ "name": "Gargamel", "type": "Child", "id": 2 } \ No newline at end of file +{ "name": "Gargamel", "type": "Child", "id": 2 } diff --git a/Tests/Fixtures/JSON/Dynamic/friends.json b/Tests/Fixtures/JSON/Dynamic/friends.json index 83debc9861..0f1f64fab6 100644 --- a/Tests/Fixtures/JSON/Dynamic/friends.json +++ b/Tests/Fixtures/JSON/Dynamic/friends.json @@ -11,4 +11,4 @@ "type": "Girl" } ] -} \ No newline at end of file +} diff --git a/Tests/Fixtures/JSON/Dynamic/girl.json b/Tests/Fixtures/JSON/Dynamic/girl.json index b4d2ab3d87..db10e7cf63 100644 --- a/Tests/Fixtures/JSON/Dynamic/girl.json +++ b/Tests/Fixtures/JSON/Dynamic/girl.json @@ -1 +1 @@ -{ "name": "Sarah", "type": "Girl", "numeric_type": 0 } \ No newline at end of file +{ "name": "Sarah", "type": "Girl", "numeric_type": 0 } diff --git a/Tests/Fixtures/JSON/Dynamic/mixed.json b/Tests/Fixtures/JSON/Dynamic/mixed.json index 51dfb6f321..649e333a79 100644 --- a/Tests/Fixtures/JSON/Dynamic/mixed.json +++ b/Tests/Fixtures/JSON/Dynamic/mixed.json @@ -1,2 +1,2 @@ [ { "name": "Blake Watters", "type": "Boy" }, - { "name": "Sarah", "type": "Girl" } ] \ No newline at end of file + { "name": "Sarah", "type": "Girl" } ] diff --git a/Tests/Fixtures/JSON/Dynamic/parent.json b/Tests/Fixtures/JSON/Dynamic/parent.json index 612a7f1895..6c83335ce4 100644 --- a/Tests/Fixtures/JSON/Dynamic/parent.json +++ b/Tests/Fixtures/JSON/Dynamic/parent.json @@ -1 +1 @@ -{ "name": "Dan", "type": "Parent", "age": 33, "id": 1 } \ No newline at end of file +{ "name": "Dan", "type": "Parent", "age": 33, "id": 1 } diff --git a/Tests/Fixtures/JSON/DynamicKeys.json b/Tests/Fixtures/JSON/DynamicKeys.json index 6b944852a3..50c6df0b2b 100644 --- a/Tests/Fixtures/JSON/DynamicKeys.json +++ b/Tests/Fixtures/JSON/DynamicKeys.json @@ -9,4 +9,4 @@ "website": "http://www.twotoasters.com/" } } -} \ No newline at end of file +} diff --git a/Tests/Fixtures/JSON/DynamicKeysWithNestedRelationship.json b/Tests/Fixtures/JSON/DynamicKeysWithNestedRelationship.json index cd8e108833..e69f381df1 100644 --- a/Tests/Fixtures/JSON/DynamicKeysWithNestedRelationship.json +++ b/Tests/Fixtures/JSON/DynamicKeysWithNestedRelationship.json @@ -30,4 +30,4 @@ } } ] -} \ No newline at end of file +} diff --git a/Tests/Fixtures/JSON/DynamicKeysWithRelationship.json b/Tests/Fixtures/JSON/DynamicKeysWithRelationship.json index f5a8a9f1c4..7ae1fe6d5b 100644 --- a/Tests/Fixtures/JSON/DynamicKeysWithRelationship.json +++ b/Tests/Fixtures/JSON/DynamicKeysWithRelationship.json @@ -13,4 +13,4 @@ } } } -} \ No newline at end of file +} diff --git a/Tests/Fixtures/JSON/NakedEvents.json b/Tests/Fixtures/JSON/NakedEvents.json index 4b2f57db40..09d92f41c2 100644 --- a/Tests/Fixtures/JSON/NakedEvents.json +++ b/Tests/Fixtures/JSON/NakedEvents.json @@ -11,4 +11,4 @@ "location": "Your House", "summary": "Hootnanny" } -] \ No newline at end of file +] diff --git a/Tests/Fixtures/JSON/RailsUser.json b/Tests/Fixtures/JSON/RailsUser.json index 3a5c90ef10..45386105b1 100644 --- a/Tests/Fixtures/JSON/RailsUser.json +++ b/Tests/Fixtures/JSON/RailsUser.json @@ -13,4 +13,4 @@ "user_interests": ["Tennis", "Magic", "Football", "Basketball", "Computers", "Programming", "Computer Science", "Music", "Movies"] } -} \ No newline at end of file +} diff --git a/Tests/Fixtures/JSON/benchmark_parents_and_children.json b/Tests/Fixtures/JSON/benchmark_parents_and_children.json new file mode 100644 index 0000000000..ba80254221 --- /dev/null +++ b/Tests/Fixtures/JSON/benchmark_parents_and_children.json @@ -0,0 +1,2062 @@ +{ + "parents": [ + { + "parentID": 1, + "name": "Megane Rosenbaum", + "children": [ + { + "name": "Miss Jazmin Dooley", + "childID": 73 + }, + { + "name": "Llewellyn Hilll", + "childID": 60 + }, + { + "name": "Adan Vandervort", + "childID": 66 + }, + { + "name": "Joshuah Langworth III", + "childID": 81 + }, + { + "name": "Yazmin Christiansen PhD", + "childID": 80 + }, + { + "name": "Ms. Alan Carter", + "childID": 64 + }, + { + "name": "Mrs. Sally Dickens", + "childID": 79 + }, + { + "name": "Ms. Norval D'Amore", + "childID": 72 + }, + { + "name": "Joan Crooks", + "childID": 59 + }, + { + "name": "Tiara Rolfson", + "childID": 84 + }, + { + "name": "Jacinthe Abernathy", + "childID": 77 + }, + { + "name": "Beaulah Tremblay", + "childID": 77 + }, + { + "name": "Fermin Douglas", + "childID": 56 + }, + { + "name": "Kole Ullrich", + "childID": 95 + }, + { + "name": "Miss Pete Gutkowski", + "childID": 65 + }, + { + "name": "Kamren Kozey", + "childID": 52 + }, + { + "name": "Vladimir Hegmann", + "childID": 84 + }, + { + "name": "Miss Dannie Grant", + "childID": 77 + }, + { + "name": "Miss Jayde Rice", + "childID": 59 + }, + { + "name": "Otilia Hartmann", + "childID": 74 + }, + { + "name": "Donnell Sanford", + "childID": 73 + }, + { + "name": "Bill Hackett", + "childID": 94 + }, + { + "name": "Bulah Sauer IV", + "childID": 70 + }, + { + "name": "Baylee Rippin", + "childID": 58 + } + ] + }, + { + "parentID": 2, + "name": "Brice Maggio", + "children": [ + { + "name": "Skye Gerhold", + "childID": 53 + }, + { + "name": "Leonie Price Sr.", + "childID": 86 + }, + { + "name": "Eulah Douglas", + "childID": 100 + }, + { + "name": "Miss Rosina Waters", + "childID": 55 + }, + { + "name": "Verda Krajcik", + "childID": 96 + }, + { + "name": "Daren Crist", + "childID": 100 + }, + { + "name": "Dock Gibson", + "childID": 99 + }, + { + "name": "Ashlynn Marquardt", + "childID": 84 + }, + { + "name": "Krystal Swaniawski", + "childID": 78 + }, + { + "name": "Sadie Wyman", + "childID": 57 + }, + { + "name": "Breanne Denesik", + "childID": 77 + }, + { + "name": "Miss Bryce Lockman", + "childID": 96 + }, + { + "name": "Jewel Hickle", + "childID": 50 + }, + { + "name": "Cordell Little", + "childID": 53 + }, + { + "name": "Ollie Balistreri", + "childID": 99 + }, + { + "name": "Delfina Wehner", + "childID": 51 + }, + { + "name": "Carmela Effertz", + "childID": 99 + }, + { + "name": "Devon Nolan", + "childID": 96 + }, + { + "name": "Miss Sandrine Dicki", + "childID": 96 + }, + { + "name": "Anderson Collier Jr.", + "childID": 65 + }, + { + "name": "Dr. Cielo Kozey", + "childID": 66 + }, + { + "name": "Tabitha Cummings", + "childID": 92 + }, + { + "name": "Amelia Goldner", + "childID": 60 + }, + { + "name": "Arielle Welch IV", + "childID": 69 + }, + { + "name": "Hallie Friesen", + "childID": 83 + }, + { + "name": "Halie Von", + "childID": 51 + } + ] + }, + { + "parentID": 3, + "name": "Mrs. Marlene Hayes", + "children": [ + { + "name": "Holden Fay", + "childID": 75 + }, + { + "name": "Giovanna Bayer", + "childID": 99 + }, + { + "name": "Olaf McClure", + "childID": 73 + }, + { + "name": "Krista McCullough", + "childID": 75 + }, + { + "name": "Monserrate Larkin", + "childID": 96 + }, + { + "name": "Alice Ledner", + "childID": 73 + }, + { + "name": "Marina Stanton Jr.", + "childID": 79 + }, + { + "name": "William Hills", + "childID": 93 + }, + { + "name": "Neal Vandervort", + "childID": 54 + }, + { + "name": "Susana Erdman", + "childID": 84 + }, + { + "name": "Dudley Blick", + "childID": 55 + }, + { + "name": "Mrs. Triston Langosh", + "childID": 100 + }, + { + "name": "Loma Kreiger", + "childID": 72 + }, + { + "name": "Ida Crona", + "childID": 80 + }, + { + "name": "Allison Pouros", + "childID": 98 + }, + { + "name": "Frederique Hansen", + "childID": 100 + }, + { + "name": "Fred Shields", + "childID": 92 + }, + { + "name": "Mr. Jaylon Lebsack", + "childID": 82 + }, + { + "name": "Noah Crist", + "childID": 90 + }, + { + "name": "Shakira Witting", + "childID": 57 + }, + { + "name": "Bradford Kunde", + "childID": 65 + } + ] + }, + { + "parentID": 4, + "name": "Ms. Sallie Keeling", + "children": [ + { + "name": "Dr. Daija Waelchi", + "childID": 57 + }, + { + "name": "Halle Wilkinson", + "childID": 78 + }, + { + "name": "Jennyfer Mante", + "childID": 75 + }, + { + "name": "Hilda Lebsack", + "childID": 50 + }, + { + "name": "Sibyl Jast", + "childID": 58 + }, + { + "name": "Maybell Ebert DVM", + "childID": 73 + }, + { + "name": "Dandre Reynolds", + "childID": 69 + }, + { + "name": "Roberto Stehr", + "childID": 60 + }, + { + "name": "Amie Rowe DVM", + "childID": 95 + }, + { + "name": "Edgardo Blanda", + "childID": 61 + }, + { + "name": "Hector Prohaska PhD", + "childID": 63 + }, + { + "name": "Alycia Schultz", + "childID": 65 + }, + { + "name": "Mr. Joannie Bauch", + "childID": 57 + }, + { + "name": "Dr. Hassan Mosciski", + "childID": 51 + } + ] + }, + { + "parentID": 5, + "name": "Dr. Elissa Bernier", + "children": [ + { + "name": "Vernie Gusikowski", + "childID": 53 + }, + { + "name": "Gennaro Wisoky", + "childID": 87 + }, + { + "name": "Preston Luettgen", + "childID": 71 + }, + { + "name": "Ada Hammes", + "childID": 68 + }, + { + "name": "Ms. Linnea Gottlieb", + "childID": 50 + }, + { + "name": "Mavis Wiegand III", + "childID": 56 + }, + { + "name": "Nathan Stroman I", + "childID": 87 + }, + { + "name": "Mr. Lilly Lemke", + "childID": 55 + }, + { + "name": "Jermain Tremblay", + "childID": 92 + }, + { + "name": "Jamison Shanahan", + "childID": 72 + }, + { + "name": "Evans Considine", + "childID": 70 + }, + { + "name": "Lorine Carter", + "childID": 50 + }, + { + "name": "Ms. Jacey Pouros", + "childID": 65 + }, + { + "name": "Rasheed Schroeder", + "childID": 77 + }, + { + "name": "Elinore Dickinson", + "childID": 53 + }, + { + "name": "Emelie Wolff V", + "childID": 62 + }, + { + "name": "Maria Grant", + "childID": 97 + }, + { + "name": "Ms. Serenity Yundt", + "childID": 69 + }, + { + "name": "Cleta Mosciski", + "childID": 76 + }, + { + "name": "Fausto Ortiz", + "childID": 86 + }, + { + "name": "Monty Metz", + "childID": 89 + } + ] + }, + { + "parentID": 6, + "name": "Jewell Kihn", + "children": [ + { + "name": "Rasheed Altenwerth", + "childID": 58 + }, + { + "name": "Theron Medhurst", + "childID": 56 + }, + { + "name": "Etha Bins", + "childID": 53 + }, + { + "name": "Caroline Dare", + "childID": 89 + }, + { + "name": "Bennett Tremblay", + "childID": 67 + }, + { + "name": "Jailyn Steuber MD", + "childID": 95 + }, + { + "name": "Pamela Effertz", + "childID": 58 + }, + { + "name": "Blair Jacobs", + "childID": 71 + }, + { + "name": "Anna Stoltenberg DVM", + "childID": 70 + }, + { + "name": "Jerel Halvorson", + "childID": 78 + }, + { + "name": "Elroy Littel", + "childID": 75 + }, + { + "name": "Sheridan Romaguera", + "childID": 82 + }, + { + "name": "Dr. Jayce Wisozk", + "childID": 70 + }, + { + "name": "Elmore Barrows", + "childID": 52 + }, + { + "name": "Kylee Ruecker", + "childID": 84 + }, + { + "name": "Pierre Koss", + "childID": 60 + }, + { + "name": "Allene Fisher", + "childID": 56 + }, + { + "name": "Marcellus Conroy III", + "childID": 79 + }, + { + "name": "Kaylie Daniel IV", + "childID": 51 + }, + { + "name": "Jason Anderson", + "childID": 71 + }, + { + "name": "Pedro Wolf", + "childID": 93 + }, + { + "name": "Broderick Kunde", + "childID": 66 + } + ] + }, + { + "parentID": 7, + "name": "Armando Hammes", + "children": [ + { + "name": "Aleen Sanford", + "childID": 97 + }, + { + "name": "Ofelia Wolff", + "childID": 87 + }, + { + "name": "Deron Goodwin", + "childID": 58 + }, + { + "name": "Natalie Volkman", + "childID": 63 + }, + { + "name": "Vanessa Hudson", + "childID": 78 + }, + { + "name": "Jamaal Feest", + "childID": 86 + }, + { + "name": "Miss Marilou Kessler", + "childID": 98 + }, + { + "name": "Rosa Armstrong", + "childID": 65 + }, + { + "name": "Ms. Claudie Ankunding", + "childID": 85 + }, + { + "name": "Franco Hamill", + "childID": 78 + }, + { + "name": "Kristin Nicolas", + "childID": 100 + } + ] + }, + { + "parentID": 8, + "name": "Hilbert West", + "children": [ + { + "name": "Eryn Steuber", + "childID": 70 + }, + { + "name": "Arlo Kling", + "childID": 70 + }, + { + "name": "Deondre Feil", + "childID": 98 + }, + { + "name": "Demarco Welch", + "childID": 86 + }, + { + "name": "Dr. Dallin Monahan", + "childID": 70 + }, + { + "name": "Blanche Jenkins", + "childID": 71 + }, + { + "name": "Emmalee Spinka MD", + "childID": 98 + }, + { + "name": "Rahul Stark", + "childID": 80 + }, + { + "name": "Yasmeen Lynch", + "childID": 90 + }, + { + "name": "Margret Auer", + "childID": 52 + }, + { + "name": "Mrs. Velda Nitzsche", + "childID": 73 + }, + { + "name": "Madaline Gusikowski", + "childID": 58 + }, + { + "name": "Kyleigh Price", + "childID": 63 + }, + { + "name": "Maia Powlowski", + "childID": 88 + }, + { + "name": "Estrella Dare", + "childID": 86 + }, + { + "name": "Lizzie Christiansen", + "childID": 57 + }, + { + "name": "Enola Purdy", + "childID": 77 + }, + { + "name": "Marcus Bosco", + "childID": 75 + }, + { + "name": "Estevan Ondricka III", + "childID": 87 + }, + { + "name": "Anissa Krajcik V", + "childID": 91 + }, + { + "name": "Malika Hamill", + "childID": 56 + }, + { + "name": "Natalia Sipes", + "childID": 59 + } + ] + }, + { + "parentID": 9, + "name": "Shanel Douglas I", + "children": [ + { + "name": "Zita Schamberger", + "childID": 50 + }, + { + "name": "Emerson Williamson", + "childID": 61 + }, + { + "name": "Macie Hoppe", + "childID": 74 + }, + { + "name": "Estell Champlin", + "childID": 73 + }, + { + "name": "Mr. Lelah Hodkiewicz", + "childID": 91 + }, + { + "name": "Cole Baumbach", + "childID": 83 + }, + { + "name": "Izabella Kovacek", + "childID": 59 + }, + { + "name": "Katelyn Abshire", + "childID": 80 + }, + { + "name": "Michele Torp", + "childID": 99 + }, + { + "name": "Dessie Murray", + "childID": 68 + }, + { + "name": "Noah Lesch", + "childID": 76 + }, + { + "name": "Jailyn Heaney", + "childID": 59 + }, + { + "name": "Manuel Dach", + "childID": 81 + }, + { + "name": "Brent Wolff DVM", + "childID": 100 + }, + { + "name": "Marcellus Moen", + "childID": 90 + }, + { + "name": "Mrs. Wilfredo Sawayn", + "childID": 51 + }, + { + "name": "Gideon Hilpert", + "childID": 54 + }, + { + "name": "Davonte DuBuque MD", + "childID": 96 + }, + { + "name": "Vivienne Langosh", + "childID": 52 + }, + { + "name": "Mozelle Hagenes", + "childID": 88 + } + ] + }, + { + "parentID": 10, + "name": "Koby Kuhic", + "children": [ + { + "name": "Jules Lueilwitz", + "childID": 70 + }, + { + "name": "Buford Bergstrom", + "childID": 100 + }, + { + "name": "Miss Gustave Murphy", + "childID": 71 + }, + { + "name": "Rudy Sanford Sr.", + "childID": 77 + }, + { + "name": "Mrs. Eloisa O'Kon", + "childID": 64 + }, + { + "name": "Elmore Wolf", + "childID": 93 + }, + { + "name": "Mrs. Beverly Farrell", + "childID": 86 + }, + { + "name": "Terry Cruickshank", + "childID": 80 + }, + { + "name": "Susie Ritchie", + "childID": 53 + }, + { + "name": "Andrew Cole", + "childID": 50 + }, + { + "name": "Arlo Prohaska", + "childID": 52 + }, + { + "name": "Jayne Wyman DVM", + "childID": 69 + } + ] + }, + { + "parentID": 11, + "name": "Miss Jovan Ebert", + "children": [ + { + "name": "Jackeline Mertz", + "childID": 93 + }, + { + "name": "Adriana Larkin", + "childID": 80 + }, + { + "name": "Ottilie Vandervort", + "childID": 56 + }, + { + "name": "Ericka Gibson V", + "childID": 52 + }, + { + "name": "Chester Turner", + "childID": 75 + }, + { + "name": "Delfina Anderson", + "childID": 56 + }, + { + "name": "Bulah White", + "childID": 53 + }, + { + "name": "Michel Bergnaum", + "childID": 53 + }, + { + "name": "Mr. Hettie Lesch", + "childID": 51 + }, + { + "name": "Alexanne Muller", + "childID": 96 + }, + { + "name": "Regan Heaney", + "childID": 96 + }, + { + "name": "Dane Stracke V", + "childID": 94 + }, + { + "name": "Macie Kilback", + "childID": 99 + }, + { + "name": "Albertha Considine", + "childID": 97 + }, + { + "name": "Winifred Littel", + "childID": 90 + }, + { + "name": "Kara Wuckert", + "childID": 92 + } + ] + }, + { + "parentID": 12, + "name": "Chelsey Zieme", + "children": [ + { + "name": "Damian Schaden", + "childID": 88 + }, + { + "name": "Ima O'Keefe", + "childID": 78 + }, + { + "name": "Henriette Berge MD", + "childID": 67 + }, + { + "name": "Paolo Wiegand", + "childID": 92 + }, + { + "name": "Wilber Lesch", + "childID": 52 + }, + { + "name": "Dale Greenholt", + "childID": 64 + }, + { + "name": "Aliya Leffler Jr.", + "childID": 50 + }, + { + "name": "Kasandra Carroll", + "childID": 64 + }, + { + "name": "Daren Upton DDS", + "childID": 99 + }, + { + "name": "Chase Heathcote", + "childID": 73 + }, + { + "name": "Lamont Wolf", + "childID": 88 + } + ] + }, + { + "parentID": 13, + "name": "Pearl Roob DDS", + "children": [ + { + "name": "Roderick Mohr", + "childID": 63 + }, + { + "name": "Rolando Becker", + "childID": 78 + }, + { + "name": "Mr. Karlie Brakus", + "childID": 55 + }, + { + "name": "Annetta Spinka", + "childID": 92 + }, + { + "name": "Nathan Walker", + "childID": 89 + }, + { + "name": "Miss Desiree Jacobs", + "childID": 63 + }, + { + "name": "Laney Corwin", + "childID": 98 + }, + { + "name": "Chanel Cole", + "childID": 58 + }, + { + "name": "Nia Prohaska", + "childID": 76 + }, + { + "name": "Rogelio Stamm", + "childID": 91 + }, + { + "name": "Abby Harris", + "childID": 97 + }, + { + "name": "Monserrate Mraz", + "childID": 65 + }, + { + "name": "Esta Harvey", + "childID": 54 + }, + { + "name": "Alejandrin Keeling", + "childID": 87 + }, + { + "name": "Lue Considine", + "childID": 65 + }, + { + "name": "Raphaelle Ward", + "childID": 56 + }, + { + "name": "Cordie Rice Jr.", + "childID": 98 + }, + { + "name": "Fabiola Rohan", + "childID": 99 + }, + { + "name": "Katrina Stroman", + "childID": 84 + }, + { + "name": "Abbigail Crist", + "childID": 53 + }, + { + "name": "Gregg Robel", + "childID": 64 + }, + { + "name": "Margret Hintz", + "childID": 54 + }, + { + "name": "Jody Lowe", + "childID": 98 + }, + { + "name": "Mr. Nya Hessel", + "childID": 53 + }, + { + "name": "Malachi Schoen Jr.", + "childID": 80 + }, + { + "name": "Gwendolyn Witting", + "childID": 77 + } + ] + }, + { + "parentID": 14, + "name": "Ms. Kris Murazik", + "children": [ + { + "name": "Dr. Murphy Bailey", + "childID": 78 + }, + { + "name": "Jennyfer Ankunding", + "childID": 88 + }, + { + "name": "Demetrius Kerluke", + "childID": 71 + }, + { + "name": "Gina Metz", + "childID": 57 + }, + { + "name": "Miss Durward Hettinger", + "childID": 100 + }, + { + "name": "Jameson Kunze IV", + "childID": 67 + }, + { + "name": "Henri Denesik MD", + "childID": 64 + }, + { + "name": "Oleta Bartell V", + "childID": 93 + }, + { + "name": "Adrianna Hegmann", + "childID": 54 + }, + { + "name": "Geo Turner II", + "childID": 78 + }, + { + "name": "Darius White", + "childID": 97 + }, + { + "name": "Vella Shanahan", + "childID": 64 + }, + { + "name": "Richie Ratke", + "childID": 83 + } + ] + }, + { + "parentID": 15, + "name": "Wilmer Boyle", + "children": [ + { + "name": "Alisha Ritchie DVM", + "childID": 59 + }, + { + "name": "Ari Dietrich", + "childID": 56 + }, + { + "name": "Catalina Roob", + "childID": 95 + }, + { + "name": "Will Hansen PhD", + "childID": 50 + }, + { + "name": "Stanton Howe", + "childID": 57 + }, + { + "name": "Pat Zemlak", + "childID": 58 + }, + { + "name": "Jazmyne Bartoletti MD", + "childID": 95 + }, + { + "name": "Idell Rowe", + "childID": 75 + }, + { + "name": "Sarina Nicolas", + "childID": 51 + }, + { + "name": "Dr. Anthony McCullough", + "childID": 88 + }, + { + "name": "Lenna Ritchie", + "childID": 74 + }, + { + "name": "Jerald Blick", + "childID": 60 + }, + { + "name": "Destini Pfeffer", + "childID": 95 + }, + { + "name": "Marianna Okuneva", + "childID": 69 + }, + { + "name": "Roosevelt Schiller Jr.", + "childID": 58 + }, + { + "name": "Rosemary Batz Sr.", + "childID": 75 + }, + { + "name": "Joy Bahringer", + "childID": 81 + }, + { + "name": "Reuben Bogan II", + "childID": 86 + }, + { + "name": "Ms. Anabelle Leuschke", + "childID": 59 + }, + { + "name": "Muhammad Toy Sr.", + "childID": 66 + }, + { + "name": "Jarret Keeling", + "childID": 50 + }, + { + "name": "Ms. Isabell Gusikowski", + "childID": 75 + }, + { + "name": "Grover Stroman", + "childID": 65 + }, + { + "name": "Mrs. Ole Bahringer", + "childID": 94 + } + ] + }, + { + "parentID": 16, + "name": "Suzanne Von", + "children": [ + { + "name": "Maud Bode", + "childID": 96 + }, + { + "name": "Vita Koelpin MD", + "childID": 77 + }, + { + "name": "Diana Stamm", + "childID": 57 + }, + { + "name": "Belle O'Conner", + "childID": 52 + }, + { + "name": "Mr. Cheyanne Huel", + "childID": 79 + }, + { + "name": "Francesca Reilly III", + "childID": 62 + }, + { + "name": "Isadore Reichert", + "childID": 83 + }, + { + "name": "Miguel Kuhlman", + "childID": 90 + }, + { + "name": "Miss Rasheed Kunze", + "childID": 100 + }, + { + "name": "King Casper", + "childID": 57 + }, + { + "name": "Daphnee Herzog", + "childID": 81 + }, + { + "name": "Rose Abbott DDS", + "childID": 94 + }, + { + "name": "Mr. Fae Gulgowski", + "childID": 63 + }, + { + "name": "Marlen Huel", + "childID": 77 + }, + { + "name": "Jalyn Jaskolski", + "childID": 60 + }, + { + "name": "Ben Bauch", + "childID": 54 + }, + { + "name": "Khalid Parker", + "childID": 64 + }, + { + "name": "Walter Gutkowski", + "childID": 52 + }, + { + "name": "Cecelia Rodriguez III", + "childID": 91 + }, + { + "name": "Adam Grant", + "childID": 61 + } + ] + }, + { + "parentID": 17, + "name": "Jerrod Cruickshank", + "children": [ + { + "name": "Tavares Friesen", + "childID": 91 + }, + { + "name": "Moses Brakus", + "childID": 100 + }, + { + "name": "Micah Windler", + "childID": 87 + }, + { + "name": "Zola Murray", + "childID": 53 + }, + { + "name": "Laverne Durgan", + "childID": 72 + }, + { + "name": "Mitchell Bernhard", + "childID": 59 + }, + { + "name": "Lavina Shields", + "childID": 61 + }, + { + "name": "Talon Jerde Jr.", + "childID": 93 + }, + { + "name": "Mr. Maude Lakin", + "childID": 97 + }, + { + "name": "Ms. Ruben Terry", + "childID": 56 + }, + { + "name": "Fredy Purdy", + "childID": 74 + }, + { + "name": "Marie Flatley", + "childID": 96 + }, + { + "name": "Nicolas Moen", + "childID": 58 + }, + { + "name": "August Gutkowski", + "childID": 51 + }, + { + "name": "Frederique Larkin", + "childID": 80 + }, + { + "name": "Kenyon Lowe", + "childID": 65 + }, + { + "name": "Javier Rippin", + "childID": 70 + }, + { + "name": "Levi Olson", + "childID": 67 + }, + { + "name": "Bret Jenkins", + "childID": 62 + }, + { + "name": "Madelyn Ratke", + "childID": 98 + }, + { + "name": "Mrs. Rae Hamill", + "childID": 99 + }, + { + "name": "Ms. Imelda Bailey", + "childID": 58 + } + ] + }, + { + "parentID": 18, + "name": "Emiliano Yost", + "children": [ + { + "name": "Cathrine Prohaska", + "childID": 60 + }, + { + "name": "Gabriel Christiansen", + "childID": 62 + }, + { + "name": "Ima O'Conner", + "childID": 50 + }, + { + "name": "Justine Schmidt", + "childID": 52 + }, + { + "name": "Amelie White", + "childID": 83 + }, + { + "name": "Vivianne Boyer V", + "childID": 100 + }, + { + "name": "Bernice King Sr.", + "childID": 100 + }, + { + "name": "Morton Powlowski", + "childID": 59 + }, + { + "name": "Linnie Wyman", + "childID": 100 + }, + { + "name": "Christop Fisher", + "childID": 99 + }, + { + "name": "Candido Kovacek", + "childID": 60 + }, + { + "name": "Wellington Bernier", + "childID": 56 + }, + { + "name": "Joel Wintheiser", + "childID": 88 + }, + { + "name": "Miles Gorczany", + "childID": 96 + }, + { + "name": "Elijah Kunze", + "childID": 96 + }, + { + "name": "Ruthe Breitenberg", + "childID": 72 + }, + { + "name": "Mrs. Camden Jast", + "childID": 60 + } + ] + }, + { + "parentID": 19, + "name": "Marco Yundt", + "children": [ + { + "name": "Rashawn Hegmann", + "childID": 59 + }, + { + "name": "Obie Hills V", + "childID": 78 + }, + { + "name": "Ms. Alfonzo Runolfsson", + "childID": 86 + }, + { + "name": "Esteban Collins", + "childID": 100 + }, + { + "name": "Verlie Green", + "childID": 99 + }, + { + "name": "Jettie Sanford", + "childID": 59 + }, + { + "name": "Dominic McLaughlin", + "childID": 78 + }, + { + "name": "Morgan Olson", + "childID": 62 + }, + { + "name": "Autumn Feest DVM", + "childID": 90 + }, + { + "name": "Aliyah Kemmer", + "childID": 86 + }, + { + "name": "Bryana Sawayn", + "childID": 100 + }, + { + "name": "Anita Emard II", + "childID": 79 + }, + { + "name": "Mr. Arlo Hudson", + "childID": 100 + }, + { + "name": "Lavern Ziemann", + "childID": 86 + }, + { + "name": "Destany Jewess", + "childID": 78 + }, + { + "name": "Jayson Dickens", + "childID": 94 + }, + { + "name": "Eliane Hudson", + "childID": 77 + }, + { + "name": "Arvid Collier MD", + "childID": 77 + }, + { + "name": "Miss Ena Torp", + "childID": 50 + }, + { + "name": "Mrs. Daphney Renner", + "childID": 57 + }, + { + "name": "Joan Erdman", + "childID": 52 + }, + { + "name": "Pasquale Bosco", + "childID": 83 + }, + { + "name": "Finn Crona", + "childID": 97 + }, + { + "name": "Miss Euna Marvin", + "childID": 51 + } + ] + }, + { + "parentID": 20, + "name": "Laurie Satterfield", + "children": [ + { + "name": "Wilfred Nitzsche", + "childID": 88 + }, + { + "name": "Joany Borer", + "childID": 90 + }, + { + "name": "Nigel Upton", + "childID": 58 + }, + { + "name": "Chesley Beier", + "childID": 56 + }, + { + "name": "Ida Pagac", + "childID": 79 + }, + { + "name": "Keyshawn Sipes", + "childID": 54 + }, + { + "name": "Makayla Cole", + "childID": 82 + }, + { + "name": "Mr. Melvina Rohan", + "childID": 65 + }, + { + "name": "Gregg Metz", + "childID": 91 + }, + { + "name": "Daniella Wintheiser", + "childID": 88 + }, + { + "name": "Greyson Hintz", + "childID": 92 + }, + { + "name": "Issac Hermiston", + "childID": 63 + } + ] + }, + { + "parentID": 21, + "name": "Nicolas Conn MD", + "children": [ + { + "name": "Ms. Vicenta Jerde", + "childID": 56 + }, + { + "name": "Hollis Walter", + "childID": 96 + }, + { + "name": "Miss Zena Stiedemann", + "childID": 83 + }, + { + "name": "Mrs. Katelin Rutherford", + "childID": 90 + }, + { + "name": "Dr. Brenna Roberts", + "childID": 65 + }, + { + "name": "Phyllis O'Conner V", + "childID": 80 + }, + { + "name": "Allie Ankunding", + "childID": 66 + }, + { + "name": "Bridie Cummerata Jr.", + "childID": 57 + }, + { + "name": "Oma Keeling", + "childID": 90 + }, + { + "name": "Horace Davis", + "childID": 77 + }, + { + "name": "Mrs. Marielle Sanford", + "childID": 53 + }, + { + "name": "Mr. Ashlee Connelly", + "childID": 92 + }, + { + "name": "Ms. Fredy Kunde", + "childID": 87 + }, + { + "name": "Emile Auer Jr.", + "childID": 98 + }, + { + "name": "Nikolas Hartmann PhD", + "childID": 84 + } + ] + }, + { + "parentID": 22, + "name": "Matilda Hammes DVM", + "children": [ + { + "name": "Destini Denesik", + "childID": 98 + }, + { + "name": "Ms. Abigayle Christiansen", + "childID": 81 + }, + { + "name": "Annabel Kling PhD", + "childID": 67 + }, + { + "name": "Dr. Kattie Zboncak", + "childID": 74 + }, + { + "name": "Mrs. Sigurd Hettinger", + "childID": 65 + }, + { + "name": "Miss Katheryn O'Connell", + "childID": 92 + }, + { + "name": "Hilario Rosenbaum", + "childID": 55 + }, + { + "name": "Miss Paxton Jones", + "childID": 62 + }, + { + "name": "Jason Schuppe", + "childID": 78 + }, + { + "name": "Clovis Ryan", + "childID": 87 + }, + { + "name": "Randi Witting", + "childID": 86 + }, + { + "name": "Cristian Connelly Sr.", + "childID": 62 + }, + { + "name": "Juston Schmidt III", + "childID": 78 + }, + { + "name": "Johnnie Blanda II", + "childID": 50 + }, + { + "name": "Irma Feest", + "childID": 87 + }, + { + "name": "Mr. Donnie Schamberger", + "childID": 59 + }, + { + "name": "Emil Fay", + "childID": 71 + }, + { + "name": "Anthony Hartmann", + "childID": 74 + }, + { + "name": "Annalise Heller", + "childID": 78 + }, + { + "name": "Laverna Blick", + "childID": 100 + }, + { + "name": "Vita Wolf", + "childID": 70 + } + ] + }, + { + "parentID": 23, + "name": "Dr. Lilian Bogan", + "children": [ + { + "name": "Joshua Hilpert Sr.", + "childID": 74 + }, + { + "name": "Carroll Hermiston", + "childID": 77 + }, + { + "name": "Pierre Ziemann", + "childID": 66 + }, + { + "name": "Gerard Goldner", + "childID": 73 + }, + { + "name": "Julian Jacobson", + "childID": 54 + }, + { + "name": "Laney Pfeffer MD", + "childID": 52 + }, + { + "name": "Dion Heathcote", + "childID": 82 + }, + { + "name": "Brooke Bauch", + "childID": 59 + }, + { + "name": "Kira Kuhn", + "childID": 72 + }, + { + "name": "Darius Miller Jr.", + "childID": 95 + }, + { + "name": "Flossie Abbott", + "childID": 78 + }, + { + "name": "Oma Schuster V", + "childID": 66 + }, + { + "name": "Gilbert Wintheiser", + "childID": 95 + }, + { + "name": "Lorenzo Daugherty", + "childID": 67 + }, + { + "name": "Aaron Stamm", + "childID": 70 + }, + { + "name": "Darian Bosco IV", + "childID": 73 + }, + { + "name": "Aiden Deckow", + "childID": 57 + }, + { + "name": "Elias Hackett", + "childID": 80 + }, + { + "name": "Will Bernier PhD", + "childID": 88 + }, + { + "name": "Samara Ledner", + "childID": 88 + }, + { + "name": "Yadira Pfannerstill", + "childID": 83 + }, + { + "name": "Shanny Schoen PhD", + "childID": 59 + }, + { + "name": "Camila Ebert", + "childID": 64 + }, + { + "name": "Ethan Hagenes", + "childID": 58 + } + ] + }, + { + "parentID": 24, + "name": "Bettye Morissette Jr.", + "children": [ + { + "name": "Jannie Hills", + "childID": 92 + }, + { + "name": "Elody Renner", + "childID": 98 + }, + { + "name": "Miss Ariel D'Amore", + "childID": 86 + }, + { + "name": "Mr. Dejon Mills", + "childID": 53 + }, + { + "name": "Marie Heaney", + "childID": 56 + }, + { + "name": "Donna Ondricka", + "childID": 72 + }, + { + "name": "Tyler Halvorson", + "childID": 64 + }, + { + "name": "Katlynn Wiegand", + "childID": 67 + }, + { + "name": "Trevor Hickle", + "childID": 58 + }, + { + "name": "Einar Tromp", + "childID": 60 + }, + { + "name": "Jacey Hartmann", + "childID": 81 + }, + { + "name": "Mr. Jacey Block", + "childID": 73 + }, + { + "name": "Dr. Earline Bergstrom", + "childID": 76 + }, + { + "name": "Filiberto Hansen", + "childID": 94 + }, + { + "name": "Keyon Beahan", + "childID": 91 + } + ] + }, + { + "parentID": 25, + "name": "Amber Brakus", + "children": [ + { + "name": "Kaci Fadel", + "childID": 53 + }, + { + "name": "Jairo Gorczany", + "childID": 75 + }, + { + "name": "Nasir Dickens", + "childID": 62 + }, + { + "name": "Miss Elvis Denesik", + "childID": 66 + }, + { + "name": "Justice Schumm", + "childID": 93 + }, + { + "name": "Francisca Gerlach", + "childID": 72 + }, + { + "name": "Americo McLaughlin Jr.", + "childID": 100 + }, + { + "name": "Alexanne Littel", + "childID": 75 + }, + { + "name": "Alaina Jones", + "childID": 98 + }, + { + "name": "Julianne O'Kon", + "childID": 58 + }, + { + "name": "Alexis Kiehn", + "childID": 64 + }, + { + "name": "Elmer Armstrong V", + "childID": 91 + }, + { + "name": "Mr. Madelyn Kunde", + "childID": 98 + }, + { + "name": "Buster Schamberger", + "childID": 64 + }, + { + "name": "Monica Boyer Jr.", + "childID": 62 + }, + { + "name": "Mrs. Karolann Luettgen", + "childID": 82 + }, + { + "name": "Gayle Keeling", + "childID": 60 + }, + { + "name": "Sammie Jenkins", + "childID": 82 + }, + { + "name": "William Crist", + "childID": 55 + }, + { + "name": "Theo Anderson V", + "childID": 76 + }, + { + "name": "Camila Hackett", + "childID": 72 + }, + { + "name": "Caitlyn Terry DVM", + "childID": 53 + }, + { + "name": "Austyn Hilll II", + "childID": 51 + }, + { + "name": "Dr. Everardo Brown", + "childID": 64 + } + ] + } + ] +} diff --git a/Tests/Fixtures/JSON/error.json b/Tests/Fixtures/JSON/error.json index e5ddd95ffa..50538a24cc 100644 --- a/Tests/Fixtures/JSON/error.json +++ b/Tests/Fixtures/JSON/error.json @@ -1 +1 @@ -{error: "this is an error"} \ No newline at end of file +{error: "this is an error"} diff --git a/Tests/Fixtures/JSON/errors.json b/Tests/Fixtures/JSON/errors.json index 19dc24bf71..97107d2bfd 100644 --- a/Tests/Fixtures/JSON/errors.json +++ b/Tests/Fixtures/JSON/errors.json @@ -1 +1 @@ -{ "errors" : ["error1", "error2"] } \ No newline at end of file +{ "errors" : ["error1", "error2"] } diff --git a/Tests/Fixtures/JSON/humans/1.json b/Tests/Fixtures/JSON/humans/1.json index 62ba362129..96d2488cba 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}} \ No newline at end of file +{"human":{"birthday":null,"created_at":null,"updated_at":null,"sex":null,"name":"Blake Watters","id":null,"age":28}} diff --git a/Tests/Fixtures/JSON/humans/all.json b/Tests/Fixtures/JSON/humans/all.json index ba80f2f8a8..d801180774 100644 --- a/Tests/Fixtures/JSON/humans/all.json +++ b/Tests/Fixtures/JSON/humans/all.json @@ -1 +1 @@ -[{"human":{"birthday":null,"created_at":null,"updated_at":null,"sex":null,"name":"Blake Watters","id":123,"age":null}},{"human":{"birthday":null,"created_at":null,"updated_at":null,"sex":null,"name":"Other","id":456,"age":null}}] \ No newline at end of file +[{"human":{"birthday":null,"created_at":null,"updated_at":null,"sex":null,"name":"Blake Watters","id":123,"age":null}},{"human":{"birthday":null,"created_at":null,"updated_at":null,"sex":null,"name":"Other","id":456,"age":null}}] diff --git a/Tests/Fixtures/JSON/humans/with_to_one_relationship.json b/Tests/Fixtures/JSON/humans/with_to_one_relationship.json index de06d9467a..3c2b500fac 100644 --- a/Tests/Fixtures/JSON/humans/with_to_one_relationship.json +++ b/Tests/Fixtures/JSON/humans/with_to_one_relationship.json @@ -1 +1 @@ -{"human":{"name":"Blake Watters","id":null,"age":28,"favorite_cat":{"name": "Asia"}}} \ No newline at end of file +{"human":{"name":"Blake Watters","id":null,"age":28,"favorite_cat":{"name": "Asia"}}} diff --git a/Tests/Fixtures/JSON/nested_user.json b/Tests/Fixtures/JSON/nested_user.json index fb5e20f739..882493ddc2 100644 --- a/Tests/Fixtures/JSON/nested_user.json +++ b/Tests/Fixtures/JSON/nested_user.json @@ -3,4 +3,4 @@ "id": 31337, "name": "Blake Watters" } -} \ No newline at end of file +} diff --git a/Tests/Fixtures/JSON/parents_and_children.json b/Tests/Fixtures/JSON/parents_and_children.json new file mode 100644 index 0000000000..7177c64419 --- /dev/null +++ b/Tests/Fixtures/JSON/parents_and_children.json @@ -0,0 +1,40 @@ +{ + "parents": [ + { + "parentID": 12345, + "name": "Joe", + "children": [ + { + "childID": 1111, + "name": "Oscar" + }, + { + "childID": 2222, + "name": "Tom" + }, + { + "childID": 3333, + "name": "Eli" + } + ] + }, + { + "parentID": 4567, + "name": "Mary", + "children": [ + { + "childID": 1, + "name": "Sally" + }, + { + "childID": 2222, + "name": "Tom" + }, + { + "childID": 3333, + "name": "Eli" + } + ] + } + ] +} diff --git a/Tests/Fixtures/JSON/user.json b/Tests/Fixtures/JSON/user.json index e3e7ca2565..b0ef7c16b0 100644 --- a/Tests/Fixtures/JSON/user.json +++ b/Tests/Fixtures/JSON/user.json @@ -27,4 +27,4 @@ "name": "Rachit Shukla" } ] -} \ No newline at end of file +} diff --git a/Tests/Logic/CoreData/NSEntityDescription+RKAdditionsTest.m b/Tests/Logic/CoreData/NSEntityDescription+RKAdditionsTest.m index 276582ede1..4d8080de4a 100644 --- a/Tests/Logic/CoreData/NSEntityDescription+RKAdditionsTest.m +++ b/Tests/Logic/CoreData/NSEntityDescription+RKAdditionsTest.m @@ -19,7 +19,7 @@ - (void)testRetrievalOfPrimaryKeyFromXcdatamodel { RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; - assertThat(entity.primaryKeyAttribute, is(equalTo(@"railsID"))); + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"railsID"))); } - (void)testRetrievalOfUnconfiguredPrimaryKeyAttributeReturnsNil @@ -29,21 +29,112 @@ - (void)testRetrievalOfUnconfiguredPrimaryKeyAttributeReturnsNil assertThat(entity.primaryKeyAttribute, is(nilValue())); } -- (void)testSettingPrimaryKeyAttributeProgramatically +- (void)testSettingPrimaryKeyAttributeNameProgramatically { RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHouse" inManagedObjectContext:objectStore.primaryManagedObjectContext]; - entity.primaryKeyAttribute = @"houseID"; - assertThat(entity.primaryKeyAttribute, is(equalTo(@"houseID"))); + entity.primaryKeyAttributeName = @"houseID"; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"houseID"))); } -- (void)testSettingExistingPrimaryKeyAttributeProgramatically +- (void)testSettingExistingPrimaryKeyAttributeNameProgramatically { RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; - assertThat(entity.primaryKeyAttribute, is(equalTo(@"railsID"))); - entity.primaryKeyAttribute = @"catID"; - assertThat(entity.primaryKeyAttribute, is(equalTo(@"catID"))); + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"railsID"))); + entity.primaryKeyAttributeName = @"catID"; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"catID"))); +} + +- (void)testSettingPrimaryKeyAttributeCreatesCachedPredicate +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"railsID"))); + assertThat([entity.predicateForPrimaryKeyAttribute predicateFormat], is(equalTo(@"railsID == $PRIMARY_KEY_VALUE"))); +} + +- (void)testThatPredicateForPrimaryKeyAttributeWithValueReturnsUsablePredicate +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"railsID"))); + NSNumber *primaryKeyValue = [NSNumber numberWithInt:12345]; + NSPredicate *predicate = [entity predicateForPrimaryKeyAttributeWithValue:primaryKeyValue]; + assertThat([predicate predicateFormat], is(equalTo(@"railsID == 12345"))); +} + +- (void)testThatPredicateForPrimaryKeyAttributeCastsStringValueToNumber +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"railsID"))); + NSPredicate *predicate = [entity predicateForPrimaryKeyAttributeWithValue:@"12345"]; + assertThat([predicate predicateFormat], is(equalTo(@"railsID == 12345"))); +} + +- (void)testThatPredicateForPrimaryKeyAttributeCastsNumberToString +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHouse" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + entity.primaryKeyAttributeName = @"city"; + NSPredicate *predicate = [entity predicateForPrimaryKeyAttributeWithValue:[NSNumber numberWithInteger:12345]]; + assertThat([predicate predicateFormat], is(equalTo(@"city == \"12345\""))); +} + +- (void)testThatPredicateForPrimaryKeyAttributeReturnsNilForEntityWithoutPrimaryKey +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHouse" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + entity.primaryKeyAttributeName = nil; + NSPredicate *predicate = [entity predicateForPrimaryKeyAttributeWithValue:@"12345"]; + assertThat([predicate predicateFormat], is(nilValue())); +} + +- (void)testRetrievalOfPrimaryKeyAttributeReturnsNilIfNotSet +{ + NSEntityDescription *entity = [NSEntityDescription new]; + assertThat(entity.primaryKeyAttribute, is(nilValue())); +} + +- (void)testRetrievalOfPrimaryKeyAttributeReturnsNilWhenSetToInvalidAttributeName +{ + NSEntityDescription *entity = [NSEntityDescription new]; + entity.primaryKeyAttributeName = @"invalidName!"; + assertThat(entity.primaryKeyAttribute, is(nilValue())); +} + +- (void)testRetrievalOfPrimaryKeyAttributeForValidAttributeName +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + entity.primaryKeyAttributeName = @"railsID"; + NSAttributeDescription *attribute = entity.primaryKeyAttribute; + assertThat(attribute, is(notNilValue())); + assertThat(attribute.name, is(equalTo(@"railsID"))); + assertThat(attribute.attributeValueClassName, is(equalTo(@"NSNumber"))); +} + +- (void)testRetrievalOfPrimaryKeyAttributeClassReturnsNilIfNotSet +{ + NSEntityDescription *entity = [NSEntityDescription new]; + assertThat([entity primaryKeyAttributeClass], is(nilValue())); +} + +- (void)testRetrievalOfPrimaryKeyAttributeClassReturnsNilWhenSetToInvalidAttributeName +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHouse" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + entity.primaryKeyAttributeName = @"invalid"; + assertThat([entity primaryKeyAttributeClass], is(nilValue())); +} + +- (void)testRetrievalOfPrimaryKeyAttributeClassForValidAttributeName +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHouse" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + entity.primaryKeyAttributeName = @"railsID"; + assertThat([entity primaryKeyAttributeClass], is(equalTo([NSNumber class]))); } @end diff --git a/Tests/Logic/CoreData/NSManagedObject+ActiveRecordTest.m b/Tests/Logic/CoreData/NSManagedObject+ActiveRecordTest.m index 1dfa94fdac..7c5c1a4c5b 100644 --- a/Tests/Logic/CoreData/NSManagedObject+ActiveRecordTest.m +++ b/Tests/Logic/CoreData/NSManagedObject+ActiveRecordTest.m @@ -20,7 +20,7 @@ - (void)testFindByPrimaryKey { RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; NSEntityDescription *entity = [RKHuman entityDescription]; - entity.primaryKeyAttribute = @"railsID"; + entity.primaryKeyAttributeName = @"railsID"; RKHuman *human = [RKHuman createEntity]; human.railsID = [NSNumber numberWithInt:12345]; @@ -35,7 +35,7 @@ - (void)testFindByPrimaryKeyInContext RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; NSManagedObjectContext *context = [[RKTestFactory managedObjectStore] newManagedObjectContext]; NSEntityDescription *entity = [RKHuman entityDescription]; - entity.primaryKeyAttribute = @"railsID"; + entity.primaryKeyAttributeName = @"railsID"; RKHuman *human = [RKHuman createInContext:context]; human.railsID = [NSNumber numberWithInt:12345]; @@ -48,4 +48,18 @@ - (void)testFindByPrimaryKeyInContext assertThat(foundHuman, is(equalTo(human))); } +- (void)testFindByPrimaryKeyWithStringValueForNumericProperty +{ + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [RKHuman entityDescription]; + entity.primaryKeyAttributeName = @"railsID"; + + RKHuman *human = [RKHuman createEntity]; + human.railsID = [NSNumber numberWithInt:12345]; + [store save:nil]; + + RKHuman *foundHuman = [RKHuman findByPrimaryKey:@"12345" inContext:store.primaryManagedObjectContext]; + assertThat(foundHuman, is(equalTo(human))); +} + @end diff --git a/Tests/Logic/CoreData/RKEntityByAttributeCacheTest.m b/Tests/Logic/CoreData/RKEntityByAttributeCacheTest.m new file mode 100644 index 0000000000..b50efc2cb9 --- /dev/null +++ b/Tests/Logic/CoreData/RKEntityByAttributeCacheTest.m @@ -0,0 +1,378 @@ +// +// RKEntityByAttributeCacheTest.m +// RestKit +// +// Created by Blake Watters on 5/1/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "NSEntityDescription+RKAdditions.h" +#import "RKEntityByAttributeCache.h" +#import "RKHuman.h" +#import "RKChild.h" + +@interface RKEntityByAttributeCacheTest : RKTestCase +@property (nonatomic, retain) RKManagedObjectStore *objectStore; +@property (nonatomic, retain) RKEntityByAttributeCache *cache; +@end + +@implementation RKEntityByAttributeCacheTest + +@synthesize objectStore = _objectStore; +@synthesize cache = _cache; + +- (void)setUp +{ + [RKTestFactory setUp]; + self.objectStore = [RKTestFactory managedObjectStore]; + + NSEntityDescription *entity = [RKHuman entityDescriptionInContext:self.objectStore.primaryManagedObjectContext]; + self.cache = [[RKEntityByAttributeCache alloc] initWithEntity:entity + attribute:@"railsID" + managedObjectContext:self.objectStore.primaryManagedObjectContext]; + // Disable cache monitoring. Tested in specific cases. + self.cache.monitorsContextForChanges = NO; +} + +- (void)tearDown +{ + self.objectStore = nil; + [RKTestFactory tearDown]; +} + +#pragma mark - Identity Tests + +- (void)testEntityIsAssigned +{ + NSEntityDescription *entity = [RKHuman entityDescriptionInContext:self.objectStore.primaryManagedObjectContext]; + assertThat(self.cache.entity, is(equalTo(entity))); +} + +- (void)testManagedObjectContextIsAssigned +{ + NSManagedObjectContext *context = self.objectStore.primaryManagedObjectContext; + assertThat(self.cache.managedObjectContext, is(equalTo(context))); +} + +- (void)testAttributeNameIsAssigned +{ + assertThat(self.cache.attribute, is(equalTo(@"railsID"))); +} + +#pragma mark - Loading and Flushing + +- (void)testLoadSetsLoadedToYes +{ + [self.cache load]; + assertThatBool(self.cache.isLoaded, is(equalToBool(YES))); +} + +- (void)testLoadSetsCountAppropriately +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + NSError *error = nil; + [self.objectStore save:&error]; + + assertThat(error, is(nilValue())); + [self.cache load]; + assertThatInteger([self.cache count], is(equalToInteger(1))); +} + +- (void)testFlushCacheRemovesObjects +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + [self.cache flush]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(NO))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(NO))); +} + +- (void)testFlushCacheReturnsCountToZero +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + [self.cache flush]; + assertThatInteger([self.cache count], is(equalToInteger(0))); +} + +#pragma mark - Retrieving Objects + +- (void)testRetrievalByNumericValue +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + [self.cache load]; + + NSManagedObject *object = [self.cache objectWithAttributeValue:[NSNumber numberWithInteger:12345]]; + assertThat(object, is(equalTo(human))); +} + +- (void)testRetrievalOfNumericPropertyByStringValue +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + [self.cache load]; + + NSManagedObject *object = [self.cache objectWithAttributeValue:@"12345"]; + assertThat(object, is(notNilValue())); + assertThat(object, is(equalTo(human))); +} + +- (void)testRetrievalOfObjectsWithAttributeValue +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + + NSArray *objects = [self.cache objectsWithAttributeValue:[NSNumber numberWithInt:12345]]; + assertThat(objects, hasCountOf(2)); + assertThat([objects objectAtIndex:0], is(instanceOf([NSManagedObject class]))); +} + +- (void)testAddingObjectToCache +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human]; + assertThatBool([self.cache containsObject:human], is(equalToBool(YES))); +} + +- (void)testAddingObjectWithDuplicateAttributeValue +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(YES))); +} + +- (void)testRemovingObjectFromCache +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human]; + assertThatBool([self.cache containsObject:human], is(equalToBool(YES))); + [self.cache removeObject:human]; + assertThatBool([self.cache containsObject:human], is(equalToBool(NO))); +} + +- (void)testRemovingObjectWithExistingAttributeValue +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(YES))); + [self.cache removeObject:human1]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(NO))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(YES))); +} + +#pragma mark - Inspecting Cache State + +- (void)testContainsObjectReturnsNoForDifferingEntities +{ + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCloud" inManagedObjectContext:self.objectStore.primaryManagedObjectContext]; + NSManagedObject *cloud = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.objectStore.primaryManagedObjectContext]; + assertThatBool([self.cache containsObject:cloud], is(equalToBool(NO))); +} + +- (void)testContainsObjectReturnsNoForSubEntities +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + RKChild *child = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + child.railsID = [NSNumber numberWithInteger:12345]; + + [self.cache addObject:human]; + assertThatBool([self.cache containsObject:human], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:child], is(equalToBool(NO))); +} + +- (void)testContainsObjectWithAttributeValue +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human]; + assertThatBool([self.cache containsObjectWithAttributeValue:[NSNumber numberWithInteger:12345]], is(equalToBool(YES))); +} + +- (void)testCount +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human3 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human3.railsID = [NSNumber numberWithInteger:123456]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + [self.cache addObject:human3]; + assertThatInteger([self.cache count], is(equalToInteger(3))); +} + +- (void)testCountOfAttributeValues +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human3 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human3.railsID = [NSNumber numberWithInteger:123456]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + [self.cache addObject:human3]; + assertThatInteger([self.cache countOfAttributeValues], is(equalToInteger(2))); +} + +- (void)testCountWithAttributeValue +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + assertThatInteger([self.cache countWithAttributeValue:[NSNumber numberWithInteger:12345]], is(equalToInteger(2))); +} + +- (void)testThatUnloadedCacheReturnsCountOfZero +{ + assertThatInteger([self.cache count], is(equalToInteger(0))); +} + +#pragma mark - Lifecycle Events + +- (void)testManagedObjectContextProcessPendingChangesAddsNewObjectsToCache +{ + self.cache.monitorsContextForChanges = YES; + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); +} + +- (void)testManagedObjectContextProcessPendingChangesIgnoresObjectsOfDifferentEntityTypes +{ + self.cache.monitorsContextForChanges = YES; + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCloud" inManagedObjectContext:self.objectStore.primaryManagedObjectContext]; + NSManagedObject *cloud = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.objectStore.primaryManagedObjectContext]; + [cloud setValue:@"Cumulus" forKey:@"name"]; + + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:cloud], is(equalToBool(NO))); +} + +- (void)testManagedObjectContextProcessPendingChangesAddsUpdatedObjectsToCache +{ + self.cache.monitorsContextForChanges = YES; + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + [self.cache removeObject:human1]; + human1.name = @"Modified Name"; + assertThatBool([self.cache containsObject:human1], is(equalToBool(NO))); + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); +} + +- (void)testManagedObjectContextProcessPendingChangesRemovesExistingObjectsFromCache +{ + self.cache.monitorsContextForChanges = YES; + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + [self.objectStore.primaryManagedObjectContext deleteObject:human1]; + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(NO))); +} + +#if TARGET_OS_IPHONE +- (void)testCacheIsFlushedOnMemoryWarning +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(YES))); + [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidReceiveMemoryWarningNotification object:self]; +} +#endif + +- (void)testCreatingProcessingAndDeletingObjectsWorksAsExpected { + self.cache.monitorsContextForChanges = YES; + + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(YES))); + [self.objectStore.primaryManagedObjectContext deleteObject:human2]; + + // Save and reload the cache. This will result in the cached temporary + // object ID's being released during the cache flush. + [self.objectStore.primaryManagedObjectContext save:nil]; + [self.cache load]; + + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(NO))); +} + +@end diff --git a/Tests/Logic/CoreData/RKEntityCacheTest.m b/Tests/Logic/CoreData/RKEntityCacheTest.m new file mode 100644 index 0000000000..0ebb30dacb --- /dev/null +++ b/Tests/Logic/CoreData/RKEntityCacheTest.m @@ -0,0 +1,175 @@ +// +// RKEntityCacheTest.m +// RestKit +// +// Created by Blake Watters on 5/2/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "NSEntityDescription+RKAdditions.h" +#import "RKEntityCache.h" +#import "RKEntityByAttributeCache.h" +#import "RKHuman.h" + +@interface RKEntityCacheTest : RKTestCase +@property (nonatomic, retain) RKManagedObjectStore *objectStore; +@property (nonatomic, retain) RKEntityCache *cache; +@property (nonatomic, retain) NSEntityDescription *entity; +@end + +@implementation RKEntityCacheTest + +@synthesize objectStore = _objectStore; +@synthesize cache = _cache; +@synthesize entity = _entity; + +- (void)setUp +{ + [RKTestFactory setUp]; + + self.objectStore = [RKTestFactory managedObjectStore]; + _cache = [[RKEntityCache alloc] initWithManagedObjectContext:self.objectStore.primaryManagedObjectContext]; + self.entity = [RKHuman entityDescriptionInContext:self.objectStore.primaryManagedObjectContext]; +} + +- (void)tearDown +{ + self.objectStore = nil; + self.cache = nil; + + [RKTestFactory tearDown]; +} + +- (void)testInitializationSetsManagedObjectContext +{ + assertThat(_cache.managedObjectContext, is(equalTo(self.objectStore.primaryManagedObjectContext))); +} + +- (void)testIsEntityCachedByAttribute +{ + assertThatBool([_cache isEntity:self.entity cachedByAttribute:@"railsID"], is(equalToBool(NO))); + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + assertThatBool([_cache isEntity:self.entity cachedByAttribute:@"railsID"], is(equalToBool(YES))); +} + +- (void)testRetrievalOfUnderlyingEntityAttributeCache +{ + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + RKEntityByAttributeCache *attributeCache = [_cache attributeCacheForEntity:self.entity attribute:@"railsID"]; + assertThat(attributeCache, is(notNilValue())); +} + +- (void)testRetrievalOfUnderlyingEntityAttributeCaches +{ + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + NSArray *caches = [_cache attributeCachesForEntity:self.entity]; + assertThat(caches, is(notNilValue())); + assertThatInteger([caches count], is(equalToInteger(1))); +} + +- (void)testRetrievalOfObjectForEntityWithAttributeValue +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + NSError *error = nil; + [self.objectStore save:&error]; + + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + NSManagedObject *fetchedObject = [self.cache objectForEntity:self.entity withAttribute:@"railsID" value:[NSNumber numberWithInteger:12345]]; + assertThat(fetchedObject, is(notNilValue())); +} + +- (void)testRetrievalOfObjectsForEntityWithAttributeValue +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + NSError *error = nil; + [self.objectStore save:&error]; + + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + NSArray *objects = [self.cache objectsForEntity:self.entity withAttribute:@"railsID" value:[NSNumber numberWithInteger:12345]]; + assertThat(objects, hasCountOf(2)); + assertThat(objects, containsInAnyOrder(human1, human2, nil)); +} + +- (void)testThatFlushEmptiesAllUnderlyingAttributeCaches +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + human1.name = @"Blake"; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + human2.name = @"Sarah"; + + [self.objectStore save:nil]; + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + [_cache cacheObjectsForEntity:self.entity byAttribute:@"name"]; + + NSArray *objects = [self.cache objectsForEntity:self.entity withAttribute:@"railsID" value:[NSNumber numberWithInteger:12345]]; + assertThat(objects, hasCountOf(2)); + assertThat(objects, containsInAnyOrder(human1, human2, nil)); + + objects = [self.cache objectsForEntity:self.entity withAttribute:@"name" value:@"Blake"]; + assertThat(objects, hasCountOf(1)); + assertThat(objects, contains(human1, nil)); + + [self.cache flush]; + objects = [self.cache objectsForEntity:self.entity withAttribute:@"railsID" value:[NSNumber numberWithInteger:12345]]; + assertThat(objects, is(empty())); + objects = [self.cache objectsForEntity:self.entity withAttribute:@"name" value:@"Blake"]; + assertThat(objects, is(empty())); +} + +- (void)testAddingObjectAddsToEachUnderlyingEntityAttributeCaches +{ + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + [_cache cacheObjectsForEntity:self.entity byAttribute:@"name"]; + + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + human1.name = @"Blake"; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + human2.name = @"Sarah"; + + [_cache addObject:human1]; + [_cache addObject:human2]; + + NSArray *objects = [self.cache objectsForEntity:self.entity withAttribute:@"railsID" value:[NSNumber numberWithInteger:12345]]; + assertThat(objects, hasCountOf(2)); + assertThat(objects, containsInAnyOrder(human1, human2, nil)); + + objects = [self.cache objectsForEntity:self.entity withAttribute:@"name" value:@"Blake"]; + assertThat(objects, hasCountOf(1)); + assertThat(objects, contains(human1, nil)); +} + +- (void)testRemovingObjectRemovesFromUnderlyingEntityAttributeCaches +{ + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + [_cache cacheObjectsForEntity:self.entity byAttribute:@"name"]; + + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + human1.name = @"Blake"; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + human2.name = @"Sarah"; + + [_cache addObject:human1]; + [_cache addObject:human2]; + + NSArray *objects = [self.cache objectsForEntity:self.entity withAttribute:@"railsID" value:[NSNumber numberWithInteger:12345]]; + assertThat(objects, hasCountOf(2)); + assertThat(objects, containsInAnyOrder(human1, human2, nil)); + + RKEntityByAttributeCache *entityAttributeCache = [self.cache attributeCacheForEntity:[RKHuman entity] attribute:@"railsID"]; + assertThatBool([entityAttributeCache containsObject:human1], is(equalToBool(YES))); + [self.cache removeObject:human1]; + assertThatBool([entityAttributeCache containsObject:human1], is(equalToBool(NO))); +} + +@end diff --git a/Tests/Logic/CoreData/RKFetchRequestMappingCacheTest.m b/Tests/Logic/CoreData/RKFetchRequestMappingCacheTest.m index f9bcd72e76..c5270d7dce 100644 --- a/Tests/Logic/CoreData/RKFetchRequestMappingCacheTest.m +++ b/Tests/Logic/CoreData/RKFetchRequestMappingCacheTest.m @@ -24,35 +24,35 @@ - (void)testFetchRequestMappingCacheReturnsObjectsWithNumericPrimaryKey NSEntityDescription *entity = [RKCat entityDescription]; RKManagedObjectMapping *mapping = [RKManagedObjectMapping mappingForClass:[RKCat class] inManagedObjectStore:objectStore]; mapping.primaryKeyAttribute = @"railsID"; - + RKCat *reginald = [RKCat createInContext:objectStore.primaryManagedObjectContext]; reginald.name = @"Reginald"; reginald.railsID = [NSNumber numberWithInt:123456]; [objectStore.primaryManagedObjectContext save:nil]; - - NSManagedObject *cachedObject = [cache findInstanceOfEntity:entity + + NSManagedObject *cachedObject = [cache findInstanceOfEntity:entity withPrimaryKeyAttribute:mapping.primaryKeyAttribute - value:[NSNumber numberWithInt:123456] + value:[NSNumber numberWithInt:123456] inManagedObjectContext:objectStore.primaryManagedObjectContext]; assertThat(cachedObject, is(equalTo(reginald))); } - (void)testFetchRequestMappingCacheReturnsObjectsWithStringPrimaryKey { - // RKEvent entity. String primary key + // RKEvent entity. String primary key RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; RKFetchRequestManagedObjectCache *cache = [RKFetchRequestManagedObjectCache new]; NSEntityDescription *entity = [RKEvent entityDescription]; RKManagedObjectMapping *mapping = [RKManagedObjectMapping mappingForClass:[RKEvent class] inManagedObjectStore:objectStore]; mapping.primaryKeyAttribute = @"eventID"; - + RKEvent *birthday = [RKEvent createInContext:objectStore.primaryManagedObjectContext]; birthday.eventID = @"e-1234-a8-b12"; [objectStore.primaryManagedObjectContext save:nil]; NSManagedObject *cachedObject = [cache findInstanceOfEntity:entity - withPrimaryKeyAttribute:mapping.primaryKeyAttribute - value:@"e-1234-a8-b12" + withPrimaryKeyAttribute:mapping.primaryKeyAttribute + value:@"e-1234-a8-b12" inManagedObjectContext:objectStore.primaryManagedObjectContext]; assertThat(cachedObject, is(equalTo(birthday))); } diff --git a/Tests/Logic/CoreData/RKInMemoryEntityCacheTest.m b/Tests/Logic/CoreData/RKInMemoryEntityCacheTest.m deleted file mode 100644 index f6736ecb82..0000000000 --- a/Tests/Logic/CoreData/RKInMemoryEntityCacheTest.m +++ /dev/null @@ -1,296 +0,0 @@ -// -// RKInMemoryEntityCacheTest.m -// RestKit -// -// Created by Jeff Arena on 1/25/12. -// Copyright (c) 2009-2012 RestKit. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import "RKTestEnvironment.h" -#import "RKHuman.h" - -@interface RKInMemoryEntityCache () -@property(nonatomic, retain) NSMutableDictionary *entityCache; - -- (BOOL)shouldCoerceAttributeToString:(NSString *)attribute forEntity:(NSEntityDescription *)entity; -- (NSManagedObject *)objectWithID:(NSManagedObjectID *)objectID inContext:(NSManagedObjectContext *)managedObjectContext; -@end - -@interface RKInMemoryEntityCacheTest : RKTestCase - -@end - -@implementation RKInMemoryEntityCacheTest - -- (void)testInitializationWithManagedObjectContext -{ - RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; - RKInMemoryEntityCache *cache = [[RKInMemoryEntityCache alloc] initWithManagedObjectContext:objectStore.primaryManagedObjectContext]; - assertThat(cache.managedObjectContext, is(equalTo(objectStore.primaryManagedObjectContext))); -} - -- (void)testShouldCoercePrimaryKeysToStringsForLookup { - RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - RKHuman* human = [RKHuman createEntity]; - human.railsID = [NSNumber numberWithInt:1234]; - [objectStore save:nil]; - - RKInMemoryEntityCache *entityCache = [[[RKInMemoryEntityCache alloc] init] autorelease]; - assertThatBool([entityCache shouldCoerceAttributeToString:@"railsID" forEntity:human.entity], is(equalToBool(YES))); -} - -- (void)testShouldCacheAllObjectsForEntityWhenAccessingEntityCache { - RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - RKHuman* human = [RKHuman createEntity]; - human.railsID = [NSNumber numberWithInt:1234]; - [objectStore save:nil]; - - RKInMemoryEntityCache *entityCache = [[[RKInMemoryEntityCache alloc] init] autorelease]; - NSMutableDictionary *humanCache = [entityCache cachedObjectsForEntity:human.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(1))); -} - -- (void)testShouldCacheAllObjectsForEntityWhenAsked { - RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - RKHuman* human = [RKHuman createEntity]; - human.railsID = [NSNumber numberWithInt:1234]; - [objectStore save:nil]; - - RKInMemoryEntityCache *entityCache = [[[RKInMemoryEntityCache alloc] init] autorelease]; - [entityCache cacheObjectsForEntity:human.entity byAttribute:@"railsID" inContext:objectStore.primaryManagedObjectContext]; - NSMutableDictionary *humanCache = [entityCache cachedObjectsForEntity:human.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(1))); -} - -- (void)testShouldRetrieveObjectsProperlyFromTheEntityCache { - RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - RKHuman* human = [RKHuman createEntity]; - human.railsID = [NSNumber numberWithInt:1234]; - [objectStore save:nil]; - - RKInMemoryEntityCache *entityCache = [[[RKInMemoryEntityCache alloc] init] autorelease]; - NSManagedObject *cachedInstance = [entityCache cachedObjectForEntity:human.entity - withAttribute:@"railsID" - value:[NSNumber numberWithInt:1234] - inContext:objectStore.primaryManagedObjectContext]; - assertThat(cachedInstance, is(equalTo(human))); -} - -- (void)testShouldCacheAnIndividualObjectWhenAsked { - RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - RKHuman* human = [RKHuman createEntity]; - human.railsID = [NSNumber numberWithInt:1234]; - [objectStore save:nil]; - - RKInMemoryEntityCache *entityCache = [[[RKInMemoryEntityCache alloc] init] autorelease]; - NSMutableDictionary *humanCache = [entityCache cachedObjectsForEntity:human.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(1))); - - RKHuman* newHuman = [RKHuman createEntity]; - newHuman.railsID = [NSNumber numberWithInt:5678]; - - [entityCache cacheObject:newHuman byAttribute:@"railsID" inContext:objectStore.primaryManagedObjectContext]; - [entityCache cacheObjectsForEntity:human.entity byAttribute:@"railsID" inContext:objectStore.primaryManagedObjectContext]; - humanCache = [entityCache cachedObjectsForEntity:human.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(2))); -} - -- (void)testShouldCacheAnIndividualObjectByPrimaryKeyValueWhenAsked { - RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - RKHuman* human = [RKHuman createEntity]; - human.railsID = [NSNumber numberWithInt:1234]; - [objectStore save:nil]; - - RKInMemoryEntityCache *entityCache = [[[RKInMemoryEntityCache alloc] init] autorelease]; - NSMutableDictionary *humanCache = [entityCache cachedObjectsForEntity:human.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(1))); - - RKHuman* newHuman = [RKHuman createEntity]; - newHuman.railsID = [NSNumber numberWithInt:5678]; - [objectStore save:nil]; - - [entityCache cacheObject:newHuman.entity - byAttribute:@"railsID" - value:[NSNumber numberWithInt:5678] - inContext:objectStore.primaryManagedObjectContext]; - [entityCache cacheObjectsForEntity:human.entity byAttribute:@"railsID" inContext:objectStore.primaryManagedObjectContext]; - - humanCache = [entityCache cachedObjectsForEntity:human.entity byAttribute:@"railsID" inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(2))); -} - -- (void)testShouldExpireACacheEntryForAnObjectWhenAsked { - RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - RKHuman* human = [RKHuman createEntity]; - human.railsID = [NSNumber numberWithInt:1234]; - [objectStore save:nil]; - - RKInMemoryEntityCache *entityCache = [[[RKInMemoryEntityCache alloc] init] autorelease]; - NSMutableDictionary *humanCache = [entityCache cachedObjectsForEntity:human.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(1))); - - [entityCache expireCacheEntryForObject:human byAttribute:@"railsID" inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([entityCache.entityCache count], is(equalToInt(0))); -} - -- (void)testShouldExpireEntityCacheWhenAsked { - RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - RKHuman* human = [RKHuman createEntity]; - human.railsID = [NSNumber numberWithInt:1234]; - [objectStore save:nil]; - - RKInMemoryEntityCache *entityCache = [[[RKInMemoryEntityCache alloc] init] autorelease]; - NSMutableDictionary *humanCache = [entityCache cachedObjectsForEntity:human.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(1))); - - [entityCache expireCacheEntriesForEntity:human.entity]; - assertThatInteger([entityCache.entityCache count], is(equalToInt(0))); -} - -#if TARGET_OS_IPHONE -- (void)testShouldExpireEntityCacheInResponseToMemoryWarning { - RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - RKHuman* human = [RKHuman createEntity]; - human.railsID = [NSNumber numberWithInt:1234]; - [objectStore save:nil]; - - RKInMemoryEntityCache *entityCache = [[[RKInMemoryEntityCache alloc] init] autorelease]; - NSMutableDictionary *humanCache = [entityCache cachedObjectsForEntity:human.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(1))); - - [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidReceiveMemoryWarningNotification object:nil]; - assertThatInteger([entityCache.entityCache count], is(equalToInt(0))); -} -#endif - -- (void)testShouldAddInstancesOfInsertedObjectsToCache { - RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - RKHuman* human = [RKHuman createEntity]; - human.railsID = [NSNumber numberWithInt:1234]; - [objectStore save:nil]; - - RKInMemoryEntityCache *entityCache = [[[RKInMemoryEntityCache alloc] init] autorelease]; - NSMutableDictionary *humanCache = [entityCache cachedObjectsForEntity:human.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(1))); - - RKHuman* newHuman = [RKHuman createEntity]; - newHuman.railsID = [NSNumber numberWithInt:5678]; - [objectStore save:nil]; - - humanCache = [entityCache cachedObjectsForEntity:human.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(2))); -} - -- (void)testShouldRemoveInstancesOfDeletedObjectsToCache { - RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - RKHuman* humanOne = [RKHuman createEntity]; - humanOne.railsID = [NSNumber numberWithInt:1234]; - - RKHuman* humanTwo = [RKHuman createEntity]; - humanTwo.railsID = [NSNumber numberWithInt:5678]; - [objectStore save:nil]; - - RKInMemoryEntityCache *entityCache = [[[RKInMemoryEntityCache alloc] init] autorelease]; - NSMutableDictionary *humanCache = [entityCache cachedObjectsForEntity:humanOne.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(2))); - - [humanTwo deleteEntity]; - [objectStore save:nil]; - - humanCache = [entityCache cachedObjectsForEntity:humanOne.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(1))); -} - -- (void)testThatDeletionOfObjectsDoesNotPermitCreationOfDuplicates { - RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - RKHuman* humanOne = [RKHuman createEntity]; - humanOne.railsID = [NSNumber numberWithInt:1234]; - - RKHuman* humanTwo = [RKHuman createEntity]; - humanTwo.railsID = [NSNumber numberWithInt:5678]; - [objectStore save:nil]; - - RKInMemoryEntityCache *entityCache = [[[RKInMemoryEntityCache alloc] init] autorelease]; - NSMutableDictionary *humanCache = [entityCache cachedObjectsForEntity:humanOne.entity - byAttribute:@"railsID" - inContext:objectStore.primaryManagedObjectContext]; - assertThatInteger([humanCache count], is(equalToInt(2))); - - [humanTwo deleteEntity]; - [objectStore save:nil]; - - RKHuman *existingReference = (RKHuman *)[entityCache cachedObjectForEntity:[RKHuman entity] withAttribute:@"railsID" value:[NSNumber numberWithInt:1234] inContext:objectStore.primaryManagedObjectContext]; - - assertThat(existingReference, is(notNilValue())); - assertThat(existingReference, is(equalTo(humanOne))); -} - -- (void)testThatRepeatedInvocationsOfLoadObjectDoesNotDuplicateObjects { - RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; - objectStore.cacheStrategy = [RKInMemoryManagedObjectCache new]; - RKObjectManager *objectManager = [RKTestFactory objectManager]; - objectManager.objectStore = objectStore; - RKManagedObjectMapping *humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:objectStore]; - humanMapping.primaryKeyAttribute = @"name"; - [humanMapping mapKeyPath:@"id" toAttribute:@"railsID"]; - [humanMapping mapAttributes:@"name", nil]; - [objectManager.mappingProvider setObjectMapping:humanMapping forKeyPath:@"human"]; - - for (NSUInteger i = 0; i < 5; i++) { - RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; - [objectManager loadObjectsAtResourcePath:@"/JSON/ArrayOfHumans.json" delegate:responseLoader]; - [responseLoader waitForResponse]; - for (RKHuman *object in [RKHuman allObjects]) { - if ([object.railsID intValue] == 201) { - [objectStore.managedObjectContextForCurrentThread deleteObject:object]; - [objectStore.managedObjectContextForCurrentThread save:nil]; - } - } - - [objectStore save:nil]; - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.2]]; - } - - NSUInteger count = [RKHuman count:nil]; - assertThatInt(count, is(equalToInt(1))); - NSArray *humans = [RKHuman allObjects]; - [humans makeObjectsPerformSelector:@selector(railsID)]; -} - -@end diff --git a/Tests/Logic/CoreData/RKManagedObjectLoaderTest.m b/Tests/Logic/CoreData/RKManagedObjectLoaderTest.m index 98f60669c7..efd461f15f 100644 --- a/Tests/Logic/CoreData/RKManagedObjectLoaderTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectLoaderTest.m @@ -90,7 +90,7 @@ - (void)testShouldDeleteObjectsMissingFromPayloadReturnedByObjectCache { [humanMapping mapAttributes:@"name", nil]; humanMapping.primaryKeyAttribute = @"railsID"; humanMapping.rootKeyPath = @"human"; - + // Create 3 objects, we will expect 2 after the load [RKHuman truncateAll]; assertThatUnsignedInteger([RKHuman count:nil], is(equalToInt(0))); @@ -214,4 +214,25 @@ - (void)testTheOnDidFailBlockIsInvokedOnFailure { assertThatBool(invoked, is(equalToBool(YES))); } +- (void)testThatObjectLoadedDidFinishLoadingIsCalledOnStoreSaveFailure { + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + objectManager.objectStore = store; + id mockStore = [OCMockObject partialMockForObject:store]; + BOOL success = NO; + [[[mockStore stub] andReturnValue:OCMOCK_VALUE(success)] save:[OCMArg anyPointer]]; + + RKObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + RKManagedObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/humans/1"]; + objectLoader.objectMapping = mapping; + + RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + id mockResponseLoader = [OCMockObject partialMockForObject:responseLoader]; + [[mockResponseLoader expect] objectLoaderDidFinishLoading:objectLoader]; + objectLoader.delegate = responseLoader; + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + [mockResponseLoader verify]; +} + @end diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m index 94bd740169..7eaded59f0 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m @@ -25,6 +25,7 @@ #import "RKHuman.h" #import "RKChild.h" #import "RKParent.h" +#import "RKBenchmark.h" @interface RKManagedObjectMappingOperationTest : RKTestCase { @@ -82,52 +83,52 @@ - (void)testShouldConnectRelationshipsByPrimaryKey { - (void)testConnectRelationshipsDoesNotLeakMemory { RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - + RKManagedObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class] inManagedObjectStore:objectStore]; catMapping.primaryKeyAttribute = @"railsID"; [catMapping mapAttributes:@"name", nil]; - + RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:objectStore]; humanMapping.primaryKeyAttribute = @"railsID"; [humanMapping mapAttributes:@"name", @"favoriteCatID", nil]; [humanMapping hasOne:@"favoriteCat" withMapping:catMapping]; [humanMapping connectRelationship:@"favoriteCat" withObjectForPrimaryKeyAttribute:@"favoriteCatID"]; - + // Create a cat to connect RKCat* cat = [RKCat object]; cat.name = @"Asia"; cat.railsID = [NSNumber numberWithInt:31337]; [objectStore save:nil]; - + NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], nil]; RKHuman* human = [RKHuman object]; RKManagedObjectMappingOperation* operation = [[RKManagedObjectMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; operation.queue = [RKMappingOperationQueue new]; NSError* error = nil; [operation performMapping:&error]; - + assertThatInteger([operation retainCount], is(equalToInteger(1))); } - (void)testConnectionOfHasManyRelationshipsByPrimaryKey { RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; - + RKManagedObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class] inManagedObjectStore:objectStore]; catMapping.primaryKeyAttribute = @"railsID"; [catMapping mapAttributes:@"name", nil]; - + RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:objectStore]; humanMapping.primaryKeyAttribute = @"railsID"; [humanMapping mapAttributes:@"name", @"favoriteCatID", nil]; [humanMapping hasOne:@"favoriteCat" withMapping:catMapping]; [humanMapping connectRelationship:@"favoriteCat" withObjectForPrimaryKeyAttribute:@"favoriteCatID"]; - + // Create a cat to connect RKCat* cat = [RKCat object]; cat.name = @"Asia"; cat.railsID = [NSNumber numberWithInt:31337]; [objectStore save:nil]; - + NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], nil]; RKHuman* human = [RKHuman object]; RKManagedObjectMappingOperation* operation = [[RKManagedObjectMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; @@ -155,17 +156,17 @@ - (void)testShouldConnectRelationshipsByPrimaryKeyWithDifferentSourceAndDestinat RKCat* asia = [RKCat object]; asia.name = @"Asia"; asia.railsID = [NSNumber numberWithInt:31337]; - + RKCat* roy = [RKCat object]; roy.name = @"Reginald Royford Williams III"; roy.railsID = [NSNumber numberWithInt:31338]; - + [objectStore save:nil]; NSArray *catIDs = [NSArray arrayWithObjects:[NSNumber numberWithInt:31337], [NSNumber numberWithInt:31338], nil]; NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"catIDs", catIDs, nil]; RKHuman* human = [RKHuman object]; - + RKManagedObjectMappingOperation* operation = [[RKManagedObjectMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; NSError* error = nil; BOOL success = [operation performMapping:&error]; @@ -199,12 +200,12 @@ - (void)testShouldLoadOrderedHasManyRelationship { RKManagedObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class] inManagedObjectStore:objectStore]; catMapping.primaryKeyAttribute = @"railsID"; [catMapping mapAttributes:@"name", nil]; - + RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:objectStore]; humanMapping.primaryKeyAttribute = @"railsID"; [humanMapping mapAttributes:@"name", @"favoriteCatID", nil]; [humanMapping mapKeyPath:@"cats" toRelationship:@"catsInOrderByAge" withMapping:catMapping]; - + NSArray* catsData = [NSArray arrayWithObject:[NSDictionary dictionaryWithObject:@"Asia" forKey:@"name"]]; NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"favoriteCatID", [NSNumber numberWithInt:31337], @"cats", catsData, nil]; RKHuman* human = [RKHuman object]; @@ -352,7 +353,7 @@ - (void)testShouldConnectRelationshipsByPrimaryKeyRegardlessOfOrder { // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary // keys are not guaranteed to return in any particular order [mappingProvider setMapping:parentMapping forKeyPath:@"parents"]; - [mappingProvider setMapping:childMapping forKeyPath:@"children"]; + [mappingProvider setMapping:childMapping forKeyPath:@"children"]; NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"ConnectingParents.json"]; RKObjectMapper *mapper = [RKObjectMapper mapperWithObject:JSON mappingProvider:mappingProvider]; @@ -363,4 +364,130 @@ - (void)testShouldConnectRelationshipsByPrimaryKeyRegardlessOfOrder { assertThat(child.father, is(notNilValue())); } +- (void)testMappingAPayloadContainingRepeatedObjectsDoesNotYieldDuplicatesWithFetchRequestMappingCache { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + store.cacheStrategy = [RKFetchRequestManagedObjectCache new]; + + RKManagedObjectMapping* childMapping = [RKManagedObjectMapping mappingForClass:[RKChild class] inManagedObjectStore:store]; + childMapping.primaryKeyAttribute = @"childID"; + [childMapping mapAttributes:@"name", @"childID", nil]; + + RKManagedObjectMapping* parentMapping = [RKManagedObjectMapping mappingForClass:[RKParent class] inManagedObjectStore:store]; + [parentMapping mapAttributes:@"parentID", @"name", nil]; + parentMapping.primaryKeyAttribute = @"parentID"; + [parentMapping mapRelationship:@"children" withMapping:childMapping]; + + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider new]; + // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary + // keys are not guaranteed to return in any particular order + [mappingProvider setObjectMapping:parentMapping forKeyPath:@"parents"]; + + NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"parents_and_children.json"]; + RKObjectMapper *mapper = [RKObjectMapper mapperWithObject:JSON mappingProvider:mappingProvider]; + [mapper performMapping]; + + NSUInteger parentCount = [RKParent count:nil]; + NSUInteger childrenCount = [RKChild count:nil]; + assertThatInteger(parentCount, is(equalToInteger(2))); + assertThatInteger(childrenCount, is(equalToInteger(4))); +} + +- (void)testMappingAPayloadContainingRepeatedObjectsDoesNotYieldDuplicatesWithInMemoryMappingCache { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + store.cacheStrategy = [RKInMemoryManagedObjectCache new]; + + RKManagedObjectMapping* childMapping = [RKManagedObjectMapping mappingForClass:[RKChild class] inManagedObjectStore:store]; + childMapping.primaryKeyAttribute = @"childID"; + [childMapping mapAttributes:@"name", @"childID", nil]; + + RKManagedObjectMapping* parentMapping = [RKManagedObjectMapping mappingForClass:[RKParent class] inManagedObjectStore:store]; + [parentMapping mapAttributes:@"parentID", @"name", nil]; + parentMapping.primaryKeyAttribute = @"parentID"; + [parentMapping mapRelationship:@"children" withMapping:childMapping]; + + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider new]; + // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary + // keys are not guaranteed to return in any particular order + [mappingProvider setObjectMapping:parentMapping forKeyPath:@"parents"]; + + NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"parents_and_children.json"]; + RKObjectMapper *mapper = [RKObjectMapper mapperWithObject:JSON mappingProvider:mappingProvider]; + [mapper performMapping]; + + NSUInteger parentCount = [RKParent count:nil]; + NSUInteger childrenCount = [RKChild count:nil]; + assertThatInteger(parentCount, is(equalToInteger(2))); + assertThatInteger(childrenCount, is(equalToInteger(4))); +} + +- (void)testMappingAPayloadContainingRepeatedObjectsPerformsAcceptablyWithFetchRequestMappingCache { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + store.cacheStrategy = [RKFetchRequestManagedObjectCache new]; + + RKManagedObjectMapping* childMapping = [RKManagedObjectMapping mappingForClass:[RKChild class] inManagedObjectStore:store]; + childMapping.primaryKeyAttribute = @"childID"; + [childMapping mapAttributes:@"name", @"childID", nil]; + + RKManagedObjectMapping* parentMapping = [RKManagedObjectMapping mappingForClass:[RKParent class] inManagedObjectStore:store]; + [parentMapping mapAttributes:@"parentID", @"name", nil]; + parentMapping.primaryKeyAttribute = @"parentID"; + [parentMapping mapRelationship:@"children" withMapping:childMapping]; + + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider new]; + // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary + // keys are not guaranteed to return in any particular order + [mappingProvider setObjectMapping:parentMapping forKeyPath:@"parents"]; + + NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"benchmark_parents_and_children.json"]; + RKObjectMapper *mapper = [RKObjectMapper mapperWithObject:JSON mappingProvider:mappingProvider]; + + RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelOff); + RKLogConfigureByName("RestKit/CoreData", RKLogLevelOff); + + [RKBenchmark report:@"Mapping with Fetch Request Cache" executionBlock:^{ + for (NSUInteger i=0; i<50; i++) { + [mapper performMapping]; + } + }]; + NSUInteger parentCount = [RKParent count:nil]; + NSUInteger childrenCount = [RKChild count:nil]; + assertThatInteger(parentCount, is(equalToInteger(25))); + assertThatInteger(childrenCount, is(equalToInteger(51))); +} + +- (void)testMappingAPayloadContainingRepeatedObjectsPerformsAcceptablyWithInMemoryMappingCache { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + store.cacheStrategy = [RKInMemoryManagedObjectCache new]; + + RKManagedObjectMapping* childMapping = [RKManagedObjectMapping mappingForClass:[RKChild class] inManagedObjectStore:store]; + childMapping.primaryKeyAttribute = @"childID"; + [childMapping mapAttributes:@"name", @"childID", nil]; + + RKManagedObjectMapping* parentMapping = [RKManagedObjectMapping mappingForClass:[RKParent class] inManagedObjectStore:store]; + [parentMapping mapAttributes:@"parentID", @"name", nil]; + parentMapping.primaryKeyAttribute = @"parentID"; + [parentMapping mapRelationship:@"children" withMapping:childMapping]; + + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider new]; + // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary + // keys are not guaranteed to return in any particular order + [mappingProvider setObjectMapping:parentMapping forKeyPath:@"parents"]; + + NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"benchmark_parents_and_children.json"]; + RKObjectMapper *mapper = [RKObjectMapper mapperWithObject:JSON mappingProvider:mappingProvider]; + + RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelOff); + RKLogConfigureByName("RestKit/CoreData", RKLogLevelOff); + + [RKBenchmark report:@"Mapping with In Memory Cache" executionBlock:^{ + for (NSUInteger i=0; i<50; i++) { + [mapper performMapping]; + } + }]; + NSUInteger parentCount = [RKParent count:nil]; + NSUInteger childrenCount = [RKChild count:nil]; + assertThatInteger(parentCount, is(equalToInteger(25))); + assertThatInteger(childrenCount, is(equalToInteger(51))); +} + @end diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingTest.m index 26e6cb4e81..4e05176ba9 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingTest.m @@ -32,6 +32,16 @@ @interface RKManagedObjectMappingTest : RKTestCase @implementation RKManagedObjectMappingTest +- (void)setUp +{ + [RKTestFactory setUp]; +} + +- (void)tearDown +{ + [RKTestFactory tearDown]; +} + - (void)testShouldReturnTheDefaultValueForACoreDataAttribute { // Load Core Data RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; @@ -103,12 +113,12 @@ - (void)testShouldMapACollectionOfObjectsWithDynamicKeys { id mockCacheStrategy = [OCMockObject partialMockForObject:objectStore.cacheStrategy]; [[[mockCacheStrategy expect] andForwardToRealObject] findInstanceOfEntity:OCMOCK_ANY - withPrimaryKeyAttribute:mapping.primaryKeyAttribute - value:@"blake" + withPrimaryKeyAttribute:mapping.primaryKeyAttribute + value:@"blake" inManagedObjectContext:objectStore.primaryManagedObjectContext]; [[[mockCacheStrategy expect] andForwardToRealObject] findInstanceOfEntity:mapping.entity - withPrimaryKeyAttribute:mapping.primaryKeyAttribute - value:@"rachit" + withPrimaryKeyAttribute:mapping.primaryKeyAttribute + value:@"rachit" inManagedObjectContext:objectStore.primaryManagedObjectContext]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"DynamicKeys.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; @@ -165,8 +175,9 @@ - (void)testThatAssigningAPrimaryKeyAttributeToAMappingWhoseEntityHasANilPrimary NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCloud" inManagedObjectContext:store.primaryManagedObjectContext]; RKManagedObjectMapping *mapping = [RKManagedObjectMapping mappingForEntity:entity inManagedObjectStore:store]; assertThat(mapping.primaryKeyAttribute, is(nilValue())); - mapping.primaryKeyAttribute = @"cloudID"; - assertThat(entity.primaryKeyAttribute, is(equalTo(@"cloudID"))); + mapping.primaryKeyAttribute = @"name"; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"name"))); + assertThat(entity.primaryKeyAttribute, is(notNilValue())); } #pragma mark - Fetched Results Cache @@ -177,12 +188,12 @@ - (void)testShouldFindExistingManagedObjectsByPrimaryKeyWithFetchedResultsCache RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; mapping.primaryKeyAttribute = @"railsID"; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"railsID"]]; - + RKHuman* human = [RKHuman object]; human.railsID = [NSNumber numberWithInt:123]; [store save:nil]; assertThatBool([RKHuman hasAtLeastOneEntity], is(equalToBool(YES))); - + NSDictionary* data = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:123] forKey:@"id"]; id object = [mapping mappableObjectForData:data]; assertThat(object, isNot(nilValue())); @@ -196,13 +207,13 @@ - (void)testShouldFindExistingManagedObjectsByPrimaryKeyPathWithFetchedResultsCa RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; mapping.primaryKeyAttribute = @"railsID"; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"monkey.id" toKeyPath:@"railsID"]]; - + [RKHuman truncateAll]; RKHuman* human = [RKHuman object]; human.railsID = [NSNumber numberWithInt:123]; [store save:nil]; assertThatBool([RKHuman hasAtLeastOneEntity], is(equalToBool(YES))); - + NSDictionary* data = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:123] forKey:@"id"]; NSDictionary* nestedDictionary = [NSDictionary dictionaryWithObject:data forKey:@"monkey"]; id object = [mapping mappableObjectForData:nestedDictionary]; @@ -219,14 +230,15 @@ - (void)testShouldFindExistingManagedObjectsByPrimaryKeyWithInMemoryCache { RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; mapping.primaryKeyAttribute = @"railsID"; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"railsID"]]; - - RKHuman* human = [RKHuman object]; + + RKHuman* human = [RKHuman createInContext:store.primaryManagedObjectContext]; human.railsID = [NSNumber numberWithInt:123]; [store save:nil]; assertThatBool([RKHuman hasAtLeastOneEntity], is(equalToBool(YES))); - + NSDictionary* data = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:123] forKey:@"id"]; - id object = [mapping mappableObjectForData:data]; + NSManagedObject *object = [mapping mappableObjectForData:data]; + assertThat([object managedObjectContext], is(equalTo(store.primaryManagedObjectContext))); assertThat(object, isNot(nilValue())); assertThat(object, is(equalTo(human))); } @@ -239,13 +251,13 @@ - (void)testShouldFindExistingManagedObjectsByPrimaryKeyPathWithInMemoryCache { RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; mapping.primaryKeyAttribute = @"railsID"; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"monkey.id" toKeyPath:@"railsID"]]; - + [RKHuman truncateAll]; RKHuman* human = [RKHuman object]; human.railsID = [NSNumber numberWithInt:123]; [store save:nil]; assertThatBool([RKHuman hasAtLeastOneEntity], is(equalToBool(YES))); - + NSDictionary* data = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:123] forKey:@"id"]; NSDictionary* nestedDictionary = [NSDictionary dictionaryWithObject:data forKey:@"monkey"]; id object = [mapping mappableObjectForData:nestedDictionary]; @@ -253,4 +265,82 @@ - (void)testShouldFindExistingManagedObjectsByPrimaryKeyPathWithInMemoryCache { assertThat(object, is(equalTo(human))); } +- (void)testMappingWithFetchRequestCacheWherePrimaryKeyAttributeOfMappingDisagreesWithEntity +{ + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + store.cacheStrategy = [RKFetchRequestManagedObjectCache new]; + [RKHuman truncateAll]; + RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + mapping.primaryKeyAttribute = @"name"; + [RKHuman entity].primaryKeyAttributeName = @"railsID"; + [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"monkey.name" toKeyPath:@"name"]]; + + [RKHuman truncateAll]; + RKHuman* human = [RKHuman object]; + human.name = @"Testing"; + [store save:nil]; + assertThatBool([RKHuman hasAtLeastOneEntity], is(equalToBool(YES))); + + NSDictionary* data = [NSDictionary dictionaryWithObject:@"Testing" forKey:@"name"]; + NSDictionary* nestedDictionary = [NSDictionary dictionaryWithObject:data forKey:@"monkey"]; + id object = [mapping mappableObjectForData:nestedDictionary]; + assertThat(object, isNot(nilValue())); + assertThat(object, is(equalTo(human))); + + id cachedObject = [store.cacheStrategy findInstanceOfEntity:[RKHuman entity] withPrimaryKeyAttribute:@"name" value:@"Testing" inManagedObjectContext:store.primaryManagedObjectContext]; + assertThat(cachedObject, is(equalTo(human))); +} + +- (void)testMappingWithInMemoryCacheWherePrimaryKeyAttributeOfMappingDisagreesWithEntity +{ + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + store.cacheStrategy = [RKInMemoryManagedObjectCache new]; + [RKHuman truncateAll]; + RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + mapping.primaryKeyAttribute = @"name"; + [RKHuman entity].primaryKeyAttributeName = @"railsID"; + [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"monkey.name" toKeyPath:@"name"]]; + + [RKHuman truncateAll]; + RKHuman* human = [RKHuman object]; + human.name = @"Testing"; + [store save:nil]; + assertThatBool([RKHuman hasAtLeastOneEntity], is(equalToBool(YES))); + + NSDictionary* data = [NSDictionary dictionaryWithObject:@"Testing" forKey:@"name"]; + NSDictionary* nestedDictionary = [NSDictionary dictionaryWithObject:data forKey:@"monkey"]; + id object = [mapping mappableObjectForData:nestedDictionary]; + assertThat(object, isNot(nilValue())); + assertThat(object, is(equalTo(human))); + + id cachedObject = [store.cacheStrategy findInstanceOfEntity:[RKHuman entity] withPrimaryKeyAttribute:@"name" value:@"Testing" inManagedObjectContext:store.primaryManagedObjectContext]; + assertThat(cachedObject, is(equalTo(human))); +} + +- (void)testThatCreationOfNewObjectWithIncorrectTypeValueForPrimaryKeyAddsToCache +{ + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + store.cacheStrategy = [RKInMemoryManagedObjectCache new]; + [RKHuman truncateAll]; + RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + mapping.primaryKeyAttribute = @"railsID"; + [RKHuman entity].primaryKeyAttributeName = @"railsID"; + [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"monkey.name" toKeyPath:@"name"]]; + [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"monkey.railsID" toKeyPath:@"railsID"]]; + + [RKHuman truncateAll]; + RKHuman* human = [RKHuman object]; + human.name = @"Testing"; + human.railsID = [NSNumber numberWithInteger:12345]; + [store save:nil]; + assertThatBool([RKHuman hasAtLeastOneEntity], is(equalToBool(YES))); + + NSDictionary* data = [NSDictionary dictionaryWithObject:@"12345" forKey:@"railsID"]; + NSDictionary* nestedDictionary = [NSDictionary dictionaryWithObject:data forKey:@"monkey"]; + RKHuman *object = [mapping mappableObjectForData:nestedDictionary]; + assertThat(object, isNot(nilValue())); + assertThat(object, is(equalTo(human))); + assertThatInteger([object.railsID integerValue], is(equalToInteger(12345))); +} + @end diff --git a/Tests/Logic/CoreData/RKManagedObjectStoreTest.m b/Tests/Logic/CoreData/RKManagedObjectStoreTest.m index 28b369e9d9..6424ab5fe7 100644 --- a/Tests/Logic/CoreData/RKManagedObjectStoreTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectStoreTest.m @@ -20,6 +20,7 @@ #import "RKTestEnvironment.h" #import "RKHuman.h" +#import "RKDirectory.h" @interface RKManagedObjectStoreTest : RKTestCase @@ -34,4 +35,24 @@ - (void)testInstantiationOfNewManagedObjectContextAssociatesWithObjectStore assertThat([context managedObjectStore], is(equalTo(store))); } +- (void)testCreationOfStoreInSpecificDirectoryRaisesIfDoesNotExist +{ + NSString *path = [[RKDirectory applicationDataDirectory] stringByAppendingPathComponent:@"/NonexistantSubdirectory"]; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path]; + assertThatBool(exists, is(equalToBool(NO))); + STAssertThrows([RKManagedObjectStore objectStoreWithStoreFilename:@"Whatever.sqlite" inDirectory:path usingSeedDatabaseName:nil managedObjectModel:nil delegate:nil], nil); +} + +- (void)testCreationOfStoryInApplicationDirectoryCreatesIfNonExistant +{ + // On OS X, the application directory is not created for you + NSString *path = [RKDirectory applicationDataDirectory]; + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; + assertThat(error, is(nilValue())); + STAssertNoThrow([RKManagedObjectStore objectStoreWithStoreFilename:@"Whatever.sqlite" inDirectory:nil usingSeedDatabaseName:nil managedObjectModel:nil delegate:nil], nil); + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path]; + assertThatBool(exists, is(equalToBool(YES))); +} + @end diff --git a/Tests/Logic/CoreData/RKManagedObjectThreadSafeInvocationTest.m b/Tests/Logic/CoreData/RKManagedObjectThreadSafeInvocationTest.m index 32c8c477e2..1c9fc704f6 100644 --- a/Tests/Logic/CoreData/RKManagedObjectThreadSafeInvocationTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectThreadSafeInvocationTest.m @@ -45,6 +45,20 @@ - (void)testShouldSerializeOneManagedObjectToManagedObjectID { assertThat([dictionary valueForKeyPath:@"human"], is(instanceOf([NSManagedObjectID class]))); } +- (void)testShouldSerializeOneManagedObjectWithKeyPathToManagedObjectID { + NSString *testKey = @"data.human"; + RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.objectStore = objectStore; + RKHuman* human = [RKHuman object]; + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithObject:human forKey:testKey]; + NSMethodSignature* signature = [self methodSignatureForSelector:@selector(informDelegateWithDictionary:)]; + RKManagedObjectThreadSafeInvocation* invocation = [RKManagedObjectThreadSafeInvocation invocationWithMethodSignature:signature]; + [invocation serializeManagedObjectsForArgument:dictionary withKeyPaths:[NSSet setWithObject:testKey]]; + assertThat([dictionary valueForKeyPath:testKey], is(instanceOf([NSManagedObjectID class]))); +} + + - (void)testShouldSerializeCollectionOfManagedObjectsToManagedObjectIDs { RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; RKObjectManager *objectManager = [RKTestFactory objectManager]; @@ -128,8 +142,8 @@ - (void)testShouldSerializeAndDeserializeManagedObjectsAcrossAThreadInvocation { [self performSelectorInBackground:@selector(createBackgroundObjects) withObject:nil]; while (_waiting) { - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - } + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } } @end diff --git a/Tests/Logic/Network/RKClientTest.m b/Tests/Logic/Network/RKClientTest.m index 8ba9a5927c..d746156541 100644 --- a/Tests/Logic/Network/RKClientTest.m +++ b/Tests/Logic/Network/RKClientTest.m @@ -28,19 +28,30 @@ @interface RKClientTest : RKTestCase @implementation RKClientTest +- (void)setUp { + [RKTestFactory setUp]; +} + +- (void)tearDown { + [RKTestFactory tearDown]; +} + - (void)testShouldDetectNetworkStatusWithAHostname { - RKClient* client = [RKClient clientWithBaseURLString:@"http://restkit.org"]; - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; // Let the runloop cycle - RKReachabilityNetworkStatus status = [client.reachabilityObserver networkStatus]; - assertThatInt(status, is(equalToInt(RKReachabilityReachableViaWiFi))); + RKClient* client = [[RKClient alloc] initWithBaseURLString:@"http://restkit.org"]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; // Let the runloop cycle + RKReachabilityNetworkStatus status = [client.reachabilityObserver networkStatus]; + assertThatInt(status, is(equalToInt(RKReachabilityReachableViaWiFi))); + [client release]; } - (void)testShouldDetectNetworkStatusWithAnIPAddressBaseName { - RKClient* client = [RKClient clientWithBaseURLString:@"http://173.45.234.197"]; - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; // Let the runloop cycle - RKReachabilityNetworkStatus status = [client.reachabilityObserver networkStatus]; - assertThatInt(status, isNot(equalToInt(RKReachabilityIndeterminate))); + RKClient *client = [[RKClient alloc] initWithBaseURLString:@"http://173.45.234.197"]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; // Let the runloop cycle + RKReachabilityNetworkStatus status = [client.reachabilityObserver networkStatus]; + assertThatInt(status, isNot(equalToInt(RKReachabilityIndeterminate))); + [client release]; } + - (void)testShouldSetTheCachePolicyOfTheRequest { RKClient* client = [RKClient clientWithBaseURLString:@"http://restkit.org"]; client.cachePolicy = RKRequestCachePolicyLoadIfOffline; @@ -52,7 +63,7 @@ - (void)testShouldInitializeTheCacheOfTheRequest { RKClient* client = [RKClient clientWithBaseURLString:@"http://restkit.org"]; client.requestCache = [[[RKRequestCache alloc] init] autorelease]; RKRequest* request = [client requestWithResourcePath:@""]; - assertThat(request.cache, is(equalTo(client.requestCache))); + assertThat(request.cache, is(equalTo(client.requestCache))); } - (void)testShouldLoadPageWithNoContentTypeInformation { @@ -146,4 +157,15 @@ - (void)testShouldAllowYouToChangeTheCacheTimeoutInterval { assertThatFloat(request.cacheTimeoutInterval, is(equalToFloat(20.0))); } +- (void)testThatRunLoopModePropertyRespected { + NSString * const dummyRunLoopMode = @"dummyRunLoopMode"; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + RKClient *client = [RKTestFactory client]; + client.runLoopMode = dummyRunLoopMode; + [client get:[[RKTestFactory baseURL] absoluteString] delegate:loader]; + while ([[NSRunLoop currentRunLoop] runMode:dummyRunLoopMode beforeDate:[[NSRunLoop currentRunLoop] limitDateForMode:dummyRunLoopMode]]) + ; + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); +} + @end diff --git a/Tests/Logic/Network/RKOAuthClientTest.m b/Tests/Logic/Network/RKOAuthClientTest.m index b7325b0b37..ba88dd12cb 100644 --- a/Tests/Logic/Network/RKOAuthClientTest.m +++ b/Tests/Logic/Network/RKOAuthClientTest.m @@ -19,6 +19,13 @@ // #import "RKTestEnvironment.h" +#import "RKPortCheck.h" + +#define RKOAuthClientTestSkipWithoutMongoDB() \ + if (! [self isMongoRunning]) { \ + NSLog(@"!! Skipping OAuth Test: MongoDB not running"); \ + return; \ + } @interface RKOAuthClientTest : RKTestCase @@ -26,17 +33,31 @@ @interface RKOAuthClientTest : RKTestCase @implementation RKOAuthClientTest +- (BOOL)isMongoRunning { + static RKPortCheck *portCheck = nil; + if (! portCheck) { + portCheck = [[RKPortCheck alloc] initWithHost:@"localhost" port:27017]; + [portCheck run]; + } + + return [portCheck isOpen]; +} + - (void)testShouldGetAccessToken{ + RKOAuthClientTestSkipWithoutMongoDB(); + RKTestResponseLoader *loader = [RKTestResponseLoader responseLoader]; RKOAuthClient *client = RKTestNewOAuthClient(loader); - client.authorizationCode = @"1234"; + client.authorizationCode = @"4fa8182d7184797dd5000002"; client.callbackURL = @"http://someURL.com"; [client validateAuthorizationCode]; [loader waitForResponse]; assertThatBool(loader.wasSuccessful, is(equalToBool(YES))); } -- (void)testShouldNotGetAccessToken{ +- (void)testShouldNotGetAccessToken { + RKOAuthClientTestSkipWithoutMongoDB(); + RKTestResponseLoader *loader = [RKTestResponseLoader responseLoader]; RKOAuthClient *client = RKTestNewOAuthClient(loader); client.authorizationCode = @"someInvalidAuthorizationCode"; @@ -45,13 +66,15 @@ - (void)testShouldNotGetAccessToken{ [loader waitForResponse]; assertThatBool(loader.wasSuccessful, is(equalToBool(NO))); - } + - (void)testShouldGetProtectedResource{ + RKOAuthClientTestSkipWithoutMongoDB(); + //TODO: Encapsulate this code in a correct manner RKTestResponseLoader *loader = [RKTestResponseLoader responseLoader]; RKOAuthClient *client = RKTestNewOAuthClient(loader); - client.authorizationCode = @"1234"; + client.authorizationCode = @"4fa8182d7184797dd5000002"; client.callbackURL = @"http://someURL.com"; [client validateAuthorizationCode]; @@ -59,7 +82,7 @@ - (void)testShouldGetProtectedResource{ RKClient *requestClient = [RKClient clientWithBaseURLString:[client authorizationURL]]; requestClient.OAuth2AccessToken = client.accessToken; requestClient.authenticationType = RKRequestAuthenticationTypeOAuth2; - RKRequest *request = [requestClient requestWithResourcePath:@"/me"]; + RKRequest *request = [requestClient requestWithResourcePath:@"/oauth2/pregen/me"]; request.delegate = resourceLoader; [request send]; [resourceLoader waitForResponse]; diff --git a/Tests/Logic/Network/RKParamsAttachmentTest.m b/Tests/Logic/Network/RKParamsAttachmentTest.m index 85c952335a..c2639143b1 100644 --- a/Tests/Logic/Network/RKParamsAttachmentTest.m +++ b/Tests/Logic/Network/RKParamsAttachmentTest.m @@ -30,14 +30,14 @@ @interface RKParamsAttachmentTest : RKTestCase { @implementation RKParamsAttachmentTest - (void)testShouldRaiseAnExceptionWhenTheAttachedFileDoesNotExist { - NSException* exception = nil; - @try { - [[RKParamsAttachment alloc] initWithName:@"woot" file:@"/this/is/an/invalid/path"]; - } - @catch (NSException* e) { - exception = e; - } - assertThat(exception, isNot(nilValue())); + NSException* exception = nil; + @try { + [[RKParamsAttachment alloc] initWithName:@"woot" file:@"/this/is/an/invalid/path"]; + } + @catch (NSException* e) { + exception = e; + } + assertThat(exception, isNot(nilValue())); } - (void)testShouldReturnAnMD5ForSimpleValues { @@ -51,8 +51,7 @@ - (void)testShouldReturnAnMD5ForNSData { } - (void)testShouldReturnAnMD5ForFiles { - NSBundle *testBundle = [NSBundle bundleWithIdentifier:@"org.restkit.unit-tests"]; - NSString *filePath = [testBundle pathForResource:@"blake" ofType:@"png"]; + NSString *filePath = [RKTestFixture pathForFixture:@"blake.png"]; RKParamsAttachment *attachment = [[[RKParamsAttachment alloc] initWithName:@"foo" file:filePath] autorelease]; assertThat([attachment MD5], is(equalTo(@"db6cb9d879b58e7e15a595632af345cd"))); } diff --git a/Tests/Logic/Network/RKParamsTest.m b/Tests/Logic/Network/RKParamsTest.m index 6d09fc4707..d29699c3bf 100644 --- a/Tests/Logic/Network/RKParamsTest.m +++ b/Tests/Logic/Network/RKParamsTest.m @@ -49,9 +49,7 @@ - (void)testShouldUploadFilesViaRKParams { [params setValue:@"two" forParam:@"value"]; [params setValue:@"three" forParam:@"value"]; [params setValue:@"four" forParam:@"value"]; - NSBundle *testBundle = [NSBundle bundleWithIdentifier:@"org.restkit.unit-tests"]; - NSString *imagePath = [testBundle pathForResource:@"blake" ofType:@"png"]; - NSData *data = [NSData dataWithContentsOfFile:imagePath]; + NSData *data = [RKTestFixture dataWithContentsOfFixture:@"blake.png"]; [params setData:data MIMEType:@"image/png" forParam:@"file"]; RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; [client post:@"/upload" params:params delegate:responseLoader]; @@ -65,9 +63,7 @@ - (void)testShouldUploadFilesViaRKParamsWithMixedTypes { NSNumber* idTema = [NSNumber numberWithInt:1234]; NSString* titulo = @"whatever"; NSString* texto = @"more text"; - NSBundle *testBundle = [NSBundle bundleWithIdentifier:@"org.restkit.unit-tests"]; - NSString *imagePath = [testBundle pathForResource:@"blake" ofType:@"png"]; - NSData *data = [NSData dataWithContentsOfFile:imagePath]; + NSData *data = [RKTestFixture dataWithContentsOfFixture:@"blake.png"]; NSNumber* cel = [NSNumber numberWithFloat:1.232442]; NSNumber* lon = [NSNumber numberWithFloat:18231.232442];; NSNumber* lat = [NSNumber numberWithFloat:13213123.232442];; @@ -108,9 +104,7 @@ - (void)testShouldProperlyCalculateContentLengthForFileUploads { [params setValue:@"two" forParam:@"value"]; [params setValue:@"three" forParam:@"value"]; [params setValue:@"four" forParam:@"value"]; - NSBundle *testBundle = [NSBundle bundleWithIdentifier:@"org.restkit.unit-tests"]; - NSString *imagePath = [testBundle pathForResource:@"blake" ofType:@"png"]; - NSData *data = [NSData dataWithContentsOfFile:imagePath]; + NSData *data = [RKTestFixture dataWithContentsOfFixture:@"blake.png"]; [params setData:data MIMEType:@"image/png" forParam:@"file"]; RKRequest *request = [client requestWithResourcePath:@"/upload"]; [request setMethod:RKRequestMethodPOST]; diff --git a/Tests/Logic/Network/RKRequestQueueTest.m b/Tests/Logic/Network/RKRequestQueueTest.m index 6bba5b1c43..89c7613f05 100644 --- a/Tests/Logic/Network/RKRequestQueueTest.m +++ b/Tests/Logic/Network/RKRequestQueueTest.m @@ -256,4 +256,27 @@ - (void)testShouldRemoveItemsFromTheQueueWithAnUnmappableResponse { assertThatUnsignedInteger(queue.loadingCount, is(equalToInt(0))); } +- (void)testThatSendingRequestToInvalidURLDoesNotGetSentTwice { + RKRequestQueue *queue = [RKRequestQueue requestQueue]; + NSURL *URL = [NSURL URLWithString:@"http://localhost:7662/RKRequestQueueExample"]; + RKRequest *request = [RKRequest requestWithURL:URL]; + RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + id mockResponseLoader = [OCMockObject partialMockForObject:responseLoader]; + [[[mockResponseLoader expect] andForwardToRealObject] request:request didFailLoadWithError:OCMOCK_ANY]; + request.delegate = responseLoader; + id mockQueueDelegate = [OCMockObject niceMockForProtocol:@protocol(RKRequestQueueDelegate)]; + __block NSUInteger invocationCount = 0; + [[mockQueueDelegate stub] requestQueue:queue willSendRequest:[OCMArg checkWithBlock:^BOOL(id request) { + invocationCount++; + return YES; + }]]; + [queue addRequest:request]; + queue.delegate = mockQueueDelegate; + [queue start]; + [mockResponseLoader waitForResponse]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; + [mockResponseLoader verify]; + assertThatInteger(invocationCount, is(equalToInteger(1))); +} + @end diff --git a/Tests/Logic/Network/RKRequestTest.m b/Tests/Logic/Network/RKRequestTest.m index e419fcb4b7..f039a97ee5 100644 --- a/Tests/Logic/Network/RKRequestTest.m +++ b/Tests/Logic/Network/RKRequestTest.m @@ -40,12 +40,16 @@ @implementation RKRequestTest - (void)setUp { [RKTestFactory setUp]; - + // Clear the cache directory [RKTestFactory clearCacheDirectory]; _methodInvocationCounter = 0; } +- (void)tearDown { + [RKTestFactory tearDown]; +} + - (int)incrementMethodInvocationCounter { return _methodInvocationCounter++; } @@ -55,18 +59,17 @@ - (int)incrementMethodInvocationCounter { * `ruby Tests/server.rb` */ - (void)testShouldSendMultiPartRequests { - NSString* URLString = [NSString stringWithFormat:@"http://127.0.0.1:4567/photo"]; - NSURL* URL = [NSURL URLWithString:URLString]; - RKRequest* request = [[RKRequest alloc] initWithURL:URL]; - RKParams* params = [[RKParams params] retain]; - NSBundle *testBundle = [NSBundle bundleWithIdentifier:@"org.restkit.unit-tests"]; - NSString* filePath = [testBundle pathForResource:@"blake" ofType:@"png"]; - [params setFile:filePath forParam:@"file"]; - [params setValue:@"this is the value" forParam:@"test"]; - request.method = RKRequestMethodPOST; - request.params = params; - RKResponse* response = [request sendSynchronously]; - assertThatInteger(response.statusCode, is(equalToInt(200))); + NSString* URLString = [NSString stringWithFormat:@"http://127.0.0.1:4567/photo"]; + NSURL* URL = [NSURL URLWithString:URLString]; + RKRequest* request = [[RKRequest alloc] initWithURL:URL]; + RKParams* params = [[RKParams params] retain]; + NSString* filePath = [RKTestFixture pathForFixture:@"blake.png"]; + [params setFile:filePath forParam:@"file"]; + [params setValue:@"this is the value" forParam:@"test"]; + request.method = RKRequestMethodPOST; + request.params = params; + RKResponse* response = [request sendSynchronously]; + assertThatInteger(response.statusCode, is(equalToInt(200))); } #pragma mark - Basics @@ -147,13 +150,26 @@ - (void)testThatSendingDataInvalidatesTimeoutTimer { [request release]; } +- (void)testThatRunLoopModePropertyRespected { + NSString * const dummyRunLoopMode = @"dummyRunLoopMode"; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + RKRequest *request = [[RKRequest alloc] initWithURL:[RKTestFactory baseURL]]; + request.delegate = loader; + request.runLoopMode = dummyRunLoopMode; + [request sendAsynchronously]; + while ([[NSRunLoop currentRunLoop] runMode:dummyRunLoopMode beforeDate:[[NSRunLoop currentRunLoop] limitDateForMode:dummyRunLoopMode]]) + ; + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); + [request release]; +} + #pragma mark - Background Policies #if TARGET_OS_IPHONE - (void)testShouldSendTheRequestWhenBackgroundPolicyIsRKRequestBackgroundPolicyNone { - NSURL* URL = [RKTestFactory baseURL]; - RKRequest* request = [[RKRequest alloc] initWithURL:URL]; + NSURL* URL = [RKTestFactory baseURL]; + RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.backgroundPolicy = RKRequestBackgroundPolicyNone; id requestMock = [OCMockObject partialMockForObject:request]; [[requestMock expect] fireAsynchronousRequest]; // Not sure what else to test on this case @@ -175,7 +191,6 @@ - (void)stubSharedApplicationWhileExecutingBlock:(void (^)(void))block { } - (void)testShouldObserveForAppBackgroundTransitionsAndCancelTheRequestWhenBackgroundPolicyIsRKRequestBackgroundPolicyCancel { - [RKTestFactory client]; [self stubSharedApplicationWhileExecutingBlock:^{ NSURL* URL = [RKTestFactory baseURL]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; @@ -185,7 +200,6 @@ - (void)testShouldObserveForAppBackgroundTransitionsAndCancelTheRequestWhenBackg [requestMock sendAsynchronously]; [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidEnterBackgroundNotification object:nil]; [requestMock verify]; - [request release]; }]; } @@ -306,9 +320,9 @@ - (void)testShouldSendTheRequestWhenTheCachePolicyIsNone { - (void)testShouldCacheTheRequestHeadersAndBodyIncludingOurOwnCustomTimestampHeader { NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; @@ -332,9 +346,9 @@ - (void)testShouldCacheTheRequestHeadersAndBodyIncludingOurOwnCustomTimestampHea - (void)testShouldGenerateAUniqueCacheKeyBasedOnTheUrlTheMethodAndTheHTTPBody { NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; @@ -357,9 +371,9 @@ - (void)testShouldGenerateAUniqueCacheKeyBasedOnTheUrlTheMethodAndTheHTTPBody { - (void)testShouldLoadFromCacheWhenWeRecieveA304 { NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; @@ -397,9 +411,9 @@ - (void)testShouldLoadFromCacheWhenWeRecieveA304 { - (void)testShouldUpdateTheInternalCacheDateWhenWeRecieveA304 { NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; @@ -444,9 +458,9 @@ - (void)testShouldUpdateTheInternalCacheDateWhenWeRecieveA304 { - (void)testShouldLoadFromTheCacheIfThereIsAnError { NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; @@ -482,9 +496,9 @@ - (void)testShouldLoadFromTheCacheIfThereIsAnError { - (void)testShouldLoadFromTheCacheIfWeAreWithinTheTimeout { NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; @@ -546,9 +560,9 @@ - (void)testShouldLoadFromTheCacheIfWeAreWithinTheTimeout { - (void)testShouldLoadFromTheCacheIfWeAreOffline { NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; @@ -589,9 +603,9 @@ - (void)testShouldLoadFromTheCacheIfWeAreOffline { - (void)testShouldCacheTheStatusCodeMIMETypeAndURL { NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; @@ -707,14 +721,10 @@ - (void)testShouldNotRaiseAnExceptionWhenAttemptingToMutateResourcePathOnAnNSURL - (void)testShouldOptionallySkipSSLValidation { NSURL *URL = [NSURL URLWithString:@"https://blakewatters.com/"]; - RKTestResponseLoader *loader = [RKTestResponseLoader responseLoader]; RKRequest *request = [RKRequest requestWithURL:URL]; - [[RKClient sharedClient] configureRequest:request]; - request.delegate = loader; request.disableCertificateValidation = YES; - [request send]; - [loader waitForResponse]; - assertThatBool([loader.response isOK], is(equalToBool(YES))); + RKResponse *response = [request sendSynchronously]; + assertThatBool([response isOK], is(equalToBool(YES))); } - (void)testShouldNotAddANonZeroContentLengthHeaderIfParamsIsSetAndThisIsAGETRequest { @@ -781,9 +791,9 @@ - (void)testShouldReturnACachePathWhenTheRequestIsUsingRKParams { request.params = params; NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; + [[NSURL URLWithString:baseURL] host]]; NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; + stringByAppendingPathComponent:cacheDirForClient]; RKRequestCache *requestCache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyForDurationOfSession]; NSString *requestCachePath = [requestCache pathForRequest:request]; NSArray *pathComponents = [requestCachePath pathComponents]; @@ -799,9 +809,9 @@ - (void)testShouldReturnNilForCachePathWhenTheRequestIsADELETE { request.method = RKRequestMethodDELETE; NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; + [[NSURL URLWithString:baseURL] host]]; NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; + stringByAppendingPathComponent:cacheDirForClient]; RKRequestCache *requestCache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyForDurationOfSession]; NSString *requestCachePath = [requestCache pathForRequest:request]; assertThat(requestCachePath, is(nilValue())); @@ -819,6 +829,42 @@ - (void)testShouldBuildAProperAuthorizationHeaderForOAuth1 { assertThat(authorization, isNot(nilValue())); } +- (void)testShouldBuildAProperAuthorizationHeaderForOAuth1ThatIsAcceptedByServer { + RKRequest *request = [RKRequest requestWithURL:[RKURL URLWithString:[NSString stringWithFormat:@"%@/oauth1/me", [RKTestFactory baseURLString]]]]; + request.authenticationType = RKRequestAuthenticationTypeOAuth1; + request.OAuth1AccessToken = @"12345"; + request.OAuth1AccessTokenSecret = @"monkey"; + request.OAuth1ConsumerKey = @"restkit_key"; + request.OAuth1ConsumerSecret = @"restkit_secret"; + [request prepareURLRequest]; + NSString *authorization = [request.URLRequest valueForHTTPHeaderField:@"Authorization"]; + assertThat(authorization, isNot(nilValue())); + + RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + request.delegate = responseLoader; + [request sendAsynchronously]; + [responseLoader waitForResponse]; + assertThatBool(responseLoader.successful, is(equalToBool(YES))); +} + +- (void)testImproperOAuth1CredentialsShouldFall { + RKRequest *request = [RKRequest requestWithURL:[RKURL URLWithString:[NSString stringWithFormat:@"%@/oauth1/me", [RKTestFactory baseURLString]]]]; + request.authenticationType = RKRequestAuthenticationTypeOAuth1; + request.OAuth1AccessToken = @"12345"; + request.OAuth1AccessTokenSecret = @"monkey"; + request.OAuth1ConsumerKey = @"restkit_key"; + request.OAuth1ConsumerSecret = @"restkit_incorrect_secret"; + [request prepareURLRequest]; + NSString *authorization = [request.URLRequest valueForHTTPHeaderField:@"Authorization"]; + assertThat(authorization, isNot(nilValue())); + + RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + request.delegate = responseLoader; + [request sendAsynchronously]; + [responseLoader waitForResponse]; + assertThatBool(responseLoader.successful, is(equalToBool(YES))); +} + - (void)testOnDidLoadResponseBlockInvocation { RKURL *URL = [[RKTestFactory baseURL] URLByAppendingResourcePath:@"/200"]; RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; @@ -885,4 +931,71 @@ - (void)testThatDELETERequestsAreNotConsideredCacheable assertThatBool([request isCacheable], is(equalToBool(NO))); } +- (void)testInvocationOfDidReceiveResponse { + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + id loaderMock = [OCMockObject partialMockForObject:loader]; + NSURL* URL = [[RKTestFactory baseURL] URLByAppendingResourcePath:@"/200"]; + RKRequest* request = [[RKRequest alloc] initWithURL:URL]; + request.delegate = loaderMock; + [[loaderMock expect] request:request didReceiveResponse:OCMOCK_ANY]; + [request sendAsynchronously]; + [loaderMock waitForResponse]; + [request release]; + [loaderMock verify]; +} + +- (void)testThatIsLoadingIsNoDuringDidFailWithErrorCallback { + NSURL *URL = [[NSURL alloc] initWithString:@"http://localhost:8765"]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + + RKClient *client = [RKClient clientWithBaseURL:URL]; + RKRequest *request = [client requestWithResourcePath:@"/invalid"]; + request.method = RKRequestMethodGET; + request.delegate = loader; + request.onDidFailLoadWithError = ^(NSError *error) { + assertThatBool([request isLoading], is(equalToBool(NO))); + }; + [request sendAsynchronously]; + [loader waitForResponse]; +} + +- (void)testThatIsLoadedIsYesDuringDidFailWithErrorCallback { + NSURL *URL = [[NSURL alloc] initWithString:@"http://localhost:8765"]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + + RKClient *client = [RKClient clientWithBaseURL:URL]; + RKRequest *request = [client requestWithResourcePath:@"/invalid"]; + request.method = RKRequestMethodGET; + request.delegate = loader; + request.onDidFailLoadWithError = ^(NSError *error) { + assertThatBool([request isLoaded], is(equalToBool(YES))); + }; + [request sendAsynchronously]; + [loader waitForResponse]; +} + +- (void)testUnavailabilityOfResponseInDidFailWithErrorCallback { + NSURL *URL = [[NSURL alloc] initWithString:@"http://localhost:8765"]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + + RKClient *client = [RKClient clientWithBaseURL:URL]; + RKRequest *request = [client requestWithResourcePath:@"/invalid"]; + request.method = RKRequestMethodGET; + request.delegate = loader; + [request sendAsynchronously]; + [loader waitForResponse]; + assertThat(request.response, is(nilValue())); +} + +- (void)testAvailabilityOfResponseWhenFailedDueTo500Response { + RKURL *URL = [[RKTestFactory baseURL] URLByAppendingResourcePath:@"/fail"]; + RKRequest *request = [RKRequest requestWithURL:URL]; + RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + request.delegate = responseLoader; + [request sendAsynchronously]; + [responseLoader waitForResponse]; + assertThat(request.response, is(notNilValue())); + assertThatInteger(request.response.statusCode, is(equalToInteger(500))); +} + @end diff --git a/Tests/Logic/Network/RKResponseTest.m b/Tests/Logic/Network/RKResponseTest.m index 60c2c35dcc..0ba76ba11e 100644 --- a/Tests/Logic/Network/RKResponseTest.m +++ b/Tests/Logic/Network/RKResponseTest.m @@ -22,7 +22,7 @@ #import "RKResponse.h" @interface RKResponseTest : RKTestCase { - RKResponse* _response; + RKResponse* _response; } @end @@ -30,199 +30,199 @@ @interface RKResponseTest : RKTestCase { @implementation RKResponseTest - (void)setUp { - _response = [[RKResponse alloc] init]; + _response = [[RKResponse alloc] init]; } - (void)testShouldConsiderResponsesLessThanOneHudredOrGreaterThanSixHundredInvalid { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 99; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isInvalid], is(equalToBool(YES))); - statusCode = 601; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isInvalid], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 99; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isInvalid], is(equalToBool(YES))); + statusCode = 601; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isInvalid], is(equalToBool(YES))); } - (void)testShouldConsiderResponsesInTheOneHudredsInformational { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 100; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isInformational], is(equalToBool(YES))); - statusCode = 199; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isInformational], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 100; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isInformational], is(equalToBool(YES))); + statusCode = 199; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isInformational], is(equalToBool(YES))); } - (void)testShouldConsiderResponsesInTheTwoHundredsSuccessful { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger twoHundred = 200; - [[[mock stub] andReturnValue:OCMOCK_VALUE(twoHundred)] statusCode]; - assertThatBool([mock isSuccessful], is(equalToBool(YES))); - twoHundred = 299; - [[[mock stub] andReturnValue:OCMOCK_VALUE(twoHundred)] statusCode]; - assertThatBool([mock isSuccessful], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger twoHundred = 200; + [[[mock stub] andReturnValue:OCMOCK_VALUE(twoHundred)] statusCode]; + assertThatBool([mock isSuccessful], is(equalToBool(YES))); + twoHundred = 299; + [[[mock stub] andReturnValue:OCMOCK_VALUE(twoHundred)] statusCode]; + assertThatBool([mock isSuccessful], is(equalToBool(YES))); } - (void)testShouldConsiderResponsesInTheThreeHundredsRedirects { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 300; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isRedirection], is(equalToBool(YES))); - statusCode = 399; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isRedirection], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 300; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isRedirection], is(equalToBool(YES))); + statusCode = 399; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isRedirection], is(equalToBool(YES))); } - (void)testShouldConsiderResponsesInTheFourHundredsClientErrors { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 400; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isClientError], is(equalToBool(YES))); - statusCode = 499; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isClientError], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 400; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isClientError], is(equalToBool(YES))); + statusCode = 499; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isClientError], is(equalToBool(YES))); } - (void)testShouldConsiderResponsesInTheFiveHundredsServerErrors { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 500; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isServerError], is(equalToBool(YES))); - statusCode = 599; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isServerError], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 500; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isServerError], is(equalToBool(YES))); + statusCode = 599; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isServerError], is(equalToBool(YES))); } - (void)testShouldConsiderATwoHundredResponseOK { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 200; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isOK], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 200; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isOK], is(equalToBool(YES))); } - (void)testShouldConsiderATwoHundredAndOneResponseCreated { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 201; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isCreated], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 201; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isCreated], is(equalToBool(YES))); } - (void)testShouldConsiderAFourOhThreeResponseForbidden { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 403; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isForbidden], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 403; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isForbidden], is(equalToBool(YES))); } - (void)testShouldConsiderAFourOhFourResponseNotFound { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 404; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isNotFound], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 404; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isNotFound], is(equalToBool(YES))); } - (void)testShouldConsiderAFourOhNineResponseConflict { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 409; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isConflict], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 409; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isConflict], is(equalToBool(YES))); } - (void)testShouldConsiderAFourHundredAndTenResponseConflict { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 410; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isGone], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 410; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isGone], is(equalToBool(YES))); } - (void)testShouldConsiderVariousThreeHundredResponsesRedirect { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 301; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isRedirect], is(equalToBool(YES))); - statusCode = 302; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isRedirect], is(equalToBool(YES))); - statusCode = 303; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isRedirect], is(equalToBool(YES))); - statusCode = 307; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isRedirect], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 301; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isRedirect], is(equalToBool(YES))); + statusCode = 302; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isRedirect], is(equalToBool(YES))); + statusCode = 303; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isRedirect], is(equalToBool(YES))); + statusCode = 307; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isRedirect], is(equalToBool(YES))); } - (void)testShouldConsiderVariousResponsesEmpty { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSInteger statusCode = 201; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isEmpty], is(equalToBool(YES))); - statusCode = 204; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isEmpty], is(equalToBool(YES))); - statusCode = 304; - [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; - assertThatBool([mock isEmpty], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSInteger statusCode = 201; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isEmpty], is(equalToBool(YES))); + statusCode = 204; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isEmpty], is(equalToBool(YES))); + statusCode = 304; + [[[mock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; + assertThatBool([mock isEmpty], is(equalToBool(YES))); } - (void)testShouldMakeTheContentTypeHeaderAccessible { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSDictionary* headers = [NSDictionary dictionaryWithObject:@"application/xml" forKey:@"Content-Type"]; - [[[mock stub] andReturn:headers] allHeaderFields]; - assertThat([mock contentType], is(equalTo(@"application/xml"))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSDictionary* headers = [NSDictionary dictionaryWithObject:@"application/xml" forKey:@"Content-Type"]; + [[[mock stub] andReturn:headers] allHeaderFields]; + assertThat([mock contentType], is(equalTo(@"application/xml"))); } // Should this return a string??? - (void)testShouldMakeTheContentLengthHeaderAccessible { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSDictionary* headers = [NSDictionary dictionaryWithObject:@"12345" forKey:@"Content-Length"]; - [[[mock stub] andReturn:headers] allHeaderFields]; - assertThat([mock contentLength], is(equalTo(@"12345"))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSDictionary* headers = [NSDictionary dictionaryWithObject:@"12345" forKey:@"Content-Length"]; + [[[mock stub] andReturn:headers] allHeaderFields]; + assertThat([mock contentLength], is(equalTo(@"12345"))); } - (void)testShouldMakeTheLocationHeaderAccessible { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSDictionary* headers = [NSDictionary dictionaryWithObject:@"/foo/bar" forKey:@"Location"]; - [[[mock stub] andReturn:headers] allHeaderFields]; - assertThat([mock location], is(equalTo(@"/foo/bar"))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSDictionary* headers = [NSDictionary dictionaryWithObject:@"/foo/bar" forKey:@"Location"]; + [[[mock stub] andReturn:headers] allHeaderFields]; + assertThat([mock location], is(equalTo(@"/foo/bar"))); } - (void)testShouldKnowIfItIsAnXMLResponse { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSDictionary* headers = [NSDictionary dictionaryWithObject:@"application/xml" forKey:@"Content-Type"]; - [[[mock stub] andReturn:headers] allHeaderFields]; - assertThatBool([mock isXML], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSDictionary* headers = [NSDictionary dictionaryWithObject:@"application/xml" forKey:@"Content-Type"]; + [[[mock stub] andReturn:headers] allHeaderFields]; + assertThatBool([mock isXML], is(equalToBool(YES))); } - (void)testShouldKnowIfItIsAnJSONResponse { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - NSDictionary* headers = [NSDictionary dictionaryWithObject:@"application/json" forKey:@"Content-Type"]; - [[[mock stub] andReturn:headers] allHeaderFields]; - assertThatBool([mock isJSON], is(equalToBool(YES))); + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + id mock = [OCMockObject partialMockForObject:response]; + NSDictionary* headers = [NSDictionary dictionaryWithObject:@"application/json" forKey:@"Content-Type"]; + [[[mock stub] andReturn:headers] allHeaderFields]; + assertThatBool([mock isJSON], is(equalToBool(YES))); } - (void)testShouldReturnParseErrorsWhenParsedBodyFails { RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mock = [OCMockObject partialMockForObject:response]; - [[[mock stub] andReturn:@"sad;sdvjnk;"] bodyAsString]; + id mock = [OCMockObject partialMockForObject:response]; + [[[mock stub] andReturn:@"sad;sdvjnk;"] bodyAsString]; [[[mock stub] andReturn:@"application/json"] MIMEType]; NSError* error = nil; id object = [mock parsedBody:&error]; @@ -243,8 +243,8 @@ - (void)testShouldNotCrashOnFailureToParseBody { - (void)testShouldNotCrashWhenParserReturnsNilWithoutAnError { RKResponse* response = [[[RKResponse alloc] init] autorelease]; - id mockResponse = [OCMockObject partialMockForObject:response]; - [[[mockResponse stub] andReturn:@""] bodyAsString]; + id mockResponse = [OCMockObject partialMockForObject:response]; + [[[mockResponse stub] andReturn:@""] bodyAsString]; [[[mockResponse stub] andReturn:RKMIMETypeJSON] MIMEType]; id mockParser = [OCMockObject mockForProtocol:@protocol(RKParser)]; id mockRegistry = [OCMockObject partialMockForObject:[RKParserRegistry sharedRegistry]]; @@ -268,4 +268,44 @@ - (void)testLoadingNonUTF8Charset { assertThatInteger([loader.response bodyEncoding], is(equalToInteger(NSASCIIStringEncoding))); } +- (void)testFollowRedirect { + RKClient* client = [RKTestFactory client]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + [client get:@"/redirection" delegate:loader]; + [loader waitForResponse]; + assertThatInteger(loader.response.statusCode, is(equalToInteger(200))); + + id body = [loader.response parsedBody:NULL]; + assertThat([body objectForKey:@"redirected"], is(equalTo([NSNumber numberWithBool:YES]))); +} + +- (void)testNoFollowRedirect { + RKClient* client = [RKTestFactory client]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + + RKRequest* request = [client requestWithResourcePath:@"/redirection"]; + request.method = RKRequestMethodGET; + request.followRedirect = NO; + request.delegate = loader; + + [request send]; + [loader waitForResponse]; + + assertThatInteger(loader.response.statusCode, is(equalToInteger(302))); + assertThat([loader.response.allHeaderFields objectForKey:@"Location"], is(equalTo(@"/redirection/target"))); +} + +- (void)testThatLoadingInvalidURLDoesNotCrashApp { + NSURL *URL = [[NSURL alloc] initWithString:@"http://localhost:5629"]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + RKClient *client = [RKClient clientWithBaseURL:URL]; + + RKRequest *request = [client requestWithResourcePath:@"/invalid"]; + request.method = RKRequestMethodGET; + request.delegate = loader; + + [request sendAsynchronously]; + [loader waitForResponse]; +} + @end diff --git a/Tests/Logic/ObjectMapping/RKObjectLoaderTest.m b/Tests/Logic/ObjectMapping/RKObjectLoaderTest.m index ee76eaf213..9c39c19a8d 100644 --- a/Tests/Logic/ObjectMapping/RKObjectLoaderTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectLoaderTest.m @@ -90,6 +90,14 @@ @interface RKObjectLoaderTest : RKTestCase { @implementation RKObjectLoaderTest +- (void)setUp { + [RKTestFactory setUp]; +} + +- (void)tearDown { + [RKTestFactory tearDown]; +} + - (RKObjectMappingProvider*)providerForComplexUser { RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; @@ -109,18 +117,16 @@ - (RKObjectMappingProvider*)errorMappingProvider { - (void)testShouldHandleTheErrorCaseAppropriately { RKObjectManager* objectManager = [RKTestFactory objectManager]; + objectManager.mappingProvider = [self errorMappingProvider]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/errors.json"]; objectLoader.delegate = responseLoader; objectLoader.method = RKRequestMethodGET; - - [objectManager setMappingProvider:[self errorMappingProvider]]; - [objectLoader sendAsynchronously]; [responseLoader waitForResponse]; assertThat(responseLoader.error, isNot(nilValue())); - assertThat([responseLoader.error localizedDescription], is(equalTo(@"error1, error2"))); NSArray* objects = [[responseLoader.error userInfo] objectForKey:RKObjectMapperErrorObjectsKey]; diff --git a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m index cf1801d218..b73435fd34 100644 --- a/Tests/Logic/ObjectMapping/RKObjectManagerTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m @@ -29,7 +29,7 @@ #import "RKObjectMapperTestModel.h" @interface RKObjectManagerTest : RKTestCase { - RKObjectManager* _objectManager; + RKObjectManager* _objectManager; } @end @@ -37,10 +37,12 @@ @interface RKObjectManagerTest : RKTestCase { @implementation RKObjectManagerTest - (void)setUp { + [RKTestFactory setUp]; + _objectManager = [RKTestFactory objectManager]; - _objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"RKTests.sqlite"]; + _objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"RKTests.sqlite"]; [RKObjectManager setSharedManager:_objectManager]; - [_objectManager.objectStore deletePersistantStore]; + [_objectManager.objectStore deletePersistentStore]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; @@ -80,8 +82,13 @@ - (void)setUp { _objectManager.router = router; } +- (void)tearDown { + [RKTestFactory tearDown]; +} + - (void)testShouldSetTheAcceptHeaderAppropriatelyForTheFormat { - assertThat([_objectManager.client.HTTPHeaders valueForKey:@"Accept"], is(equalTo(@"application/json"))); + + assertThat([_objectManager.client.HTTPHeaders valueForKey:@"Accept"], is(equalTo(@"application/json"))); } // TODO: Move to Core Data specific spec file... @@ -117,7 +124,7 @@ - (void)testShouldDeleteACoreDataBackedTargetObjectOnError { objectLoader.method = RKRequestMethodPOST; objectLoader.targetObject = temporaryHuman; objectLoader.serializationMapping = mapping; - [objectLoader send]; + [objectLoader send]; [loader waitForResponse]; assertThat(temporaryHuman.managedObjectContext, is(equalTo(nil))); @@ -139,7 +146,7 @@ - (void)testShouldNotDeleteACoreDataBackedTargetObjectOnErrorIfItWasAlreadySaved objectLoader.method = RKRequestMethodPOST; objectLoader.targetObject = temporaryHuman; objectLoader.serializationMapping = mapping; - [objectLoader send]; + [objectLoader send]; [loader waitForResponse]; assertThat(temporaryHuman.managedObjectContext, is(equalTo(_objectManager.objectStore.primaryManagedObjectContext))); @@ -149,31 +156,31 @@ - (void)testShouldNotDeleteACoreDataBackedTargetObjectOnErrorIfItWasAlreadySaved - (void)testShouldLoadAHuman { assertThatBool([RKClient sharedClient].isNetworkReachable, is(equalToBool(YES))); RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; - [_objectManager loadObjectsAtResourcePath:@"/JSON/humans/1.json" delegate:loader]; - [loader waitForResponse]; + [_objectManager loadObjectsAtResourcePath:@"/JSON/humans/1.json" delegate:loader]; + [loader waitForResponse]; assertThat(loader.error, is(nilValue())); assertThat(loader.objects, isNot(empty())); - RKHuman* blake = (RKHuman*)[loader.objects objectAtIndex:0]; - assertThat(blake.name, is(equalTo(@"Blake Watters"))); + RKHuman* blake = (RKHuman*)[loader.objects objectAtIndex:0]; + assertThat(blake.name, is(equalTo(@"Blake Watters"))); } - (void)testShouldLoadAllHumans { RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; - [_objectManager loadObjectsAtResourcePath:@"/JSON/humans/all.json" delegate:loader]; - [loader waitForResponse]; - NSArray* humans = (NSArray*) loader.objects; - assertThatUnsignedInteger([humans count], is(equalToInt(2))); - assertThat([humans objectAtIndex:0], is(instanceOf([RKHuman class]))); + [_objectManager loadObjectsAtResourcePath:@"/JSON/humans/all.json" delegate:loader]; + [loader waitForResponse]; + NSArray* humans = (NSArray*) loader.objects; + assertThatUnsignedInteger([humans count], is(equalToInt(2))); + assertThat([humans objectAtIndex:0], is(instanceOf([RKHuman class]))); } - (void)testShouldHandleConnectionFailures { - NSString* localBaseURL = [NSString stringWithFormat:@"http://127.0.0.1:3001"]; - RKObjectManager* modelManager = [RKObjectManager managerWithBaseURLString:localBaseURL]; + NSString* localBaseURL = [NSString stringWithFormat:@"http://127.0.0.1:3001"]; + RKObjectManager* modelManager = [RKObjectManager managerWithBaseURLString:localBaseURL]; modelManager.client.requestQueue.suspended = NO; RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; - [modelManager loadObjectsAtResourcePath:@"/JSON/humans/1" delegate:loader]; - [loader waitForResponse]; - assertThatBool(loader.wasSuccessful, is(equalToBool(NO))); + [modelManager loadObjectsAtResourcePath:@"/JSON/humans/1" delegate:loader]; + [loader waitForResponse]; + assertThatBool(loader.wasSuccessful, is(equalToBool(NO))); } - (void)testShouldPOSTAnObject { @@ -362,7 +369,7 @@ - (void)testInitializationOfRoutedPathViaSendObjectMethodUsingBlock [objectManager.router routeClass:[RKObjectMapperTestModel class] toResourcePath:@"/human/1"]; objectManager.serializationMIMEType = RKMIMETypeJSON; RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; - + RKObjectMapperTestModel *object = [RKObjectMapperTestModel new]; [objectManager putObject:object usingBlock:^(RKObjectLoader *loader) { loader.delegate = responseLoader; @@ -370,4 +377,35 @@ - (void)testInitializationOfRoutedPathViaSendObjectMethodUsingBlock [responseLoader waitForResponse]; } +- (void)testThatInitializationOfObjectManagerInitializesNetworkStatusFromClient { + RKReachabilityObserver *observer = [[RKReachabilityObserver alloc] initWithHost:@"google.com"]; + id mockObserver = [OCMockObject partialMockForObject:observer]; + BOOL yes = YES; + [[[mockObserver stub] andReturnValue:OCMOCK_VALUE(yes)] isReachabilityDetermined]; + [[[mockObserver stub] andReturnValue:OCMOCK_VALUE(yes)] isNetworkReachable]; + RKClient *client = [RKTestFactory client]; + client.reachabilityObserver = mockObserver; + RKObjectManager *manager = [[RKObjectManager alloc] init]; + manager.client = client; + assertThatInteger(manager.networkStatus, is(equalToInteger(RKObjectManagerNetworkStatusOnline))); +} + +- (void)testThatMutationOfUnderlyingClientReachabilityObserverUpdatesManager { + RKObjectManager *manager = [RKTestFactory objectManager]; + RKReachabilityObserver *observer = [[RKReachabilityObserver alloc] initWithHost:@"google.com"]; + assertThatInteger(manager.networkStatus, is(equalToInteger(RKObjectManagerNetworkStatusOnline))); + manager.client.reachabilityObserver = observer; + assertThatInteger(manager.networkStatus, is(equalToInteger(RKObjectManagerNetworkStatusUnknown))); +} + +- (void)testThatReplacementOfUnderlyingClientUpdatesManagerReachabilityObserver { + RKObjectManager *manager = [RKTestFactory objectManager]; + RKReachabilityObserver *observer = [[RKReachabilityObserver alloc] initWithHost:@"google.com"]; + RKClient *client = [RKTestFactory client]; + client.reachabilityObserver = observer; + assertThatInteger(manager.networkStatus, is(equalToInteger(RKObjectManagerNetworkStatusOnline))); + manager.client = client; + assertThatInteger(manager.networkStatus, is(equalToInteger(RKObjectManagerNetworkStatusUnknown))); +} + @end diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m index 139a790b50..24fc769fbf 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m @@ -1131,6 +1131,44 @@ - (void)testShouldSetNilForNSNullValues { [mockUser verify]; } +- (void)testDelegateIsInformedWhenANilValueIsMappedForNSNullWithExistingValue { + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; + [mapping addAttributeMapping:nameMapping]; + + NSDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; + [dictionary setValue:[NSNull null] forKey:@"name"]; + RKTestUser* user = [RKTestUser user]; + user.name = @"Blake Watters"; + id mockDelegate = [OCMockObject mockForProtocol:@protocol(RKObjectMappingOperationDelegate)]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + operation.delegate = mockDelegate; + NSError* error = nil; + [[mockDelegate expect] objectMappingOperation:operation didFindMapping:nameMapping forKeyPath:@"name"]; + [[mockDelegate expect] objectMappingOperation:operation didSetValue:nil forKeyPath:@"name" usingMapping:nameMapping]; + [operation performMapping:&error]; + [mockDelegate verify]; +} + +- (void)testDelegateIsInformedWhenUnchangedValueIsSkipped { + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; + [mapping addAttributeMapping:nameMapping]; + + NSDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; + [dictionary setValue:@"Blake Watters" forKey:@"name"]; + RKTestUser* user = [RKTestUser user]; + user.name = @"Blake Watters"; + id mockDelegate = [OCMockObject mockForProtocol:@protocol(RKObjectMappingOperationDelegate)]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; + operation.delegate = mockDelegate; + NSError* error = nil; + [[mockDelegate expect] objectMappingOperation:operation didFindMapping:nameMapping forKeyPath:@"name"]; + [[mockDelegate expect] objectMappingOperation:operation didNotSetUnchangedValue:@"Blake Watters" forKeyPath:@"name" usingMapping:nameMapping]; + [operation performMapping:&error]; + [mockDelegate verify]; +} + - (void)testShouldOptionallySetDefaultValueForAMissingKeyPath { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; @@ -1223,10 +1261,10 @@ - (void)testShouldMapANestedObjectToOrderedSetCollection { RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; RKObjectAttributeMapping* cityMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"city" toKeyPath:@"city"]; [addressMapping addAttributeMapping:cityMapping]; - + RKObjectRelationshipMapping* hasOneMapping = [RKObjectRelationshipMapping mappingFromKeyPath:@"address" toKeyPath:@"friendsOrderedSet" withMapping:addressMapping]; [userMapping addRelationshipMapping:hasOneMapping]; - + RKObjectMapper* mapper = [RKObjectMapper new]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKTestUser* user = [RKTestUser user]; @@ -1285,10 +1323,10 @@ - (void)testShouldMapANestedArrayIntoAnOrderedSet { RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [userMapping addAttributeMapping:nameMapping]; - + RKObjectRelationshipMapping* hasManyMapping = [RKObjectRelationshipMapping mappingFromKeyPath:@"friends" toKeyPath:@"friendsOrderedSet" withMapping:userMapping]; [userMapping addRelationshipMapping:hasManyMapping]; - + RKObjectMapper* mapper = [RKObjectMapper new]; id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKTestUser* user = [RKTestUser user]; @@ -1327,6 +1365,33 @@ - (void)testShouldNotSetThePropertyWhenTheNestedObjectIsIdentical { [mapper release]; } +- (void)testSkippingOfIdenticalObjectsInformsDelegate { + RKTestUser* user = [RKTestUser user]; + RKTestAddress* address = [RKTestAddress address]; + address.addressID = [NSNumber numberWithInt:1234]; + user.address = address; + id mockUser = [OCMockObject partialMockForObject:user]; + [[mockUser reject] setAddress:OCMOCK_ANY]; + + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; + [userMapping addAttributeMapping:nameMapping]; + RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; + RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"addressID"]; + [addressMapping addAttributeMapping:idMapping]; + + RKObjectRelationshipMapping* hasOneMapping = [RKObjectRelationshipMapping mappingFromKeyPath:@"address" toKeyPath:@"address" withMapping:addressMapping]; + [userMapping addRelationshipMapping:hasOneMapping]; + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKObjectMappingOperation *operation = [RKObjectMappingOperation mappingOperationFromObject:userInfo toObject:user withMapping:userMapping]; + id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKObjectMappingOperationDelegate)]; + [[mockDelegate expect] objectMappingOperation:operation didNotSetUnchangedValue:address forKeyPath:@"address" usingMapping:hasOneMapping]; + operation.delegate = mockDelegate; + [operation performMapping:nil]; + [mockDelegate verify]; +} + - (void)testShouldNotSetThePropertyWhenTheNestedObjectCollectionIsIdentical { RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; @@ -1767,16 +1832,16 @@ - (void)testShouldLetYouAppendADateFormatterToTheList { assertThat([RKObjectMapping defaultDateFormatters], hasCountOf(4)); } -- (void)testShouldAllowNewlyAddedDateFormatterToRunFirst { +- (void)testShouldAllowNewlyAddedDateFormatterToRunFirst { [RKObjectMapping setDefaultDateFormatters:nil]; NSDateFormatter *newDateFormatter = [[NSDateFormatter new] autorelease]; [newDateFormatter setDateFormat:@"dd/MM/yyyy"]; [RKObjectMapping addDefaultDateFormatter:newDateFormatter]; - + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping *birthDateMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"favorite_date" toKeyPath:@"favoriteDate"]; [mapping addAttributeMapping:birthDateMapping]; - + NSDictionary *dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKTestUser *user = [RKTestUser user]; RKObjectMappingOperation *operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; @@ -1808,7 +1873,7 @@ - (void)testShouldReturnNilForEmptyDateValues { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* birthDateMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"birthdate" toKeyPath:@"birthDate"]; [mapping addAttributeMapping:birthDateMapping]; - + NSDictionary* dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; NSMutableDictionary *mutableDictionary = [dictionary mutableCopy]; [mutableDictionary setValue:@"" forKey:@"birthdate"]; @@ -1817,7 +1882,7 @@ - (void)testShouldReturnNilForEmptyDateValues { [mutableDictionary release]; NSError* error = nil; [operation performMapping:&error]; - + assertThat(user.birthDate, is(equalTo(nil))); } @@ -1914,20 +1979,20 @@ - (void)testUpdatingArrayOfExistingCats { humanMapping.primaryKeyAttribute = @"railsID"; RKObjectMappingProvider *provider = [RKObjectMappingProvider mappingProvider]; [provider setObjectMapping:humanMapping forKeyPath:@"human"]; - + // Create instances that should match the fixture RKHuman *human1 = [RKHuman createInContext:objectStore.primaryManagedObjectContext]; human1.railsID = [NSNumber numberWithInt:201]; RKHuman *human2 = [RKHuman createInContext:objectStore.primaryManagedObjectContext]; human2.railsID = [NSNumber numberWithInt:202]; [objectStore save:nil]; - + RKObjectMapper *mapper = [RKObjectMapper mapperWithObject:array mappingProvider:provider]; RKObjectMappingResult *result = [mapper performMapping]; assertThat(result, is(notNilValue())); - + NSArray *humans = [result asCollection]; - assertThat(humans, hasCountOf(2)); + assertThat(humans, hasCountOf(2)); assertThat([humans objectAtIndex:0], is(equalTo(human1))); assertThat([humans objectAtIndex:1], is(equalTo(human2))); } diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingOperationTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingOperationTest.m index 4871f3bbd9..1a7c5209ff 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingOperationTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingOperationTest.m @@ -58,7 +58,7 @@ - (BOOL)validateBoolString:(id *)ioValue error:(NSError **)outError { } else if ([(NSObject *)*ioValue isKindOfClass:[NSString class]] && [(NSString *)*ioValue isEqualToString:@"MODIFY"]) { *ioValue = @"modified value"; return YES; - } + } return YES; } diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingProviderTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingProviderTest.m index 6dce8244dd..2f7f0fba08 100644 --- a/Tests/Logic/ObjectMapping/RKObjectMappingProviderTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingProviderTest.m @@ -31,7 +31,7 @@ #import "RKOrderedDictionary.h" @interface RKObjectMappingProviderTest : RKTestCase { - RKObjectManager* _objectManager; + RKObjectManager* _objectManager; } @end @@ -40,9 +40,9 @@ @implementation RKObjectMappingProviderTest - (void)setUp { _objectManager = [RKTestFactory objectManager]; - _objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"RKTests.sqlite"]; + _objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"RKTests.sqlite"]; [RKObjectManager setSharedManager:_objectManager]; - [_objectManager.objectStore deletePersistantStore]; + [_objectManager.objectStore deletePersistentStore]; } - (void)testShouldFindAnExistingObjectMappingForAClass { diff --git a/Tests/Logic/ObjectMapping/RKObjectPaginatorTest.m b/Tests/Logic/ObjectMapping/RKObjectPaginatorTest.m index d5c94c3d7b..09b0b16e19 100644 --- a/Tests/Logic/ObjectMapping/RKObjectPaginatorTest.m +++ b/Tests/Logic/ObjectMapping/RKObjectPaginatorTest.m @@ -88,15 +88,15 @@ - (void)waitForLoad { self.paginatedObjects = nil; self.paginationError = nil; - NSDate* startDate = [NSDate date]; - - while (loading) { - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - if ([[NSDate date] timeIntervalSinceDate:startDate] > self.timeout) { - [NSException raise:@"RKTestPaginatorDelegateTimeoutException" format:@"*** Operation timed out after %f seconds...", self.timeout]; - loading = NO; - } - } + NSDate* startDate = [NSDate date]; + + while (loading) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + if ([[NSDate date] timeIntervalSinceDate:startDate] > self.timeout) { + [NSException raise:@"RKTestPaginatorDelegateTimeoutException" format:@"*** Operation timed out after %f seconds...", self.timeout]; + loading = NO; + } + } } #pragma mark - RKObjectPaginatorDelegate @@ -135,6 +135,14 @@ @implementation RKObjectPaginatorTest static NSString * const RKObjectPaginatorTestResourcePathPattern = @"/paginate?per_page=:perPage&page=:currentPage"; +- (void)setUp { + [RKTestFactory setUp]; +} + +- (void)tearDown { + [RKTestFactory tearDown]; +} + - (RKObjectMappingProvider *)paginationMappingProvider { RKObjectMapping *paginationMapping = [RKObjectMapping mappingForClass:[RKObjectPaginator class]]; [paginationMapping mapKeyPath:@"current_page" toAttribute:@"currentPage"]; diff --git a/Tests/Logic/ObjectMapping/RKParserRegistryTest.m b/Tests/Logic/ObjectMapping/RKParserRegistryTest.m index d5461ef4c6..0304510c93 100644 --- a/Tests/Logic/ObjectMapping/RKParserRegistryTest.m +++ b/Tests/Logic/ObjectMapping/RKParserRegistryTest.m @@ -55,7 +55,7 @@ - (void)testShouldAutoconfigureBasedOnReflection { - (void)testRetrievalOfExactStringMatchForMIMEType { RKParserRegistry* registry = [[RKParserRegistry new] autorelease]; - [registry setParserClass:[RKJSONParserJSONKit class] forMIMEType:RKMIMETypeJSON]; + [registry setParserClass:[RKJSONParserJSONKit class] forMIMEType:RKMIMETypeJSON]; id parser = [registry parserForMIMEType:RKMIMETypeJSON]; assertThat(parser, is(instanceOf([RKJSONParserJSONKit class]))); } @@ -73,13 +73,13 @@ - (void)testRetrievalOfExactStringMatchIsFavoredOverRegularExpression { RKParserRegistry *registry = [[RKParserRegistry new] autorelease]; NSError *error = nil; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"application/xml\\+\\w+" options:0 error:&error]; - [registry setParserClass:[RKJSONParserJSONKit class] forMIMETypeRegularExpression:regex]; + [registry setParserClass:[RKJSONParserJSONKit class] forMIMETypeRegularExpression:regex]; [registry setParserClass:[RKXMLParserXMLReader class] forMIMEType:@"application/xml+whatever"]; - + // Exact match id exactParser = [registry parserForMIMEType:@"application/xml+whatever"]; assertThat(exactParser, is(instanceOf([RKXMLParserXMLReader class]))); - + // Fallback to regex id regexParser = [registry parserForMIMEType:@"application/xml+different"]; assertThat(regexParser, is(instanceOf([RKJSONParserJSONKit class]))); diff --git a/Tests/Logic/Support/NSArray+RKAdditionsTest.m b/Tests/Logic/Support/NSArray+RKAdditionsTest.m new file mode 100644 index 0000000000..4c376a2654 --- /dev/null +++ b/Tests/Logic/Support/NSArray+RKAdditionsTest.m @@ -0,0 +1,86 @@ +// +// NSArray+RKAdditionsTest.m +// RestKit +// +// Created by Blake Watters on 4/10/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "NSArray+RKAdditions.h" +#import "RKTestUser.h" + +@interface NSArray_RKAdditionsTest : RKTestCase +@end + +@implementation NSArray_RKAdditionsTest + +#pragma mark - sectionsGroupedByKeyPath Tests + +- (void)testReturnsEmptyArrayWhenGroupingEmptyArray +{ + NSArray *objects = [NSArray array]; + assertThat([objects sectionsGroupedByKeyPath:@"whatever"], is(empty())); +} + +- (void)testReturnsSingleSectionWhenGroupingSingleObject +{ + RKTestUser *user = [RKTestUser new]; + user.name = @"Blake"; + user.country = @"USA"; + NSArray *users = [NSArray arrayWithObject:user]; + + NSArray *sections = [users sectionsGroupedByKeyPath:@"country"]; + assertThat(sections, hasCountOf(1)); +} + +- (void)testReturnsTwoSectionsWhenGroupingThreeObjectsWithTwoUniqueValues +{ + RKTestUser *user1 = [RKTestUser new]; + user1.name = @"Blake"; + user1.country = @"USA"; + + RKTestUser *user2 = [RKTestUser new]; + user2.name = @"Colin"; + user2.country = @"USA"; + + RKTestUser *user3 = [RKTestUser new]; + user3.name = @"Pepe"; + user3.country = @"Spain"; + + NSArray *users = [NSArray arrayWithObjects:user1, user2, user3, nil]; + + NSArray *sections = [users sectionsGroupedByKeyPath:@"country"]; + assertThat(sections, hasCountOf(2)); + assertThat([sections objectAtIndex:0], contains(user1, user2, nil)); + assertThat([sections objectAtIndex:1], contains(user3, nil)); +} + +- (void)testCreationOfSingleSectionForNullValues +{ + RKTestUser *user1 = [RKTestUser new]; + user1.name = @"Blake"; + user1.country = @"USA"; + + RKTestUser *user2 = [RKTestUser new]; + user2.name = @"Expatriate"; + user2.country = nil; + + RKTestUser *user3 = [RKTestUser new]; + user3.name = @"John Doe"; + user3.country = nil; + + RKTestUser *user4 = [RKTestUser new]; + user4.name = @"Pepe"; + user4.country = @"Spain"; + + NSArray *users = [NSArray arrayWithObjects:user1, user2, user3, user4, nil]; + + NSArray *sections = [users sectionsGroupedByKeyPath:@"country"]; + assertThat(sections, hasCountOf(3)); + assertThat([sections objectAtIndex:0], contains(user1, nil)); + assertThat([sections objectAtIndex:1], contains(user2, user3, nil)); + assertThat([sections objectAtIndex:2], contains(user4, nil)); +} + +@end diff --git a/Tests/Logic/Support/RKCacheTest.m b/Tests/Logic/Support/RKCacheTest.m new file mode 100644 index 0000000000..6520555c03 --- /dev/null +++ b/Tests/Logic/Support/RKCacheTest.m @@ -0,0 +1,32 @@ +// +// RKCacheTest.m +// RestKit +// +// Created by Blake Watters on 4/17/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "RKDirectory.h" + +@interface RKCacheTest : RKTestCase + +@end + +@implementation RKCacheTest + +- (void)testCreationOfIntermediateDirectories +{ + NSString *cachePath = [RKDirectory cachesDirectory]; + NSString *subPath = [cachePath stringByAppendingPathComponent:@"TestPath"]; + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtPath:cachePath error:&error]; + + [[RKCache alloc] initWithPath:subPath subDirectories:nil]; + BOOL isDirectory; + BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:subPath isDirectory:&isDirectory]; + assertThatBool(fileExists, is(equalToBool(YES))); + assertThatBool(isDirectory, is(equalToBool(YES))); +} + +@end diff --git a/Tests/Logic/Support/RKDotNetDateFormatterTest.m b/Tests/Logic/Support/RKDotNetDateFormatterTest.m index 021528a476..de14dbf9a2 100644 --- a/Tests/Logic/Support/RKDotNetDateFormatterTest.m +++ b/Tests/Logic/Support/RKDotNetDateFormatterTest.m @@ -77,5 +77,16 @@ - (void)testShouldCreateADotNetStringFromADateBefore1970WithoutAnOffset { assertThat(string, is(equalTo(@"/Date(-1000212360000+0000)/"))); } +- (void)testShouldCreateADateWithGetObjectValueForString { + RKDotNetDateFormatter *formatter = [RKDotNetDateFormatter dotNetDateFormatter]; + NSString *dotNetString = @"/Date(1000212360000-0400)/"; + + NSDate *date = nil; + NSString *errorDescription = nil; + BOOL success = [formatter getObjectValue:&date forString:dotNetString errorDescription:&errorDescription]; + + assertThatBool(success, is(equalToBool(YES))); + assertThat([date description], is(equalTo(@"2001-09-11 12:46:00 +0000"))); +} @end diff --git a/Tests/Logic/Support/RKObjectiveCppTest.mm b/Tests/Logic/Support/RKObjectiveCppTest.mm new file mode 100644 index 0000000000..1a11c97aa6 --- /dev/null +++ b/Tests/Logic/Support/RKObjectiveCppTest.mm @@ -0,0 +1,22 @@ +// +// RKObjectiveCppTest.mm +// RestKit +// +// Created by Blake Watters on 4/11/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" + +@interface RKObjectiveCppTest : RKTestCase + +@end + +@implementation RKObjectiveCppTest + +- (void)testCompiles +{ + // Nothing to do. +} + +@end \ No newline at end of file diff --git a/Tests/Models/Data Model.xcdatamodel/elements b/Tests/Models/Data Model.xcdatamodel/elements index cdf5982f2a..0a34e7feb5 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 8c7d53041f..e3b3a05f40 100644 Binary files a/Tests/Models/Data Model.xcdatamodel/layout and b/Tests/Models/Data Model.xcdatamodel/layout differ diff --git a/Tests/Models/RKHuman.m b/Tests/Models/RKHuman.m index 2e48a55ded..e4d175a8c1 100644 --- a/Tests/Models/RKHuman.m +++ b/Tests/Models/RKHuman.m @@ -40,7 +40,7 @@ @implementation RKHuman @dynamic catsInOrderByAge; - (NSString*)polymorphicResourcePath { - return @"/this/is/the/path"; + return @"/this/is/the/path"; } @end diff --git a/Tests/Models/RKMappableAssociation.h b/Tests/Models/RKMappableAssociation.h index 4c0ee9cedd..3a41f09626 100644 --- a/Tests/Models/RKMappableAssociation.h +++ b/Tests/Models/RKMappableAssociation.h @@ -21,7 +21,7 @@ #import @interface RKMappableAssociation : NSObject { - NSString* _testString; + NSString* _testString; NSDate* _date; } diff --git a/Tests/Models/RKMappableObject.h b/Tests/Models/RKMappableObject.h index 5645115c11..7abc4cb39d 100644 --- a/Tests/Models/RKMappableObject.h +++ b/Tests/Models/RKMappableObject.h @@ -22,12 +22,12 @@ #import "RKMappableAssociation.h" @interface RKMappableObject : NSObject { - NSDate* _dateTest; - NSNumber* _numberTest; - NSString* _stringTest; + NSDate* _dateTest; + NSNumber* _numberTest; + NSString* _stringTest; NSURL* _urlTest; - RKMappableAssociation* _hasOne; - NSSet* _hasMany; + RKMappableAssociation* _hasOne; + NSSet* _hasMany; } @property (nonatomic, retain) NSDate* dateTest; @@ -37,4 +37,4 @@ @property (nonatomic, retain) RKMappableAssociation* hasOne; @property (nonatomic, retain) NSSet* hasMany; -@end \ No newline at end of file +@end diff --git a/Tests/Models/RKObjectMapperTestModel.h b/Tests/Models/RKObjectMapperTestModel.h index 9502e2dae5..c77b42d5ee 100644 --- a/Tests/Models/RKObjectMapperTestModel.h +++ b/Tests/Models/RKObjectMapperTestModel.h @@ -22,9 +22,9 @@ #import @interface RKObjectMapperTestModel : NSObject { - NSString* _name; - NSNumber* _age; - NSDate* _createdAt; + NSString* _name; + NSNumber* _age; + NSDate* _createdAt; } @property (nonatomic,retain) NSString* name; diff --git a/Tests/Models/RKSearchable.h b/Tests/Models/RKSearchable.h index d50eb2d015..c1e5f93530 100644 --- a/Tests/Models/RKSearchable.h +++ b/Tests/Models/RKSearchable.h @@ -4,13 +4,13 @@ // // Created by Blake Watters on 7/26/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,7 +20,7 @@ #import #import -#import "RKSearchableManagedObject.h" +#import @interface RKSearchable : RKSearchableManagedObject diff --git a/Tests/Models/RKSearchable.m b/Tests/Models/RKSearchable.m index d7900719b9..13c9501f40 100644 --- a/Tests/Models/RKSearchable.m +++ b/Tests/Models/RKSearchable.m @@ -4,13 +4,13 @@ // // Created by Blake Watters on 7/26/11. // Copyright (c) 2009-2012 RestKit. All rights reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/Tests/Models/RKTestAddress.m b/Tests/Models/RKTestAddress.m index dd4715f396..e05865bbb3 100644 --- a/Tests/Models/RKTestAddress.m +++ b/Tests/Models/RKTestAddress.m @@ -29,4 +29,4 @@ - (BOOL)isEqual:(id)object { } } -@end \ No newline at end of file +@end diff --git a/Tests/Models/RKTestUser.m b/Tests/Models/RKTestUser.m index 3edc16f48c..ddb5689f77 100644 --- a/Tests/Models/RKTestUser.m +++ b/Tests/Models/RKTestUser.m @@ -36,10 +36,15 @@ + (RKTestUser*)user { // to determine if assocation values should be set - (BOOL)isEqual:(id)object { if ([object isKindOfClass:[RKTestUser class]]) { - return [[(RKTestUser*)object userID] isEqualToNumber:self.userID]; - } else { - return NO; + if ([(RKTestUser*)object userID] == nil && self.userID == nil) { + // No primary key -- consult superclass + return [super isEqual:object]; + } else { + return [[(RKTestUser*)object userID] isEqualToNumber:self.userID]; + } } + + return NO; } - (id)valueForUndefinedKey:(NSString *)key { diff --git a/Tests/RKTestEnvironment.h b/Tests/RKTestEnvironment.h index dccbac1fbd..132ba8343f 100644 --- a/Tests/RKTestEnvironment.h +++ b/Tests/RKTestEnvironment.h @@ -25,22 +25,13 @@ #import #import -#import "RestKit.h" -#import "Testing.h" -#import "RKManagedObjectStore.h" - -//////////////////////////////////////////////////////////////////////////// -// OCMock - For some reason this macro is incorrect. Note the use of __typeof - -#undef OCMOCK_VALUE -#define OCMOCK_VALUE(variable) [NSValue value:&variable withObjCType:@encode(__typeof(variable))] +#import +#import +#import RKOAuthClient* RKTestNewOAuthClient(RKTestResponseLoader* loader); -// TODO: Figure out how to extract... -void RKTestClearCacheDirectory(void); - -/* +/* Base class for RestKit test cases. Provides initialization of testing infrastructure. */ diff --git a/Tests/RKTestEnvironment.m b/Tests/RKTestEnvironment.m index 0530889af0..40b7d7086d 100644 --- a/Tests/RKTestEnvironment.m +++ b/Tests/RKTestEnvironment.m @@ -22,24 +22,22 @@ #import "RKTestEnvironment.h" #import "RKParserRegistry.h" -RKOAuthClient* RKTestNewOAuthClient(RKTestResponseLoader* loader){ +RKOAuthClient* RKTestNewOAuthClient(RKTestResponseLoader* loader) +{ [loader setTimeout:10]; - RKOAuthClient* client = [RKOAuthClient clientWithClientID:@"appID" secret:@"appSecret"]; + RKOAuthClient *client = [RKOAuthClient clientWithClientID:@"4fa42a4a7184796662000001" secret:@"restkit_secret"]; client.delegate = loader; - client.authorizationURL = [NSString stringWithFormat:@"%@/oauth/authorize", [RKTestFactory baseURLString]]; + client.authorizationURL = [NSString stringWithFormat:@"%@/oauth2/pregen/token", [RKTestFactory baseURLString]]; return client; } -void RKTestClearCacheDirectory(void) { - -} - @implementation RKTestCase + (void)initialize { - // Configure fixture bundle - NSBundle *fixtureBundle = [NSBundle bundleWithIdentifier:@"org.restkit.unit-tests"]; + // Configure fixture bundle. The 'org.restkit.tests' identifier is shared between + // the logic and application test bundles + NSBundle *fixtureBundle = [NSBundle bundleWithIdentifier:@"org.restkit.tests"]; [RKTestFixture setFixtureBundle:fixtureBundle]; // Ensure the required directories exist @@ -61,15 +59,18 @@ + (void)initialize @end @implementation SenTestCase (MethodSwizzling) + - (void)swizzleMethod:(SEL)aOriginalMethod inClass:(Class)aOriginalClass withMethod:(SEL)aNewMethod fromClass:(Class)aNewClass - executeBlock:(void (^)(void))aBlock { + executeBlock:(void (^)(void))aBlock +{ Method originalMethod = class_getClassMethod(aOriginalClass, aOriginalMethod); Method mockMethod = class_getInstanceMethod(aNewClass, aNewMethod); method_exchangeImplementations(originalMethod, mockMethod); aBlock(); method_exchangeImplementations(mockMethod, originalMethod); } + @end diff --git a/Tests/Server/.gitignore b/Tests/Server/.gitignore new file mode 100644 index 0000000000..a573ceeaa0 --- /dev/null +++ b/Tests/Server/.gitignore @@ -0,0 +1,2 @@ +*.log +*.pid diff --git a/Tests/Server/Rakefile b/Tests/Server/Rakefile new file mode 100644 index 0000000000..154ac07ff5 --- /dev/null +++ b/Tests/Server/Rakefile @@ -0,0 +1,8 @@ +require 'rubygems' +require 'bundler' +require 'rspec/core/rake_task' + +desc "Run unit tests" +RSpec::Core::RakeTask.new do |t| + ENV['RACK_ENV'] = 'test' +end \ No newline at end of file diff --git a/Tests/Server/fixtures.rb b/Tests/Server/fixtures.rb new file mode 100644 index 0000000000..a61beefbf4 --- /dev/null +++ b/Tests/Server/fixtures.rb @@ -0,0 +1,5 @@ +RESTKIT_OAUTH2_CLIENT_ID = "4fa42a4a7184796662000001"; +RESTKIT_OAUTH2_CLIENT_SECRET = "restkit_secret" +RESTKIT_CLIENT_PARAMS = { + :id => RESTKIT_OAUTH2_CLIENT_ID, :secret => RESTKIT_OAUTH2_CLIENT_SECRET, :display_name=>"RestKit", :scope=>%w{read write} +} diff --git a/Tests/Server/lib/restkit/network/oauth1.rb b/Tests/Server/lib/restkit/network/oauth1.rb new file mode 100644 index 0000000000..796f0f7064 --- /dev/null +++ b/Tests/Server/lib/restkit/network/oauth1.rb @@ -0,0 +1,161 @@ +require 'rack/auth/abstract/request' +require 'simple_oauth' +require 'ruby_debug' + +TOKEN_SECRET = 'monkey' +CLIENT_SECRET = 'restkit_secret' + +module RestKit + module Network + module OAuth1 + class Middleware + + # This class modified from https://github.com/tonywok/forcefield + + # Copyright (c) 2011 EdgeCase, Tony Schneider + # + # Permission is hereby granted, free of charge, to any person obtaining + # a copy of this software and associated documentation files (the + # "Software"), to deal in the Software without restriction, including + # without limitation the rights to use, copy, modify, merge, publish, + # distribute, sublicense, and/or sell copies of the Software, and to + # permit persons to whom the Software is furnished to do so, subject to + # the following conditions: + # + # The above copyright notice and this permission notice shall be + # included in all copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + def initialize(app) + @app = app + end + + def call(env) + # rebuild the original path so that signatures match + env.delete 'SCRIPT_NAME' + env['PATH_INFO'] = '/oauth1' + env['PATH_INFO'] + + @request = RestKit::Network::OAuth1::Request.new(env) + + @request.with_valid_request do + if client_verified? + @app.call(env) + else + [401, {}, ["Unauthorized."]] + end + end + end + + private + + def client_verified? + @request.verify_signature("restkit_secret") + end + end + + class Request < Rack::Auth::AbstractRequest + + # This class modified from https://github.com/tonywok/forcefield + + # Copyright (c) 2011 EdgeCase, Tony Schneider + # + # Permission is hereby granted, free of charge, to any person obtaining + # a copy of this software and associated documentation files (the + # "Software"), to deal in the Software without restriction, including + # without limitation the rights to use, copy, modify, merge, publish, + # distribute, sublicense, and/or sell copies of the Software, and to + # permit persons to whom the Software is furnished to do so, subject to + # the following conditions: + # + # The above copyright notice and this permission notice shall be + # included in all copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + # This method encapsulates the various checks we need to make against the request's + # Authorization header before we deem it ready for verification. + # Upon passing the checks, we yield to the block so that simple_oauth can determine + # whether or not the request has been properly signed. + # + def with_valid_request + if provided? # #provided? defined in Rack::Auth::AbstractRequest + if !oauth? + [400, {}, ["Bad request. No auth scheme provided."]] + elsif params[:consumer_key].nil? + [400, {}, ["Bad request. No consumer key provided."]] + elsif params[:signature].nil? + [400, {}, ["Bad request. No signature provided."]] + elsif params[:signature_method].nil? + [400, {}, ["Bad request. No signature method provided."]] + else + yield(request.env) + end + else + [400, {}, ["Bad request."]] + end + end + + def verify_signature(client_secret) + return false unless client_secret + header = SimpleOAuth::Header.new(request.request_method, request.url, included_request_params, auth_header) + + header.valid?({:consumer_secret => CLIENT_SECRET, :token_secret => TOKEN_SECRET}) + end + + def consumer_key + params[:consumer_key] + end + + private + + def params + @params ||= SimpleOAuth::Header.parse(auth_header) + end + + # #scheme is defined as an instance method on Rack::Auth::AbstractRequest + # + def oauth? + scheme == :oauth + end + + def auth_header + @env[authorization_key] + end + + # only include request params if Content-Type is set to application/x-www/form-urlencoded + # (see http://tools.ietf.org/html/rfc5849#section-3.4.1) + # + def included_request_params + request.content_type == "application/x-www-form-urlencoded" ? request.params : nil + end + end + + class App < Sinatra::Base + configure do + enable :logging, :dump_errors + end + + get '/oauth1/me' do + response = {'user_id' => 1, 'name' => 'Rod'} + content_type 'application/json' + response + end + end + + end + end +end \ No newline at end of file diff --git a/Tests/Server/lib/restkit/network/oauth2.rb b/Tests/Server/lib/restkit/network/oauth2.rb index 573f086d7d..d9e5a4b3b4 100644 --- a/Tests/Server/lib/restkit/network/oauth2.rb +++ b/Tests/Server/lib/restkit/network/oauth2.rb @@ -1,37 +1,150 @@ +require "rack/oauth2/sinatra" +require "rack/oauth2/server" + +# Override this so we can specify an ObjectId manually +module Rack + module OAuth2 + class Server + class AuthRequest + class << self + def create(client, scope, redirect_uri, response_type, state, id = nil) + scope = Utils.normalize_scope(scope) & client.scope # Only allowed scope + fields = { :client_id=>client.id, :scope=>scope, :redirect_uri=>client.redirect_uri || redirect_uri, + :response_type=>response_type, :state=>state, + :grant_code=>nil, :authorized_at=>nil, + :created_at=>Time.now.to_i, :revoked=>nil } + fields[:_id] = BSON::ObjectId.from_string(id) if id + fields[:_id] = collection.insert(fields) + Server.new_instance self, fields + end + end + end + end + end +end + module RestKit module Network - class OAuth2 < Sinatra::Base - ACCESS_TOKEN = '581b50dca15a9d41eb280d5cbd52c7da4fb564621247848171508dd9d0dfa551a2efe9d06e110e62335abf13b6446a5c49e4bf6007cd90518fbbb0d1535b4dbc' + module OAuth2 + module Scenarios + class App + def initialize(app) + @base_url = '/oauth2/basic' + end + + def db_setup + Rack::OAuth2::Server.options.database = Mongo::Connection.new[ENV["DB"]] + Rack::OAuth2::Server.options.collection_prefix = "oauth2_prefix" + end + + def setup(env) + Rack::OAuth2::Server.options.authorize_path = "/authorize" + Rack::OAuth2::Server.options.access_token_path = "/token" + Rack::OAuth2::Server.options.authorization_types = ["code", "token"] + create_fixtures + end + + def create_fixtures + @client = Rack::OAuth2::Server.register(RESTKIT_CLIENT_PARAMS) + end + + def teardown(response) + if redirect?(response) + response[1]["Location"].gsub!(/\/authorize/, @base_url + '/authorize') + response[1]["Location"].gsub!(/\/token/, @base_url + '/token') + response[1]["Location"].gsub!(/\/me/, @base_url + '/me') + end + end + + def redirect?(response) + response && response[1] && response[1]["Location"] + end + + def drop_all + Rack::OAuth2::Server::Client.collection.drop + Rack::OAuth2::Server::AuthRequest.collection.drop + Rack::OAuth2::Server::AccessGrant.collection.drop + Rack::OAuth2::Server::AccessToken.collection.drop + Rack::OAuth2::Server::Issuer.collection.drop + end + + def call(env) + env.delete 'SCRIPT_NAME' # otherwise Rack::OAuth::Server will merge this back into path_info + db_setup + + if env['PATH_INFO'] =~ /^\/reset/ + drop_all + [200, {}, "Aye, aye!"] + else + setup env + response = OAuth2App.call(env) + teardown response + response + end + end + + def create_access_grant(identity, client, scope, id = nil, redirect_uri = nil, expires = nil) + raise ArgumentError, "Identity must be String or Integer" unless String === identity || Integer === identity + scope = Rack::OAuth2::Server::Utils.normalize_scope(scope) & client.scope # Only allowed scope + expires_at = Time.now.to_i + (expires || 300) + id = Rack::OAuth2::Server.secure_random unless id + fields = { :_id=>id, :identity=>identity, :scope=>scope, + :client_id=>client.id, :redirect_uri=>client.redirect_uri || redirect_uri, + :created_at=>Time.now.to_i, :expires_at=>expires_at, :granted_at=>nil, + :access_token=>nil, :revoked=>nil } + Rack::OAuth2::Server::AccessGrant.collection.insert fields + Rack::OAuth2::Server.new_instance Rack::OAuth2::Server::AccessGrant, fields + end + end + + class PregeneratedTokens < App + def initialize(app) + @base_url = '/oauth2/pregen' + end + + def setup(env) + drop_all + super + end + + def create_fixtures + @client = Rack::OAuth2::Server.register(RESTKIT_CLIENT_PARAMS) + @auth_request = Rack::OAuth2::Server::AuthRequest.find('4fa8182d7184797dd5000001') || Rack::OAuth2::Server::AuthRequest.create(@client, @client.scope, @client.redirect_uri.to_s, 'code', nil, '4fa8182d7184797dd5000001') + @access_grant = create_access_grant("Identity", @client, @client.scope, '4fa8182d7184797dd5000002', @client.redirect_uri) - post '/oauth/authorize' do - authorization_code = params[:code] - response = "" - if "1234" == authorization_code - response = { 'access_token' => ACCESS_TOKEN, 'timeout' => 31337 }.to_json - else - response = {'error' => 'invalid_grant', 'error_description' => 'authorization code not valid'}.to_json + @auth_request.grant_code = @access_grant.code + Rack::OAuth2::Server::AuthRequest.collection.update({:_id=>@auth_request.id, :revoked=>nil}, {:$set=>{ :grant_code=>@access_grant.code, :authorized_at=> Time.now}}) + end end - content_type 'application/json' - response end - get '/me' do - access_token = params[:Authorization] - tokenHeader = 'OAuth2 ' + ACCESS_TOKEN - response = '' - if access_token.nil? - status 401 - response = {'message' => "A valid access_token is required to access."}.to_json - elsif tokenHeader == access_token - response = {'user_id' => 1, 'name' => 'Rod'} - else - status 401 - response = {'message' => "Bad credentials"}.to_json + class OAuth2App < Sinatra::Base + register Rack::OAuth2::Sinatra + + configure do + enable :logging, :dump_errors + end + + set :sessions, true + set :show_exceptions, false + + get "/authorize" do + if oauth.client + "client: #{oauth.client.display_name}\nscope: #{oauth.scope.join(", ")}\nauthorization: #{oauth.authorization}" + else + "No client" + end end + + oauth_required "/me" + + get '/me' do + response = {'user_id' => 1, 'name' => 'Rod'} content_type 'application/json' response + end + end - end end end \ No newline at end of file diff --git a/Tests/Server/lib/restkit/network/redirection.rb b/Tests/Server/lib/restkit/network/redirection.rb new file mode 100644 index 0000000000..489cdb1cde --- /dev/null +++ b/Tests/Server/lib/restkit/network/redirection.rb @@ -0,0 +1,13 @@ +module RestKit + module Network + class Redirection < Sinatra::Base + get '/redirection' do + [302, {"Location" => '/redirection/target'}, ""] + end + + get '/redirection/target' do + [200, {"Content-Type" => "application/json"}, {"redirected" => true}.to_json] + end + end + end +end \ No newline at end of file diff --git a/Tests/Server/server.rb b/Tests/Server/server.rb index cb8a61c3c4..3169a84453 100644 --- a/Tests/Server/server.rb +++ b/Tests/Server/server.rb @@ -12,12 +12,17 @@ # No debugging... end +ENV["DB"] = "rack_oauth2_server" + # Import the RestKit Test server $: << File.join(File.expand_path(File.dirname(__FILE__)), 'lib') +require File.expand_path(File.dirname(__FILE__)) + '/fixtures' require 'restkit/network/authentication' require 'restkit/network/etags' require 'restkit/network/timeout' +require 'restkit/network/oauth1' require 'restkit/network/oauth2' +require 'restkit/network/redirection' class Person < Struct.new(:name, :age) def to_json(*args) @@ -37,7 +42,7 @@ class RestKitTestServer < Sinatra::Base use RestKit::Network::Authentication use RestKit::Network::ETags use RestKit::Network::Timeout - use RestKit::Network::OAuth2 + use RestKit::Network::Redirection def render_fixture(path, options = {}) send_file File.join(settings.public_folder, path), options @@ -94,6 +99,12 @@ def render_fixture(path, options = {}) content_type 'application/json' "".to_json end + + get '/204' do + status 204 + content_type 'application/json' + "".to_json + end get '/403' do status 403 diff --git a/Tests/Server/server.ru b/Tests/Server/server.ru new file mode 100644 index 0000000000..acd4d777ca --- /dev/null +++ b/Tests/Server/server.ru @@ -0,0 +1,24 @@ +require File.expand_path(File.dirname(__FILE__)) + '/server' + +# Basic OAuth 2.0 implementation +map '/oauth2/basic' do + run RestKit::Network::OAuth2::Scenarios::App.new(nil) +end + +map '/oauth2/pregen' do + run RestKit::Network::OAuth2::Scenarios::PregeneratedTokens.new(nil) +end + +# Should just be /reset +map '/oauth2reset' do + run RestKit::Network::OAuth2::Scenarios::App.new(nil) +end + +map '/oauth1' do + use RestKit::Network::OAuth1::Middleware + run RestKit::Network::OAuth1::App +end + +map '/' do + run RestKitTestServer +end \ No newline at end of file diff --git a/Tests/Server/spec/oauth1_spec.rb b/Tests/Server/spec/oauth1_spec.rb new file mode 100644 index 0000000000..8d9631bb3f --- /dev/null +++ b/Tests/Server/spec/oauth1_spec.rb @@ -0,0 +1,75 @@ +require File.dirname(__FILE__) + "/spec_helper" +require 'simple_oauth' + +describe "OAuth1" do + def app + Rack::Builder.parse_file(File.dirname(__FILE__) + "/../server.ru").first + end + + before(:all) do + @oauth = { + :token => "12345", + :consumer_key => "restkit", + :consumer_secret => "restkit_secret", + :token_secret => "monkey" + } + end + + def header_without(key = nil) + oauth_header = SimpleOAuth::Header.new(:get, "http://example.org/oauth1/me", nil, @oauth) + attributes = oauth_header.signed_attributes.clone + attributes.delete key if key + "OAuth " + attributes.sort_by{|k,v| k.to_s }.map{|k,v| %(#{k}="#{SimpleOAuth::Header.encode(v)}") }.join(', ') + end + + context "Dispatching a valid OAuth1.0a header" do + it "should succeed" do + header 'Authorization', header_without(nil) + get "/oauth1/me" + + last_response.should be_successful + end + end + + context "Dispatching an invalid OAuth1.0a header" do + context "without a consumer key" do + it "should fail" do + header 'Authorization', header_without(:oauth_consumer_key) + get "/oauth1/me" + + last_response.status.should eq 400 + last_response.body.should eq "Bad request. No consumer key provided." + end + end + + context "without a signature" do + it "should fail" do + header 'Authorization', header_without(:oauth_signature) + get "/oauth1/me" + + last_response.status.should eq 400 + last_response.body.should eq "Bad request. No signature provided." + end + end + + context "without a signature method" do + it "should fail" do + header 'Authorization', header_without(:oauth_signature_method) + get "/oauth1/me" + + last_response.status.should eq 400 + last_response.body.should eq "Bad request. No signature method provided." + end + end + + context "without a header at all" do + it "should fail" do + get "/oauth1/me" + + last_response.status.should eq 400 + last_response.body.should eq "Bad request." + end + end + end + +end \ No newline at end of file diff --git a/Tests/Server/spec/oauth2_spec.rb b/Tests/Server/spec/oauth2_spec.rb new file mode 100644 index 0000000000..e01353fdb9 --- /dev/null +++ b/Tests/Server/spec/oauth2_spec.rb @@ -0,0 +1,95 @@ +require File.dirname(__FILE__) + "/spec_helper" + +describe "OAuth2" do + def app + Rack::Builder.parse_file(File.dirname(__FILE__) + "/../server.ru").first + end + + before(:all) do + clear_database + @client = Server.register(RESTKIT_CLIENT_PARAMS) + @params = {:redirect_uri=>'http://restkit.org/', :client_id=>@client.id, :client_secret=>@client.secret, :response_type=>"code", :scope=>"read write", :state=>"bring this back" } + end + + def clear_database + Rack::OAuth2::Server::Client.collection.drop + Rack::OAuth2::Server::AuthRequest.collection.drop + Rack::OAuth2::Server::AccessGrant.collection.drop + Rack::OAuth2::Server::AccessToken.collection.drop + Rack::OAuth2::Server::Issuer.collection.drop + end + + context "Resetting the OAuth2 pathway" do + it "should clear the database" do + clear_database + Rack::OAuth2::Server::Client.collection.count.should eq 0 + Rack::OAuth2::Server::AuthRequest.collection.count.should eq 0 + Rack::OAuth2::Server::AccessGrant.collection.count.should eq 0 + Rack::OAuth2::Server::AccessToken.collection.count.should eq 0 + Rack::OAuth2::Server::Issuer.collection.count.should eq 0 + + Rack::OAuth2::Server.register(RESTKIT_CLIENT_PARAMS) + Rack::OAuth2::Server::Client.collection.count.should eq 1 + + get "/oauth2/basic/authorize?" + Rack::Utils.build_query(@params) + get last_response["Location"] if last_response.status == 303 + + Rack::OAuth2::Server::AuthRequest.collection.count.should eq 1 + + get "/oauth2reset/reset" + + Rack::OAuth2::Server::Client.collection.count.should eq 0 + Rack::OAuth2::Server::AuthRequest.collection.count.should eq 0 + Rack::OAuth2::Server::AccessGrant.collection.count.should eq 0 + Rack::OAuth2::Server::AccessToken.collection.count.should eq 0 + Rack::OAuth2::Server::Issuer.collection.count.should eq 0 + end + end + + context "Given a basic OAuth2 setup" do + context "Requesting the authorization URL" do + + context "With valid params" do + it "Should report the client" do + get "/oauth2/basic/authorize?" + Rack::Utils.build_query(@params) + get last_response["Location"] if last_response.status == 303 + + response = last_response.body.split("\n").inject({}) { |h,l| n,v = l.split(/:\s*/) ; h[n.downcase] = v ; h } + response["client"].should eq @client.display_name + end + end + + context "Without valid params" do + it "Should fail" do + get "/oauth2/basic/authorize" + last_response.should_not be_successful + last_response.should_not be_redirect + end + end + end + + context "And a pre-generated authorization token and access grant" do + context "Requesting the authorization URL with the token" do + it "should report the client" do + get "/oauth2/pregen/authorize?" + Rack::Utils.build_query({:authorization => '4fa8182d7184797dd5000001'}) + response = last_response.body.split("\n").inject({}) { |h,l| n,v = l.split(/:\s*/) ; h[n.downcase] = v ; h } + response["client"].should eq @client.display_name + end + end + + context "Requesting the token URL with the access grant token" do + it "should return a token" do + params = { + :code => '4fa8182d7184797dd5000002', + :client_id => @client.id, + :client_secret => @client.secret, + :redirect_uri => @client.redirect_uri, + :grant_type => 'authorization_code' + } + post "/oauth2/pregen/token", params + JSON.parse(last_response.body)["access_token"].should_not be_nil + end + end + end + end +end \ No newline at end of file diff --git a/Tests/Server/spec/spec_helper.rb b/Tests/Server/spec/spec_helper.rb new file mode 100644 index 0000000000..9a545ec72d --- /dev/null +++ b/Tests/Server/spec/spec_helper.rb @@ -0,0 +1,14 @@ +require File.dirname(__FILE__) + "/../server" +require File.expand_path(File.dirname(__FILE__)) + '/../fixtures' +require "rubygems" +require "test/unit" +require "rack/test" +require "rack/oauth2/server" + +Rack::OAuth2::Server.options.database = Mongo::Connection.new[ENV["DB"]] +Rack::OAuth2::Server.options.collection_prefix = "oauth2_prefix" + +RSpec.configure do |conf| + conf.include Rack::Test::Methods + conf.include Rack::OAuth2 +end \ No newline at end of file diff --git a/Tests/Server/thin.yml b/Tests/Server/thin.yml new file mode 100644 index 0000000000..bc8c8b16f3 --- /dev/null +++ b/Tests/Server/thin.yml @@ -0,0 +1,13 @@ +--- +environment: development +address: 0.0.0.0 +port: 4567 +timeout: 30 +log: Tests/Server/server.log +pid: Tests/Server/server.pid +max_conns: 1024 +max_persistent_conns: 512 +require: [] +wait: 30 +rackup: Tests/Server/server.ru +daemonize: true diff --git a/Tests/cibuild b/Tests/cibuild new file mode 100755 index 0000000000..2d6b84d495 --- /dev/null +++ b/Tests/cibuild @@ -0,0 +1,24 @@ +#!/bin/bash -e + +# Configure RVM +[[ -s "$HOME/.rvm/scripts/rvm" ]] && source "$HOME/.rvm/scripts/rvm" +source .rvmrc + +# Git submodules +git submodule update --init + +# Bundler +bundle + +# Run the build +rake server:stop || true +rake server:start +rake test +rake server:stop + +# Build & Publish Docs +if [ -n $DOCS_DESTINATION ]; then + VERSION=`echo $GIT_BRANCH|sed -e 's/origin\///'` + echo "Building documentation for branch $VERSION and publishing to '$DOCS_DESTINATION'" + rake docs:publish[$VERSION,$DOCS_DESTINATION] +fi diff --git a/VERSION b/VERSION index 78bc1abd14..571215736a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.10.0 +0.10.1 diff --git a/Vendor/cocoa-oauth/GCOAuth.h b/Vendor/cocoa-oauth/GCOAuth.h index 2e5a0a03c9..b9495de453 100644 --- a/Vendor/cocoa-oauth/GCOAuth.h +++ b/Vendor/cocoa-oauth/GCOAuth.h @@ -85,7 +85,7 @@ tokenSecret:(NSString *)tokenSecret; /* - Creates and returns a URL request that will perform a POST HTTPS operation. All + Creates and returns a URL request that will perform a POST HTTP operation. All data will be sent as form URL encoded. Restrictions on the arguments to this method are the same as the GET request methods. */ @@ -135,10 +135,21 @@ consumerSecret:(NSString *)consumerSecret accessToken:(NSString *)accessToken tokenSecret:(NSString *)tokenSecret; +@end +@interface NSString (GCOAuthAdditions) +// better percent escape +- (NSString *)pcen; @end +@interface NSURL (GCOAuthURL) + +/* + Get host:port from URL unless port is 80 or 443 (http://tools.ietf.org/html/rfc5849#section-3.4.1.2). Otherwis reurn only host. + */ +- (NSString *)hostAndPort; +@end /* XAuth example (because you may otherwise be scratching your head): diff --git a/Vendor/cocoa-oauth/GCOAuth.m b/Vendor/cocoa-oauth/GCOAuth.m index b646754a25..94d247a280 100644 --- a/Vendor/cocoa-oauth/GCOAuth.m +++ b/Vendor/cocoa-oauth/GCOAuth.m @@ -86,13 +86,6 @@ + (NSURLRequest *)URLRequestForPath:(NSString *)path consumerSecret:(NSString *)consumerSecret accessToken:(NSString *)accessToken tokenSecret:(NSString *)tokenSecret; - -@end -@interface NSString (GCOAuthAdditions) - -// better percent escape -- (NSString *)pcen; - @end @implementation GCOAuth @@ -182,8 +175,8 @@ - (NSString *)signatureBase { NSURL *URL = self.URL; NSString *URLString = [NSString stringWithFormat:@"%@://%@%@", [[URL scheme] lowercaseString], - [[URL host] lowercaseString], - [[URL path] lowercaseString]]; + [[URL hostAndPort] lowercaseString], + [URL path]]; // create components NSArray *components = [NSArray arrayWithObjects: @@ -411,6 +404,17 @@ + (NSURLRequest *)URLRequestForPath:(NSString *)path } @end + +@implementation NSURL (GCOAuthURL) +- (NSString *)hostAndPort { + if ([self port] != nil && [[self port] intValue] != 80 && [[self port] intValue] != 443) { + return [NSString stringWithFormat:@"%@:%@", [self host], [self port]]; + } else { + return [self host]; + } +} +@end + @implementation NSString (GCOAuthAdditions) - (NSString *)pcen { CFStringRef string = CFURLCreateStringByAddingPercentEscapes(NULL, diff --git a/Vendor/iso8601parser/ISO8601DateFormatter.m b/Vendor/iso8601parser/ISO8601DateFormatter.m index a7693e06ae..6a3b65a8e3 100644 --- a/Vendor/iso8601parser/ISO8601DateFormatter.m +++ b/Vendor/iso8601parser/ISO8601DateFormatter.m @@ -6,6 +6,11 @@ #import #import "ISO8601DateFormatter.h" +#import "RKLog.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitSupport #ifndef DEFAULT_TIME_SEPARATOR # define DEFAULT_TIME_SEPARATOR ':' @@ -58,7 +63,7 @@ - (id) init { if ((self = [super init])) { parsingCalendar = [[self makeCalendarWithDesiredConfiguration] retain]; unparsingCalendar = [[self makeCalendarWithDesiredConfiguration] retain]; - + format = ISO8601DateFormatCalendar; timeSeparator = ISO8601DefaultTimeSeparatorCharacter; includeTime = NO; @@ -68,12 +73,12 @@ - (id) init { } - (void) dealloc { [defaultTimeZone release]; - + [unparsingFormatter release]; [lastUsedFormatString release]; [parsingCalendar release]; [unparsingCalendar release]; - + [super dealloc]; } @@ -82,7 +87,7 @@ - (void) setDefaultTimeZone:(NSTimeZone *)tz { if (defaultTimeZone != tz) { [defaultTimeZone release]; defaultTimeZone = [tz retain]; - + unparsingCalendar.timeZone = defaultTimeZone; } } @@ -155,41 +160,41 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out } - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange { NSDate *now = [NSDate date]; - + NSDateComponents *components = [[[NSDateComponents alloc] init] autorelease]; NSDateComponents *nowComponents = [parsingCalendar components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:now]; - + NSUInteger - //Date - year, - month_or_week = 0U, - day = 0U, - //Time - hour = 0U; + //Date + year, + month_or_week = 0U, + day = 0U, + //Time + hour = 0U; NSTimeInterval - minute = 0.0, - second = 0.0; + minute = 0.0, + second = 0.0; //Time zone NSInteger tz_hour = 0; NSInteger tz_minute = 0; - + enum { monthAndDate, week, dateOnly } dateSpecification = monthAndDate; - + BOOL strict = self.parsesStrictly; unichar timeSep = self.timeSeparator; - + if (strict) timeSep = ISO8601DefaultTimeSeparatorCharacter; NSAssert(timeSep != '\0', @"Time separator must not be NUL."); - + BOOL isValidDate = ([string length] > 0U); NSTimeZone *timeZone = nil; - + const unsigned char *ch = (const unsigned char *)[string UTF8String]; - + NSRange range = { 0U, 0U }; const unsigned char *start_of_date = NULL; if (strict && isspace(*ch)) { @@ -202,18 +207,18 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out if (!isspace(ch[i])) break; } - + range.location = i; ch += i; start_of_date = ch; - + NSUInteger segment; NSUInteger num_leading_hyphens = 0U, num_digits = 0U; - + if (*ch == 'T') { //There is no date here, only a time. Set the date to now; then we'll parse the time. isValidDate = isdigit(*++ch); - + year = nowComponents.year; month_or_week = nowComponents.month; day = nowComponents.day; @@ -222,7 +227,7 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out ++num_leading_hyphens; ++ch; } - + segment = read_segment(ch, &ch, &num_digits); switch(num_digits) { case 0: @@ -240,7 +245,7 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out } else isValidDate = NO; break; - + case 8: //YYYY MM DD if (num_leading_hyphens > 0U) isValidDate = NO; @@ -251,7 +256,7 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out year = segment / 100U; } break; - + case 6: //YYMMDD (implicit century) if (num_leading_hyphens > 0U) isValidDate = NO; @@ -264,14 +269,14 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out year += segment / 100U; } break; - + case 4: switch(num_leading_hyphens) { case 0: //YYYY year = segment; - + if (*ch == '-') ++ch; - + if (!isdigit(*ch)) { if (*ch == 'W') goto parseWeekAndDay; @@ -284,54 +289,54 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out day = segment % 100U; month_or_week = segment / 100U; break; - + case 2: //MM month_or_week = segment; - + if (*ch == '-') ++ch; if (!isdigit(*ch)) day = 1U; else day = read_segment(ch, &ch, NULL); break; - + case 3: //DDD day = segment % 1000U; dateSpecification = dateOnly; if (strict && (day > (365U + is_leap_year(year)))) isValidDate = NO; break; - + default: isValidDate = NO; } } break; - + case 1: //YYMM month_or_week = segment % 100U; year = segment / 100U; - + if (*ch == '-') ++ch; if (!isdigit(*ch)) day = 1U; else day = read_segment(ch, &ch, NULL); - + break; - + case 2: //MMDD day = segment % 100U; month_or_week = segment / 100U; year = nowComponents.year; - + break; - + default: isValidDate = NO; } //switch(num_leading_hyphens) (4 digits) break; - + case 1: if (strict) { //Two digits only - never just one. @@ -356,7 +361,7 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out year = nowComponents.year; year -= (year % 100U); year += segment; - + if (*++ch == 'W') goto parseWeekAndDay; else if (!isdigit(*ch)) { @@ -364,13 +369,13 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out } else { //Get month and/or date. segment = read_segment_4digits(ch, &ch, &num_digits); - NSLog(@"(%@) parsing month; segment is %lu and ch is %s", string, (unsigned long)segment, ch); + RKLogTrace(@"(%@) parsing month; segment is %lu and ch is %s", string, (unsigned long)segment, ch); switch(num_digits) { case 4: //YY-MMDD day = segment % 100U; month_or_week = segment / 100U; break; - + case 1: //YY-M; YY-M-DD (extension) if (strict) { isValidDate = NO; @@ -386,7 +391,7 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out } else day = 1U; break; - + case 3: //Ordinal date. day = segment; dateSpecification = dateOnly; @@ -397,7 +402,7 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out year = nowComponents.year; year -= (year % 100U); year += segment; - + parseWeekAndDay: //*ch should be 'W' here. if (!isdigit(*++ch)) { //Not really a week-based date; just a year followed by '-W'. @@ -419,24 +424,24 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out month_or_week = day = 1U; } break; - + case 1:; //-YY; -YY-MM (implicit century) - NSLog(@"(%@) found %lu digits and one hyphen, so this is either -YY or -YY-MM; segment (year) is %lu", string, (unsigned long)num_digits, (unsigned long)segment); + RKLogTrace(@"(%@) found %lu digits and one hyphen, so this is either -YY or -YY-MM; segment (year) is %lu", string, (unsigned long)num_digits, (unsigned long)segment); NSUInteger current_year = nowComponents.year; NSUInteger current_century = (current_year % 100U); year = segment + (current_year - current_century); if (num_digits == 1U) //implied decade year += current_century - (current_year % 10U); - + if (*ch == '-') { ++ch; month_or_week = read_segment_2digits(ch, &ch); - NSLog(@"(%@) month is %lu", string, (unsigned long)month_or_week); + RKLogTrace(@"(%@) month is %lu", string, (unsigned long)month_or_week); } - + day = 1U; break; - + case 2: //--MM; --MM-DD year = nowComponents.year; month_or_week = segment; @@ -445,18 +450,18 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out day = read_segment_2digits(ch, &ch); } break; - + case 3: //---DD year = nowComponents.year; month_or_week = nowComponents.month; day = segment; break; - + default: isValidDate = NO; } //switch(num_leading_hyphens) (2 digits) break; - + case 7: //YYYY DDD (ordinal date) if (num_leading_hyphens > 0U) isValidDate = NO; @@ -468,7 +473,7 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out isValidDate = NO; } break; - + case 3: //--DDD (ordinal date, implicit year) //Technically, the standard only allows one hyphen. But it says that two hyphens is the logical implementation, and one was dropped for brevity. So I have chosen to allow the missing hyphen. if ((num_leading_hyphens < 1U) || ((num_leading_hyphens > 2U) && !strict)) @@ -481,15 +486,15 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out isValidDate = NO; } break; - + default: isValidDate = NO; } } - + if (isValidDate) { if (isspace(*ch) || (*ch == 'T')) ++ch; - + if (isdigit(*ch)) { hour = read_segment_2digits(ch, &ch); if (*ch == timeSep) { @@ -516,16 +521,16 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out } } } - + if (!strict) { if (isspace(*ch)) ++ch; } - + switch(*ch) { case 'Z': timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; break; - + case '+': case '-':; BOOL negative = (*ch == '-'); @@ -538,10 +543,10 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out } tz_hour = (NSInteger)segment; if (negative) tz_hour = -tz_hour; - + //Optional separator. if (*ch == timeSep) ++ch; - + if (isdigit(*ch)) { //Read minute offset. segment = *ch - '0'; @@ -552,7 +557,7 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out tz_minute = segment; if (negative) tz_minute = -tz_minute; } - + NSTimeInterval timeZoneOffset = (tz_hour * 3600) + (tz_minute * 60); NSNumber *offsetNum = [NSNumber numberWithDouble:timeZoneOffset]; timeZone = [timeZonesByOffset objectForKey:offsetNum]; @@ -565,19 +570,19 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out } } } - + if (isValidDate) { components.year = year; components.day = day; components.hour = hour; components.minute = (NSInteger)minute; components.second = (NSInteger)second; - + switch(dateSpecification) { case monthAndDate: components.month = month_or_week; break; - + case week:; //Adapted from . //This works by converting the week date into an ordinal date, then letting the next case handle it. @@ -589,25 +594,25 @@ - (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSUInteger Jan1Weekday = (isLeapYear + G) % 7U; enum { monday, tuesday, wednesday, thursday/*, friday, saturday, sunday*/ }; components.day = ((8U - Jan1Weekday) + (7U * (Jan1Weekday > thursday))) + (day - 1U) + (7U * (month_or_week - 2)); - + case dateOnly: //An "ordinal date". break; } } } //if (!(strict && isdigit(ch[0]))) - + if (outRange) { if (isValidDate) range.length = ch - start_of_date; else range.location = NSNotFound; - + *outRange = range; } if (outTimeZone) { *outTimeZone = timeZone; } - + return components; } @@ -617,15 +622,15 @@ - (NSDate *) dateFromString:(NSString *)string { - (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone { return [self dateFromString:string timeZone:outTimeZone range:NULL]; } -- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange { +- (NSDate *)dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange { NSTimeZone *timeZone = nil; NSDateComponents *components = [self dateComponentsFromString:string timeZone:&timeZone range:outRange]; if (outTimeZone) *outTimeZone = timeZone; parsingCalendar.timeZone = timeZone; - NSLog(@"TIMEZONE: %@", timeZone); - + RKLogDebug(@"TIMEZONE: %@", timeZone); + return [parsingCalendar dateFromComponents:components]; } @@ -677,26 +682,26 @@ - (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone { - (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat timeZone:(NSTimeZone *)timeZone { if (includeTime) dateFormat = [dateFormat stringByAppendingFormat:@"'T'%@", [self replaceColonsInString:ISO_TIME_FORMAT withTimeSeparator:self.timeSeparator]]; - + unparsingCalendar.timeZone = timeZone; - + if (dateFormat != lastUsedFormatString) { [unparsingFormatter release]; unparsingFormatter = nil; - + [lastUsedFormatString release]; lastUsedFormatString = [dateFormat retain]; } - + if (!unparsingFormatter) { unparsingFormatter = [[NSDateFormatter alloc] init]; unparsingFormatter.formatterBehavior = NSDateFormatterBehavior10_4; unparsingFormatter.dateFormat = dateFormat; unparsingFormatter.calendar = unparsingCalendar; } - + NSString *str = [unparsingFormatter stringForObjectValue:date]; - + if (includeTime) { NSInteger offset = [timeZone secondsFromGMT]; offset /= 60; //bring down to minutes @@ -705,16 +710,16 @@ - (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat else str = [str stringByAppendingFormat:ISO_TIMEZONE_OFFSET_FORMAT, offset / 60, offset % 60]; } - + //Undo the change we made earlier unparsingCalendar.timeZone = self.defaultTimeZone; - + return str; } - (NSString *) stringForObjectValue:(id)value { NSParameterAssert([value isKindOfClass:[NSDate class]]); - + return [self stringFromDate:(NSDate *)value]; } @@ -726,14 +731,14 @@ - (NSString *) stringForObjectValue:(id)value { - (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone { unparsingCalendar.timeZone = timeZone; NSDateComponents *components = [unparsingCalendar components:NSYearCalendarUnit | NSWeekdayCalendarUnit | NSDayCalendarUnit fromDate:date]; - + //Determine the ordinal date. NSDateComponents *startOfYearComponents = [unparsingCalendar components:NSYearCalendarUnit fromDate:date]; startOfYearComponents.month = 1; startOfYearComponents.day = 1; NSDateComponents *ordinalComponents = [unparsingCalendar components:NSDayCalendarUnit fromDate:[unparsingCalendar dateFromComponents:startOfYearComponents] toDate:date options:0]; ordinalComponents.day += 1; - + enum { monday, tuesday, wednesday, thursday, friday, saturday, sunday }; @@ -743,25 +748,25 @@ - (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZ july, august, september, october, november, december }; - + NSInteger year = components.year; NSInteger week = 0; //The old unparser added 6 to [calendarDate dayOfWeek], which was zero-based; components.weekday is one-based, so we now add only 5. NSInteger dayOfWeek = (components.weekday + 5) % 7; NSInteger dayOfYear = ordinalComponents.day; - + NSInteger prevYear = year - 1; - + BOOL yearIsLeapYear = is_leap_year(year); BOOL prevYearIsLeapYear = is_leap_year(prevYear); - + NSInteger YY = prevYear % 100; NSInteger C = prevYear - YY; NSInteger G = YY + YY / 4; NSInteger Jan1Weekday = (((((C / 100) % 4) * 5) + G) % 7); - + NSInteger weekday = ((dayOfYear + Jan1Weekday) - 1) % 7; - + if((dayOfYear <= (7 - Jan1Weekday)) && (Jan1Weekday > thursday)) { week = 52 + ((Jan1Weekday == friday) || ((Jan1Weekday == saturday) && prevYearIsLeapYear)); --year; @@ -775,20 +780,20 @@ - (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZ week = J / 7 - (Jan1Weekday > thursday); } } - + NSString *timeString; if(includeTime) { NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; unichar timeSep = self.timeSeparator; if (!timeSep) timeSep = ISO8601DefaultTimeSeparatorCharacter; formatter.dateFormat = [self replaceColonsInString:ISO_TIME_WITH_TIMEZONE_FORMAT withTimeSeparator:timeSep]; - + timeString = [formatter stringForObjectValue:date]; - + [formatter release]; } else timeString = @""; - + return [NSString stringWithFormat:@"%lu-W%02lu-%02lu%@", (unsigned long)year, (unsigned long)week, ((unsigned long)dayOfWeek) + 1U, timeString]; } @@ -797,101 +802,101 @@ - (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZ static NSUInteger read_segment(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits) { NSUInteger num_digits = 0U; NSUInteger value = 0U; - + while(isdigit(*str)) { value *= 10U; value += *str - '0'; ++num_digits; ++str; } - + if (next) *next = str; if (out_num_digits) *out_num_digits = num_digits; - + return value; } static NSUInteger read_segment_4digits(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits) { NSUInteger num_digits = 0U; NSUInteger value = 0U; - + if (isdigit(*str)) { value += *(str++) - '0'; ++num_digits; } - + if (isdigit(*str)) { value *= 10U; value += *(str++) - '0'; ++num_digits; } - + if (isdigit(*str)) { value *= 10U; value += *(str++) - '0'; ++num_digits; } - + if (isdigit(*str)) { value *= 10U; value += *(str++) - '0'; ++num_digits; } - + if (next) *next = str; if (out_num_digits) *out_num_digits = num_digits; - + return value; } static NSUInteger read_segment_2digits(const unsigned char *str, const unsigned char **next) { NSUInteger value = 0U; - + if (isdigit(*str)) value += *str - '0'; - + if (isdigit(*++str)) { value *= 10U; value += *(str++) - '0'; } - + if (next) *next = str; - + return value; } //strtod doesn't support ',' as a separator. This does. static double read_double(const unsigned char *str, const unsigned char **next) { double value = 0.0; - + if (str) { NSUInteger int_value = 0; - + while(isdigit(*str)) { int_value *= 10U; int_value += (*(str++) - '0'); } value = int_value; - + if (((*str == ',') || (*str == '.'))) { ++str; - + register double multiplier, multiplier_multiplier; multiplier = multiplier_multiplier = 0.1; - + while(isdigit(*str)) { value += (*(str++) - '0') * multiplier; multiplier *= multiplier_multiplier; } } } - + if (next) *next = str; - + return value; } static BOOL is_leap_year(NSUInteger year) { return \ - ((year % 4U) == 0U) + ((year % 4U) == 0U) && (((year % 100U) != 0U) - || ((year % 400U) == 0U)); + || ((year % 400U) == 0U)); } diff --git a/config.ru b/config.ru deleted file mode 100644 index e9dafd9210..0000000000 --- a/config.ru +++ /dev/null @@ -1,3 +0,0 @@ -require File.expand_path(File.dirname(__FILE__)) + '/Tests/Server/server' - -run RestKitTestServer